diff --git a/crates/algokit_transact/src/lib.rs b/crates/algokit_transact/src/lib.rs index 04fcb054..cce55f2c 100644 --- a/crates/algokit_transact/src/lib.rs +++ b/crates/algokit_transact/src/lib.rs @@ -13,9 +13,10 @@ pub use traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId, Transa pub use transactions::{ ApplicationCallTransactionBuilder, ApplicationCallTransactionFields, AssetConfigTransactionBuilder, AssetConfigTransactionFields, AssetTransferTransactionBuilder, - AssetTransferTransactionFields, BoxReference, FeeParams, OnApplicationComplete, - PaymentTransactionBuilder, PaymentTransactionFields, SignedTransaction, StateSchema, - Transaction, TransactionHeader, TransactionHeaderBuilder, + AssetTransferTransactionFields, BoxReference, FeeParams, KeyRegistrationTransactionBuilder, + KeyRegistrationTransactionFields, OnApplicationComplete, PaymentTransactionBuilder, + PaymentTransactionFields, SignedTransaction, StateSchema, Transaction, TransactionHeader, + TransactionHeaderBuilder, }; #[cfg(test)] diff --git a/crates/algokit_transact/src/test_utils/key_registration.rs b/crates/algokit_transact/src/test_utils/key_registration.rs new file mode 100644 index 00000000..bf102bc1 --- /dev/null +++ b/crates/algokit_transact/src/test_utils/key_registration.rs @@ -0,0 +1,90 @@ +use crate::{test_utils::TransactionHeaderMother, KeyRegistrationTransactionBuilder}; +use base64::{prelude::BASE64_STANDARD, Engine}; + +pub struct KeyRegistrationTransactionMother {} + +impl KeyRegistrationTransactionMother { + pub fn online_key_registration() -> KeyRegistrationTransactionBuilder { + // https://lora.algokit.io/testnet/transaction/UCWQQKWB3CMPVK6EU2ML7CN5IDYZJVVSVS3RXYEOLJUURX44SUKQ + let mut header = TransactionHeaderMother::testnet() + .sender( + "PKASUHJJ7HALD6BXBIOLQTRFHAP6HF2TAYQ734E325FGDRB66EE6MYQGTM" + .parse() + .unwrap(), + ) + .first_valid(53287880) + .last_valid(53288880) + .fee(2000000) + .build() + .unwrap(); + header.genesis_id = None; + KeyRegistrationTransactionBuilder::default() + .header( + header + ) + .vote_key( + BASE64_STANDARD + .decode("jXzwxM2vUp0/wdazgu6be7BesDn9NKCDaEfvwKMmhTE=") + .unwrap() + .try_into() + .unwrap() + ) + .selection_key( + BASE64_STANDARD + .decode("pi8u2HhXe6qB5IIMTSn2vKiWkDhMCOk1G2G3oyaeSlA=") + .unwrap() + .try_into() + .unwrap() + ) + .state_proof_key( + BASE64_STANDARD + .decode("+h0VzqDJIOEaYTaCGDZMV0jZKQ4ShsVrhyyObOu+s3yF1+oLp2b4l/WGDFp1+kObVVyoNcCYyuE15OsyAhYZxg==") + .unwrap() + .try_into() + .unwrap() + ) + .vote_first(53287679) + .vote_last(56287679) + .vote_key_dilution(1733) + .to_owned() + } + + pub fn offline_key_registration() -> KeyRegistrationTransactionBuilder { + // https://lora.algokit.io/testnet/transaction/WAXJLC44RILOSYX73PJULCAWC43DNBU4AXMWHIRARXK4GO2LHEDQ + let mut header = TransactionHeaderMother::testnet() + .sender( + "W5LKXE4BG7OND7KPGSXPDOOYQDITPNS7NSDU7672TO6I4RDNSEFWXRPISQ" + .parse() + .unwrap(), + ) + .first_valid(52556882) + .last_valid(52557882) + .fee(1000) + .build() + .unwrap(); + header.genesis_id = None; + KeyRegistrationTransactionBuilder::default() + .header(header) + .to_owned() + } + + pub fn non_participation_key_registration() -> KeyRegistrationTransactionBuilder { + // https://lora.algokit.io/testnet/transaction/ACAP6ZGMGNTLUO3IQ26P22SRKYWTQQO3MF64GX7QO6NICDUFPM5A + let mut header = TransactionHeaderMother::testnet() + .sender( + "4UMX2FKZ636VEL74WR66Z5PSRVDC2QAH6GRPP2DTVSBPPADBDY2JB3PN2U" + .parse() + .unwrap(), + ) + .first_valid(3321800) + .last_valid(3322800) + .fee(1000) + .build() + .unwrap(); + header.genesis_id = None; + KeyRegistrationTransactionBuilder::default() + .header(header) + .non_participation(true) + .to_owned() + } +} diff --git a/crates/algokit_transact/src/test_utils/mod.rs b/crates/algokit_transact/src/test_utils/mod.rs index 3b0dd036..ee86a04a 100644 --- a/crates/algokit_transact/src/test_utils/mod.rs +++ b/crates/algokit_transact/src/test_utils/mod.rs @@ -1,5 +1,6 @@ mod application_call; mod asset_config; +mod key_registration; use crate::{ transactions::{AssetTransferTransactionBuilder, PaymentTransactionBuilder}, @@ -16,6 +17,7 @@ use std::{fs::File, str::FromStr}; pub use application_call::ApplicationCallTransactionMother; pub use asset_config::AssetConfigTransactionMother; +pub use key_registration::KeyRegistrationTransactionMother; pub struct TransactionHeaderMother {} impl TransactionHeaderMother { @@ -387,6 +389,27 @@ impl TestDataMother { TransactionTestData::new(transaction, SIGNING_PRIVATE_KEY) } + pub fn online_key_registration() -> TransactionTestData { + let transaction = KeyRegistrationTransactionMother::online_key_registration() + .build() + .unwrap(); + TransactionTestData::new(transaction, SIGNING_PRIVATE_KEY) + } + + pub fn offline_key_registration() -> TransactionTestData { + let transaction = KeyRegistrationTransactionMother::offline_key_registration() + .build() + .unwrap(); + TransactionTestData::new(transaction, SIGNING_PRIVATE_KEY) + } + + pub fn non_participation_key_registration() -> TransactionTestData { + let transaction = KeyRegistrationTransactionMother::non_participation_key_registration() + .build() + .unwrap(); + TransactionTestData::new(transaction, SIGNING_PRIVATE_KEY) + } + pub fn export(path: &std::path::Path, transform: Option) where F: Fn(&TransactionTestData) -> T, @@ -406,6 +429,9 @@ impl TestDataMother { "asset_create": Self::asset_create().as_json(&transform), "asset_destroy": Self::asset_destroy().as_json(&transform), "asset_reconfigure": Self::asset_reconfigure().as_json(&transform), + "online_key_registration": Self::online_key_registration().as_json(&transform), + "offline_key_registration": Self::offline_key_registration().as_json(&transform), + "non_participation_key_registration": Self::non_participation_key_registration().as_json(&transform), })); let file = File::create(path).expect("Failed to create export file"); @@ -464,49 +490,6 @@ mod tests { data.id, String::from("VAHP4FRJH4GRV6ID2BZRK5VYID376EV3VE6T2TKKDFJBBDOXWCCA") ); - assert_eq!( - data.id_raw, - [ - 168, 14, 254, 22, 41, 63, 13, 26, 249, 3, 208, 115, 21, 118, 184, 64, 247, 255, 18, - 187, 169, 61, 61, 77, 74, 25, 82, 16, 141, 215, 176, 132 - ] - ); - assert_eq!( - data.unsigned_bytes, - vec![ - 84, 88, 138, 164, 97, 97, 109, 116, 205, 3, 232, 164, 97, 114, 99, 118, 196, 32, - 138, 24, 8, 153, 89, 167, 60, 236, 255, 238, 91, 198, 115, 190, 137, 254, 3, 35, - 198, 98, 195, 33, 65, 123, 138, 200, 132, 194, 74, 0, 44, 25, 163, 102, 101, 101, - 205, 3, 232, 162, 102, 118, 206, 3, 13, 0, 56, 163, 103, 101, 110, 172, 116, 101, - 115, 116, 110, 101, 116, 45, 118, 49, 46, 48, 162, 103, 104, 196, 32, 72, 99, 181, - 24, 164, 179, 200, 78, 200, 16, 242, 45, 79, 16, 129, 203, 15, 113, 240, 89, 167, - 172, 32, 222, 198, 47, 127, 112, 229, 9, 58, 34, 162, 108, 118, 206, 3, 13, 1, 0, - 163, 115, 110, 100, 196, 32, 72, 118, 175, 30, 96, 187, 134, 238, 76, 228, 146, - 219, 137, 200, 222, 52, 40, 86, 146, 168, 129, 190, 15, 103, 21, 24, 5, 31, 88, 27, - 201, 123, 164, 116, 121, 112, 101, 165, 97, 120, 102, 101, 114, 164, 120, 97, 105, - 100, 206, 6, 107, 40, 157 - ] - ); - assert_eq!( - data.signed_bytes, - vec![ - 130, 163, 115, 105, 103, 196, 64, 115, 182, 105, 213, 69, 248, 151, 218, 20, 41, - 12, 239, 153, 18, 26, 187, 149, 210, 109, 148, 39, 180, 210, 255, 64, 224, 207, 43, - 40, 165, 103, 14, 125, 13, 50, 33, 75, 66, 56, 124, 233, 253, 215, 254, 157, 18, 7, - 244, 15, 55, 31, 76, 190, 117, 201, 189, 5, 26, 44, 249, 196, 219, 73, 0, 163, 116, - 120, 110, 138, 164, 97, 97, 109, 116, 205, 3, 232, 164, 97, 114, 99, 118, 196, 32, - 138, 24, 8, 153, 89, 167, 60, 236, 255, 238, 91, 198, 115, 190, 137, 254, 3, 35, - 198, 98, 195, 33, 65, 123, 138, 200, 132, 194, 74, 0, 44, 25, 163, 102, 101, 101, - 205, 3, 232, 162, 102, 118, 206, 3, 13, 0, 56, 163, 103, 101, 110, 172, 116, 101, - 115, 116, 110, 101, 116, 45, 118, 49, 46, 48, 162, 103, 104, 196, 32, 72, 99, 181, - 24, 164, 179, 200, 78, 200, 16, 242, 45, 79, 16, 129, 203, 15, 113, 240, 89, 167, - 172, 32, 222, 198, 47, 127, 112, 229, 9, 58, 34, 162, 108, 118, 206, 3, 13, 1, 0, - 163, 115, 110, 100, 196, 32, 72, 118, 175, 30, 96, 187, 134, 238, 76, 228, 146, - 219, 137, 200, 222, 52, 40, 86, 146, 168, 129, 190, 15, 103, 21, 24, 5, 31, 88, 27, - 201, 123, 164, 116, 121, 112, 101, 165, 97, 120, 102, 101, 114, 164, 120, 97, 105, - 100, 206, 6, 107, 40, 157 - ] - ); } #[test] @@ -580,4 +563,29 @@ mod tests { String::from("U4XH6AS5UUYQI4IZ3E5JSUEIU64Y3FGNYKLH26W4HRY7T6PK745A") ); } + + #[test] + fn test_online_key_registration_snapshot() { + let data = TestDataMother::online_key_registration(); + assert_eq!( + data.id, + String::from("UCWQQKWB3CMPVK6EU2ML7CN5IDYZJVVSVS3RXYEOLJUURX44SUKQ") + ); + } + #[test] + fn test_offline_key_registration_snapshot() { + let data = TestDataMother::offline_key_registration(); + assert_eq!( + data.id, + String::from("WAXJLC44RILOSYX73PJULCAWC43DNBU4AXMWHIRARXK4GO2LHEDQ") + ); + } + #[test] + fn test_non_participation_key_registration_snapshot() { + let data = TestDataMother::non_participation_key_registration(); + assert_eq!( + data.id, + String::from("ACAP6ZGMGNTLUO3IQ26P22SRKYWTQQO3MF64GX7QO6NICDUFPM5A") + ); + } } diff --git a/crates/algokit_transact/src/transactions/asset_config.rs b/crates/algokit_transact/src/transactions/asset_config.rs index 0e38453a..d14196a3 100644 --- a/crates/algokit_transact/src/transactions/asset_config.rs +++ b/crates/algokit_transact/src/transactions/asset_config.rs @@ -436,7 +436,7 @@ impl AssetConfigTransactionBuilder { let d = self.build_fields()?; d.validate().map_err(|errors| { AssetConfigTransactionBuilderError::ValidationError(format!( - "Asset confiurationg validation failed: {}", + "Asset config validation failed: {}", errors.join("\n") )) })?; diff --git a/crates/algokit_transact/src/transactions/key_registration.rs b/crates/algokit_transact/src/transactions/key_registration.rs new file mode 100644 index 00000000..a5c21b4b --- /dev/null +++ b/crates/algokit_transact/src/transactions/key_registration.rs @@ -0,0 +1,531 @@ +//! Key registration transaction module for AlgoKit Core. +//! +//! This module provides functionality for creating and managing key registration transactions, +//! which are used to register accounts online or offline for participation in Algorand consensus. + +use crate::traits::Validate; +use crate::transactions::common::{TransactionHeader, TransactionValidationError}; +use crate::utils::{is_false_opt, is_zero_opt}; +use crate::Transaction; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, skip_serializing_none, Bytes}; + +/// Represents a key registration transaction that registers an account online or offline +/// for participation in Algorand consensus. +#[serde_as] +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Builder)] +#[builder( + name = "KeyRegistrationTransactionBuilder", + setter(strip_option), + build_fn(name = "build_fields") +)] +pub struct KeyRegistrationTransactionFields { + /// Common transaction header fields. + #[serde(flatten)] + pub header: TransactionHeader, + + /// Root participation public key (32 bytes). + #[serde(rename = "votekey")] + #[serde_as(as = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[builder(default)] + pub vote_key: Option<[u8; 32]>, + + /// VRF public key (32 bytes). + #[serde(rename = "selkey")] + #[serde_as(as = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[builder(default)] + pub selection_key: Option<[u8; 32]>, + + /// State proof key (64 bytes). + #[serde(rename = "sprfkey")] + #[serde_as(as = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[builder(default)] + pub state_proof_key: Option<[u8; 64]>, + + /// First round for which the participation key is valid. + #[serde(rename = "votefst")] + #[serde(skip_serializing_if = "is_zero_opt")] + #[serde(default)] + #[builder(default)] + pub vote_first: Option, + + /// Last round for which the participation key is valid. + #[serde(rename = "votelst")] + #[serde(skip_serializing_if = "is_zero_opt")] + #[serde(default)] + #[builder(default)] + pub vote_last: Option, + + /// Key dilution for the 2-level participation key. + #[serde(rename = "votekd")] + #[serde(skip_serializing_if = "is_zero_opt")] + #[serde(default)] + #[builder(default)] + pub vote_key_dilution: Option, + + /// Mark account as non-reward earning. + #[serde(rename = "nonpart")] + #[serde(skip_serializing_if = "is_false_opt")] + #[serde(default)] + #[builder(default)] + pub non_participation: Option, +} + +impl KeyRegistrationTransactionFields { + pub fn validate_for_online(&self) -> Result<(), Vec> { + let mut errors = Vec::new(); + + if self.vote_key.is_none() { + errors.push(TransactionValidationError::RequiredField( + "Vote key".to_string(), + )); + } + if self.selection_key.is_none() { + errors.push(TransactionValidationError::RequiredField( + "Selection key".to_string(), + )); + } + if self.state_proof_key.is_none() { + errors.push(TransactionValidationError::RequiredField( + "State proof key".to_string(), + )); + } + if self.vote_first.is_none() { + errors.push(TransactionValidationError::RequiredField( + "Vote first".to_string(), + )); + } + if self.vote_last.is_none() { + errors.push(TransactionValidationError::RequiredField( + "Vote last".to_string(), + )); + } + if let (Some(first), Some(last)) = (self.vote_first, self.vote_last) { + if first >= last { + errors.push(TransactionValidationError::ArbitraryConstraint( + "Vote first must be less than vote last".to_string(), + )); + } + } + if self.vote_key_dilution.is_none() { + errors.push(TransactionValidationError::RequiredField( + "Vote key dilution".to_string(), + )); + } + + if self.non_participation.is_some_and(|v| v) { + errors.push(TransactionValidationError::ArbitraryConstraint( + "Online key registration cannot have non participation flag set".to_string(), + )); + } + + match errors.is_empty() { + true => Ok(()), + false => Err(errors), + } + } +} + +impl KeyRegistrationTransactionBuilder { + pub fn build(&self) -> Result { + let d = self.build_fields()?; + d.validate().map_err(|errors| { + KeyRegistrationTransactionBuilderError::ValidationError(format!( + "Key registration validation failed: {}", + errors.join("\n") + )) + })?; + Ok(Transaction::KeyRegistration(d)) + } +} + +impl Validate for KeyRegistrationTransactionFields { + fn validate(&self) -> Result<(), Vec> { + let has_any_participation_fields = self.vote_key.is_some() + || self.selection_key.is_some() + || self.state_proof_key.is_some() + || self.vote_first.is_some() + || self.vote_last.is_some() + || self.vote_key_dilution.is_some(); + + match has_any_participation_fields { + true => { + // Online key registration + self.validate_for_online() + .map_err(|errors| errors.iter().map(|e| e.to_string()).collect()) + } + false => { + // Offline key registration (including non-participating) + // No participation fields present - inherently valid offline state + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{KeyRegistrationTransactionMother, TransactionHeaderMother}; + + #[test] + fn test_validate_valid_online_key_registration() { + let online_key_reg = KeyRegistrationTransactionMother::online_key_registration() + .build_fields() + .unwrap(); + + let result = online_key_reg.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_valid_offline_key_registration() { + let offline_key_reg = KeyRegistrationTransactionMother::offline_key_registration() + .build_fields() + .unwrap(); + + let result = offline_key_reg.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_valid_non_participation_key_registration() { + let non_part_key_reg = + KeyRegistrationTransactionMother::non_participation_key_registration() + .build_fields() + .unwrap(); + + let result = non_part_key_reg.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_online_missing_vote_key() { + let mut key_reg = KeyRegistrationTransactionMother::online_key_registration() + .build_fields() + .unwrap(); + key_reg.vote_key = None; // Missing required field + + let result = key_reg.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert!(errors.iter().any(|e| e.contains("Vote key is required"))); + } + + #[test] + fn test_validate_online_multiple_missing_fields() { + let key_reg = KeyRegistrationTransactionFields { + header: TransactionHeaderMother::example().build().unwrap(), + vote_key: None, // Missing + selection_key: None, // Missing + state_proof_key: Some([3u8; 64]), + vote_first: None, // Missing + vote_last: Some(200), + vote_key_dilution: None, // Missing + non_participation: None, + }; + + let result = key_reg.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert_eq!(errors.len(), 4); // Should have 4 missing field errors + + let error_text = errors.join("\n"); + assert!(error_text.contains("Vote key is required")); + assert!(error_text.contains("Selection key is required")); + assert!(error_text.contains("Vote first is required")); + assert!(error_text.contains("Vote key dilution is required")); + } + + #[test] + fn test_validate_invalid_vote_round_range() { + let key_reg = KeyRegistrationTransactionMother::online_key_registration() + .vote_first(200) // Greater than vote_last + .vote_last(100) + .build_fields() + .unwrap(); + + let result = key_reg.validate(); + assert!(result.is_err()); + let errors: Vec = result.unwrap_err(); + assert!(errors + .iter() + .any(|e| e.contains("Vote first must be less than vote last"))); + } + + #[test] + fn test_validate_equal_vote_rounds() { + let key_reg = KeyRegistrationTransactionMother::online_key_registration() + .vote_first(100) // Equal to vote_last + .vote_last(100) + .build_fields() + .unwrap(); + + let result = key_reg.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert!(errors + .iter() + .any(|e| e.contains("Vote first must be less than vote last"))); + } + + #[test] + fn test_validate_online_with_non_participation_flag() { + let key_reg: KeyRegistrationTransactionFields = + KeyRegistrationTransactionMother::online_key_registration() + .non_participation(true) // Invalid for online registration + .build_fields() + .unwrap(); + + let result = key_reg.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + assert!(errors + .iter() + .any(|e| e.contains("Online key registration cannot have non participation flag set"))); + } + + #[test] + fn test_validate_offline_with_participation_fields() { + let key_reg: KeyRegistrationTransactionFields = + KeyRegistrationTransactionMother::offline_key_registration() + .vote_key([1u8; 32]) // Should not be set for offline + .build_fields() + .unwrap(); + + let result = key_reg.validate(); + assert!(result.is_err()); + let errors = result.unwrap_err(); + // Since vote_key is set, it should be treated as online registration with missing required fields + assert!(errors + .iter() + .any(|e| e.contains("Selection key is required"))); + assert!(errors + .iter() + .any(|e| e.contains("State proof key is required"))); + assert!(errors.iter().any(|e| e.contains("Vote first is required"))); + assert!(errors.iter().any(|e| e.contains("Vote last is required"))); + assert!(errors + .iter() + .any(|e| e.contains("Vote key dilution is required"))); + } + + #[test] + fn test_builder_validation_integration() { + // Test that the builder properly validates + let result = KeyRegistrationTransactionMother::offline_key_registration() + .vote_key([1u8; 32]) + // Missing state_proof_key and other required fields + .build(); + + assert!(result.is_err()); + + // Test valid builder + let result = KeyRegistrationTransactionMother::online_key_registration().build(); + + assert!(result.is_ok()); + } + + #[test] + fn test_non_participation_serialization_skipping() { + use crate::AlgorandMsgpack; + + // Test that non_participation: Some(false) is skipped during serialization (same as None) + let key_reg_none = KeyRegistrationTransactionMother::offline_key_registration() + .build_fields() + .unwrap(); + + let mut key_reg_false = key_reg_none.clone(); + key_reg_false.non_participation = Some(false); + + let mut key_reg_true = key_reg_none.clone(); + key_reg_true.non_participation = Some(true); + + // Encode all three transactions + let encoded_none = Transaction::KeyRegistration(key_reg_none).encode().unwrap(); + let encoded_false = Transaction::KeyRegistration(key_reg_false) + .encode() + .unwrap(); + let encoded_true = Transaction::KeyRegistration(key_reg_true).encode().unwrap(); + + // None and Some(false) should produce identical serialization + assert_eq!( + encoded_none, encoded_false, + "Serialization of non_participation: None should be identical to Some(false)" + ); + + // Some(true) should be different (larger, since it includes the field) + assert_ne!( + encoded_none, encoded_true, + "Serialization of non_participation: Some(true) should be different from None/false" + ); + + // The true version should be larger since it includes the field + assert!( + encoded_true.len() > encoded_none.len(), + "Serialization with non_participation: Some(true) should be larger" + ); + } + + // Integration tests for encoding and transaction functionality + mod integration_tests { + use crate::{ + constants::ALGORAND_SIGNATURE_BYTE_LENGTH, + test_utils::{ + AddressMother, KeyRegistrationTransactionMother, TransactionHeaderMother, + TransactionMother, + }, + traits::TransactionId, + transactions::FeeParams, + AlgorandMsgpack, SignedTransaction, Transaction, Transactions, + }; + + #[test] + fn test_online_key_registration_transaction_encoding() { + let tx_builder = KeyRegistrationTransactionMother::online_key_registration(); + let key_reg_tx_fields = tx_builder.build_fields().unwrap(); + let key_reg_tx = tx_builder.build().unwrap(); + + let encoded = key_reg_tx.encode().unwrap(); + let decoded = Transaction::decode(&encoded).unwrap(); + assert_eq!(decoded, key_reg_tx); + assert_eq!(decoded, Transaction::KeyRegistration(key_reg_tx_fields)); + + let signed_tx = SignedTransaction { + transaction: key_reg_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, key_reg_tx); + + let raw_encoded = key_reg_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_offline_key_registration_transaction_encoding() { + let tx_builder = KeyRegistrationTransactionMother::offline_key_registration(); + let key_reg_tx_fields = tx_builder.build_fields().unwrap(); + let key_reg_tx = tx_builder.build().unwrap(); + + // Test focuses on encoding/decoding behavior, not transaction type categorization + + let encoded = key_reg_tx.encode().unwrap(); + let decoded = Transaction::decode(&encoded).unwrap(); + assert_eq!(decoded, key_reg_tx); + assert_eq!(decoded, Transaction::KeyRegistration(key_reg_tx_fields)); + + let signed_tx = SignedTransaction { + transaction: key_reg_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, key_reg_tx); + } + + #[test] + fn test_non_participation_key_registration_transaction_encoding() { + let tx_builder = KeyRegistrationTransactionMother::non_participation_key_registration(); + let key_reg_tx_fields = tx_builder.build_fields().unwrap(); + let key_reg_tx = tx_builder.build().unwrap(); + + // Verify non-participation flag is set correctly + assert_eq!(key_reg_tx_fields.non_participation, Some(true)); + + let encoded = key_reg_tx.encode().unwrap(); + let decoded = Transaction::decode(&encoded).unwrap(); + assert_eq!(decoded, key_reg_tx); + assert_eq!(decoded, Transaction::KeyRegistration(key_reg_tx_fields)); + } + + #[test] + fn test_key_registration_transaction_id() { + let tx_builder = KeyRegistrationTransactionMother::online_key_registration(); + let key_reg_tx = tx_builder.build().unwrap(); + + let signed_tx = SignedTransaction { + transaction: key_reg_tx.clone(), + signature: Some([0; ALGORAND_SIGNATURE_BYTE_LENGTH]), + auth_address: None, + }; + + // Test that transaction ID can be generated + let tx_id = key_reg_tx.id().unwrap(); + let tx_id_raw = key_reg_tx.id_raw().unwrap(); + + assert_eq!(signed_tx.id().unwrap(), tx_id); + assert_eq!(signed_tx.id_raw().unwrap(), tx_id_raw); + + // Transaction ID should be non-empty + assert!(!tx_id.is_empty()); + assert_ne!(tx_id_raw, [0u8; 32]); + } + + #[test] + fn test_key_registration_fee_calculation() { + let key_reg_tx = KeyRegistrationTransactionMother::online_key_registration() + .build() + .unwrap(); + + let updated_transaction = key_reg_tx + .assign_fee(FeeParams { + fee_per_byte: 1, + min_fee: 1000, + extra_fee: None, + max_fee: None, + }) + .unwrap(); + + // Fee should be calculated based on transaction size + assert!(updated_transaction.header().fee.unwrap() >= 1000); + } + + #[test] + fn test_key_registration_in_transaction_group() { + let header_builder = TransactionHeaderMother::testnet() + .sender(AddressMother::neil()) + .first_valid(51532821) + .last_valid(51533021) + .to_owned(); + + let key_reg_tx = KeyRegistrationTransactionMother::online_key_registration() + .header(header_builder.build().unwrap()) + .build() + .unwrap(); + + let payment_tx = TransactionMother::simple_payment() + .header(header_builder.build().unwrap()) + .build() + .unwrap(); + + let txs = vec![key_reg_tx, payment_tx]; + let grouped_txs = txs.assign_group().unwrap(); + + assert_eq!(grouped_txs.len(), 2); + + // Both transactions should have the same group ID + let group_id = grouped_txs[0].header().group.unwrap(); + assert_eq!(grouped_txs[1].header().group.unwrap(), group_id); + + // Group ID should be non-zero + assert_ne!(group_id, [0u8; 32]); + } + } +} diff --git a/crates/algokit_transact/src/transactions/mod.rs b/crates/algokit_transact/src/transactions/mod.rs index 8032676d..b4f3097b 100644 --- a/crates/algokit_transact/src/transactions/mod.rs +++ b/crates/algokit_transact/src/transactions/mod.rs @@ -8,6 +8,7 @@ mod application_call; mod asset_config; mod asset_transfer; mod common; +mod key_registration; mod payment; use application_call::{application_call_deserializer, application_call_serializer}; @@ -21,6 +22,7 @@ pub use asset_config::{ }; pub use asset_transfer::{AssetTransferTransactionBuilder, AssetTransferTransactionFields}; pub use common::{TransactionHeader, TransactionHeaderBuilder}; +pub use key_registration::{KeyRegistrationTransactionBuilder, KeyRegistrationTransactionFields}; pub use payment::{PaymentTransactionBuilder, PaymentTransactionFields}; use crate::constants::{ @@ -54,12 +56,9 @@ pub enum Transaction { #[serde(deserialize_with = "application_call_deserializer")] #[serde(rename = "appl")] ApplicationCall(ApplicationCallTransactionFields), - // All the below transaction variants will be implemented in the future - // #[serde(rename = "afrz")] - // AssetFreeze(...), - // #[serde(rename = "keyreg")] - // KeyRegistration(...), + #[serde(rename = "keyreg")] + KeyRegistration(KeyRegistrationTransactionFields), } pub struct FeeParams { @@ -76,6 +75,7 @@ impl Transaction { Transaction::AssetTransfer(a) => &a.header, Transaction::AssetConfig(a) => &a.header, Transaction::ApplicationCall(a) => &a.header, + Transaction::KeyRegistration(k) => &k.header, } } @@ -85,6 +85,7 @@ impl Transaction { Transaction::AssetTransfer(a) => &mut a.header, Transaction::AssetConfig(a) => &mut a.header, Transaction::ApplicationCall(a) => &mut a.header, + Transaction::KeyRegistration(k) => &mut k.header, } } diff --git a/crates/algokit_transact_ffi/polytest.toml b/crates/algokit_transact_ffi/polytest.toml index 4b0ed86e..79dc33e0 100644 --- a/crates/algokit_transact_ffi/polytest.toml +++ b/crates/algokit_transact_ffi/polytest.toml @@ -23,6 +23,10 @@ groups = ["Generic Transaction Tests"] desc = "Transaction Group tests" groups = ["Transaction Group Tests"] +[suite."Key Registration"] +desc = "Tests for key registration transactions" +groups = ["Transaction Tests"] + # Test Group: Generic Transaction Tests [group."Generic Transaction Tests"] diff --git a/crates/algokit_transact_ffi/src/lib.rs b/crates/algokit_transact_ffi/src/lib.rs index 24dc0606..b9c4b444 100644 --- a/crates/algokit_transact_ffi/src/lib.rs +++ b/crates/algokit_transact_ffi/src/lib.rs @@ -10,6 +10,7 @@ use serde_bytes::ByteBuf; pub use transactions::ApplicationCallTransactionFields; pub use transactions::AssetConfigTransactionFields; +pub use transactions::KeyRegistrationTransactionFields; // thiserror is used to easily create errors than can be propagated to the language bindings // UniFFI will create classes for errors (i.e. `MsgPackError.EncodingError` in Python) @@ -211,6 +212,8 @@ pub struct Transaction { asset_config: Option, application_call: Option, + + key_registration: Option, } impl TryFrom for algokit_transact::Transaction { @@ -222,6 +225,7 @@ impl TryFrom for algokit_transact::Transaction { tx.payment.is_some(), tx.asset_transfer.is_some(), tx.asset_config.is_some(), + tx.key_registration.is_some(), tx.application_call.is_some(), ] .into_iter() @@ -239,6 +243,9 @@ impl TryFrom for algokit_transact::Transaction { TransactionType::AssetTransfer => { Ok(algokit_transact::Transaction::AssetTransfer(tx.try_into()?)) } + TransactionType::KeyRegistration => Ok(algokit_transact::Transaction::KeyRegistration( + tx.try_into()?, + )), TransactionType::AssetConfig => { Ok(algokit_transact::Transaction::AssetConfig(tx.try_into()?)) } @@ -262,11 +269,20 @@ impl TryFrom for algokit_transact::TransactionHeader { first_valid: tx.first_valid, last_valid: tx.last_valid, genesis_id: tx.genesis_id, - genesis_hash: tx.genesis_hash.map(bytebuf_to_byte32).transpose()?, + genesis_hash: tx + .genesis_hash + .map(|buf| bytebuf_to_bytes::<32>(&buf)) + .transpose()?, note: tx.note.map(ByteBuf::into_vec), rekey_to: tx.rekey_to.map(TryInto::try_into).transpose()?, - lease: tx.lease.map(bytebuf_to_byte32).transpose()?, - group: tx.group.map(bytebuf_to_byte32).transpose()?, + lease: tx + .lease + .map(|buf| bytebuf_to_bytes::<32>(&buf)) + .transpose()?, + group: tx + .group + .map(|buf| bytebuf_to_bytes::<32>(&buf)) + .transpose()?, }) } } @@ -353,6 +369,7 @@ impl TryFrom for Transaction { None, None, None, + None, ) } algokit_transact::Transaction::AssetTransfer(asset_transfer) => { @@ -364,6 +381,7 @@ impl TryFrom for Transaction { Some(asset_transfer_fields), None, None, + None, ) } algokit_transact::Transaction::AssetConfig(asset_config) => { @@ -375,6 +393,7 @@ impl TryFrom for Transaction { None, Some(asset_config_fields), None, + None, ) } algokit_transact::Transaction::ApplicationCall(application_call) => { @@ -386,6 +405,19 @@ impl TryFrom for Transaction { None, None, Some(application_call_fields), + None, + ) + } + algokit_transact::Transaction::KeyRegistration(key_registration) => { + let key_registration_fields = key_registration.clone().into(); + build_transaction( + key_registration.header, + TransactionType::KeyRegistration, + None, + None, + None, + None, + Some(key_registration_fields), ) } } @@ -438,12 +470,12 @@ impl TryFrom for algokit_transact::SignedTransaction { } } -fn bytebuf_to_byte32(buf: ByteBuf) -> Result { - let vec = buf.to_vec(); - vec.try_into().map_err(|_| { - AlgoKitTransactError::DecodingError( - "Expected 32 bytes but got a different length".to_string(), - ) +fn bytebuf_to_bytes(buf: &ByteBuf) -> Result<[u8; N], AlgoKitTransactError> { + buf.to_vec().try_into().map_err(|_| { + AlgoKitTransactError::DecodingError(format!( + "Expected {} bytes but got a different length", + N + )) }) } @@ -458,6 +490,7 @@ fn build_transaction( asset_transfer: Option, asset_config: Option, application_call: Option, + key_registration: Option, ) -> Result { Ok(Transaction { transaction_type, @@ -475,6 +508,7 @@ fn build_transaction( asset_transfer, asset_config, application_call, + key_registration, }) } @@ -492,6 +526,7 @@ pub fn get_encoded_transaction_type(bytes: &[u8]) -> Result Ok(TransactionType::AssetTransfer), algokit_transact::Transaction::AssetConfig(_) => Ok(TransactionType::AssetConfig), algokit_transact::Transaction::ApplicationCall(_) => Ok(TransactionType::ApplicationCall), + algokit_transact::Transaction::KeyRegistration(_) => Ok(TransactionType::KeyRegistration), } } diff --git a/crates/algokit_transact_ffi/src/transactions/asset_config.rs b/crates/algokit_transact_ffi/src/transactions/asset_config.rs index f983dabc..2de49186 100644 --- a/crates/algokit_transact_ffi/src/transactions/asset_config.rs +++ b/crates/algokit_transact_ffi/src/transactions/asset_config.rs @@ -146,7 +146,10 @@ impl TryFrom for algokit_transact::AssetConfigTransactionFields { let data = tx.clone().asset_config.unwrap(); let header: algokit_transact::TransactionHeader = tx.try_into()?; - let metadata_hash = data.metadata_hash.map(bytebuf_to_byte32).transpose()?; + let metadata_hash = data + .metadata_hash + .map(|buf| bytebuf_to_bytes::<32>(&buf)) + .transpose()?; let transaction_fields = algokit_transact::AssetConfigTransactionFields { header, @@ -166,7 +169,7 @@ impl TryFrom for algokit_transact::AssetConfigTransactionFields { transaction_fields.validate().map_err(|errors| { AlgoKitTransactError::DecodingError(format!( - "Asset configuration validation failed: {}", + "Asset config validation failed: {}", errors.join("\n") )) })?; diff --git a/crates/algokit_transact_ffi/src/transactions/keyreg.rs b/crates/algokit_transact_ffi/src/transactions/keyreg.rs new file mode 100644 index 00000000..3fa23848 --- /dev/null +++ b/crates/algokit_transact_ffi/src/transactions/keyreg.rs @@ -0,0 +1,94 @@ +//! Represents a key registration transaction. +//! +//! This module provides FFI-compatible structures and conversions for key registration +//! transactions that can be used across language bindings. +use crate::*; + +#[cfg(feature = "ffi_wasm")] +use tsify_next::Tsify; + +#[ffi_record] +pub struct KeyRegistrationTransactionFields { + /// Root participation public key (32 bytes) + pub vote_key: Option, + + /// VRF public key (32 bytes) + pub selection_key: Option, + + /// State proof key (64 bytes) + pub state_proof_key: Option, + + /// First round for which the participation key is valid + pub vote_first: Option, + + /// Last round for which the participation key is valid + pub vote_last: Option, + + /// Key dilution for the 2-level participation key + pub vote_key_dilution: Option, + + /// Mark account as non-reward earning + pub non_participation: Option, +} + +impl From for KeyRegistrationTransactionFields { + fn from(tx: algokit_transact::KeyRegistrationTransactionFields) -> Self { + Self { + vote_key: tx.vote_key.map(|bytes| ByteBuf::from(bytes.to_vec())), + selection_key: tx.selection_key.map(|bytes| ByteBuf::from(bytes.to_vec())), + state_proof_key: tx + .state_proof_key + .map(|bytes| ByteBuf::from(bytes.to_vec())), + vote_first: tx.vote_first, + vote_last: tx.vote_last, + vote_key_dilution: tx.vote_key_dilution, + non_participation: tx.non_participation, + } + } +} + +impl TryFrom for algokit_transact::KeyRegistrationTransactionFields { + type Error = AlgoKitTransactError; + + fn try_from(tx: crate::Transaction) -> Result { + if tx.transaction_type != crate::TransactionType::KeyRegistration + || tx.key_registration.is_none() + { + return Err(Self::Error::DecodingError( + "Key Registration data missing".to_string(), + )); + } + + let data = tx.clone().key_registration.unwrap(); + let header: algokit_transact::TransactionHeader = tx.try_into()?; + + let transaction_fields = algokit_transact::KeyRegistrationTransactionFields { + header, + vote_key: data + .vote_key + .map(|buf| bytebuf_to_bytes::<32>(&buf)) + .transpose()?, + selection_key: data + .selection_key + .map(|buf| bytebuf_to_bytes::<32>(&buf)) + .transpose()?, + state_proof_key: data + .state_proof_key + .map(|buf| bytebuf_to_bytes::<64>(&buf)) + .transpose()?, + vote_first: data.vote_first, + vote_last: data.vote_last, + vote_key_dilution: data.vote_key_dilution, + non_participation: data.non_participation, + }; + + transaction_fields.validate().map_err(|errors| { + AlgoKitTransactError::DecodingError(format!( + "Key registration validation failed: {}", + errors.join("\n") + )) + })?; + + Ok(transaction_fields) + } +} diff --git a/crates/algokit_transact_ffi/src/transactions/mod.rs b/crates/algokit_transact_ffi/src/transactions/mod.rs index 1fb3c6c5..ba9d980e 100644 --- a/crates/algokit_transact_ffi/src/transactions/mod.rs +++ b/crates/algokit_transact_ffi/src/transactions/mod.rs @@ -3,3 +3,6 @@ pub mod asset_config; pub use application_call::ApplicationCallTransactionFields; pub use asset_config::AssetConfigTransactionFields; + +pub mod keyreg; +pub use keyreg::KeyRegistrationTransactionFields; diff --git a/crates/algokit_transact_ffi/test_data.json b/crates/algokit_transact_ffi/test_data.json index 9d86b8d8..b8c59109 100644 --- a/crates/algokit_transact_ffi/test_data.json +++ b/crates/algokit_transact_ffi/test_data.json @@ -45187,6 +45187,2906 @@ 103 ] }, + "nonParticipationKeyRegistration": { + "id": "ACAP6ZGMGNTLUO3IQ26P22SRKYWTQQO3MF64GX7QO6NICDUFPM5A", + "idRaw": [ + 0, + 128, + 255, + 100, + 204, + 51, + 102, + 186, + 59, + 104, + 134, + 188, + 253, + 106, + 81, + 86, + 45, + 56, + 65, + 219, + 97, + 125, + 195, + 95, + 240, + 119, + 154, + 129, + 14, + 133, + 123, + 58 + ], + "rekeyedSenderAuthAddress": { + "address": "BKDYDIDVSZCP75JVCB76P3WBJRY6HWAIFDSEOKYHJY5WMNJ2UWJ65MYETU", + "pubKey": [ + 10, + 135, + 129, + 160, + 117, + 150, + 68, + 255, + 245, + 53, + 16, + 127, + 231, + 238, + 193, + 76, + 113, + 227, + 216, + 8, + 40, + 228, + 71, + 43, + 7, + 78, + 59, + 102, + 53, + 58, + 165, + 147 + ] + }, + "rekeyedSenderSignedBytes": [ + 131, + 164, + 115, + 103, + 110, + 114, + 196, + 32, + 10, + 135, + 129, + 160, + 117, + 150, + 68, + 255, + 245, + 53, + 16, + 127, + 231, + 238, + 193, + 76, + 113, + 227, + 216, + 8, + 40, + 228, + 71, + 43, + 7, + 78, + 59, + 102, + 53, + 58, + 165, + 147, + 163, + 115, + 105, + 103, + 196, + 64, + 82, + 219, + 125, + 199, + 43, + 23, + 55, + 250, + 27, + 182, + 198, + 240, + 29, + 21, + 227, + 132, + 188, + 92, + 117, + 134, + 111, + 97, + 2, + 28, + 162, + 119, + 40, + 118, + 206, + 98, + 71, + 85, + 161, + 26, + 57, + 205, + 43, + 50, + 227, + 199, + 221, + 180, + 51, + 61, + 126, + 226, + 104, + 247, + 160, + 149, + 223, + 68, + 192, + 149, + 96, + 199, + 233, + 4, + 140, + 3, + 203, + 84, + 242, + 3, + 163, + 116, + 120, + 110, + 135, + 163, + 102, + 101, + 101, + 205, + 3, + 232, + 162, + 102, + 118, + 206, + 0, + 50, + 175, + 200, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 0, + 50, + 179, + 176, + 167, + 110, + 111, + 110, + 112, + 97, + 114, + 116, + 195, + 163, + 115, + 110, + 100, + 196, + 32, + 229, + 25, + 125, + 21, + 89, + 246, + 253, + 82, + 47, + 252, + 180, + 125, + 236, + 245, + 242, + 141, + 70, + 45, + 64, + 7, + 241, + 162, + 247, + 232, + 115, + 172, + 130, + 247, + 128, + 97, + 30, + 52, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103 + ], + "signedBytes": [ + 130, + 163, + 115, + 105, + 103, + 196, + 64, + 82, + 219, + 125, + 199, + 43, + 23, + 55, + 250, + 27, + 182, + 198, + 240, + 29, + 21, + 227, + 132, + 188, + 92, + 117, + 134, + 111, + 97, + 2, + 28, + 162, + 119, + 40, + 118, + 206, + 98, + 71, + 85, + 161, + 26, + 57, + 205, + 43, + 50, + 227, + 199, + 221, + 180, + 51, + 61, + 126, + 226, + 104, + 247, + 160, + 149, + 223, + 68, + 192, + 149, + 96, + 199, + 233, + 4, + 140, + 3, + 203, + 84, + 242, + 3, + 163, + 116, + 120, + 110, + 135, + 163, + 102, + 101, + 101, + 205, + 3, + 232, + 162, + 102, + 118, + 206, + 0, + 50, + 175, + 200, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 0, + 50, + 179, + 176, + 167, + 110, + 111, + 110, + 112, + 97, + 114, + 116, + 195, + 163, + 115, + 110, + 100, + 196, + 32, + 229, + 25, + 125, + 21, + 89, + 246, + 253, + 82, + 47, + 252, + 180, + 125, + 236, + 245, + 242, + 141, + 70, + 45, + 64, + 7, + 241, + 162, + 247, + 232, + 115, + 172, + 130, + 247, + 128, + 97, + 30, + 52, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103 + ], + "signingPrivateKey": [ + 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 + ], + "transaction": { + "fee": 1000, + "firstValid": 3321800, + "genesisHash": [ + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34 + ], + "keyRegistration": { + "nonParticipation": true + }, + "lastValid": 3322800, + "sender": { + "address": "4UMX2FKZ636VEL74WR66Z5PSRVDC2QAH6GRPP2DTVSBPPADBDY2JB3PN2U", + "pubKey": [ + 229, + 25, + 125, + 21, + 89, + 246, + 253, + 82, + 47, + 252, + 180, + 125, + 236, + 245, + 242, + 141, + 70, + 45, + 64, + 7, + 241, + 162, + 247, + 232, + 115, + 172, + 130, + 247, + 128, + 97, + 30, + 52 + ] + }, + "transactionType": "KeyRegistration" + }, + "unsignedBytes": [ + 84, + 88, + 135, + 163, + 102, + 101, + 101, + 205, + 3, + 232, + 162, + 102, + 118, + 206, + 0, + 50, + 175, + 200, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 0, + 50, + 179, + 176, + 167, + 110, + 111, + 110, + 112, + 97, + 114, + 116, + 195, + 163, + 115, + 110, + 100, + 196, + 32, + 229, + 25, + 125, + 21, + 89, + 246, + 253, + 82, + 47, + 252, + 180, + 125, + 236, + 245, + 242, + 141, + 70, + 45, + 64, + 7, + 241, + 162, + 247, + 232, + 115, + 172, + 130, + 247, + 128, + 97, + 30, + 52, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103 + ] + }, + "offlineKeyRegistration": { + "id": "WAXJLC44RILOSYX73PJULCAWC43DNBU4AXMWHIRARXK4GO2LHEDQ", + "idRaw": [ + 176, + 46, + 149, + 139, + 156, + 138, + 22, + 233, + 98, + 255, + 219, + 211, + 69, + 136, + 22, + 23, + 54, + 54, + 134, + 156, + 5, + 217, + 99, + 162, + 32, + 141, + 213, + 195, + 59, + 75, + 57, + 7 + ], + "rekeyedSenderAuthAddress": { + "address": "BKDYDIDVSZCP75JVCB76P3WBJRY6HWAIFDSEOKYHJY5WMNJ2UWJ65MYETU", + "pubKey": [ + 10, + 135, + 129, + 160, + 117, + 150, + 68, + 255, + 245, + 53, + 16, + 127, + 231, + 238, + 193, + 76, + 113, + 227, + 216, + 8, + 40, + 228, + 71, + 43, + 7, + 78, + 59, + 102, + 53, + 58, + 165, + 147 + ] + }, + "rekeyedSenderSignedBytes": [ + 131, + 164, + 115, + 103, + 110, + 114, + 196, + 32, + 10, + 135, + 129, + 160, + 117, + 150, + 68, + 255, + 245, + 53, + 16, + 127, + 231, + 238, + 193, + 76, + 113, + 227, + 216, + 8, + 40, + 228, + 71, + 43, + 7, + 78, + 59, + 102, + 53, + 58, + 165, + 147, + 163, + 115, + 105, + 103, + 196, + 64, + 189, + 220, + 32, + 136, + 199, + 234, + 169, + 51, + 214, + 17, + 225, + 131, + 94, + 247, + 38, + 92, + 187, + 153, + 195, + 116, + 217, + 131, + 87, + 16, + 197, + 148, + 162, + 211, + 218, + 172, + 114, + 3, + 153, + 153, + 178, + 253, + 11, + 15, + 221, + 204, + 161, + 37, + 110, + 102, + 187, + 50, + 12, + 155, + 27, + 150, + 191, + 131, + 42, + 174, + 90, + 46, + 10, + 90, + 50, + 43, + 250, + 149, + 205, + 9, + 163, + 116, + 120, + 110, + 134, + 163, + 102, + 101, + 101, + 205, + 3, + 232, + 162, + 102, + 118, + 206, + 3, + 33, + 244, + 82, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 3, + 33, + 248, + 58, + 163, + 115, + 110, + 100, + 196, + 32, + 183, + 86, + 171, + 147, + 129, + 55, + 220, + 209, + 253, + 79, + 52, + 174, + 241, + 185, + 216, + 128, + 209, + 55, + 182, + 95, + 108, + 135, + 79, + 251, + 250, + 155, + 188, + 142, + 68, + 109, + 145, + 11, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103 + ], + "signedBytes": [ + 130, + 163, + 115, + 105, + 103, + 196, + 64, + 189, + 220, + 32, + 136, + 199, + 234, + 169, + 51, + 214, + 17, + 225, + 131, + 94, + 247, + 38, + 92, + 187, + 153, + 195, + 116, + 217, + 131, + 87, + 16, + 197, + 148, + 162, + 211, + 218, + 172, + 114, + 3, + 153, + 153, + 178, + 253, + 11, + 15, + 221, + 204, + 161, + 37, + 110, + 102, + 187, + 50, + 12, + 155, + 27, + 150, + 191, + 131, + 42, + 174, + 90, + 46, + 10, + 90, + 50, + 43, + 250, + 149, + 205, + 9, + 163, + 116, + 120, + 110, + 134, + 163, + 102, + 101, + 101, + 205, + 3, + 232, + 162, + 102, + 118, + 206, + 3, + 33, + 244, + 82, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 3, + 33, + 248, + 58, + 163, + 115, + 110, + 100, + 196, + 32, + 183, + 86, + 171, + 147, + 129, + 55, + 220, + 209, + 253, + 79, + 52, + 174, + 241, + 185, + 216, + 128, + 209, + 55, + 182, + 95, + 108, + 135, + 79, + 251, + 250, + 155, + 188, + 142, + 68, + 109, + 145, + 11, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103 + ], + "signingPrivateKey": [ + 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 + ], + "transaction": { + "fee": 1000, + "firstValid": 52556882, + "genesisHash": [ + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34 + ], + "keyRegistration": {}, + "lastValid": 52557882, + "sender": { + "address": "W5LKXE4BG7OND7KPGSXPDOOYQDITPNS7NSDU7672TO6I4RDNSEFWXRPISQ", + "pubKey": [ + 183, + 86, + 171, + 147, + 129, + 55, + 220, + 209, + 253, + 79, + 52, + 174, + 241, + 185, + 216, + 128, + 209, + 55, + 182, + 95, + 108, + 135, + 79, + 251, + 250, + 155, + 188, + 142, + 68, + 109, + 145, + 11 + ] + }, + "transactionType": "KeyRegistration" + }, + "unsignedBytes": [ + 84, + 88, + 134, + 163, + 102, + 101, + 101, + 205, + 3, + 232, + 162, + 102, + 118, + 206, + 3, + 33, + 244, + 82, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 3, + 33, + 248, + 58, + 163, + 115, + 110, + 100, + 196, + 32, + 183, + 86, + 171, + 147, + 129, + 55, + 220, + 209, + 253, + 79, + 52, + 174, + 241, + 185, + 216, + 128, + 209, + 55, + 182, + 95, + 108, + 135, + 79, + 251, + 250, + 155, + 188, + 142, + 68, + 109, + 145, + 11, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103 + ] + }, + "onlineKeyRegistration": { + "id": "UCWQQKWB3CMPVK6EU2ML7CN5IDYZJVVSVS3RXYEOLJUURX44SUKQ", + "idRaw": [ + 160, + 173, + 8, + 42, + 193, + 216, + 152, + 250, + 171, + 196, + 166, + 152, + 191, + 137, + 189, + 64, + 241, + 148, + 214, + 178, + 172, + 183, + 27, + 224, + 142, + 90, + 105, + 72, + 223, + 156, + 149, + 21 + ], + "rekeyedSenderAuthAddress": { + "address": "BKDYDIDVSZCP75JVCB76P3WBJRY6HWAIFDSEOKYHJY5WMNJ2UWJ65MYETU", + "pubKey": [ + 10, + 135, + 129, + 160, + 117, + 150, + 68, + 255, + 245, + 53, + 16, + 127, + 231, + 238, + 193, + 76, + 113, + 227, + 216, + 8, + 40, + 228, + 71, + 43, + 7, + 78, + 59, + 102, + 53, + 58, + 165, + 147 + ] + }, + "rekeyedSenderSignedBytes": [ + 131, + 164, + 115, + 103, + 110, + 114, + 196, + 32, + 10, + 135, + 129, + 160, + 117, + 150, + 68, + 255, + 245, + 53, + 16, + 127, + 231, + 238, + 193, + 76, + 113, + 227, + 216, + 8, + 40, + 228, + 71, + 43, + 7, + 78, + 59, + 102, + 53, + 58, + 165, + 147, + 163, + 115, + 105, + 103, + 196, + 64, + 20, + 166, + 252, + 4, + 86, + 193, + 231, + 220, + 171, + 119, + 139, + 25, + 206, + 212, + 40, + 150, + 27, + 230, + 32, + 71, + 87, + 45, + 14, + 68, + 99, + 44, + 36, + 190, + 155, + 232, + 11, + 159, + 241, + 72, + 208, + 164, + 157, + 34, + 29, + 16, + 32, + 72, + 77, + 247, + 225, + 144, + 96, + 250, + 110, + 200, + 51, + 169, + 194, + 205, + 250, + 38, + 92, + 191, + 82, + 247, + 239, + 161, + 180, + 4, + 163, + 116, + 120, + 110, + 140, + 163, + 102, + 101, + 101, + 206, + 0, + 30, + 132, + 128, + 162, + 102, + 118, + 206, + 3, + 45, + 27, + 200, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 3, + 45, + 31, + 176, + 166, + 115, + 101, + 108, + 107, + 101, + 121, + 196, + 32, + 166, + 47, + 46, + 216, + 120, + 87, + 123, + 170, + 129, + 228, + 130, + 12, + 77, + 41, + 246, + 188, + 168, + 150, + 144, + 56, + 76, + 8, + 233, + 53, + 27, + 97, + 183, + 163, + 38, + 158, + 74, + 80, + 163, + 115, + 110, + 100, + 196, + 32, + 122, + 129, + 42, + 29, + 41, + 249, + 192, + 177, + 248, + 55, + 10, + 28, + 184, + 78, + 37, + 56, + 31, + 227, + 151, + 83, + 6, + 33, + 253, + 240, + 155, + 215, + 74, + 97, + 196, + 62, + 241, + 9, + 167, + 115, + 112, + 114, + 102, + 107, + 101, + 121, + 196, + 64, + 250, + 29, + 21, + 206, + 160, + 201, + 32, + 225, + 26, + 97, + 54, + 130, + 24, + 54, + 76, + 87, + 72, + 217, + 41, + 14, + 18, + 134, + 197, + 107, + 135, + 44, + 142, + 108, + 235, + 190, + 179, + 124, + 133, + 215, + 234, + 11, + 167, + 102, + 248, + 151, + 245, + 134, + 12, + 90, + 117, + 250, + 67, + 155, + 85, + 92, + 168, + 53, + 192, + 152, + 202, + 225, + 53, + 228, + 235, + 50, + 2, + 22, + 25, + 198, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103, + 167, + 118, + 111, + 116, + 101, + 102, + 115, + 116, + 206, + 3, + 45, + 26, + 255, + 166, + 118, + 111, + 116, + 101, + 107, + 100, + 205, + 6, + 197, + 167, + 118, + 111, + 116, + 101, + 107, + 101, + 121, + 196, + 32, + 141, + 124, + 240, + 196, + 205, + 175, + 82, + 157, + 63, + 193, + 214, + 179, + 130, + 238, + 155, + 123, + 176, + 94, + 176, + 57, + 253, + 52, + 160, + 131, + 104, + 71, + 239, + 192, + 163, + 38, + 133, + 49, + 167, + 118, + 111, + 116, + 101, + 108, + 115, + 116, + 206, + 3, + 90, + 225, + 191 + ], + "signedBytes": [ + 130, + 163, + 115, + 105, + 103, + 196, + 64, + 20, + 166, + 252, + 4, + 86, + 193, + 231, + 220, + 171, + 119, + 139, + 25, + 206, + 212, + 40, + 150, + 27, + 230, + 32, + 71, + 87, + 45, + 14, + 68, + 99, + 44, + 36, + 190, + 155, + 232, + 11, + 159, + 241, + 72, + 208, + 164, + 157, + 34, + 29, + 16, + 32, + 72, + 77, + 247, + 225, + 144, + 96, + 250, + 110, + 200, + 51, + 169, + 194, + 205, + 250, + 38, + 92, + 191, + 82, + 247, + 239, + 161, + 180, + 4, + 163, + 116, + 120, + 110, + 140, + 163, + 102, + 101, + 101, + 206, + 0, + 30, + 132, + 128, + 162, + 102, + 118, + 206, + 3, + 45, + 27, + 200, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 3, + 45, + 31, + 176, + 166, + 115, + 101, + 108, + 107, + 101, + 121, + 196, + 32, + 166, + 47, + 46, + 216, + 120, + 87, + 123, + 170, + 129, + 228, + 130, + 12, + 77, + 41, + 246, + 188, + 168, + 150, + 144, + 56, + 76, + 8, + 233, + 53, + 27, + 97, + 183, + 163, + 38, + 158, + 74, + 80, + 163, + 115, + 110, + 100, + 196, + 32, + 122, + 129, + 42, + 29, + 41, + 249, + 192, + 177, + 248, + 55, + 10, + 28, + 184, + 78, + 37, + 56, + 31, + 227, + 151, + 83, + 6, + 33, + 253, + 240, + 155, + 215, + 74, + 97, + 196, + 62, + 241, + 9, + 167, + 115, + 112, + 114, + 102, + 107, + 101, + 121, + 196, + 64, + 250, + 29, + 21, + 206, + 160, + 201, + 32, + 225, + 26, + 97, + 54, + 130, + 24, + 54, + 76, + 87, + 72, + 217, + 41, + 14, + 18, + 134, + 197, + 107, + 135, + 44, + 142, + 108, + 235, + 190, + 179, + 124, + 133, + 215, + 234, + 11, + 167, + 102, + 248, + 151, + 245, + 134, + 12, + 90, + 117, + 250, + 67, + 155, + 85, + 92, + 168, + 53, + 192, + 152, + 202, + 225, + 53, + 228, + 235, + 50, + 2, + 22, + 25, + 198, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103, + 167, + 118, + 111, + 116, + 101, + 102, + 115, + 116, + 206, + 3, + 45, + 26, + 255, + 166, + 118, + 111, + 116, + 101, + 107, + 100, + 205, + 6, + 197, + 167, + 118, + 111, + 116, + 101, + 107, + 101, + 121, + 196, + 32, + 141, + 124, + 240, + 196, + 205, + 175, + 82, + 157, + 63, + 193, + 214, + 179, + 130, + 238, + 155, + 123, + 176, + 94, + 176, + 57, + 253, + 52, + 160, + 131, + 104, + 71, + 239, + 192, + 163, + 38, + 133, + 49, + 167, + 118, + 111, + 116, + 101, + 108, + 115, + 116, + 206, + 3, + 90, + 225, + 191 + ], + "signingPrivateKey": [ + 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 + ], + "transaction": { + "fee": 2000000, + "firstValid": 53287880, + "genesisHash": [ + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34 + ], + "keyRegistration": { + "selectionKey": [ + 166, + 47, + 46, + 216, + 120, + 87, + 123, + 170, + 129, + 228, + 130, + 12, + 77, + 41, + 246, + 188, + 168, + 150, + 144, + 56, + 76, + 8, + 233, + 53, + 27, + 97, + 183, + 163, + 38, + 158, + 74, + 80 + ], + "stateProofKey": [ + 250, + 29, + 21, + 206, + 160, + 201, + 32, + 225, + 26, + 97, + 54, + 130, + 24, + 54, + 76, + 87, + 72, + 217, + 41, + 14, + 18, + 134, + 197, + 107, + 135, + 44, + 142, + 108, + 235, + 190, + 179, + 124, + 133, + 215, + 234, + 11, + 167, + 102, + 248, + 151, + 245, + 134, + 12, + 90, + 117, + 250, + 67, + 155, + 85, + 92, + 168, + 53, + 192, + 152, + 202, + 225, + 53, + 228, + 235, + 50, + 2, + 22, + 25, + 198 + ], + "voteFirst": 53287679, + "voteKey": [ + 141, + 124, + 240, + 196, + 205, + 175, + 82, + 157, + 63, + 193, + 214, + 179, + 130, + 238, + 155, + 123, + 176, + 94, + 176, + 57, + 253, + 52, + 160, + 131, + 104, + 71, + 239, + 192, + 163, + 38, + 133, + 49 + ], + "voteKeyDilution": 1733, + "voteLast": 56287679 + }, + "lastValid": 53288880, + "sender": { + "address": "PKASUHJJ7HALD6BXBIOLQTRFHAP6HF2TAYQ734E325FGDRB66EE6MYQGTM", + "pubKey": [ + 122, + 129, + 42, + 29, + 41, + 249, + 192, + 177, + 248, + 55, + 10, + 28, + 184, + 78, + 37, + 56, + 31, + 227, + 151, + 83, + 6, + 33, + 253, + 240, + 155, + 215, + 74, + 97, + 196, + 62, + 241, + 9 + ] + }, + "transactionType": "KeyRegistration" + }, + "unsignedBytes": [ + 84, + 88, + 140, + 163, + 102, + 101, + 101, + 206, + 0, + 30, + 132, + 128, + 162, + 102, + 118, + 206, + 3, + 45, + 27, + 200, + 162, + 103, + 104, + 196, + 32, + 72, + 99, + 181, + 24, + 164, + 179, + 200, + 78, + 200, + 16, + 242, + 45, + 79, + 16, + 129, + 203, + 15, + 113, + 240, + 89, + 167, + 172, + 32, + 222, + 198, + 47, + 127, + 112, + 229, + 9, + 58, + 34, + 162, + 108, + 118, + 206, + 3, + 45, + 31, + 176, + 166, + 115, + 101, + 108, + 107, + 101, + 121, + 196, + 32, + 166, + 47, + 46, + 216, + 120, + 87, + 123, + 170, + 129, + 228, + 130, + 12, + 77, + 41, + 246, + 188, + 168, + 150, + 144, + 56, + 76, + 8, + 233, + 53, + 27, + 97, + 183, + 163, + 38, + 158, + 74, + 80, + 163, + 115, + 110, + 100, + 196, + 32, + 122, + 129, + 42, + 29, + 41, + 249, + 192, + 177, + 248, + 55, + 10, + 28, + 184, + 78, + 37, + 56, + 31, + 227, + 151, + 83, + 6, + 33, + 253, + 240, + 155, + 215, + 74, + 97, + 196, + 62, + 241, + 9, + 167, + 115, + 112, + 114, + 102, + 107, + 101, + 121, + 196, + 64, + 250, + 29, + 21, + 206, + 160, + 201, + 32, + 225, + 26, + 97, + 54, + 130, + 24, + 54, + 76, + 87, + 72, + 217, + 41, + 14, + 18, + 134, + 197, + 107, + 135, + 44, + 142, + 108, + 235, + 190, + 179, + 124, + 133, + 215, + 234, + 11, + 167, + 102, + 248, + 151, + 245, + 134, + 12, + 90, + 117, + 250, + 67, + 155, + 85, + 92, + 168, + 53, + 192, + 152, + 202, + 225, + 53, + 228, + 235, + 50, + 2, + 22, + 25, + 198, + 164, + 116, + 121, + 112, + 101, + 166, + 107, + 101, + 121, + 114, + 101, + 103, + 167, + 118, + 111, + 116, + 101, + 102, + 115, + 116, + 206, + 3, + 45, + 26, + 255, + 166, + 118, + 111, + 116, + 101, + 107, + 100, + 205, + 6, + 197, + 167, + 118, + 111, + 116, + 101, + 107, + 101, + 121, + 196, + 32, + 141, + 124, + 240, + 196, + 205, + 175, + 82, + 157, + 63, + 193, + 214, + 179, + 130, + 238, + 155, + 123, + 176, + 94, + 176, + 57, + 253, + 52, + 160, + 131, + 104, + 71, + 239, + 192, + 163, + 38, + 133, + 49, + 167, + 118, + 111, + 116, + 101, + 108, + 115, + 116, + 206, + 3, + 90, + 225, + 191 + ] + }, "optInAssetTransfer": { "id": "JIDBHDPLBASULQZFI4EY5FJWR6VQRMPPFSGYBKE2XKW65N3UQJXA", "idRaw": [ diff --git a/crates/algokit_transact_ffi/test_plan.md b/crates/algokit_transact_ffi/test_plan.md index a3615de9..b2ae2e04 100644 --- a/crates/algokit_transact_ffi/test_plan.md +++ b/crates/algokit_transact_ffi/test_plan.md @@ -1,5 +1,4 @@ # Polytest Test Plan - ## Test Suites ### Payment @@ -32,6 +31,12 @@ | --- | --- | | [Transaction Group Tests](#transaction-group-tests) | Tests that apply to collections of transactions | +### Key Registration + +| Name | Description | +| --- | --- | +| [Transaction Tests](#transaction-tests) | Tests that apply to all transaction types | + ## Test Groups ### Generic Transaction Tests diff --git a/packages/python/algokit_transact/tests/__init__.py b/packages/python/algokit_transact/tests/__init__.py index 678f30a4..c198aad2 100644 --- a/packages/python/algokit_transact/tests/__init__.py +++ b/packages/python/algokit_transact/tests/__init__.py @@ -10,6 +10,7 @@ AssetTransferTransactionFields, AssetConfigTransactionFields, ApplicationCallTransactionFields, + KeyRegistrationTransactionFields, OnApplicationComplete, StateSchema, ) @@ -39,6 +40,9 @@ class TestData: application_create: TransactionTestData application_update: TransactionTestData application_delete: TransactionTestData + online_key_registration: TransactionTestData + offline_key_registration: TransactionTestData + non_participation_key_registration: TransactionTestData def convert_values(obj: Any) -> Any: @@ -132,6 +136,11 @@ def create_transaction_test_data(test_data: dict[str, Any]) -> TransactionTestDa "field_name": "application_call", "field_class": ApplicationCallTransactionFields, }, + "KeyRegistration": { + "type": TransactionType.KEY_REGISTRATION, + "field_name": "key_registration", + "field_class": KeyRegistrationTransactionFields, + }, } # Get the transaction type configuration diff --git a/packages/python/algokit_transact/tests/test_key_registration.py b/packages/python/algokit_transact/tests/test_key_registration.py new file mode 100644 index 00000000..16328f7f --- /dev/null +++ b/packages/python/algokit_transact/tests/test_key_registration.py @@ -0,0 +1,127 @@ +import pytest + +from tests.transaction_asserts import ( + assert_assign_fee, + assert_decode_with_prefix, + assert_decode_without_prefix, + assert_encode, + assert_encode_with_auth_address, + assert_encode_with_signature, + assert_encoded_transaction_type, + assert_example, + assert_transaction_id, +) +from . import TEST_DATA + + +online_key_registration = TEST_DATA.online_key_registration +offline_key_registration = TEST_DATA.offline_key_registration +non_participation_key_registration = TEST_DATA.non_participation_key_registration + +# Polytest Suite: Key Registration + +# Polytest Group: Transaction Tests +txn_test_data = { + "online key registration": TEST_DATA.online_key_registration, + "offline key registration": TEST_DATA.offline_key_registration, + "non-participation key registration": TEST_DATA.non_participation_key_registration, +} + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_example(test_data): + """A human-readable example of forming a transaction and signing it""" + assert_example(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_get_transaction_id(test_data): + """A transaction id can be obtained from a transaction""" + assert_transaction_id(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_assign_fee(test_data): + """A fee can be calculated and assigned to a transaction""" + assert_assign_fee(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_get_encoded_transaction_type(test_data): + """The transaction type of an encoded transaction can be retrieved""" + assert_encoded_transaction_type(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_decode_without_prefix(test_data): + """A transaction without TX prefix and valid fields is decoded properly""" + assert_decode_without_prefix(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_decode_with_prefix(test_data): + """A transaction with TX prefix and valid fields is decoded properly""" + assert_decode_with_prefix(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_encode_with_auth_address(test_data): + """An auth address can be attached to a encoded transaction with a signature""" + assert_encode_with_auth_address(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_encode_with_signature(test_data): + """A signature can be attached to a encoded transaction""" + assert_encode_with_signature(test_data) + + +@pytest.mark.group_transaction_tests +@pytest.mark.parametrize( + "test_data", + txn_test_data.values(), + ids=txn_test_data.keys(), +) +def test_encode(test_data): + """A transaction with valid fields is encoded properly""" + assert_encode(test_data) diff --git a/packages/typescript/algokit_transact/__tests__/common.ts b/packages/typescript/algokit_transact/__tests__/common.ts index 805f2258..bef1debd 100644 --- a/packages/typescript/algokit_transact/__tests__/common.ts +++ b/packages/typescript/algokit_transact/__tests__/common.ts @@ -14,7 +14,7 @@ const defaultReviver = (key: string, value: unknown) => { if ( typeof value === "number" && - ["fee", "amount", "firstValid", "lastValid", "assetId", "total", "appId", "extraProgramPages", "numUints", "numByteSlices"].includes( + ["fee", "amount", "firstValid", "lastValid", "assetId", "total", "appId", "extraProgramPages", "numUints", "numByteSlices", "voteFirst", "voteLast", "voteKeyDilution"].includes( key, ) ) { @@ -39,18 +39,20 @@ export type TransactionTestData = { rekeyedSenderSignedBytes: Uint8Array; }; -export const testData = - parseJson< - Record< - | "simplePayment" - | "optInAssetTransfer" - | "assetCreate" - | "assetDestroy" - | "assetReconfigure" - | "applicationCall" - | "applicationCreate" - | "applicationUpdate" - | "applicationDelete", - TransactionTestData - > - >(jsonString); +export const testData = parseJson< + Record< + | "simplePayment" + | "optInAssetTransfer" + | "assetCreate" + | "assetDestroy" + | "assetReconfigure" + | "applicationCall" + | "applicationCreate" + | "applicationUpdate" + | "applicationDelete" + | "onlineKeyRegistration" + | "offlineKeyRegistration" + | "nonParticipationKeyRegistration", + TransactionTestData + > +>(jsonString); diff --git a/packages/typescript/algokit_transact/__tests__/key_registration.test.ts b/packages/typescript/algokit_transact/__tests__/key_registration.test.ts new file mode 100644 index 00000000..a608f312 --- /dev/null +++ b/packages/typescript/algokit_transact/__tests__/key_registration.test.ts @@ -0,0 +1,76 @@ +import { expect, test, describe } from "bun:test"; +import { testData } from "./common"; +import * as ed from "@noble/ed25519"; +import { + encodeTransaction, + decodeTransaction, + getEncodedTransactionType, + Transaction, + addressFromPubKey, + assignFee, + SignedTransaction, + encodeSignedTransaction, +} from ".."; +import { + assertAssignFee, + assertDecodeWithoutPrefix, + assertDecodeWithPrefix, + assertEncode, + assertEncodedTransactionType, + assertEncodeWithAuthAddress, + assertEncodeWithSignature, + assertExample, + assertTransactionId, +} from "./transaction_asserts"; + +const txnTestData = Object.entries({ + ["online key registration"]: testData.onlineKeyRegistration, + ["offline key registration"]: testData.offlineKeyRegistration, + ["non-participation key registration"]: testData.nonParticipationKeyRegistration, +}); + +describe("Key Registration", () => { + // Polytest Suite: Key Registration + + describe("Transaction Tests", () => { + // Polytest Group: Transaction Tests + + for (const [label, testData] of txnTestData) { + test("example", async () => { + await assertExample(label, testData); + }); + + test("get transaction id", () => { + assertTransactionId(label, testData); + }); + + test("assign fee", () => { + assertAssignFee(label, testData); + }); + + test("get encoded transaction type", () => { + assertEncodedTransactionType(label, testData); + }); + + test("decode without prefix", () => { + assertDecodeWithoutPrefix(label, testData); + }); + + test("decode with prefix", () => { + assertDecodeWithPrefix(label, testData); + }); + + test("encode with auth address", async () => { + await assertEncodeWithAuthAddress(label, testData); + }); + + test("encode with signature", async () => { + await assertEncodeWithSignature(label, testData); + }); + + test("encode", () => { + assertEncode(label, testData); + }); + } + }); +});