diff --git a/Cargo.lock b/Cargo.lock index a62c4561c..7b5a28682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,7 @@ dependencies = [ "futures", "hex", "indexer_client", + "insta", "lazy_static", "log", "num-bigint", @@ -1862,6 +1863,8 @@ checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ "console", "once_cell", + "pest", + "pest_derive", "serde", "similar", ] diff --git a/crates/algokit_abi/src/method.rs b/crates/algokit_abi/src/abi_method.rs similarity index 92% rename from crates/algokit_abi/src/method.rs rename to crates/algokit_abi/src/abi_method.rs index 9fee36721..7ce648f96 100644 --- a/crates/algokit_abi/src/method.rs +++ b/crates/algokit_abi/src/abi_method.rs @@ -1,13 +1,12 @@ +use crate::DefaultValueSource; use crate::abi_type::ABIType; use crate::abi_value::ABIValue; +use crate::constants::VOID_RETURN_TYPE; use crate::error::ABIError; use sha2::{Digest, Sha512_256}; use std::fmt::Display; use std::str::FromStr; -/// Constant for void return type in method signatures. -const VOID_RETURN_TYPE: &str = "void"; - /// Represents a transaction type that can be used as an ABI method argument. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ABITransactionType { @@ -158,7 +157,7 @@ impl FromStr for ABIMethodArgType { } /// Represents a parsed ABI method, including its name, arguments, and return type. -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone)] pub struct ABIMethod { /// The name of the method. pub name: String, @@ -170,57 +169,7 @@ pub struct ABIMethod { pub description: Option, } -/// Represents an argument in an ABI method. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ABIMethodArg { - /// The type of the argument. - pub arg_type: ABIMethodArgType, - /// An optional name for the argument. - pub name: Option, - /// An optional description of the argument. - pub description: Option, -} - impl ABIMethod { - /// Creates a new ABI method. - pub fn new( - name: String, - args: Vec, - returns: Option, - description: Option, - ) -> Self { - Self { - name, - args, - returns, - description, - } - } - - /// Returns the number of transaction arguments in the method. - pub fn transaction_arg_count(&self) -> usize { - self.args - .iter() - .filter(|arg| arg.arg_type.is_transaction()) - .count() - } - - /// Returns the number of reference arguments in the method. - pub fn reference_arg_count(&self) -> usize { - self.args - .iter() - .filter(|arg| arg.arg_type.is_reference()) - .count() - } - - /// Returns the number of value-type arguments in the method. - pub fn value_arg_count(&self) -> usize { - self.args - .iter() - .filter(|arg| arg.arg_type.is_value_type()) - .count() - } - /// Returns the method selector, which is the first 4 bytes of the SHA-512/256 hash of the method signature. pub fn selector(&self) -> Result, ABIError> { let signature = self.signature()?; @@ -279,6 +228,71 @@ impl ABIMethod { } } +/// Default value information for ABI method arguments. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ABIDefaultValue { + /// Base64 encoded bytes, base64 ARC4 encoded uint64, or UTF-8 method selector + pub data: String, + /// Where the default value is coming from + pub source: DefaultValueSource, + /// How the data is encoded. This is the encoding for the data provided here, not the arg type + pub value_type: Option, +} + +/// Represents an argument in an ABI method. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ABIMethodArg { + /// The type of the argument. + pub arg_type: ABIMethodArgType, + /// An optional name for the argument. + pub name: Option, + /// An optional description of the argument. + pub description: Option, + /// An optional default value for the argument. + pub default_value: Option, +} + +impl ABIMethod { + /// Creates a new ABI method. + pub fn new( + name: String, + args: Vec, + returns: Option, + description: Option, + ) -> Self { + Self { + name, + args, + returns, + description, + } + } + + /// Returns the number of transaction arguments in the method. + pub fn transaction_arg_count(&self) -> usize { + self.args + .iter() + .filter(|arg| arg.arg_type.is_transaction()) + .count() + } + + /// Returns the number of reference arguments in the method. + pub fn reference_arg_count(&self) -> usize { + self.args + .iter() + .filter(|arg| arg.arg_type.is_reference()) + .count() + } + + /// Returns the number of value-type arguments in the method. + pub fn value_arg_count(&self) -> usize { + self.args + .iter() + .filter(|arg| arg.arg_type.is_value_type()) + .count() + } +} + impl FromStr for ABIMethod { type Err = ABIError; @@ -323,7 +337,7 @@ impl FromStr for ABIMethod { for (i, arg_type) in arguments.iter().enumerate() { let _type = ABIMethodArgType::from_str(arg_type)?; let arg_name = Some(format!("arg{}", i)); - let arg = ABIMethodArg::new(_type, arg_name, None); + let arg = ABIMethodArg::new(_type, arg_name, None, None); args.push(arg); } @@ -347,11 +361,13 @@ impl ABIMethodArg { arg_type: ABIMethodArgType, name: Option, description: Option, + default_value: Option, ) -> Self { Self { arg_type, name, description, + default_value, } } } @@ -424,7 +440,6 @@ fn split_arguments_by_comma(args_str: &str) -> Result, ABIError> { mod tests { use super::*; use crate::abi_type::parse_tuple_content; - use hex; use rstest::rstest; // Transaction type parsing with round-trip validation @@ -517,19 +532,6 @@ mod tests { assert!(ABIMethod::from_str(signature).is_err()); } - // Method selector verification - critical for hash correctness - #[rstest] - #[case("add(uint64,uint64)uint64", "fe6bdf69")] - #[case("optIn()void", "29314d95")] - #[case("deposit(pay,uint64)void", "f2355b55")] - #[case("bootstrap(pay,pay,application)void", "895c2a3b")] - fn method_selector(#[case] signature: &str, #[case] expected_hex: &str) { - let method = ABIMethod::from_str(signature).unwrap(); - let selector = method.selector().unwrap(); - assert_eq!(hex::encode(&selector), expected_hex); - assert_eq!(selector.len(), 4); - } - // ARC-4 tuple parsing - essential cases #[rstest] #[case("uint64,string,bool", vec!["uint64", "string", "bool"])] @@ -549,15 +551,6 @@ mod tests { assert!(parse_tuple_content(input).is_err()); } - // Signature round-trip - #[rstest] - #[case("add(uint64,uint64)uint64")] - #[case("optIn()void")] - fn signature_round_trip(#[case] signature: &str) { - let method = ABIMethod::from_str(signature).unwrap(); - assert_eq!(method.signature().unwrap(), signature); - } - // Method argument type predicates #[test] fn method_arg_type_predicates() { @@ -569,17 +562,4 @@ mod tests { assert!(!ref_arg.is_transaction() && ref_arg.is_reference() && !ref_arg.is_value_type()); assert!(!val_arg.is_transaction() && !val_arg.is_reference() && val_arg.is_value_type()); } - - // Edge cases - #[test] - fn empty_method_name_error() { - let method = ABIMethod::new("".to_string(), vec![], None, None); - assert!(method.signature().is_err()); - } - - #[test] - fn selector_length() { - let method = ABIMethod::new("test".to_string(), vec![], None, None); - assert_eq!(method.selector().unwrap().len(), 4); - } } diff --git a/crates/algokit_abi/src/abi_type.rs b/crates/algokit_abi/src/abi_type.rs index 424f429bd..bdd451008 100644 --- a/crates/algokit_abi/src/abi_type.rs +++ b/crates/algokit_abi/src/abi_type.rs @@ -1,12 +1,13 @@ use crate::{ - ABIError, ABIValue, + ABIError, ABIValue, StructField, constants::{ ALGORAND_PUBLIC_KEY_BYTE_LENGTH, BITS_PER_BYTE, MAX_BIT_SIZE, MAX_PRECISION, STATIC_ARRAY_REGEX, UFIXED_REGEX, }, - types::collections::tuple::find_bool_sequence_end, + types::collections::{r#struct::ABIStruct, tuple::find_bool_sequence_end}, }; use std::{ + collections::HashMap, fmt::{Display, Formatter, Result as FmtResult}, str::FromStr, }; @@ -98,6 +99,14 @@ pub enum ABIType { StaticArray(Box, usize), /// A dynamic-length array of another ABI type. DynamicArray(Box), + /// A struct type with named fields. + Struct(ABIStruct), + /// Raw byteslice without the length prefix that is specified in ARC-4. + AVMBytes, + /// A utf-8 string without the length prefix that is specified in ARC-4. + AVMString, + /// A 64-bit unsigned integer. + AVMUint64, } impl AsRef for ABIType { @@ -125,6 +134,10 @@ impl ABIType { ABIType::String => self.encode_string(value), ABIType::Byte => self.encode_byte(value), ABIType::Bool => self.encode_bool(value), + ABIType::Struct(struct_type) => struct_type.encode(value), + ABIType::AVMBytes => self.encode_avm_bytes(value), + ABIType::AVMString => self.encode_avm_string(value), + ABIType::AVMUint64 => self.encode_avm_uint64(value), } } @@ -146,6 +159,10 @@ impl ABIType { ABIType::Tuple(_) => self.decode_tuple(bytes), ABIType::StaticArray(_, _size) => self.decode_static_array(bytes), ABIType::DynamicArray(_) => self.decode_dynamic_array(bytes), + ABIType::Struct(struct_type) => struct_type.decode(bytes), + ABIType::AVMBytes => self.decode_avm_bytes(bytes), + ABIType::AVMString => self.decode_avm_string(bytes), + ABIType::AVMUint64 => self.decode_avm_uint64(bytes), } } @@ -153,7 +170,10 @@ impl ABIType { match self { ABIType::StaticArray(child_type, _) => child_type.is_dynamic(), ABIType::Tuple(child_types) => child_types.iter().any(|t| t.is_dynamic()), - ABIType::DynamicArray(_) | ABIType::String => true, + ABIType::DynamicArray(_) | ABIType::String | ABIType::AVMBytes | ABIType::AVMString => { + true + } + ABIType::Struct(struct_type) => struct_type.to_tuple_type().is_dynamic(), _ => false, } } @@ -165,6 +185,7 @@ impl ABIType { ABIType::Address => Ok(ALGORAND_PUBLIC_KEY_BYTE_LENGTH), ABIType::Bool => Ok(1), ABIType::Byte => Ok(1), + ABIType::AVMUint64 => Ok(8), ABIType::StaticArray(child_type, size) => match child_type.as_ref() { ABIType::Bool => Ok((*size).div_ceil(BITS_PER_BYTE as usize)), _ => Ok(Self::get_size(child_type)? * *size), @@ -190,14 +211,32 @@ impl ABIType { } Ok(size) } + ABIType::Struct(struct_type) => { + let tuple_type = struct_type.to_tuple_type(); + Self::get_size(&tuple_type) + } ABIType::String => Err(ABIError::DecodingError { message: format!("Failed to get size, {} is a dynamic type", abi_type), }), ABIType::DynamicArray(_) => Err(ABIError::DecodingError { message: format!("Failed to get size, {} is a dynamic type", abi_type), }), + ABIType::AVMBytes => Err(ABIError::DecodingError { + message: format!("Failed to get size, {} is a dynamic type", abi_type), + }), + ABIType::AVMString => Err(ABIError::DecodingError { + message: format!("Failed to get size, {} is a dynamic type", abi_type), + }), } } + + pub(crate) fn from_struct( + struct_name: &str, + structs: &HashMap>, + ) -> Result { + let struct_type = ABIStruct::get_abi_struct_type(struct_name, structs)?; + Ok(Self::Struct(struct_type)) + } } impl Display for ABIType { @@ -221,6 +260,10 @@ impl Display for ABIType { ABIType::DynamicArray(child_type) => { write!(f, "{}[]", child_type) } + ABIType::Struct(struct_type) => write!(f, "{}", struct_type), // TODO: test this to make sure the method selector is correct + ABIType::AVMBytes => write!(f, "AVMBytes"), + ABIType::AVMString => write!(f, "AVMString"), + ABIType::AVMUint64 => write!(f, "AVMUint64"), } } } @@ -321,6 +364,9 @@ impl FromStr for ABIType { "bool" => Ok(ABIType::Bool), "address" => Ok(ABIType::Address), "string" => Ok(ABIType::String), + "AVMBytes" => Ok(ABIType::AVMBytes), + "AVMString" => Ok(ABIType::AVMString), + "AVMUint64" => Ok(ABIType::AVMUint64), _ => Err(ABIError::ValidationError { message: format!("Cannot convert string '{}' to an ABI type", s), }), diff --git a/crates/algokit_abi/src/abi_value.rs b/crates/algokit_abi/src/abi_value.rs index 9faa39d09..c7456825a 100644 --- a/crates/algokit_abi/src/abi_value.rs +++ b/crates/algokit_abi/src/abi_value.rs @@ -1,7 +1,9 @@ use num_bigint::BigUint; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; /// Represents a value that can be encoded or decoded as an ABI type. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ABIValue { /// A boolean value. Bool(bool), @@ -15,6 +17,10 @@ pub enum ABIValue { Array(Vec), /// An Algorand address. Address(String), + /// Raw bytes. + Bytes(Vec), + /// A struct value represented as a key-value map. + Struct(HashMap), } impl From for ABIValue { @@ -83,6 +89,12 @@ impl From> for ABIValue { } } +impl From> for ABIValue { + fn from(value: HashMap) -> Self { + ABIValue::Struct(value) + } +} + impl ABIValue { /// Create an ABIValue::Byte from a u8 value pub fn from_byte(value: u8) -> Self { @@ -93,6 +105,53 @@ impl ABIValue { pub fn from_address>(value: S) -> Self { ABIValue::Address(value.into()) } + + /// Create an ABIValue::Struct from a HashMap + pub fn from_struct(value: HashMap) -> Self { + ABIValue::Struct(value) + } +} + +impl Hash for ABIValue { + fn hash(&self, state: &mut H) { + match self { + ABIValue::Bool(b) => { + 0u8.hash(state); + b.hash(state); + } + ABIValue::Uint(u) => { + 1u8.hash(state); + u.to_bytes_be().hash(state); + } + ABIValue::String(s) => { + 2u8.hash(state); + s.hash(state); + } + ABIValue::Byte(b) => { + 3u8.hash(state); + b.hash(state); + } + ABIValue::Array(arr) => { + 4u8.hash(state); + arr.hash(state); + } + ABIValue::Address(addr) => { + 5u8.hash(state); + addr.hash(state); + } + ABIValue::Bytes(bytes) => { + 6u8.hash(state); + bytes.hash(state); + } + ABIValue::Struct(map) => { + 7u8.hash(state); + // For HashMap, we need to hash in a consistent order + let mut pairs: Vec<_> = map.iter().collect(); + pairs.sort_by_key(|(k, _)| *k); + pairs.hash(state); + } + } + } } #[cfg(test)] @@ -172,4 +231,18 @@ mod tests { let value = ABIValue::from_address(addr_str); assert_eq!(value, ABIValue::Address(addr_str.to_string())); } + + #[test] + fn test_from_struct() { + let mut struct_map = HashMap::new(); + struct_map.insert("name".to_string(), ABIValue::String("Alice".to_string())); + struct_map.insert("age".to_string(), ABIValue::Uint(BigUint::from(30u32))); + + let value = ABIValue::from_struct(struct_map.clone()); + assert_eq!(value, ABIValue::Struct(struct_map.clone())); + + // Test with From trait + let value2 = ABIValue::from(struct_map.clone()); + assert_eq!(value2, ABIValue::Struct(struct_map)); + } } diff --git a/crates/algokit_abi/src/arc56_contract.rs b/crates/algokit_abi/src/arc56_contract.rs index 2eda68b96..1d755e513 100644 --- a/crates/algokit_abi/src/arc56_contract.rs +++ b/crates/algokit_abi/src/arc56_contract.rs @@ -1,9 +1,9 @@ +use crate::abi_method::{ABIDefaultValue, ABIMethod, ABIMethodArg, ABIMethodArgType}; use crate::abi_type::ABIType; +use crate::constants::VOID_RETURN_TYPE; use crate::error::ABIError; -use crate::method::{ABIMethod, ABIMethodArg, ABIMethodArgType}; use base64::{Engine as _, engine::general_purpose}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::HashMap; use std::str::FromStr; @@ -192,6 +192,24 @@ pub struct EventArg { pub struct_name: Option, } +/// Describes a single key in app storage with parsed ABI types. +#[derive(Debug, Clone)] +pub struct ABIStorageKey { + pub key: String, + pub key_type: ABIType, + pub value_type: ABIType, + pub desc: Option, +} + +/// Describes a storage map with parsed ABI types. +#[derive(Debug, Clone)] +pub struct ABIStorageMap { + pub key_type: ABIType, + pub value_type: ABIType, + pub desc: Option, + pub prefix: Option, +} + /// ARC-28 events are described using an extension of the original interface. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Event { @@ -212,7 +230,7 @@ pub struct Actions { } /// Source of default value -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum DefaultValueSource { Box, @@ -429,56 +447,17 @@ pub struct Method { } impl Method { - /// Get the ABI signature for this method - pub fn get_signature(&self) -> Result { - self.to_abi_method()?.signature() - } - - /// Convert to ABIMethod - pub fn to_abi_method(&self) -> Result { - let args: Result, ABIError> = self + pub fn signature(&self) -> Result { + let args_str = self .args .iter() - .map(|arg| { - let arg_type = ABIMethodArgType::from_str(&arg.arg_type)?; - Ok(ABIMethodArg::new( - arg_type, - arg.name.clone(), - arg.desc.clone(), - )) - }) - .collect(); - - let returns = if self.returns.return_type == "void" { - None - } else { - Some(ABIType::from_str(&self.returns.return_type)?) - }; - - Ok(ABIMethod::new( - self.name.clone(), - args?, - returns, - self.desc.clone(), - )) - } -} - -// Allow direct fallible conversion from a ARC-0056 Method reference to an ABIMethod -impl TryFrom<&Method> for ABIMethod { - type Error = ABIError; - - fn try_from(value: &Method) -> Result { - value.to_abi_method() - } -} + .map(|arg| arg.arg_type.as_str()) + .collect::>() + .join(","); -// Also support owned conversion to avoid extra clones when possible -impl TryFrom for ABIMethod { - type Error = ABIError; + let signature = format!("{}({}){}", self.name, args_str, self.returns.return_type); - fn try_from(value: Method) -> Result { - value.to_abi_method() + Ok(signature) } } @@ -558,7 +537,7 @@ impl Arc56Contract { } /// Get a method by name or signature - pub fn get_arc56_method(&self, method_name_or_signature: &str) -> Result<&Method, ABIError> { + pub fn get_method(&self, method_name_or_signature: &str) -> Result<&Method, ABIError> { if !method_name_or_signature.contains('(') { // Filter by method name let methods: Vec<&Method> = self @@ -578,7 +557,7 @@ impl Arc56Contract { if methods.len() > 1 { let signatures: Result, ABIError> = - methods.iter().map(|m| m.get_signature()).collect(); + methods.iter().map(|m| m.signature()).collect(); let signatures = signatures?; return Err(ABIError::ValidationError { message: format!( @@ -597,7 +576,7 @@ impl Arc56Contract { self.methods .iter() .find(|m| { - m.get_signature() + m.signature() .is_ok_and(|sig| sig == method_name_or_signature) }) .ok_or_else(|| ABIError::ValidationError { @@ -609,44 +588,215 @@ impl Arc56Contract { } } - /// Get ABI struct from ABI tuple - pub fn get_abi_struct_from_abi_tuple( - decoded_tuple: &[Value], - struct_fields: &[StructField], - structs: &HashMap>, - ) -> HashMap { - let mut result = HashMap::new(); - - for (i, field) in struct_fields.iter().enumerate() { - let key = field.name.clone(); - let mut value = decoded_tuple.get(i).cloned().unwrap_or(Value::Null); - - match &field.field_type { - StructFieldType::Value(type_name) => { - if let Some(nested_fields) = structs.get(type_name) { - if let Some(arr) = value.as_array() { - value = Value::Object( - Self::get_abi_struct_from_abi_tuple(arr, nested_fields, structs) - .into_iter() - .collect(), - ); - } - } - } - StructFieldType::Nested(nested_fields) => { - if let Some(arr) = value.as_array() { - value = Value::Object( - Self::get_abi_struct_from_abi_tuple(arr, nested_fields, structs) - .into_iter() - .collect(), - ); - } - } - } + /// Build an ABIMethod from an ARC-56 Method + fn to_abi_method(&self, method: &Method) -> Result { + // Resolve argument types + let args: Result, ABIError> = method + .args + .iter() + .map(|arg| { + let arg_type = self.resolve_method_arg_type(arg)?; + let default_value = self.resolve_default_value(&arg.default_value)?; + + Ok(ABIMethodArg::new( + arg_type, + arg.name.clone(), + arg.desc.clone(), + default_value, + )) + }) + .collect(); + + // Resolve return type + let returns = if method.returns.return_type == VOID_RETURN_TYPE { + None + } else if let Some(struct_name) = &method.returns.struct_name { + Some(ABIType::from_struct(struct_name, &self.structs)?) + } else { + Some(ABIType::from_str(&method.returns.return_type)?) + }; + + Ok(ABIMethod::new( + method.name.clone(), + args?, + returns, + method.desc.clone(), + )) + } - result.insert(key, value); + fn resolve_method_arg_type(&self, arg: &MethodArg) -> Result { + if let Some(struct_name) = &arg.struct_name { + let abi_type = ABIType::from_struct(struct_name, &self.structs)?; + return Ok(ABIMethodArgType::Value(abi_type)); } - result + ABIMethodArgType::from_str(&arg.arg_type) + } + + fn resolve_default_value( + &self, + default_value: &Option, + ) -> Result, ABIError> { + let resolved_default_value = if let Some(default_value) = default_value { + let resolved_value_type = if let Some(ref value_type) = default_value.value_type { + let abi_type = if self.structs.contains_key(value_type) { + ABIType::from_struct(value_type, &self.structs) + } else { + ABIType::from_str(value_type) + }?; + Some(abi_type) + } else { + None + }; + + Some(ABIDefaultValue { + data: default_value.data.clone(), + source: default_value.source.clone(), + value_type: resolved_value_type, + }) + } else { + None + }; + + Ok(resolved_default_value) + } + + /// Get a method by name or signature and convert to ABIMethod + pub fn find_abi_method(&self, method_name_or_signature: &str) -> Result { + let arc56_method = self.get_method(method_name_or_signature)?; + self.to_abi_method(arc56_method) + } + + pub fn get_global_abi_storage_key(&self, key_name: &str) -> Result { + let storage_key = self.state.keys.global_state.get(key_name).ok_or_else(|| { + ABIError::ValidationError { + message: format!( + "Global storage key '{}' not found in contract '{}'", + key_name, self.name + ), + } + })?; + self.convert_storage_key(storage_key) + } + + pub fn get_local_abi_storage_key(&self, key_name: &str) -> Result { + let storage_key = + self.state + .keys + .local_state + .get(key_name) + .ok_or_else(|| ABIError::ValidationError { + message: format!( + "Local storage key '{}' not found in contract '{}'", + key_name, self.name + ), + })?; + self.convert_storage_key(storage_key) + } + + pub fn get_global_abi_storage_keys(&self) -> Result, ABIError> { + self.state + .keys + .global_state + .iter() + .map(|(name, storage_key)| { + let abi_storage_key = self.convert_storage_key(storage_key)?; + Ok((name.clone(), abi_storage_key)) + }) + .collect() + } + + pub fn get_local_abi_storage_keys(&self) -> Result, ABIError> { + self.state + .keys + .local_state + .iter() + .map(|(name, storage_key)| { + let abi_storage_key = self.convert_storage_key(storage_key)?; + Ok((name.clone(), abi_storage_key)) + }) + .collect() + } + + pub fn get_box_abi_storage_keys(&self) -> Result, ABIError> { + self.state + .keys + .box_keys + .iter() + .map(|(name, storage_key)| { + let abi_storage_key = self.convert_storage_key(storage_key)?; + Ok((name.clone(), abi_storage_key)) + }) + .collect() + } + + fn convert_storage_key(&self, storage_key: &StorageKey) -> Result { + let key_type = self.resolve_storage_type(&storage_key.key_type)?; + let value_type = self.resolve_storage_type(&storage_key.value_type)?; + + Ok(ABIStorageKey { + key: storage_key.key.clone(), + key_type, + value_type, + desc: storage_key.desc.clone(), + }) + } + + fn convert_storage_map(&self, storage_map: &StorageMap) -> Result { + let key_type = self.resolve_storage_type(&storage_map.key_type)?; + let value_type = self.resolve_storage_type(&storage_map.value_type)?; + + Ok(ABIStorageMap { + key_type, + value_type, + desc: storage_map.desc.clone(), + prefix: storage_map.prefix.clone(), + }) + } + + pub fn get_global_abi_storage_maps(&self) -> Result, ABIError> { + self.state + .maps + .global_state + .iter() + .map(|(name, storage_map)| { + let abi_storage_map = self.convert_storage_map(storage_map)?; + Ok((name.clone(), abi_storage_map)) + }) + .collect() + } + + pub fn get_local_abi_storage_maps(&self) -> Result, ABIError> { + self.state + .maps + .local_state + .iter() + .map(|(name, storage_map)| { + let abi_storage_map = self.convert_storage_map(storage_map)?; + Ok((name.clone(), abi_storage_map)) + }) + .collect() + } + + pub fn get_box_abi_storage_maps(&self) -> Result, ABIError> { + self.state + .maps + .box_maps + .iter() + .map(|(name, storage_map)| { + let abi_storage_map = self.convert_storage_map(storage_map)?; + Ok((name.clone(), abi_storage_map)) + }) + .collect() + } + + fn resolve_storage_type(&self, type_str: &str) -> Result { + if self.structs.contains_key(type_str) { + ABIType::from_struct(type_str, &self.structs) + } else { + ABIType::from_str(type_str).map_err(|e| ABIError::ValidationError { + message: format!("Failed to parse storage type '{}': {}", type_str, e), + }) + } } } diff --git a/crates/algokit_abi/src/constants.rs b/crates/algokit_abi/src/constants.rs index 5414da84d..559f02cf1 100644 --- a/crates/algokit_abi/src/constants.rs +++ b/crates/algokit_abi/src/constants.rs @@ -26,3 +26,6 @@ pub static STATIC_ARRAY_REGEX: LazyLock = LazyLock::new(|| { pub static UFIXED_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^ufixed([1-9][\d]*)x([1-9][\d]*)$").expect("Invalid ufixed regex") }); + +/// Constant for void return type in method signatures. +pub const VOID_RETURN_TYPE: &str = "void"; diff --git a/crates/algokit_abi/src/lib.rs b/crates/algokit_abi/src/lib.rs index b6a13b658..d2836e81c 100644 --- a/crates/algokit_abi/src/lib.rs +++ b/crates/algokit_abi/src/lib.rs @@ -1,10 +1,10 @@ //! A library for encoding and decoding Algorand ABI types as defined in [ARC-4](https://arc.algorand.foundation/ARCs/arc-0004). +pub mod abi_method; pub mod abi_type; pub mod abi_value; pub mod arc56_contract; pub mod constants; pub mod error; -pub mod method; pub mod types; pub mod utils; @@ -13,7 +13,7 @@ pub use abi_value::ABIValue; pub use arc56_contract::*; pub use error::ABIError; -pub use method::{ +pub use abi_method::{ ABIMethod, ABIMethodArg, ABIMethodArgType, ABIReferenceType, ABIReferenceValue, ABIReturn, ABITransactionType, }; diff --git a/crates/algokit_abi/src/types/collections/mod.rs b/crates/algokit_abi/src/types/collections/mod.rs index bfc1e56c9..af0c2c0fc 100644 --- a/crates/algokit_abi/src/types/collections/mod.rs +++ b/crates/algokit_abi/src/types/collections/mod.rs @@ -1,3 +1,6 @@ pub mod array_dynamic; pub mod array_static; +pub mod r#struct; pub mod tuple; + +pub use r#struct::*; diff --git a/crates/algokit_abi/src/types/collections/struct.rs b/crates/algokit_abi/src/types/collections/struct.rs new file mode 100644 index 000000000..c064b4e12 --- /dev/null +++ b/crates/algokit_abi/src/types/collections/struct.rs @@ -0,0 +1,397 @@ +use crate::arc56_contract::{ + StructField as Arc56StructField, StructFieldType as Arc56StructFieldType, +}; +use crate::{ABIError, ABIType, ABIValue}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::str::FromStr; + +/// Represents an ABI struct type with named fields +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ABIStruct { + /// The name of the struct type + pub name: String, + /// The fields of the struct in order + pub fields: Vec, +} + +/// Represents the type of a struct field +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StructFieldType { + Type(ABIType), + Fields(Vec), +} + +/// Represents a field in a struct +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StructField { + pub name: String, + pub field_type: StructFieldType, +} + +impl ABIStruct { + pub(crate) fn get_abi_struct_type( + struct_name: &str, + structs: &HashMap>, + ) -> Result { + let arc56_fields = structs + .get(struct_name) + .ok_or_else(|| ABIError::ValidationError { + message: format!("Struct '{}' not found in ARC-56 definition", struct_name), + })?; + + let mut fields = Vec::new(); + for arc56_field in arc56_fields { + let field_type = Self::resolve_field_type(&arc56_field.field_type, structs)?; + fields.push(StructField { + name: arc56_field.name.clone(), + field_type, + }); + } + + Ok(Self { + name: struct_name.to_string(), + fields, + }) + } + + fn resolve_field_type( + field_type: &Arc56StructFieldType, + structs: &HashMap>, + ) -> Result { + match field_type { + Arc56StructFieldType::Value(type_str) => { + // Check if this is a reference to another struct + if structs.contains_key(type_str) { + let nested_struct = Self::get_abi_struct_type(type_str, structs)?; + Ok(StructFieldType::Type(ABIType::Struct(nested_struct))) + } else { + // Parse as regular ABI type + let abi_type = ABIType::from_str(type_str)?; + Ok(StructFieldType::Type(abi_type)) + } + } + Arc56StructFieldType::Nested(nested_fields) => { + // Handle anonymous nested struct fields + let mut resolved_fields = Vec::new(); + for nested_field in nested_fields { + let field_type = Self::resolve_field_type(&nested_field.field_type, structs)?; + resolved_fields.push(StructField { + name: nested_field.name.clone(), + field_type, + }); + } + Ok(StructFieldType::Fields(resolved_fields)) + } + } + } + + pub(crate) fn to_tuple_type(&self) -> ABIType { + Self::fields_to_tuple_type(&self.fields) + } + + fn fields_to_tuple_type(fields: &[StructField]) -> ABIType { + let child_types: Vec = fields + .iter() + .map(|field| match &field.field_type { + StructFieldType::Fields(nested_fields) => Self::fields_to_tuple_type(nested_fields), + StructFieldType::Type(ABIType::Struct(struct_type)) => struct_type.to_tuple_type(), + StructFieldType::Type(other_type) => other_type.clone(), + }) + .collect(); + ABIType::Tuple(child_types) + } + + /// Encode struct value using tuple encoding + pub(crate) fn encode(&self, value: &ABIValue) -> Result, ABIError> { + match value { + ABIValue::Struct(value) => { + let tuple_values = self.value_to_tuple_values(value)?; + let tuple_type = self.to_tuple_type(); + tuple_type.encode(&ABIValue::Array(tuple_values)) + } + _ => Err(ABIError::ValidationError { + message: format!("Cannot encode non-struct value as struct '{}'", self.name), + }), + } + } + + /// Decode bytes using tuple decoding + pub(crate) fn decode(&self, bytes: &[u8]) -> Result { + let tuple_type = self.to_tuple_type(); + let decoded_tuple = tuple_type.decode(bytes)?; + + match decoded_tuple { + ABIValue::Array(tuple_values) => { + let value = self.get_value_from_tuple_values(tuple_values)?; + Ok(ABIValue::Struct(value)) + } + _ => Err(ABIError::DecodingError { + message: format!( + "Expected array from tuple decode for struct '{}'", + self.name + ), + }), + } + } + + /// Convert a struct value (HashMap) to a tuple value (Vec) for encoding + fn value_to_tuple_values( + &self, + value: &HashMap, + ) -> Result, ABIError> { + Self::field_values_to_tuple_values(&self.fields, value, &self.name) + } + + fn field_values_to_tuple_values( + fields: &[StructField], + struct_value: &HashMap, + struct_name: &str, + ) -> Result, ABIError> { + fields + .iter() + .map(|field| { + let value = + struct_value + .get(&field.name) + .ok_or_else(|| ABIError::ValidationError { + message: format!( + "Missing field '{}' in struct '{}'", + field.name, struct_name + ), + })?; + + match (&field.field_type, value) { + ( + StructFieldType::Fields(nested_fields), + ABIValue::Struct(nested_struct_value), + ) => { + let nested_tuple_values = Self::field_values_to_tuple_values( + nested_fields, + nested_struct_value, + "anonymous", + )?; + Ok(ABIValue::Array(nested_tuple_values)) + } + ( + StructFieldType::Type(ABIType::Struct(nested_struct)), + ABIValue::Struct(nested_struct_value), + ) => { + let nested_tuple_values = + nested_struct.value_to_tuple_values(nested_struct_value)?; + Ok(ABIValue::Array(nested_tuple_values)) + } + _ => Ok(value.clone()), + } + }) + .collect() + } + + fn get_value_from_tuple_values( + &self, + tuple_values: Vec, + ) -> Result, ABIError> { + if tuple_values.len() != self.fields.len() { + return Err(ABIError::ValidationError { + message: format!( + "Tuple length {} doesn't match struct '{}' field count {}", + tuple_values.len(), + self.name, + self.fields.len() + ), + }); + } + + Self::get_field_values(&self.fields, tuple_values) + } + + fn get_field_values( + fields: &[StructField], + values: Vec, + ) -> Result, ABIError> { + fields + .iter() + .zip(values) + .map(|(field, value)| { + let processed_value = match (&field.field_type, value) { + (StructFieldType::Fields(nested_fields), ABIValue::Array(nested_tuple)) => { + let nested_map = Self::get_field_values(nested_fields, nested_tuple)?; + ABIValue::Struct(nested_map) + } + ( + StructFieldType::Type(ABIType::Struct(nested_struct)), + ABIValue::Array(nested_tuple), + ) => { + let nested_value = + nested_struct.get_value_from_tuple_values(nested_tuple)?; + ABIValue::Struct(nested_value) + } + (_, other_value) => other_value, + }; + Ok((field.name.clone(), processed_value)) + }) + .collect() + } +} + +impl Display for ABIStruct { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let tuple_type = self.to_tuple_type(); + write!(f, "{}", tuple_type) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::abi_type::BitSize; + use std::collections::HashMap; + + #[test] + fn test_struct_and_tuple_encode_decode_should_match() { + // Create tuple type: (uint8,(uint16,string,string[]),(bool,byte),(byte,address)) + let tuple_type = ABIType::Tuple(vec![ + ABIType::Uint(BitSize::new(8).unwrap()), + ABIType::Tuple(vec![ + ABIType::Uint(BitSize::new(16).unwrap()), + ABIType::String, + ABIType::DynamicArray(Box::new(ABIType::String)), + ]), + ABIType::Tuple(vec![ABIType::Bool, ABIType::Byte]), + ABIType::Tuple(vec![ABIType::Byte, ABIType::Address]), + ]); + + // Create nested struct type "Struct 2" + let struct2 = ABIStruct { + name: "Struct 2".to_string(), + fields: vec![ + StructField { + name: "Struct 2 field 1".to_string(), + field_type: StructFieldType::Type(ABIType::Uint(BitSize::new(16).unwrap())), + }, + StructField { + name: "Struct 2 field 2".to_string(), + field_type: StructFieldType::Type(ABIType::String), + }, + StructField { + name: "Struct 2 field 3".to_string(), + field_type: StructFieldType::Type(ABIType::DynamicArray(Box::new( + ABIType::String, + ))), + }, + ], + }; + + // Create main struct type "Struct 1" + let struct_type = ABIStruct { + name: "Struct 1".to_string(), + fields: vec![ + StructField { + name: "field 1".to_string(), + field_type: StructFieldType::Type(ABIType::Uint(BitSize::new(8).unwrap())), + }, + StructField { + name: "field 2".to_string(), + field_type: StructFieldType::Type(ABIType::Struct(struct2)), + }, + StructField { + name: "field 3".to_string(), + field_type: StructFieldType::Fields(vec![ + StructField { + name: "field 3 child 1".to_string(), + field_type: StructFieldType::Type(ABIType::Bool), + }, + StructField { + name: "field 3 child 2".to_string(), + field_type: StructFieldType::Type(ABIType::Byte), + }, + ]), + }, + StructField { + name: "field 4".to_string(), + field_type: StructFieldType::Type(ABIType::Tuple(vec![ + ABIType::Byte, + ABIType::Address, + ])), + }, + ], + }; + + // Create tuple value: [123, [65432, 'hello', ['world 1', 'world 2', 'world 3']], [false, 88], [222, 'BEKKSMPBTPIGBYJGKD4XK7E7ZQJNZIHJVYFQWW3HNI32JHSH3LOGBRY3LE']] + let tuple_value = ABIValue::Array(vec![ + ABIValue::Uint(123u8.into()), + ABIValue::Array(vec![ + ABIValue::Uint(65432u16.into()), + ABIValue::String("hello".to_string()), + ABIValue::Array(vec![ + ABIValue::String("world 1".to_string()), + ABIValue::String("world 2".to_string()), + ABIValue::String("world 3".to_string()), + ]), + ]), + ABIValue::Array(vec![ABIValue::Bool(false), ABIValue::Byte(88)]), + ABIValue::Array(vec![ + ABIValue::Byte(222), + ABIValue::Address( + "BEKKSMPBTPIGBYJGKD4XK7E7ZQJNZIHJVYFQWW3HNI32JHSH3LOGBRY3LE".to_string(), + ), + ]), + ]); + + // Create struct value + let mut field3_value = HashMap::new(); + field3_value.insert("field 3 child 1".to_string(), ABIValue::Bool(false)); + field3_value.insert("field 3 child 2".to_string(), ABIValue::Byte(88)); + + let mut field2_value = HashMap::new(); + field2_value.insert( + "Struct 2 field 1".to_string(), + ABIValue::Uint(65432u16.into()), + ); + field2_value.insert( + "Struct 2 field 2".to_string(), + ABIValue::String("hello".to_string()), + ); + field2_value.insert( + "Struct 2 field 3".to_string(), + ABIValue::Array(vec![ + ABIValue::String("world 1".to_string()), + ABIValue::String("world 2".to_string()), + ABIValue::String("world 3".to_string()), + ]), + ); + + let mut struct_value_map = HashMap::new(); + struct_value_map.insert("field 1".to_string(), ABIValue::Uint(123u8.into())); + struct_value_map.insert("field 2".to_string(), ABIValue::Struct(field2_value)); + struct_value_map.insert("field 3".to_string(), ABIValue::Struct(field3_value)); + struct_value_map.insert( + "field 4".to_string(), + ABIValue::Array(vec![ + ABIValue::Byte(222), + ABIValue::Address( + "BEKKSMPBTPIGBYJGKD4XK7E7ZQJNZIHJVYFQWW3HNI32JHSH3LOGBRY3LE".to_string(), + ), + ]), + ); + + let struct_value = ABIValue::Struct(struct_value_map); + + // Test encoding - tuple and struct should produce same bytes + let encoded_tuple = tuple_type.encode(&tuple_value).unwrap(); + let encoded_struct = struct_type.encode(&struct_value).unwrap(); + assert_eq!(encoded_tuple, encoded_struct); + + // Test decoding tuple + let decoded_tuple = tuple_type.decode(&encoded_tuple).unwrap(); + assert_eq!(decoded_tuple, tuple_value); + + // Test decoding struct from tuple encoding + let decoded_struct = struct_type.decode(&encoded_tuple).unwrap(); + assert_eq!(decoded_struct, struct_value); + + // Verify struct to tuple type conversion matches expected tuple type + let converted_tuple_type = struct_type.to_tuple_type(); + assert_eq!(converted_tuple_type, tuple_type); + } +} diff --git a/crates/algokit_abi/src/types/mod.rs b/crates/algokit_abi/src/types/mod.rs index 386170e55..cc7afe093 100644 --- a/crates/algokit_abi/src/types/mod.rs +++ b/crates/algokit_abi/src/types/mod.rs @@ -1,2 +1,4 @@ pub mod collections; pub mod primitives; + +pub use collections::*; diff --git a/crates/algokit_abi/src/types/primitives/avm.rs b/crates/algokit_abi/src/types/primitives/avm.rs new file mode 100644 index 000000000..d5ab6d1ef --- /dev/null +++ b/crates/algokit_abi/src/types/primitives/avm.rs @@ -0,0 +1,73 @@ +use crate::{ABIError, ABIType, ABIValue}; +use std::str::FromStr; + +impl ABIType { + pub(crate) fn encode_avm_bytes(&self, value: &ABIValue) -> Result, ABIError> { + match self { + ABIType::AVMBytes => match value { + ABIValue::Bytes(bytes) => Ok(bytes.clone()), + _ => Err(ABIError::EncodingError { + message: "ABI value mismatch, expected bytes for AVMBytes".to_string(), + }), + }, + _ => Err(ABIError::EncodingError { + message: "ABI type mismatch, expected AVMBytes".to_string(), + }), + } + } + + pub(crate) fn decode_avm_bytes(&self, bytes: &[u8]) -> Result { + match self { + ABIType::AVMBytes => Ok(ABIValue::Bytes(bytes.to_vec())), + _ => Err(ABIError::DecodingError { + message: "ABI type mismatch, expected AVMBytes".to_string(), + }), + } + } + + pub(crate) fn encode_avm_string(&self, value: &ABIValue) -> Result, ABIError> { + match self { + ABIType::AVMString => match value { + ABIValue::String(s) => Ok(s.as_bytes().to_vec()), + _ => Err(ABIError::EncodingError { + message: "ABI value mismatch, expected string for AVMString".to_string(), + }), + }, + _ => Err(ABIError::EncodingError { + message: "ABI type mismatch, expected AVMString".to_string(), + }), + } + } + + pub(crate) fn decode_avm_string(&self, bytes: &[u8]) -> Result { + match self { + ABIType::AVMString => { + let s = String::from_utf8(bytes.to_vec()).map_err(|e| ABIError::DecodingError { + message: format!("Invalid UTF-8 string for AVMString: {}", e), + })?; + Ok(ABIValue::String(s)) + } + _ => Err(ABIError::DecodingError { + message: "ABI type mismatch, expected AVMString".to_string(), + }), + } + } + + pub(crate) fn encode_avm_uint64(&self, value: &ABIValue) -> Result, ABIError> { + match self { + ABIType::AVMUint64 => ABIType::from_str("uint64")?.encode(value), + _ => Err(ABIError::EncodingError { + message: "ABI type mismatch, expected AVMUint64".to_string(), + }), + } + } + + pub(crate) fn decode_avm_uint64(&self, bytes: &[u8]) -> Result { + match self { + ABIType::AVMUint64 => ABIType::from_str("uint64")?.decode(bytes), + _ => Err(ABIError::DecodingError { + message: "ABI type mismatch, expected AVMUint64".to_string(), + }), + } + } +} diff --git a/crates/algokit_abi/src/types/primitives/mod.rs b/crates/algokit_abi/src/types/primitives/mod.rs index 8fff0cd65..1d00cd0ba 100644 --- a/crates/algokit_abi/src/types/primitives/mod.rs +++ b/crates/algokit_abi/src/types/primitives/mod.rs @@ -1,4 +1,5 @@ pub mod address; +pub mod avm; pub mod bool; pub mod byte; pub mod string; diff --git a/crates/algokit_test_artifacts/contracts/boxmap/application.arc56.json b/crates/algokit_test_artifacts/contracts/boxmap/application.arc56.json new file mode 100644 index 000000000..6f55e9fc3 --- /dev/null +++ b/crates/algokit_test_artifacts/contracts/boxmap/application.arc56.json @@ -0,0 +1,483 @@ +{ + "name": "BoxMapTest", + "desc": "", + "methods": [ + { + "name": "setValue", + "args": [ + { + "name": "key", + "type": "uint64" + }, + { + "name": "value", + "type": "string" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [ + "NoOp" + ], + "call": [] + } + } + ], + "arcs": [ + 4, + 56 + ], + "structs": {}, + "state": { + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": { + "bMap": { + "keyType": "uint64", + "valueType": "string", + "prefix": "Yg==" + } + } + } + }, + "bareActions": { + "create": [], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "teal": 1, + "source": "box_map.algo.ts:3", + "pc": [ + 0 + ] + }, + { + "teal": 2, + "source": "box_map.algo.ts:3", + "pc": [ + 1, + 2, + 3 + ] + }, + { + "teal": 14, + "source": "box_map.algo.ts:3", + "pc": [ + 4, + 5 + ] + }, + { + "teal": 15, + "source": "box_map.algo.ts:3", + "pc": [ + 6 + ] + }, + { + "teal": 16, + "source": "box_map.algo.ts:3", + "pc": [ + 7, + 8 + ] + }, + { + "teal": 17, + "source": "box_map.algo.ts:3", + "pc": [ + 9 + ] + }, + { + "teal": 18, + "source": "box_map.algo.ts:3", + "pc": [ + 10, + 11 + ] + }, + { + "teal": 19, + "source": "box_map.algo.ts:3", + "pc": [ + 12 + ] + }, + { + "teal": 20, + "source": "box_map.algo.ts:3", + "pc": [ + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + }, + { + "teal": 24, + "source": "box_map.algo.ts:3", + "errorMessage": "The requested action is not implemented in this contract. Are you using the correct OnComplete? Did you set your app ID?", + "pc": [ + 39 + ] + }, + { + "teal": 29, + "source": "box_map.algo.ts:6", + "pc": [ + 40, + 41, + 42 + ] + }, + { + "teal": 30, + "source": "box_map.algo.ts:6", + "pc": [ + 43, + 44, + 45 + ] + }, + { + "teal": 33, + "source": "box_map.algo.ts:6", + "pc": [ + 46, + 47, + 48 + ] + }, + { + "teal": 34, + "source": "box_map.algo.ts:6", + "pc": [ + 49 + ] + }, + { + "teal": 37, + "source": "box_map.algo.ts:6", + "pc": [ + 50, + 51, + 52 + ] + }, + { + "teal": 38, + "source": "box_map.algo.ts:6", + "pc": [ + 53 + ] + }, + { + "teal": 39, + "source": "box_map.algo.ts:6", + "pc": [ + 54 + ] + }, + { + "teal": 43, + "source": "box_map.algo.ts:6", + "pc": [ + 55, + 56, + 57 + ] + }, + { + "teal": 47, + "source": "box_map.algo.ts:7", + "pc": [ + 58, + 59, + 60 + ] + }, + { + "teal": 48, + "source": "box_map.algo.ts:7", + "pc": [ + 61, + 62 + ] + }, + { + "teal": 49, + "source": "box_map.algo.ts:7", + "pc": [ + 63 + ] + }, + { + "teal": 50, + "source": "box_map.algo.ts:7", + "pc": [ + 64 + ] + }, + { + "teal": 51, + "source": "box_map.algo.ts:7", + "pc": [ + 65 + ] + }, + { + "teal": 52, + "source": "box_map.algo.ts:7", + "pc": [ + 66 + ] + }, + { + "teal": 53, + "source": "box_map.algo.ts:7", + "pc": [ + 67 + ] + }, + { + "teal": 54, + "source": "box_map.algo.ts:7", + "pc": [ + 68, + 69 + ] + }, + { + "teal": 55, + "source": "box_map.algo.ts:7", + "pc": [ + 70 + ] + }, + { + "teal": 56, + "source": "box_map.algo.ts:7", + "pc": [ + 71 + ] + }, + { + "teal": 57, + "source": "box_map.algo.ts:7", + "pc": [ + 72 + ] + }, + { + "teal": 58, + "source": "box_map.algo.ts:7", + "pc": [ + 73, + 74, + 75 + ] + }, + { + "teal": 59, + "source": "box_map.algo.ts:7", + "pc": [ + 76 + ] + }, + { + "teal": 60, + "source": "box_map.algo.ts:7", + "pc": [ + 77 + ] + }, + { + "teal": 61, + "source": "box_map.algo.ts:7", + "pc": [ + 78 + ] + }, + { + "teal": 62, + "source": "box_map.algo.ts:6", + "pc": [ + 79 + ] + }, + { + "teal": 65, + "source": "box_map.algo.ts:3", + "pc": [ + 80 + ] + }, + { + "teal": 66, + "source": "box_map.algo.ts:3", + "pc": [ + 81 + ] + }, + { + "teal": 69, + "source": "box_map.algo.ts:3", + "pc": [ + 82, + 83, + 84, + 85, + 86, + 87 + ] + }, + { + "teal": 70, + "source": "box_map.algo.ts:3", + "pc": [ + 88, + 89, + 90 + ] + }, + { + "teal": 71, + "source": "box_map.algo.ts:3", + "pc": [ + 91, + 92, + 93, + 94 + ] + }, + { + "teal": 74, + "source": "box_map.algo.ts:3", + "errorMessage": "this contract does not implement the given ABI method for create NoOp", + "pc": [ + 95 + ] + }, + { + "teal": 77, + "source": "box_map.algo.ts:3", + "pc": [ + 96, + 97, + 98, + 99, + 100, + 101 + ] + }, + { + "teal": 78, + "source": "box_map.algo.ts:3", + "pc": [ + 102, + 103, + 104 + ] + }, + { + "teal": 79, + "source": "box_map.algo.ts:3", + "pc": [ + 105, + 106, + 107, + 108 + ] + }, + { + "teal": 82, + "source": "box_map.algo.ts:3", + "errorMessage": "this contract does not implement the given ABI method for call NoOp", + "pc": [ + 109 + ] + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCmludGNibG9jayAxCgovLyBUaGlzIFRFQUwgd2FzIGdlbmVyYXRlZCBieSBURUFMU2NyaXB0IHYwLjEwNi4zCi8vIGh0dHBzOi8vZ2l0aHViLmNvbS9hbGdvcmFuZGZvdW5kYXRpb24vVEVBTFNjcmlwdAoKLy8gVGhpcyBjb250cmFjdCBpcyBjb21wbGlhbnQgd2l0aCBhbmQvb3IgaW1wbGVtZW50cyB0aGUgZm9sbG93aW5nIEFSQ3M6IFsgQVJDNCBdCgovLyBUaGUgZm9sbG93aW5nIHRlbiBsaW5lcyBvZiBURUFMIGhhbmRsZSBpbml0aWFsIHByb2dyYW0gZmxvdwovLyBUaGlzIHBhdHRlcm4gaXMgdXNlZCB0byBtYWtlIGl0IGVhc3kgZm9yIGFueW9uZSB0byBwYXJzZSB0aGUgc3RhcnQgb2YgdGhlIHByb2dyYW0gYW5kIGRldGVybWluZSBpZiBhIHNwZWNpZmljIGFjdGlvbiBpcyBhbGxvd2VkCi8vIEhlcmUsIGFjdGlvbiByZWZlcnMgdG8gdGhlIE9uQ29tcGxldGUgaW4gY29tYmluYXRpb24gd2l0aCB3aGV0aGVyIHRoZSBhcHAgaXMgYmVpbmcgY3JlYXRlZCBvciBjYWxsZWQKLy8gRXZlcnkgcG9zc2libGUgYWN0aW9uIGZvciB0aGlzIGNvbnRyYWN0IGlzIHJlcHJlc2VudGVkIGluIHRoZSBzd2l0Y2ggc3RhdGVtZW50Ci8vIElmIHRoZSBhY3Rpb24gaXMgbm90IGltcGxlbWVudGVkIGluIHRoZSBjb250cmFjdCwgaXRzIHJlc3BlY3RpdmUgYnJhbmNoIHdpbGwgYmUgIipOT1RfSU1QTEVNRU5URUQiIHdoaWNoIGp1c3QgY29udGFpbnMgImVyciIKdHhuIEFwcGxpY2F0aW9uSUQKIQpwdXNoaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoICpjYWxsX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpjcmVhdGVfTm9PcCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQKCipOT1RfSU1QTEVNRU5URUQ6CgkvLyBUaGUgcmVxdWVzdGVkIGFjdGlvbiBpcyBub3QgaW1wbGVtZW50ZWQgaW4gdGhpcyBjb250cmFjdC4gQXJlIHlvdSB1c2luZyB0aGUgY29ycmVjdCBPbkNvbXBsZXRlPyBEaWQgeW91IHNldCB5b3VyIGFwcCBJRD8KCWVycgoKLy8gc2V0VmFsdWUodWludDY0LHN0cmluZyl2b2lkCiphYmlfcm91dGVfc2V0VmFsdWU6CgkvLyB2YWx1ZTogc3RyaW5nCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAyCglleHRyYWN0IDIgMAoKCS8vIGtleTogdWludDY0Cgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglidG9pCgoJLy8gZXhlY3V0ZSBzZXRWYWx1ZSh1aW50NjQsc3RyaW5nKXZvaWQKCWNhbGxzdWIgc2V0VmFsdWUKCWludGMgMCAvLyAxCglyZXR1cm4KCi8vIHNldFZhbHVlKGtleTogdWludDY0LCB2YWx1ZTogc3RyaW5nKTogdm9pZApzZXRWYWx1ZToKCXByb3RvIDIgMAoKCS8vIGJveF9tYXAuYWxnby50czo3CgkvLyB0aGlzLmJNYXAoa2V5KS52YWx1ZSA9IHZhbHVlCglwdXNoYnl0ZXMgMHg2MiAvLyAiYiIKCWZyYW1lX2RpZyAtMSAvLyBrZXk6IHVpbnQ2NAoJaXRvYgoJY29uY2F0CglkdXAKCWJveF9kZWwKCXBvcAoJZnJhbWVfZGlnIC0yIC8vIHZhbHVlOiBzdHJpbmcKCWR1cAoJbGVuCglpdG9iCglleHRyYWN0IDYgMgoJc3dhcAoJY29uY2F0Cglib3hfcHV0CglyZXRzdWIKCiphYmlfcm91dGVfY3JlYXRlQXBwbGljYXRpb246CglpbnRjIDAgLy8gMQoJcmV0dXJuCgoqY3JlYXRlX05vT3A6CglwdXNoYnl0ZXMgMHhiODQ0N2IzNiAvLyBtZXRob2QgImNyZWF0ZUFwcGxpY2F0aW9uKCl2b2lkIgoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAoJbWF0Y2ggKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbgoKCS8vIHRoaXMgY29udHJhY3QgZG9lcyBub3QgaW1wbGVtZW50IHRoZSBnaXZlbiBBQkkgbWV0aG9kIGZvciBjcmVhdGUgTm9PcAoJZXJyCgoqY2FsbF9Ob09wOgoJcHVzaGJ5dGVzIDB4ZTA2NWM0NzAgLy8gbWV0aG9kICJzZXRWYWx1ZSh1aW50NjQsc3RyaW5nKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX3NldFZhbHVlCgoJLy8gdGhpcyBjb250cmFjdCBkb2VzIG5vdCBpbXBsZW1lbnQgdGhlIGdpdmVuIEFCSSBtZXRob2QgZm9yIGNhbGwgTm9PcAoJZXJy", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEw" + }, + "byteCode": { + "approval": "CiABATEYFIEGCzEZCI0MADkAAAAAAAAAAAAAACsAAAAAAAAAAAAAADYaAlcCADYaAReIAAIiQ4oCAIABYov/FlBJvEiL/kkVFlcGAkxQv4kiQ4AEuER7NjYaAI4B//EAgATgZcRwNhoAjgH/uwA=", + "clear": "Cg==" + }, + "compilerInfo": { + "compiler": "algod", + "compilerVersion": { + "major": 4, + "minor": 0, + "patch": 2, + "commitHash": "6b940281" + } + } +} \ No newline at end of file diff --git a/crates/algokit_test_artifacts/contracts/boxmap/approval.teal b/crates/algokit_test_artifacts/contracts/boxmap/approval.teal new file mode 100644 index 000000000..0c807ac15 --- /dev/null +++ b/crates/algokit_test_artifacts/contracts/boxmap/approval.teal @@ -0,0 +1,82 @@ +#pragma version 10 +intcblock 1 + +// This TEAL was generated by TEALScript v0.106.3 +// https://github.com/algorandfoundation/TEALScript + +// This contract is compliant with and/or implements the following ARCs: [ ARC4 ] + +// The following ten lines of TEAL handle initial program flow +// This pattern is used to make it easy for anyone to parse the start of the program and determine if a specific action is allowed +// Here, action refers to the OnComplete in combination with whether the app is being created or called +// Every possible action for this contract is represented in the switch statement +// If the action is not implemented in the contract, its respective branch will be "*NOT_IMPLEMENTED" which just contains "err" +txn ApplicationID +! +pushint 6 +* +txn OnCompletion ++ +switch *call_NoOp *NOT_IMPLEMENTED *NOT_IMPLEMENTED *NOT_IMPLEMENTED *NOT_IMPLEMENTED *NOT_IMPLEMENTED *create_NoOp *NOT_IMPLEMENTED *NOT_IMPLEMENTED *NOT_IMPLEMENTED *NOT_IMPLEMENTED *NOT_IMPLEMENTED + +*NOT_IMPLEMENTED: + // The requested action is not implemented in this contract. Are you using the correct OnComplete? Did you set your app ID? + err + +// setValue(uint64,string)void +*abi_route_setValue: + // value: string + txna ApplicationArgs 2 + extract 2 0 + + // key: uint64 + txna ApplicationArgs 1 + btoi + + // execute setValue(uint64,string)void + callsub setValue + intc 0 // 1 + return + +// setValue(key: uint64, value: string): void +setValue: + proto 2 0 + + // box_map.algo.ts:7 + // this.bMap(key).value = value + pushbytes 0x62 // "b" + frame_dig -1 // key: uint64 + itob + concat + dup + box_del + pop + frame_dig -2 // value: string + dup + len + itob + extract 6 2 + swap + concat + box_put + retsub + +*abi_route_createApplication: + intc 0 // 1 + return + +*create_NoOp: + pushbytes 0xb8447b36 // method "createApplication()void" + txna ApplicationArgs 0 + match *abi_route_createApplication + + // this contract does not implement the given ABI method for create NoOp + err + +*call_NoOp: + pushbytes 0xe065c470 // method "setValue(uint64,string)void" + txna ApplicationArgs 0 + match *abi_route_setValue + + // this contract does not implement the given ABI method for call NoOp + err diff --git a/crates/algokit_test_artifacts/contracts/hello_world/application.arc56.json b/crates/algokit_test_artifacts/contracts/hello_world/application.arc56.json new file mode 100644 index 000000000..4f8133a0a --- /dev/null +++ b/crates/algokit_test_artifacts/contracts/hello_world/application.arc56.json @@ -0,0 +1,58 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + } + ], + "name": "hello", + "returns": { + "type": "string" + }, + "events": [] + } + ], + "name": "HelloWorld", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + } +} \ No newline at end of file diff --git a/crates/algokit_test_artifacts/contracts/nested_contract/application.arc56.json b/crates/algokit_test_artifacts/contracts/nested_contract/application.arc56.json new file mode 100644 index 000000000..22be161bf --- /dev/null +++ b/crates/algokit_test_artifacts/contracts/nested_contract/application.arc56.json @@ -0,0 +1,190 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "uint64", + "desc": "The first number", + "name": "a" + }, + { + "type": "uint64", + "desc": "The second number", + "name": "b" + }, + { + "type": "string", + "desc": "The operation to perform. Can be either 'sum' or 'difference'", + "name": "operation" + } + ], + "name": "doMath", + "returns": { + "type": "uint64", + "desc": "The result of the operation" + }, + "desc": "A method that takes two numbers and does either addition or subtraction", + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "name": "txn" + } + ], + "name": "txnArg", + "returns": { + "type": "address" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [], + "name": "helloWorld", + "returns": { + "type": "string" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "appl", + "name": "call" + } + ], + "name": "methodArg", + "returns": { + "type": "uint64" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "name": "txn" + }, + { + "type": "appl", + "name": "call" + } + ], + "name": "nestedTxnArg", + "returns": { + "type": "uint64" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "name": "txn0" + }, + { + "type": "appl", + "name": "call1" + }, + { + "type": "pay", + "name": "txn2" + }, + { + "type": "appl", + "name": "call3" + } + ], + "name": "doubleNestedTxnArg", + "returns": { + "type": "uint64" + }, + "events": [] + }, + { + "actions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "args": [], + "name": "createApplication", + "returns": { + "type": "void" + }, + "events": [] + } + ], + "name": "TestContract", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "desc": "", + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgovLyBUaGlzIFRFQUwgd2FzIGdlbmVyYXRlZCBieSBURUFMU2NyaXB0IHYwLjg2LjAKLy8gaHR0cHM6Ly9naXRodWIuY29tL2FsZ29yYW5kZm91bmRhdGlvbi9URUFMU2NyaXB0CgovLyBUaGlzIGNvbnRyYWN0IGlzIGNvbXBsaWFudCB3aXRoIGFuZC9vciBpbXBsZW1lbnRzIHRoZSBmb2xsb3dpbmcgQVJDczogWyBBUkM0IF0KCi8vIFRoZSBmb2xsb3dpbmcgdGVuIGxpbmVzIG9mIFRFQUwgaGFuZGxlIGluaXRpYWwgcHJvZ3JhbSBmbG93Ci8vIFRoaXMgcGF0dGVybiBpcyB1c2VkIHRvIG1ha2UgaXQgZWFzeSBmb3IgYW55b25lIHRvIHBhcnNlIHRoZSBzdGFydCBvZiB0aGUgcHJvZ3JhbSBhbmQgZGV0ZXJtaW5lIGlmIGEgc3BlY2lmaWMgYWN0aW9uIGlzIGFsbG93ZWQKLy8gSGVyZSwgYWN0aW9uIHJlZmVycyB0byB0aGUgT25Db21wbGV0ZSBpbiBjb21iaW5hdGlvbiB3aXRoIHdoZXRoZXIgdGhlIGFwcCBpcyBiZWluZyBjcmVhdGVkIG9yIGNhbGxlZAovLyBFdmVyeSBwb3NzaWJsZSBhY3Rpb24gZm9yIHRoaXMgY29udHJhY3QgaXMgcmVwcmVzZW50ZWQgaW4gdGhlIHN3aXRjaCBzdGF0ZW1lbnQKLy8gSWYgdGhlIGFjdGlvbiBpcyBub3QgaW1wbGVtZW50ZWQgaW4gdGhlIGNvbnRyYWN0LCBpdHMgcmVzcGVjdGl2ZSBicmFuY2ggd2lsbCBiZSAiKk5PVF9JTVBMRU1FTlRFRCIgd2hpY2gganVzdCBjb250YWlucyAiZXJyIgp0eG4gQXBwbGljYXRpb25JRAohCmludCA2CioKdHhuIE9uQ29tcGxldGlvbgorCnN3aXRjaCAqY2FsbF9Ob09wICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqY3JlYXRlX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVECgoqTk9UX0lNUExFTUVOVEVEOgoJZXJyCgovLyBnZXRTdW0oYTogbnVtYmVyLCBiOiBudW1iZXIpOiBudW1iZXIKLy8KLy8gQ2FsY3VsYXRlcyB0aGUgc3VtIG9mIHR3byBudW1iZXJzCi8vCi8vIEBwYXJhbSBhCi8vIEBwYXJhbSBiCi8vIEByZXR1cm5zIFRoZSBzdW0gb2YgYSBhbmQgYgpnZXRTdW06Cglwcm90byAyIDEKCgkvLyB0ZXN0X2NvbnRyYWN0L3Rlc3QuYWxnby50czoxMgoJLy8gcmV0dXJuIGEgKyBiOwoJZnJhbWVfZGlnIC0xIC8vIGE6IG51bWJlcgoJZnJhbWVfZGlnIC0yIC8vIGI6IG51bWJlcgoJKwoJcmV0c3ViCgovLyBnZXREaWZmZXJlbmNlKGE6IG51bWJlciwgYjogbnVtYmVyKTogbnVtYmVyCi8vCi8vIENhbGN1bGF0ZXMgdGhlIGRpZmZlcmVuY2UgYmV0d2VlbiB0d28gbnVtYmVycwovLwovLyBAcGFyYW0gYQovLyBAcGFyYW0gYgovLyBAcmV0dXJucyBUaGUgZGlmZmVyZW5jZSBiZXR3ZWVuIGEgYW5kIGIuCmdldERpZmZlcmVuY2U6Cglwcm90byAyIDEKCgkvLyB0ZXN0X2NvbnRyYWN0L3Rlc3QuYWxnby50czoyMwoJLy8gcmV0dXJuIGEgPj0gYiA/IGEgLSBiIDogYiAtIGE7CglmcmFtZV9kaWcgLTEgLy8gYTogbnVtYmVyCglmcmFtZV9kaWcgLTIgLy8gYjogbnVtYmVyCgk+PQoJYnogKnRlcm5hcnkwX2ZhbHNlCglmcmFtZV9kaWcgLTEgLy8gYTogbnVtYmVyCglmcmFtZV9kaWcgLTIgLy8gYjogbnVtYmVyCgktCgliICp0ZXJuYXJ5MF9lbmQKCip0ZXJuYXJ5MF9mYWxzZToKCWZyYW1lX2RpZyAtMiAvLyBiOiBudW1iZXIKCWZyYW1lX2RpZyAtMSAvLyBhOiBudW1iZXIKCS0KCip0ZXJuYXJ5MF9lbmQ6CglyZXRzdWIKCi8vIGRvTWF0aCh1aW50NjQsdWludDY0LHN0cmluZyl1aW50NjQKKmFiaV9yb3V0ZV9kb01hdGg6CgkvLyBUaGUgQUJJIHJldHVybiBwcmVmaXgKCWJ5dGUgMHgxNTFmN2M3NQoKCS8vIG9wZXJhdGlvbjogc3RyaW5nCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAzCglleHRyYWN0IDIgMAoKCS8vIGI6IHVpbnQ2NAoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgoJYnRvaQoKCS8vIGE6IHVpbnQ2NAoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQoJYnRvaQoKCS8vIGV4ZWN1dGUgZG9NYXRoKHVpbnQ2NCx1aW50NjQsc3RyaW5nKXVpbnQ2NAoJY2FsbHN1YiBkb01hdGgKCWl0b2IKCWNvbmNhdAoJbG9nCglpbnQgMQoJcmV0dXJuCgovLyBkb01hdGgoYTogbnVtYmVyLCBiOiBudW1iZXIsIG9wZXJhdGlvbjogc3RyaW5nKTogbnVtYmVyCi8vCi8vIEEgbWV0aG9kIHRoYXQgdGFrZXMgdHdvIG51bWJlcnMgYW5kIGRvZXMgZWl0aGVyIGFkZGl0aW9uIG9yIHN1YnRyYWN0aW9uCi8vCi8vIEBwYXJhbSBhIFRoZSBmaXJzdCBudW1iZXIKLy8gQHBhcmFtIGIgVGhlIHNlY29uZCBudW1iZXIKLy8gQHBhcmFtIG9wZXJhdGlvbiBUaGUgb3BlcmF0aW9uIHRvIHBlcmZvcm0uIENhbiBiZSBlaXRoZXIgJ3N1bScgb3IgJ2RpZmZlcmVuY2UnCi8vCi8vIEByZXR1cm5zIFRoZSByZXN1bHQgb2YgdGhlIG9wZXJhdGlvbgpkb01hdGg6Cglwcm90byAzIDEKCgkvLyBQdXNoIGVtcHR5IGJ5dGVzIGFmdGVyIHRoZSBmcmFtZSBwb2ludGVyIHRvIHJlc2VydmUgc3BhY2UgZm9yIGxvY2FsIHZhcmlhYmxlcwoJYnl0ZSAweAoKCS8vICppZjBfY29uZGl0aW9uCgkvLyB0ZXN0X2NvbnRyYWN0L3Rlc3QuYWxnby50czozOAoJLy8gb3BlcmF0aW9uID09PSAnc3VtJwoJZnJhbWVfZGlnIC0zIC8vIG9wZXJhdGlvbjogc3RyaW5nCglieXRlIDB4NzM3NTZkIC8vICJzdW0iCgk9PQoJYnogKmlmMF9lbHNlaWYxX2NvbmRpdGlvbgoKCS8vICppZjBfY29uc2VxdWVudAoJLy8gdGVzdF9jb250cmFjdC90ZXN0LmFsZ28udHM6MzkKCS8vIHJlc3VsdCA9IHRoaXMuZ2V0U3VtKGEsIGIpCglmcmFtZV9kaWcgLTIgLy8gYjogbnVtYmVyCglmcmFtZV9kaWcgLTEgLy8gYTogbnVtYmVyCgljYWxsc3ViIGdldFN1bQoJZnJhbWVfYnVyeSAwIC8vIHJlc3VsdDogbnVtYmVyCgliICppZjBfZW5kCgoqaWYwX2Vsc2VpZjFfY29uZGl0aW9uOgoJLy8gdGVzdF9jb250cmFjdC90ZXN0LmFsZ28udHM6NDAKCS8vIG9wZXJhdGlvbiA9PT0gJ2RpZmZlcmVuY2UnCglmcmFtZV9kaWcgLTMgLy8gb3BlcmF0aW9uOiBzdHJpbmcKCWJ5dGUgMHg2NDY5NjY2NjY1NzI2NTZlNjM2NSAvLyAiZGlmZmVyZW5jZSIKCT09CglieiAqaWYwX2Vsc2UKCgkvLyAqaWYwX2Vsc2VpZjFfY29uc2VxdWVudAoJLy8gdGVzdF9jb250cmFjdC90ZXN0LmFsZ28udHM6NDEKCS8vIHJlc3VsdCA9IHRoaXMuZ2V0RGlmZmVyZW5jZShhLCBiKQoJZnJhbWVfZGlnIC0yIC8vIGI6IG51bWJlcgoJZnJhbWVfZGlnIC0xIC8vIGE6IG51bWJlcgoJY2FsbHN1YiBnZXREaWZmZXJlbmNlCglmcmFtZV9idXJ5IDAgLy8gcmVzdWx0OiBudW1iZXIKCWIgKmlmMF9lbmQKCippZjBfZWxzZToKCWVyciAvLyAnSW52YWxpZCBvcGVyYXRpb24nCgoqaWYwX2VuZDoKCS8vIHRlc3RfY29udHJhY3QvdGVzdC5hbGdvLnRzOjQ0CgkvLyByZXR1cm4gcmVzdWx0OwoJZnJhbWVfZGlnIDAgLy8gcmVzdWx0OiBudW1iZXIKCgkvLyBzZXQgdGhlIHN1YnJvdXRpbmUgcmV0dXJuIHZhbHVlCglmcmFtZV9idXJ5IDAKCXJldHN1YgoKLy8gdHhuQXJnKHBheSlhZGRyZXNzCiphYmlfcm91dGVfdHhuQXJnOgoJLy8gVGhlIEFCSSByZXR1cm4gcHJlZml4CglieXRlIDB4MTUxZjdjNzUKCgkvLyB0eG46IHBheQoJdHhuIEdyb3VwSW5kZXgKCWludCAxCgktCglkdXAKCWd0eG5zIFR5cGVFbnVtCglpbnQgcGF5Cgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSB0eG5BcmcocGF5KWFkZHJlc3MKCWNhbGxzdWIgdHhuQXJnCgljb25jYXQKCWxvZwoJaW50IDEKCXJldHVybgoKLy8gdHhuQXJnKHR4bjogUGF5VHhuKTogQWRkcmVzcwp0eG5Bcmc6Cglwcm90byAxIDEKCgkvLyB0ZXN0X2NvbnRyYWN0L3Rlc3QuYWxnby50czo0OAoJLy8gcmV0dXJuIHR4bi5zZW5kZXI7CglmcmFtZV9kaWcgLTEgLy8gdHhuOiBQYXlUeG4KCWd0eG5zIFNlbmRlcgoJcmV0c3ViCgovLyBoZWxsb1dvcmxkKClzdHJpbmcKKmFiaV9yb3V0ZV9oZWxsb1dvcmxkOgoJLy8gVGhlIEFCSSByZXR1cm4gcHJlZml4CglieXRlIDB4MTUxZjdjNzUKCgkvLyBleGVjdXRlIGhlbGxvV29ybGQoKXN0cmluZwoJY2FsbHN1YiBoZWxsb1dvcmxkCglkdXAKCWxlbgoJaXRvYgoJZXh0cmFjdCA2IDIKCXN3YXAKCWNvbmNhdAoJY29uY2F0Cglsb2cKCWludCAxCglyZXR1cm4KCi8vIGhlbGxvV29ybGQoKTogc3RyaW5nCmhlbGxvV29ybGQ6Cglwcm90byAwIDEKCgkvLyB0ZXN0X2NvbnRyYWN0L3Rlc3QuYWxnby50czo1MgoJLy8gcmV0dXJuICdIZWxsbywgV29ybGQhJzsKCWJ5dGUgMHg0ODY1NmM2YzZmMmMyMDU3NmY3MjZjNjQyMSAvLyAiSGVsbG8sIFdvcmxkISIKCXJldHN1YgoKLy8gbWV0aG9kQXJnKGFwcGwpdWludDY0CiphYmlfcm91dGVfbWV0aG9kQXJnOgoJLy8gVGhlIEFCSSByZXR1cm4gcHJlZml4CglieXRlIDB4MTUxZjdjNzUKCgkvLyBjYWxsOiBhcHBsCgl0eG4gR3JvdXBJbmRleAoJaW50IDEKCS0KCWR1cAoJZ3R4bnMgVHlwZUVudW0KCWludCBhcHBsCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBtZXRob2RBcmcoYXBwbCl1aW50NjQKCWNhbGxzdWIgbWV0aG9kQXJnCglpdG9iCgljb25jYXQKCWxvZwoJaW50IDEKCXJldHVybgoKLy8gbWV0aG9kQXJnKGNhbGw6IEFwcENhbGxUeG4pOiBBcHBJRAptZXRob2RBcmc6Cglwcm90byAxIDEKCgkvLyB0ZXN0X2NvbnRyYWN0L3Rlc3QuYWxnby50czo1NgoJLy8gcmV0dXJuIGNhbGwuYXBwbGljYXRpb25JRAoJZnJhbWVfZGlnIC0xIC8vIGNhbGw6IEFwcENhbGxUeG4KCWd0eG5zIEFwcGxpY2F0aW9uSUQKCXJldHN1YgoKLy8gbmVzdGVkVHhuQXJnKHBheSxhcHBsKXVpbnQ2NAoqYWJpX3JvdXRlX25lc3RlZFR4bkFyZzoKCS8vIFRoZSBBQkkgcmV0dXJuIHByZWZpeAoJYnl0ZSAweDE1MWY3Yzc1CgoJLy8gY2FsbDogYXBwbAoJdHhuIEdyb3VwSW5kZXgKCWludCAxCgktCglkdXAKCWd0eG5zIFR5cGVFbnVtCglpbnQgYXBwbAoJPT0KCWFzc2VydAoKCS8vIHR4bjogcGF5Cgl0eG4gR3JvdXBJbmRleAoJaW50IDIKCS0KCWR1cAoJZ3R4bnMgVHlwZUVudW0KCWludCBwYXkKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIG5lc3RlZFR4bkFyZyhwYXksYXBwbCl1aW50NjQKCWNhbGxzdWIgbmVzdGVkVHhuQXJnCglpdG9iCgljb25jYXQKCWxvZwoJaW50IDEKCXJldHVybgoKLy8gbmVzdGVkVHhuQXJnKHR4bjogUGF5VHhuLCBjYWxsOiBBcHBDYWxsVHhuKTogQXBwSUQKbmVzdGVkVHhuQXJnOgoJcHJvdG8gMiAxCgoJLy8gdGVzdF9jb250cmFjdC90ZXN0LmFsZ28udHM6NjAKCS8vIHJldHVybiBjYWxsLmFwcGxpY2F0aW9uSUQKCWZyYW1lX2RpZyAtMiAvLyBjYWxsOiBBcHBDYWxsVHhuCglndHhucyBBcHBsaWNhdGlvbklECglyZXRzdWIKCi8vIGRvdWJsZU5lc3RlZFR4bkFyZyhwYXksYXBwbCxwYXksYXBwbCl1aW50NjQKKmFiaV9yb3V0ZV9kb3VibGVOZXN0ZWRUeG5Bcmc6CgkvLyBUaGUgQUJJIHJldHVybiBwcmVmaXgKCWJ5dGUgMHgxNTFmN2M3NQoKCS8vIGNhbGwzOiBhcHBsCgl0eG4gR3JvdXBJbmRleAoJaW50IDEKCS0KCWR1cAoJZ3R4bnMgVHlwZUVudW0KCWludCBhcHBsCgk9PQoJYXNzZXJ0CgoJLy8gdHhuMjogcGF5Cgl0eG4gR3JvdXBJbmRleAoJaW50IDIKCS0KCWR1cAoJZ3R4bnMgVHlwZUVudW0KCWludCBwYXkKCT09Cglhc3NlcnQKCgkvLyBjYWxsMTogYXBwbAoJdHhuIEdyb3VwSW5kZXgKCWludCAzCgktCglkdXAKCWd0eG5zIFR5cGVFbnVtCglpbnQgYXBwbAoJPT0KCWFzc2VydAoKCS8vIHR4bjA6IHBheQoJdHhuIEdyb3VwSW5kZXgKCWludCA0CgktCglkdXAKCWd0eG5zIFR5cGVFbnVtCglpbnQgcGF5Cgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBkb3VibGVOZXN0ZWRUeG5BcmcocGF5LGFwcGwscGF5LGFwcGwpdWludDY0CgljYWxsc3ViIGRvdWJsZU5lc3RlZFR4bkFyZwoJaXRvYgoJY29uY2F0Cglsb2cKCWludCAxCglyZXR1cm4KCi8vIGRvdWJsZU5lc3RlZFR4bkFyZyh0eG4wOiBQYXlUeG4sIGNhbGwxOiBBcHBDYWxsVHhuLCB0eG4yOiBQYXlUeG4sIGNhbGwzOiBBcHBDYWxsVHhuKTogQXBwSUQKZG91YmxlTmVzdGVkVHhuQXJnOgoJcHJvdG8gNCAxCgoJLy8gdGVzdF9jb250cmFjdC90ZXN0LmFsZ28udHM6NjQKCS8vIHJldHVybiBjYWxsMS5hcHBsaWNhdGlvbklECglmcmFtZV9kaWcgLTIgLy8gY2FsbDE6IEFwcENhbGxUeG4KCWd0eG5zIEFwcGxpY2F0aW9uSUQKCXJldHN1YgoKKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCipjcmVhdGVfTm9PcDoKCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uCgllcnIKCipjYWxsX05vT3A6CgltZXRob2QgImRvTWF0aCh1aW50NjQsdWludDY0LHN0cmluZyl1aW50NjQiCgltZXRob2QgInR4bkFyZyhwYXkpYWRkcmVzcyIKCW1ldGhvZCAiaGVsbG9Xb3JsZCgpc3RyaW5nIgoJbWV0aG9kICJtZXRob2RBcmcoYXBwbCl1aW50NjQiCgltZXRob2QgIm5lc3RlZFR4bkFyZyhwYXksYXBwbCl1aW50NjQiCgltZXRob2QgImRvdWJsZU5lc3RlZFR4bkFyZyhwYXksYXBwbCxwYXksYXBwbCl1aW50NjQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2RvTWF0aCAqYWJpX3JvdXRlX3R4bkFyZyAqYWJpX3JvdXRlX2hlbGxvV29ybGQgKmFiaV9yb3V0ZV9tZXRob2RBcmcgKmFiaV9yb3V0ZV9uZXN0ZWRUeG5BcmcgKmFiaV9yb3V0ZV9kb3VibGVOZXN0ZWRUeG5BcmcKCWVycg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEw" + } +} \ No newline at end of file diff --git a/crates/algokit_test_artifacts/contracts/testing_app/application.arc56.json b/crates/algokit_test_artifacts/contracts/testing_app/application.arc56.json new file mode 100644 index 000000000..0b6044c3f --- /dev/null +++ b/crates/algokit_test_artifacts/contracts/testing_app/application.arc56.json @@ -0,0 +1,426 @@ +{ + "arcs": [], + "bareActions": { + "call": [ + "DeleteApplication", + "UpdateApplication" + ], + "create": [ + "NoOp", + "OptIn" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "value" + } + ], + "name": "call_abi", + "returns": { + "type": "string" + }, + "events": [], + "readonly": true + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "name": "txn" + }, + { + "type": "string", + "name": "value" + } + ], + "name": "call_abi_txn", + "returns": { + "type": "string" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [], + "name": "call_abi_foreign_refs", + "returns": { + "type": "string" + }, + "events": [], + "readonly": true + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "uint64", + "name": "int1" + }, + { + "type": "uint64", + "name": "int2" + }, + { + "type": "string", + "name": "bytes1" + }, + { + "type": "byte[4]", + "name": "bytes2" + } + ], + "name": "set_global", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "uint64", + "name": "int1" + }, + { + "type": "uint64", + "name": "int2" + }, + { + "type": "string", + "name": "bytes1" + }, + { + "type": "byte[4]", + "name": "bytes2" + } + ], + "name": "set_local", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "byte[4]", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "name": "set_box", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [], + "name": "error", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "args": [ + { + "type": "string", + "name": "input" + } + ], + "name": "create_abi", + "returns": { + "type": "string" + }, + "events": [] + }, + { + "actions": { + "call": [ + "UpdateApplication" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "input" + } + ], + "name": "update_abi", + "returns": { + "type": "string" + }, + "events": [] + }, + { + "actions": { + "call": [ + "DeleteApplication" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "input" + } + ], + "name": "delete_abi", + "returns": { + "type": "string" + }, + "events": [] + }, + { + "actions": { + "call": [ + "OptIn" + ], + "create": [] + }, + "args": [], + "name": "opt_in", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "defaultValue": { + "data": "ZGVmYXVsdCB2YWx1ZQ==", + "source": "literal", + "type": "AVMString" + }, + "name": "arg_with_default" + } + ], + "name": "default_value", + "returns": { + "type": "string" + }, + "events": [], + "readonly": true + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "defaultValue": { + "data": "default_value", + "source": "method" + }, + "name": "arg_with_default" + } + ], + "name": "default_value_from_abi", + "returns": { + "type": "string" + }, + "events": [], + "readonly": true + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "uint64", + "defaultValue": { + "data": "aW50MQ==", + "source": "global", + "type": "uint64" + }, + "name": "arg_with_default" + } + ], + "name": "default_value_from_global_state", + "returns": { + "type": "uint64" + }, + "events": [], + "readonly": true + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "defaultValue": { + "data": "bG9jYWxfYnl0ZXMx", + "source": "local", + "type": "AVMString" + }, + "name": "arg_with_default" + } + ], + "name": "default_value_from_local_state", + "returns": { + "type": "string" + }, + "events": [], + "readonly": true + } + ], + "name": "TestingApp", + "state": { + "keys": { + "box": {}, + "global": { + "bytes1": { + "key": "Ynl0ZXMx", + "keyType": "AVMString", + "valueType": "AVMBytes", + "desc": "" + }, + "bytes2": { + "key": "Ynl0ZXMy", + "keyType": "AVMString", + "valueType": "AVMBytes", + "desc": "" + }, + "int1": { + "key": "aW50MQ==", + "keyType": "AVMString", + "valueType": "AVMUint64", + "desc": "" + }, + "int2": { + "key": "aW50Mg==", + "keyType": "AVMString", + "valueType": "AVMUint64", + "desc": "" + }, + "value": { + "key": "dmFsdWU=", + "keyType": "AVMString", + "valueType": "AVMUint64", + "desc": "" + } + }, + "local": { + "local_bytes1": { + "key": "bG9jYWxfYnl0ZXMx", + "keyType": "AVMString", + "valueType": "AVMBytes", + "desc": "" + }, + "local_bytes2": { + "key": "bG9jYWxfYnl0ZXMy", + "keyType": "AVMString", + "valueType": "AVMBytes", + "desc": "" + }, + "local_int1": { + "key": "bG9jYWxfaW50MQ==", + "keyType": "AVMString", + "valueType": "AVMUint64", + "desc": "" + }, + "local_int2": { + "key": "bG9jYWxfaW50Mg==", + "keyType": "AVMString", + "valueType": "AVMUint64", + "desc": "" + } + } + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 2, + "ints": 3 + }, + "local": { + "bytes": 2, + "ints": 2 + } + } + }, + "structs": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMCA1IFRNUExfVVBEQVRBQkxFIFRNUExfREVMRVRBQkxFCmJ5dGVjYmxvY2sgMHggMHgxNTFmN2M3NQp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhmMTdlODBhNSAvLyAiY2FsbF9hYmkoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDMxCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MGE5MmE4MWUgLy8gImNhbGxfYWJpX3R4bihwYXksc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDMwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YWQ3NTYwMmMgLy8gImNhbGxfYWJpX2ZvcmVpZ25fcmVmcygpc3RyaW5nIgo9PQpibnogbWFpbl9sMjkKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGNmOGRlYSAvLyAic2V0X2dsb2JhbCh1aW50NjQsdWludDY0LHN0cmluZyxieXRlWzRdKXZvaWQiCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGNlYzI4MzRhIC8vICJzZXRfbG9jYWwodWludDY0LHVpbnQ2NCxzdHJpbmcsYnl0ZVs0XSl2b2lkIgo9PQpibnogbWFpbl9sMjcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGI0YTIzMCAvLyAic2V0X2JveChieXRlWzRdLHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0NGQwZGEwZCAvLyAiZXJyb3IoKXZvaWQiCj09CmJueiBtYWluX2wyNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDlkNTIzMDQwIC8vICJjcmVhdGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyNAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDNjYTVjZWI3IC8vICJ1cGRhdGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDI3MWI0ZWU5IC8vICJkZWxldGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU3NGI1NWM4IC8vICJkZWZhdWx0X3ZhbHVlKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDQ2ZDIxMWEzIC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDBjZmNiYjAwIC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fZ2xvYmFsX3N0YXRlKHVpbnQ2NCl1aW50NjQiCj09CmJueiBtYWluX2wxOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGQwZjBiYWY4IC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fbG9jYWxfc3RhdGUoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDE3CmVycgptYWluX2wxNzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tbG9jYWxzdGF0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTg6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWdsb2JhbHN0YXRlY2FzdGVyXzMyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tYWJpY2FzdGVyXzMxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVjYXN0ZXJfMzAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBvcHRpbmNhc3Rlcl8yOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWFiaWNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjM6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFiaWNhc3Rlcl8yNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYWJpY2FzdGVyXzI2CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBlcnJvcmNhc3Rlcl8yNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Ym94Y2FzdGVyXzI0CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBzZXRsb2NhbGNhc3Rlcl8yMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjg6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Z2xvYmFsY2FzdGVyXzIyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMjEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYml0eG5jYXN0ZXJfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYmljYXN0ZXJfMTkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w0MAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sMzkKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDQgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDM4CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wzNwplcnIKbWFpbl9sMzc6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIGRlbGV0ZV8xMgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzg6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIHVwZGF0ZV8xMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzk6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CmFzc2VydApjYWxsc3ViIGNyZWF0ZV84CmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzgKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjYWxsX2FiaQpjYWxsYWJpXzA6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyYzIwIC8vICJIZWxsbywgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gaXRvYQppdG9hXzE6CnByb3RvIDEgMQpmcmFtZV9kaWcgLTEKaW50Y18wIC8vIDAKPT0KYm56IGl0b2FfMV9sNQpmcmFtZV9kaWcgLTEKaW50Y18yIC8vIDEwCi8KaW50Y18wIC8vIDAKPgpibnogaXRvYV8xX2w0CmJ5dGVjXzAgLy8gIiIKaXRvYV8xX2wzOgpwdXNoYnl0ZXMgMHgzMDMxMzIzMzM0MzUzNjM3MzgzOSAvLyAiMDEyMzQ1Njc4OSIKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAolCmludGNfMSAvLyAxCmV4dHJhY3QzCmNvbmNhdApiIGl0b2FfMV9sNgppdG9hXzFfbDQ6CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwpjYWxsc3ViIGl0b2FfMQpiIGl0b2FfMV9sMwppdG9hXzFfbDU6CnB1c2hieXRlcyAweDMwIC8vICIwIgppdG9hXzFfbDY6CnJldHN1YgoKLy8gY2FsbF9hYmlfdHhuCmNhbGxhYml0eG5fMjoKcHJvdG8gMiAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NTM2NTZlNzQyMCAvLyAiU2VudCAiCmZyYW1lX2RpZyAtMgpndHhucyBBbW91bnQKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJlMjAgLy8gIi4gIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGNhbGxfYWJpX2ZvcmVpZ25fcmVmcwpjYWxsYWJpZm9yZWlnbnJlZnNfMzoKcHJvdG8gMCAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDE3MDcwM2EyMCAvLyAiQXBwOiAiCnR4bmEgQXBwbGljYXRpb25zIDEKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJjMjA0MTczNzM2NTc0M2EyMCAvLyAiLCBBc3NldDogIgpjb25jYXQKdHhuYSBBc3NldHMgMApjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4MmMyMDQxNjM2MzZmNzU2ZTc0M2EyMCAvLyAiLCBBY2NvdW50OiAiCmNvbmNhdAp0eG5hIEFjY291bnRzIDAKaW50Y18wIC8vIDAKZ2V0Ynl0ZQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4M2EgLy8gIjoiCmNvbmNhdAp0eG5hIEFjY291bnRzIDAKaW50Y18xIC8vIDEKZ2V0Ynl0ZQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBzZXRfZ2xvYmFsCnNldGdsb2JhbF80Ogpwcm90byA0IDAKcHVzaGJ5dGVzIDB4Njk2ZTc0MzEgLy8gImludDEiCmZyYW1lX2RpZyAtNAphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg2OTZlNzQzMiAvLyAiaW50MiIKZnJhbWVfZGlnIC0zCmFwcF9nbG9iYWxfcHV0CnB1c2hieXRlcyAweDYyNzk3NDY1NzMzMSAvLyAiYnl0ZXMxIgpmcmFtZV9kaWcgLTIKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcHVzaGJ5dGVzIDB4NjI3OTc0NjU3MzMyIC8vICJieXRlczIiCmZyYW1lX2RpZyAtMQphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHNldF9sb2NhbApzZXRsb2NhbF81Ogpwcm90byA0IDAKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2OTZlNzQzMSAvLyAibG9jYWxfaW50MSIKZnJhbWVfZGlnIC00CmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2OTZlNzQzMiAvLyAibG9jYWxfaW50MiIKZnJhbWVfZGlnIC0zCmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2Mjc5NzQ2NTczMzEgLy8gImxvY2FsX2J5dGVzMSIKZnJhbWVfZGlnIC0yCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2Mjc5NzQ2NTczMzIgLy8gImxvY2FsX2J5dGVzMiIKZnJhbWVfZGlnIC0xCmFwcF9sb2NhbF9wdXQKcmV0c3ViCgovLyBzZXRfYm94CnNldGJveF82Ogpwcm90byAyIDAKZnJhbWVfZGlnIC0yCmJveF9kZWwKcG9wCmZyYW1lX2RpZyAtMgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYm94X3B1dApyZXRzdWIKCi8vIGVycm9yCmVycm9yXzc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAovLyBEZWxpYmVyYXRlIGVycm9yCmFzc2VydApyZXRzdWIKCi8vIGNyZWF0ZQpjcmVhdGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGJ5dGVzIDB4NzY2MTZjNzU2NSAvLyAidmFsdWUiCnB1c2hpbnQgVE1QTF9WQUxVRSAvLyBUTVBMX1ZBTFVFCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gY3JlYXRlX2FiaQpjcmVhdGVhYmlfOToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB1cGRhdGUKdXBkYXRlXzEwOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDQgLy8gVE1QTF9VUERBVEFCTEUKLy8gQ2hlY2sgYXBwIGlzIHVwZGF0YWJsZQphc3NlcnQKcmV0c3ViCgovLyB1cGRhdGVfYWJpCnVwZGF0ZWFiaV8xMToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDQgLy8gVE1QTF9VUERBVEFCTEUKLy8gQ2hlY2sgYXBwIGlzIHVwZGF0YWJsZQphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gZGVsZXRlCmRlbGV0ZV8xMjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FiaQpkZWxldGVhYmlfMTM6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xNDoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gZGVmYXVsdF92YWx1ZQpkZWZhdWx0dmFsdWVfMTU6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGRlZmF1bHRfdmFsdWVfZnJvbV9hYmkKZGVmYXVsdHZhbHVlZnJvbWFiaV8xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDE0MjQ5MmMyMCAvLyAiQUJJLCAiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fZ2xvYmFsX3N0YXRlCmRlZmF1bHR2YWx1ZWZyb21nbG9iYWxzdGF0ZV8xNzoKcHJvdG8gMSAxCmludGNfMCAvLyAwCmZyYW1lX2RpZyAtMQpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fbG9jYWxfc3RhdGUKZGVmYXVsdHZhbHVlZnJvbWxvY2Fsc3RhdGVfMTg6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDRjNmY2MzYxNmMyMDczNzQ2MTc0NjUyYzIwIC8vICJMb2NhbCBzdGF0ZSwgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gY2FsbF9hYmlfY2FzdGVyCmNhbGxhYmljYXN0ZXJfMTk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGxhYmlfMApmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBjYWxsX2FiaV90eG5fY2FzdGVyCmNhbGxhYml0eG5jYXN0ZXJfMjA6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmludGNfMCAvLyAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDIKdHhuIEdyb3VwSW5kZXgKaW50Y18xIC8vIDEKLQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKY2FsbHN1YiBjYWxsYWJpdHhuXzIKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gY2FsbF9hYmlfZm9yZWlnbl9yZWZzX2Nhc3RlcgpjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMjE6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmNhbGxzdWIgY2FsbGFiaWZvcmVpZ25yZWZzXzMKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gc2V0X2dsb2JhbF9jYXN0ZXIKc2V0Z2xvYmFsY2FzdGVyXzIyOgpwcm90byAwIDAKaW50Y18wIC8vIDAKZHVwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKYnRvaQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpidG9pCmZyYW1lX2J1cnkgMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmZyYW1lX2J1cnkgMgp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpmcmFtZV9kaWcgMgpmcmFtZV9kaWcgMwpjYWxsc3ViIHNldGdsb2JhbF80CnJldHN1YgoKLy8gc2V0X2xvY2FsX2Nhc3RlcgpzZXRsb2NhbGNhc3Rlcl8yMzoKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKYnRvaQpmcmFtZV9idXJ5IDEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpmcmFtZV9idXJ5IDIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgNApmcmFtZV9idXJ5IDMKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKZnJhbWVfZGlnIDMKY2FsbHN1YiBzZXRsb2NhbF81CnJldHN1YgoKLy8gc2V0X2JveF9jYXN0ZXIKc2V0Ym94Y2FzdGVyXzI0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKY2FsbHN1YiBzZXRib3hfNgpyZXRzdWIKCi8vIGVycm9yX2Nhc3RlcgplcnJvcmNhc3Rlcl8yNToKcHJvdG8gMCAwCmNhbGxzdWIgZXJyb3JfNwpyZXRzdWIKCi8vIGNyZWF0ZV9hYmlfY2FzdGVyCmNyZWF0ZWFiaWNhc3Rlcl8yNjoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgY3JlYXRlYWJpXzkKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gdXBkYXRlX2FiaV9jYXN0ZXIKdXBkYXRlYWJpY2FzdGVyXzI3Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiB1cGRhdGVhYmlfMTEKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gZGVsZXRlX2FiaV9jYXN0ZXIKZGVsZXRlYWJpY2FzdGVyXzI4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBkZWxldGVhYmlfMTMKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl8yOToKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTQKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Nhc3RlcgpkZWZhdWx0dmFsdWVjYXN0ZXJfMzA6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGRlZmF1bHR2YWx1ZV8xNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fYWJpX2Nhc3RlcgpkZWZhdWx0dmFsdWVmcm9tYWJpY2FzdGVyXzMxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tYWJpXzE2CmZyYW1lX2J1cnkgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGRlZmF1bHRfdmFsdWVfZnJvbV9nbG9iYWxfc3RhdGVfY2FzdGVyCmRlZmF1bHR2YWx1ZWZyb21nbG9iYWxzdGF0ZWNhc3Rlcl8zMjoKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWdsb2JhbHN0YXRlXzE3CmZyYW1lX2J1cnkgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKaXRvYgpjb25jYXQKbG9nCnJldHN1YgoKLy8gZGVmYXVsdF92YWx1ZV9mcm9tX2xvY2FsX3N0YXRlX2Nhc3RlcgpkZWZhdWx0dmFsdWVmcm9tbG9jYWxzdGF0ZWNhc3Rlcl8zMzoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWxvY2Fsc3RhdGVfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1Yg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" + } +} \ No newline at end of file diff --git a/crates/algokit_test_artifacts/contracts/testing_app_puya/application.arc56.json b/crates/algokit_test_artifacts/contracts/testing_app_puya/application.arc56.json new file mode 100644 index 000000000..ba6987b4a --- /dev/null +++ b/crates/algokit_test_artifacts/contracts/testing_app_puya/application.arc56.json @@ -0,0 +1,189 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "byte[]", + "name": "value" + } + ], + "name": "set_box_bytes", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "name": "set_box_str", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "uint32", + "name": "value" + } + ], + "name": "set_box_int", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "uint512", + "name": "value" + } + ], + "name": "set_box_int512", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "byte[4]", + "name": "value" + } + ], + "name": "set_box_static", + "returns": { + "type": "void" + }, + "events": [] + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "(string,uint64)", + "name": "value", + "struct": "DummyStruct" + } + ], + "name": "set_struct", + "returns": { + "type": "void" + }, + "events": [] + } + ], + "name": "TestPuyaBoxes", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": { + "DummyStruct": [ + { + "name": "name", + "type": "string" + }, + { + "name": "id", + "type": "uint64" + } + ] + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuYXBwcm92YWxfcHJvZ3JhbToKICAgIGludGNibG9jayAxIDAKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5fX3B1eWFfYXJjNF9yb3V0ZXJfXygpIC0+IHVpbnQ2NDoKX19wdXlhX2FyYzRfcm91dGVyX186CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjExCiAgICAvLyBjbGFzcyBUZXN0UHV5YUJveGVzKEFSQzRDb250cmFjdCk6CiAgICBwcm90byAwIDEKICAgIHR4biBOdW1BcHBBcmdzCiAgICBieiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgyMDJmYTczYSAweGRmN2VlYTRhIDB4MzY4OGVkMmMgMHg1ZDE3MjBkZCAweGY4MDY2NjVjIDB4ODFkMjYwZTIgLy8gbWV0aG9kICJzZXRfYm94X2J5dGVzKHN0cmluZyxieXRlW10pdm9pZCIsIG1ldGhvZCAic2V0X2JveF9zdHIoc3RyaW5nLHN0cmluZyl2b2lkIiwgbWV0aG9kICJzZXRfYm94X2ludChzdHJpbmcsdWludDMyKXZvaWQiLCBtZXRob2QgInNldF9ib3hfaW50NTEyKHN0cmluZyx1aW50NTEyKXZvaWQiLCBtZXRob2QgInNldF9ib3hfc3RhdGljKHN0cmluZyxieXRlWzRdKXZvaWQiLCBtZXRob2QgInNldF9zdHJ1Y3Qoc3RyaW5nLChzdHJpbmcsdWludDY0KSl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9ieXRlc19yb3V0ZUAyIF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfc3RyX3JvdXRlQDMgX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9pbnRfcm91dGVANCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X2ludDUxMl9yb3V0ZUA1IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfc3RhdGljX3JvdXRlQDYgX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X3N0cnVjdF9yb3V0ZUA3CiAgICBpbnRjXzEgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X2J5dGVzX3JvdXRlQDI6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjIwCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgZXh0cmFjdCAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjAKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X2J5dGVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X3N0cl9yb3V0ZUAzOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToyNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MTEKICAgIC8vIGNsYXNzIFRlc3RQdXlhQm94ZXMoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X3N0cgogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9pbnRfcm91dGVANDoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjExCiAgICAvLyBjbGFzcyBUZXN0UHV5YUJveGVzKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjI4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2V0X2JveF9pbnQKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfaW50NTEyX3JvdXRlQDU6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjMyCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozMgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNldF9ib3hfaW50NTEyCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X3N0YXRpY19yb3V0ZUA2OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozNgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MTEKICAgIC8vIGNsYXNzIFRlc3RQdXlhQm94ZXMoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MzYKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X3N0YXRpYwogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X3N0cnVjdF9yb3V0ZUA3OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgc2V0X3N0cnVjdAogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgYm56IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTQKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDE0OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgaW50Y18xIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X2J5dGVzKG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfYnl0ZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjIwLTIxCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZXRfYm94X2J5dGVzKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogQnl0ZXMpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjIKICAgIC8vIHNlbGYuYm94X2J5dGVzW25hbWVdID0gdmFsdWUKICAgIHB1c2hieXRlcyAiYm94X2J5dGVzIgogICAgZnJhbWVfZGlnIC0yCiAgICBjb25jYXQKICAgIGR1cAogICAgYm94X2RlbAogICAgcG9wCiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X3N0cihuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfYm94X3N0cjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjQtMjUKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNldF9ib3hfc3RyKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogYXJjNC5TdHJpbmcpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjYKICAgIC8vIHNlbGYuYm94X3N0cltuYW1lXSA9IHZhbHVlCiAgICBwdXNoYnl0ZXMgImJveF9zdHIiCiAgICBmcmFtZV9kaWcgLTIKICAgIGNvbmNhdAogICAgZHVwCiAgICBib3hfZGVsCiAgICBwb3AKICAgIGZyYW1lX2RpZyAtMQogICAgYm94X3B1dAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkMy5jb250cmFjdC5UZXN0UHV5YUJveGVzLnNldF9ib3hfaW50KG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfaW50OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToyOC0yOQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2V0X2JveF9pbnQoc2VsZiwgbmFtZTogYXJjNC5TdHJpbmcsIHZhbHVlOiBhcmM0LlVJbnQzMikgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozMAogICAgLy8gc2VsZi5ib3hfaW50W25hbWVdID0gdmFsdWUKICAgIHB1c2hieXRlcyAiYm94X2ludCIKICAgIGZyYW1lX2RpZyAtMgogICAgY29uY2F0CiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X2ludDUxMihuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfYm94X2ludDUxMjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MzItMzMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNldF9ib3hfaW50NTEyKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogYXJjNC5VSW50NTEyKSAtPiBOb25lOgogICAgcHJvdG8gMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjM0CiAgICAvLyBzZWxmLmJveF9pbnQ1MTJbbmFtZV0gPSB2YWx1ZQogICAgcHVzaGJ5dGVzICJib3hfaW50NTEyIgogICAgZnJhbWVfZGlnIC0yCiAgICBjb25jYXQKICAgIGZyYW1lX2RpZyAtMQogICAgYm94X3B1dAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkMy5jb250cmFjdC5UZXN0UHV5YUJveGVzLnNldF9ib3hfc3RhdGljKG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfc3RhdGljOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozNi0zOQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2V0X2JveF9zdGF0aWMoCiAgICAvLyAgICAgc2VsZiwgbmFtZTogYXJjNC5TdHJpbmcsIHZhbHVlOiBhcmM0LlN0YXRpY0FycmF5W2FyYzQuQnl0ZSwgTGl0ZXJhbFs0XV0KICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MAogICAgLy8gc2VsZi5ib3hfc3RhdGljW25hbWVdID0gdmFsdWUuY29weSgpCiAgICBwdXNoYnl0ZXMgImJveF9zdGF0aWMiCiAgICBmcmFtZV9kaWcgLTIKICAgIGNvbmNhdAogICAgZnJhbWVfZGlnIC0xCiAgICBib3hfcHV0CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuc2V0X3N0cnVjdChuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfc3RydWN0OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0Mi00MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBzZXRfc3RydWN0KHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogRHVtbXlTdHJ1Y3QpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6NDQKICAgIC8vIGFzc2VydCBuYW1lLmJ5dGVzID09IHZhbHVlLm5hbWUuYnl0ZXMsICJOYW1lIG11c3QgbWF0Y2ggaWQgb2Ygc3RydWN0IgogICAgZnJhbWVfZGlnIC0xCiAgICBpbnRjXzEgLy8gMAogICAgZXh0cmFjdF91aW50MTYKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBOYW1lIG11c3QgbWF0Y2ggaWQgb2Ygc3RydWN0CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjQ1CiAgICAvLyBvcC5Cb3gucHV0KG5hbWUuYnl0ZXMsIHZhbHVlLmJ5dGVzKQogICAgZnJhbWVfZGlnIC0yCiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1Ygo=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuY2xlYXJfc3RhdGVfcHJvZ3JhbToKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + } +} \ No newline at end of file diff --git a/crates/algokit_test_artifacts/src/lib.rs b/crates/algokit_test_artifacts/src/lib.rs index 30c46fbe6..53775f9e8 100644 --- a/crates/algokit_test_artifacts/src/lib.rs +++ b/crates/algokit_test_artifacts/src/lib.rs @@ -73,6 +73,8 @@ pub mod nested_contract { /// Contract for testing nested application call scenarios /// and complex transaction composition. pub const APPLICATION: &str = include_str!("../contracts/nested_contract/application.json"); + pub const APPLICATION_ARC56: &str = + include_str!("../contracts/nested_contract/application.arc56.json"); } /// Nested struct storage contract artifacts @@ -155,11 +157,27 @@ pub mod nested_contract_calls { /// Testing app contract artifacts pub mod testing_app { - /// General-purpose testing contract (ARC32) + /// General-purpose testing contract (ARC56) /// /// Contract with updatable/deletable template variables and /// various methods for comprehensive app deployer testing. pub const APPLICATION: &str = include_str!("../contracts/testing_app/application.json"); + pub const APPLICATION_ARC56: &str = + include_str!("../contracts/testing_app/application.arc56.json"); +} + +/// HelloWorld contract artifacts +pub mod hello_world { + /// HelloWorld contract (ARC56) + pub const APPLICATION_ARC56: &str = + include_str!("../contracts/hello_world/application.arc56.json"); +} + +/// Testing app (puya compiler) contract artifacts +pub mod testing_app_puya { + /// Testing app (puya compiler) contract (ARC56) + pub const APPLICATION_ARC56: &str = + include_str!("../contracts/testing_app_puya/application.arc56.json"); } /// Resource population contract artifacts @@ -176,3 +194,10 @@ pub mod resource_population { pub const APPLICATION_V9: &str = include_str!("../contracts/resource_population/ResourcePackerv9.arc32.json"); } + +pub mod box_map_test { + /// Box map testing contract (ARC56) + /// + /// Contract for testing box map operations and complex data handling. + pub const APPLICATION_ARC56: &str = include_str!("../contracts/boxmap/application.arc56.json"); +} diff --git a/crates/algokit_transact/src/constants.rs b/crates/algokit_transact/src/constants.rs index d8ac3753a..d92530dc3 100644 --- a/crates/algokit_transact/src/constants.rs +++ b/crates/algokit_transact/src/constants.rs @@ -28,3 +28,5 @@ pub const MAX_BOX_REFERENCES: usize = 8; // Application state schema limits pub const MAX_GLOBAL_STATE_KEYS: u32 = 64; pub const MAX_LOCAL_STATE_KEYS: u32 = 16; + +pub const MAX_SIMULATE_OPCODE_BUDGET: u64 = 320_000; diff --git a/crates/algokit_utils/Cargo.toml b/crates/algokit_utils/Cargo.toml index f8f88b169..d6e4d2b61 100644 --- a/crates/algokit_utils/Cargo.toml +++ b/crates/algokit_utils/Cargo.toml @@ -26,7 +26,7 @@ dotenvy = "0.15" log = "0.4.27" reqwest = { version = "0.12.19", features = ["blocking"] } snafu = { workspace = true } -tokio = { version = "1.45.1", features = ["time"] } +tokio = { version = "1.45.1", features = ["time", "sync"] } # Dependencies used in algod client integrations tests serde = { version = "1.0", features = ["derive"] } @@ -47,3 +47,4 @@ tokio-test = "^0.4" algokit_test_artifacts = { path = "../algokit_test_artifacts" } env_logger = "0.11" rstest = { workspace = true } +insta = { version = "1.43", features = ["json", "redactions"] } diff --git a/crates/algokit_utils/src/applications/app_client/compilation.rs b/crates/algokit_utils/src/applications/app_client/compilation.rs new file mode 100644 index 000000000..f6503a5d4 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/compilation.rs @@ -0,0 +1,119 @@ +use super::{AppClient, AppClientError}; +use crate::{ + Config, EventType, + applications::app_client::types::CompilationParams, + clients::app_manager::DeploymentMetadata, + config::{AppCompiledEventData, EventData}, +}; + +impl AppClient { + /// Compile the application's approval and clear programs with optional template parameters. + pub async fn compile( + &self, + compilation_params: &CompilationParams, + ) -> Result<(Vec, Vec), AppClientError> { + let approval = self.compile_approval(compilation_params).await?; + let clear = self.compile_clear(compilation_params).await?; + + // Emit AppCompiled event when debug flag is enabled + if Config::debug() { + let app_name = self.app_name.clone(); + let approval_map = self + .algorand() + .app() + .get_compilation_result(&String::from_utf8_lossy(&approval)) + .and_then(|c| c.source_map); + let clear_map = self + .algorand() + .app() + .get_compilation_result(&String::from_utf8_lossy(&clear)) + .and_then(|c| c.source_map); + + let event = AppCompiledEventData { + app_name, + approval_source_map: approval_map, + clear_source_map: clear_map, + }; + Config::events() + .emit(EventType::AppCompiled, EventData::AppCompiled(event)) + .await; + } + + Ok((approval, clear)) + } + + async fn compile_approval( + &self, + compilation_params: &CompilationParams, + ) -> Result, AppClientError> { + let source = + self.app_spec + .source + .as_ref() + .ok_or_else(|| AppClientError::CompilationError { + message: "Missing source in app spec".to_string(), + })?; + + // 1) Decode TEAL from ARC-56 source + let teal = source + .get_decoded_approval() + .map_err(|e| AppClientError::CompilationError { + message: e.to_string(), + })?; + + // 2-4) Compile via AppManager helper with template params and deploy-time controls + let metadata = + if compilation_params.updatable.is_some() || compilation_params.deletable.is_some() { + Some(DeploymentMetadata { + updatable: compilation_params.updatable, + deletable: compilation_params.deletable, + }) + } else { + None + }; + + let compiled = self + .algorand() + .app() + .compile_teal_template( + &teal, + compilation_params.deploy_time_params.as_ref(), + metadata.as_ref(), + ) + .await + .map_err(|e| AppClientError::AppManagerError { source: e })?; + + // Return TEAL source bytes (TransactionSender will pull compiled bytes from cache) + Ok(compiled.teal.into_bytes()) + } + + async fn compile_clear( + &self, + compilation_params: &CompilationParams, + ) -> Result, AppClientError> { + let source = + self.app_spec + .source + .as_ref() + .ok_or_else(|| AppClientError::CompilationError { + message: "Missing source in app spec".to_string(), + })?; + + // 1) Decode TEAL from ARC-56 source + let teal = source + .get_decoded_clear() + .map_err(|e| AppClientError::CompilationError { + message: e.to_string(), + })?; + + // 2-4) Compile via AppManager helper with template params; no deploy-time controls for clear + let compiled = self + .algorand() + .app() + .compile_teal_template(&teal, compilation_params.deploy_time_params.as_ref(), None) + .await + .map_err(|e| AppClientError::AppManagerError { source: e })?; + + Ok(compiled.teal.into_bytes()) + } +} diff --git a/crates/algokit_utils/src/applications/app_client/error.rs b/crates/algokit_utils/src/applications/app_client/error.rs new file mode 100644 index 000000000..d7dfd3948 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/error.rs @@ -0,0 +1,53 @@ +use crate::clients::app_manager::AppManagerError; +use crate::clients::client_manager::ClientManagerError; +use crate::transactions::TransactionSenderError; +use crate::{ComposerError, TransactionResultError}; +use algokit_abi::error::ABIError; +use algokit_transact::AlgoKitTransactError; +use snafu::Snafu; + +#[derive(Debug, Snafu)] +pub enum AppClientError { + #[snafu(display( + "No app ID found for network {network_names:?}. Available keys in spec: {available:?}" + ))] + AppIdNotFound { + network_names: Vec, + available: Vec, + }, + #[snafu(display("Network error: {message}"))] + Network { message: String }, + #[snafu(display("Lookup error: {message}"))] + Lookup { message: String }, + #[snafu(display("Method not found: {message}"))] + MethodNotFound { message: String }, + #[snafu(display("ABI error: {source}"))] + ABIError { source: ABIError }, + #[snafu(display("Transaction sender error: {source}"))] + TransactionSenderError { source: TransactionSenderError }, + #[snafu(display("App manager error: {source}"))] + AppManagerError { source: AppManagerError }, + #[snafu(display("Compilation error: {message}"))] + CompilationError { message: String }, + #[snafu(display("Validation error: {message}"))] + ValidationError { message: String }, + #[snafu(display("{message}"))] + LogicError { + message: String, + logic: Box, + }, + #[snafu(display("Transact error: {source}"))] + TransactError { source: AlgoKitTransactError }, + #[snafu(display("Params builder error: {message}"))] + ParamsBuilderError { message: String }, + #[snafu(display("Composer error: {source}"))] + ComposerError { source: ComposerError }, + #[snafu(display("App state error: {message}"))] + AppStateError { message: String }, + #[snafu(display("Decode error: {message}"))] + DecodeError { message: String }, + #[snafu(display("Client manager error: {source}"))] + ClientManagerError { source: ClientManagerError }, + #[snafu(display("Transaction result error: {source}"))] + TransactionResultError { source: TransactionResultError }, +} diff --git a/crates/algokit_utils/src/applications/app_client/error_transformation.rs b/crates/algokit_utils/src/applications/app_client/error_transformation.rs new file mode 100644 index 000000000..cf6888dc9 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/error_transformation.rs @@ -0,0 +1,391 @@ +use super::types::LogicError; +use super::{AppClient, AppSourceMaps}; +use crate::transactions::TransactionResultError; +use algokit_abi::arc56_contract::PcOffsetMethod; +use lazy_static::lazy_static; +use regex::Regex; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct LogicErrorData { + pub transaction_id: String, + pub message: String, + pub pc: u64, +} + +lazy_static! { + static ref LOGIC_ERROR_RE: Regex = Regex::new( + r".*transaction (?P[A-Z2-7]{52}): logic eval error: (?P.*)\. Details: .*pc=(?P[0-9]+).*" + ) + .unwrap(); + static ref INNER_LOGIC_ERROR_RE: Regex = + Regex::new(r"inner tx (\d+) failed:.*?pc=([0-9]+)").unwrap(); +} + +pub(crate) fn extract_logic_error_data(error_str: &str) -> Option { + let caps = LOGIC_ERROR_RE.captures(error_str)?; + let pc = if let Some(inner) = INNER_LOGIC_ERROR_RE.captures(error_str) { + inner + .get(2) + .and_then(|m| m.as_str().parse::().ok()) + .unwrap_or_else(|| caps["pc"].parse::().unwrap_or(0)) + } else { + caps["pc"].parse::().unwrap_or(0) + }; + Some(LogicErrorData { + transaction_id: caps["transaction_id"].to_string(), + message: caps["message"].to_string(), + pc, + }) +} + +impl AppClient { + /// Import compiled source maps for approval and clear programs. + pub fn import_source_maps(&mut self, source_maps: AppSourceMaps) { + self.source_maps = Some(source_maps); + } + + /// Export compiled source maps if available. + pub fn export_source_maps(&self) -> Option { + self.source_maps.clone() + } +} + +impl AppClient { + /// Create an enhanced LogicError from a transaction error, applying source maps if available. + pub fn expose_logic_error( + &self, + error: &TransactionResultError, + is_clear_state_program: bool, + ) -> LogicError { + let err_str = format!("{}", error); + let parsed_logic_error_data = extract_logic_error_data(&err_str); + let (mut line_no_opt, mut listing) = + self.apply_source_map_for_message(&err_str, is_clear_state_program); + let source_map = self.get_source_map(is_clear_state_program).cloned(); + let transaction_id = Self::extract_transaction_id(&err_str); + let pc_opt = Self::extract_pc(&err_str); + + let mut logic = LogicError { + message: err_str.clone(), + program: None, + source_map, + transaction_id, + pc: pc_opt, + line_no: line_no_opt, + lines: if listing.is_empty() { + None + } else { + Some(listing.clone()) + }, + traces: None, + logic_error_str: Some(err_str.clone()), + }; + + let (tx_id, parsed_pc, msg_msg) = if let Some(p) = parsed_logic_error_data { + ( + Some(p.transaction_id.clone()), + Some(p.pc), + Some(p.message.clone()), + ) + } else { + (logic.transaction_id.clone(), logic.pc, None) + }; + + let mut arc56_error_message: Option = None; + let mut arc56_line_no: Option = None; + let mut arc56_listing: Vec = Vec::new(); + + if let Some(si_model) = self.app_spec().source_info.as_ref() { + let program_source_info = if is_clear_state_program { + &si_model.clear + } else { + &si_model.approval + }; + + let mut arc56_pc = parsed_pc.unwrap_or(0); + + if matches!( + program_source_info.pc_offset_method, + PcOffsetMethod::Cblocks + ) { + // Apply CBLOCKS offset only if compiled program bytes are available via cache + if let Some(bytes) = self.get_program_bytes(is_clear_state_program) { + let offset = Self::get_constant_block_offset(&bytes); + arc56_pc = arc56_pc.saturating_sub(offset as u64); + } + } + + if arc56_pc > 0 { + if let Some(source_info) = program_source_info + .source_info + .iter() + .find(|s| s.pc.iter().any(|v| *v as u64 == arc56_pc)) + { + if let Some(em) = &source_info.error_message { + arc56_error_message = Some(em.clone()); + } + if arc56_line_no.is_none() { + if let Some(teal_line) = source_info.teal { + arc56_line_no = Some(teal_line as u64); + } + } + } + } + + if arc56_line_no.is_some() + && self.app_spec().source.is_some() + && self.get_source_map(is_clear_state_program).is_none() + { + if let Some(teal_src) = self.decode_teal(is_clear_state_program) { + let center = arc56_line_no.unwrap(); + arc56_listing = Self::annotated_teal_snippet(&teal_src, center, 3); + } + } + } + + if line_no_opt.is_none() && arc56_line_no.is_some() { + line_no_opt = arc56_line_no; + logic.line_no = line_no_opt; + } + if listing.is_empty() && !arc56_listing.is_empty() { + listing = arc56_listing; + logic.lines = Some(listing.clone()); + } + + if let Some(emsg) = arc56_error_message.or(msg_msg) { + let app_id_from_msg = Self::extract_app_id(&err_str); + let app_id = app_id_from_msg + .or_else(|| Some(self.app_id().to_string())) + .unwrap_or_else(|| "N/A".to_string()); + let txid_str = tx_id.unwrap_or_else(|| "N/A".to_string()); + let runtime_msg = format!( + "Runtime error when executing {} (appId: {}) in transaction {}: {}", + self.app_spec().name, + app_id, + txid_str, + emsg + ); + logic.message = runtime_msg.clone(); + } + + logic + } + + /// Extract transaction id from an error string. + fn extract_transaction_id(error_str: &str) -> Option { + let re = regex::Regex::new(r"transaction ([A-Z2-7]{52})").unwrap(); + re.captures(error_str) + .and_then(|caps| caps.get(1)) + .map(|m| m.as_str().to_string()) + } + + /// Compute line and listing using a source map when available. + fn apply_source_map_for_message( + &self, + error_str: &str, + is_clear_state_program: bool, + ) -> (Option, Vec) { + let pc_opt = Self::extract_pc(error_str); + if let Some(pc) = pc_opt { + if let Some((line_no, listing)) = self.apply_source_map(pc, is_clear_state_program) { + return (Some(line_no), listing); + } + } + (None, Vec::new()) + } + + /// Extract program counter from an error string. + fn extract_pc(s: &str) -> Option { + for token in s.split(|c: char| c.is_whitespace() || c == ',' || c == ';') { + if let Some(idx) = token.find('=') { + let (k, v) = token.split_at(idx); + if k.ends_with("pc") { + if let Ok(parsed_logic_error_data) = v.trim_start_matches('=').parse::() { + return Some(parsed_logic_error_data); + } + } + } + } + None + } + + /// Map pc to TEAL line and extract a short snippet. + fn apply_source_map( + &self, + pc: u64, + is_clear_state_program: bool, + ) -> Option<(u64, Vec)> { + let map = self.get_source_map(is_clear_state_program)?; + let line_no = Self::map_pc_to_line(map, pc)?; + let listing = Self::truncate_listing(map, line_no, 3); + Some((line_no, listing)) + } + + /// Get the selected program's source map. + fn get_source_map(&self, is_clear_state_program: bool) -> Option<&JsonValue> { + let maps = self.source_maps.as_ref()?; + if is_clear_state_program { + maps.clear_source_map.as_ref() + } else { + maps.approval_source_map.as_ref() + } + } + + /// Map a program counter to a source line using the pc array. + fn map_pc_to_line(map: &JsonValue, pc: u64) -> Option { + let pcs = map.get("pc")?.as_array()?; + let mut best_line: Option = None; + for (i, entry) in pcs.iter().enumerate() { + if let Some(pc_val) = entry.as_u64() { + if pc_val == pc { + return Some(i as u64 + 1); + } + if pc_val < pc { + best_line = Some(i as u64 + 1); + } + } + } + best_line + } + + /// Format a numbered snippet around a source line from a source map. + fn truncate_listing(map: &JsonValue, center_line: u64, context: usize) -> Vec { + let mut lines: Vec = Vec::new(); + if let Some(source) = map.get("source").and_then(|s| s.as_str()) { + let src_lines: Vec<&str> = source.lines().collect(); + let total = src_lines.len(); + let center = center_line.saturating_sub(1) as usize; + let start = center.saturating_sub(context); + let end = (center + context + 1).min(total); + for (i, line) in src_lines.iter().enumerate().take(end).skip(start) { + lines.push(format!("{:>4} | {}", i + 1, line)); + } + } + lines + } + + /// Format a numbered snippet around a source line from raw TEAL. + fn truncate_teal_source(source: &str, center_line: u64, context: usize) -> Vec { + let mut lines: Vec = Vec::new(); + let src_lines: Vec<&str> = source.lines().collect(); + let total = src_lines.len(); + if total == 0 { + return lines; + } + let center = center_line.saturating_sub(1) as usize; + let start = center.saturating_sub(context); + let end = (center + context + 1).min(total); + for (i, line) in src_lines.iter().enumerate().take(end).skip(start) { + lines.push(format!("{:>4} | {}", i + 1, line)); + } + lines + } + + /// Like truncate_teal_source but adds a subtle error marker on the center line. + fn annotated_teal_snippet(source: &str, center_line: u64, context: usize) -> Vec { + let mut lines = Self::truncate_teal_source(source, center_line, context); + // Try to mark the line that equals center_line if present + let needle = format!("{:>4} |", center_line); + let mut marked = false; + for entry in &mut lines { + if entry.starts_with(&needle) { + *entry = format!("{}\t<-- Error", entry); + marked = true; + break; + } + } + // Fallback: mark middle line if exact match not found + if !marked && !lines.is_empty() { + let mid = lines.len() / 2; + lines[mid] = format!("{}\t<-- Error", lines[mid]); + } + lines + } + + /// Calculate the offset after initial constant blocks in a TEAL program (CBLOCKS). + fn get_constant_block_offset(program: &[u8]) -> usize { + const BYTE_CBLOCK: u8 = 38; // bytecblock + const INT_CBLOCK: u8 = 32; // intcblock + if program.is_empty() { + return 0; + } + let mut i = 1; // skip version byte + let len = program.len(); + let mut bytec_off: Option = None; + let mut intc_off: Option = None; + + while i < len { + let op = program[i]; + i += 1; + if op != BYTE_CBLOCK && op != INT_CBLOCK { + break; + } + if i >= len { + break; + } + let values_remaining = program[i] as usize; + i += 1; + for _ in 0..values_remaining { + if op == BYTE_CBLOCK { + if i >= len { + break; + } + let elem_len = program[i] as usize; + i += 1 + elem_len.min(len.saturating_sub(i)); + } else { + while i < len { + let b = program[i]; + i += 1; + if (b & 0x80) == 0 { + break; + } + } + } + } + let off = i; + if op == BYTE_CBLOCK { + bytec_off = Some(off) + } else { + intc_off = Some(off) + } + + if i >= len { + break; + } + let next = program[i]; + if next != BYTE_CBLOCK && next != INT_CBLOCK { + break; + } + } + bytec_off.or(intc_off).unwrap_or(0) + } + + /// Try to get compiled program bytes for the app from the compilation cache. + /// This avoids async calls; returns None if not available. + fn get_program_bytes(&self, is_clear_state_program: bool) -> Option> { + let teal_src = self.decode_teal(is_clear_state_program)?; + self.algorand() + .app() + .get_compilation_result(&teal_src) + .map(|c| c.compiled_base64_to_bytes) + } + + /// Decode base64 TEAL source from the app spec. + fn decode_teal(&self, is_clear_state_program: bool) -> Option { + let src = self.app_spec().source.as_ref()?; + if is_clear_state_program { + src.get_decoded_clear().ok() + } else { + src.get_decoded_approval().ok() + } + } + + /// Extract app id from an error string. + fn extract_app_id(error_str: &str) -> Option { + let re = regex::Regex::new(r"app=(\d+)").ok()?; + re.captures(error_str) + .and_then(|caps| caps.get(1)) + .map(|m| m.as_str().to_string()) + } +} diff --git a/crates/algokit_utils/src/applications/app_client/mod.rs b/crates/algokit_utils/src/applications/app_client/mod.rs new file mode 100644 index 000000000..0397a985a --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/mod.rs @@ -0,0 +1,380 @@ +use crate::applications::AppDeployer; +use crate::clients::app_manager::{AppState, BoxName}; +use crate::clients::network_client::NetworkDetails; +use crate::transactions::{TransactionComposerConfig, TransactionSigner}; +use crate::{AlgorandClient, clients::app_manager::BoxIdentifier}; +use crate::{SendParams, SendTransactionResult}; +use algokit_abi::{ABIType, ABIValue, Arc56Contract}; +use algokit_transact::Address; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +/// A box value decoded according to an ABI type +#[derive(Debug, Clone)] +pub struct BoxABIValue { + pub name: BoxName, + pub value: ABIValue, +} + +/// A box name and its raw value +#[derive(Debug, Clone)] +pub struct BoxValue { + pub name: BoxName, + pub value: Vec, +} +mod compilation; +mod error; +mod error_transformation; +mod params_builder; +mod sender; +mod state_accessor; +mod transaction_builder; +mod types; +mod utils; +pub use error::AppClientError; +use params_builder::ParamsBuilder; +pub use sender::TransactionSender; +pub use state_accessor::StateAccessor; +pub use transaction_builder::TransactionBuilder; +pub use types::{ + AppClientBareCallParams, AppClientMethodCallParams, AppClientParams, AppSourceMaps, + CompilationParams, FundAppAccountParams, +}; + +type BoxNameFilter = Box bool>; + +/// A client for interacting with an Algorand smart contract application (ARC-56 focused). +pub struct AppClient { + app_id: u64, + app_spec: Arc56Contract, + algorand: AlgorandClient, + default_sender: Option, + default_signer: Option>, + source_maps: Option, + app_name: Option, + transaction_composer_config: Option, +} + +impl AppClient { + /// Create a new client from parameters. + pub fn new(params: AppClientParams) -> Self { + Self { + app_id: params.app_id, + app_spec: params.app_spec, + algorand: params.algorand, + default_sender: params.default_sender, + default_signer: params.default_signer, + source_maps: params.source_maps, + app_name: params.app_name, + transaction_composer_config: params.transaction_composer_config, + } + } + + /// Construct from the current network using app_spec.networks mapping. + /// + /// Matches on either the network alias ("localnet", "testnet", "mainnet") + /// or the network's genesis hash present in the node's suggested params. + pub async fn from_network( + app_spec: Arc56Contract, + algorand: AlgorandClient, + app_name: Option, + default_sender: Option, + default_signer: Option>, + source_maps: Option, + transaction_composer_config: Option, + ) -> Result { + let network = algorand + .client() + .network() + .await + .map_err(|e| AppClientError::Network { + message: e.to_string(), + })?; + + let candidate_keys = Self::candidate_network_keys(&network); + let (app_id, available_keys) = match &app_spec.networks { + Some(nets) => ( + Self::find_app_id_in_networks(&candidate_keys, nets), + nets.keys().cloned().collect(), + ), + None => (None, Vec::new()), + }; + + let app_id = app_id.ok_or_else(|| AppClientError::AppIdNotFound { + network_names: candidate_keys.clone(), + available: available_keys, + })?; + + Ok(Self::new(AppClientParams { + app_id, + app_spec, + algorand, + app_name, + default_sender, + default_signer, + source_maps, + transaction_composer_config, + })) + } + + /// Construct from creator address and application name via indexer lookup. + #[allow(clippy::too_many_arguments)] + pub async fn from_creator_and_name( + creator_address: &str, + app_name: &str, + app_spec: Arc56Contract, + algorand: AlgorandClient, + default_sender: Option, + default_signer: Option>, + source_maps: Option, + ignore_cache: Option, + transaction_composer_config: Option, + ) -> Result { + let address = Address::from_str(creator_address).map_err(|e| AppClientError::Lookup { + message: format!("Invalid creator address: {}", e), + })?; + + let indexer_client = algorand + .client() + .indexer() + .map_err(|e| AppClientError::ClientManagerError { source: e })?; + let mut app_deployer = AppDeployer::new( + algorand.app().clone(), + algorand.send().clone(), + Some(indexer_client), + ); + + let lookup = app_deployer + .get_creator_apps_by_name(&address, ignore_cache) + .await + .map_err(|e| AppClientError::Lookup { + message: e.to_string(), + })?; + + let app_metadata = lookup + .apps + .get(app_name) + .ok_or_else(|| AppClientError::Lookup { + message: format!( + "App not found for creator {} and name {}", + creator_address, app_name + ), + })?; + + Ok(Self::new(AppClientParams { + app_id: app_metadata.app_id, + app_spec, + algorand, + app_name: Some(app_name.to_string()), + default_sender, + default_signer, + source_maps, + transaction_composer_config, + })) + } + + fn candidate_network_keys(network: &NetworkDetails) -> Vec { + let mut names = vec![network.genesis_hash.clone()]; + if network.is_localnet { + names.push("localnet".to_string()); + } + if network.is_mainnet { + names.push("mainnet".to_string()); + } + if network.is_testnet { + names.push("testnet".to_string()); + } + names + } + + fn find_app_id_in_networks( + candidate_keys: &[String], + networks: &HashMap, + ) -> Option { + for key in candidate_keys { + if let Some(net) = networks.get(key) { + return Some(net.app_id); + } + } + None + } + + /// Get the application ID. + pub fn app_id(&self) -> u64 { + self.app_id + } + /// Get the ARC-56 application specification. + pub fn app_spec(&self) -> &Arc56Contract { + &self.app_spec + } + /// Get the Algorand client instance. + pub fn algorand(&self) -> &AlgorandClient { + &self.algorand + } + /// Get the application name if configured. + pub fn app_name(&self) -> Option<&String> { + self.app_name.as_ref() + } + /// Get the default sender address if configured. + pub fn default_sender(&self) -> Option<&String> { + self.default_sender.as_ref() + } + + /// Get the application's account address. + pub fn app_address(&self) -> Address { + Address::from_app_id(&self.app_id) + } + + fn get_sender_address(&self, sender: &Option) -> Result { + let sender_str = sender + .as_ref() + .or(self.default_sender.as_ref()) + .ok_or_else(|| AppClientError::ValidationError { + message: format!( + "No sender provided and no default sender configured for app {}", + self.app_name.as_deref().unwrap_or("") + ), + })?; + Address::from_str(sender_str).map_err(|e| AppClientError::ValidationError { + message: format!("Invalid sender address: {}", e), + }) + } + + /// Resolve the signer for a transaction based on the sender and default configuration. + /// Returns the provided signer, or the default_signer if sender matches default_sender. + pub(crate) fn resolve_signer( + &self, + sender: Option, + signer: Option>, + ) -> Option> { + signer.or_else(|| { + let should_use_default = sender.is_none() || sender == self.default_sender; + + should_use_default + .then(|| self.default_signer.clone()) + .flatten() + }) + } + + /// Fund the application's account with Algos. + pub async fn fund_app_account( + &self, + params: FundAppAccountParams, + send_params: Option, + ) -> Result { + self.send().fund_app_account(params, send_params).await + } + + /// Get the application's global state. + pub async fn get_global_state(&self) -> Result, AppState>, AppClientError> { + self.algorand + .app() + .get_global_state(self.app_id) + .await + .map_err(|e| AppClientError::AppManagerError { source: e }) + } + + /// Get the application's local state for a specific account. + pub async fn get_local_state( + &self, + address: &str, + ) -> Result, AppState>, AppClientError> { + self.algorand + .app() + .get_local_state(self.app_id, address) + .await + .map_err(|e| AppClientError::AppManagerError { source: e }) + } + + /// Get all box names for the application. + pub async fn get_box_names(&self) -> Result, AppClientError> { + self.algorand + .app() + .get_box_names(self.app_id) + .await + .map_err(|e| AppClientError::AppManagerError { source: e }) + } + + /// Get all box values (names and contents) for the application. + pub async fn get_box_values(&self) -> Result, AppClientError> { + let names = self.get_box_names().await?; + let mut values = Vec::new(); + for name in names { + let value = self.get_box_value(&name.name_raw).await?; + values.push(BoxValue { name, value }); + } + Ok(values) + } + + /// Get the raw value of a specific box. + pub async fn get_box_value(&self, name: &BoxIdentifier) -> Result, AppClientError> { + self.algorand + .app() + .get_box_value(self.app_id, name) + .await + .map_err(|e| AppClientError::AppManagerError { source: e }) + } + + /// Get a single box value decoded according to an ABI type. + pub async fn get_box_value_from_abi_type( + &self, + name: &BoxIdentifier, + abi_type: &ABIType, + ) -> Result { + self.algorand + .app() + .get_box_value_from_abi_type(self.app_id, name, abi_type) + .await + .map_err(|e| AppClientError::AppManagerError { source: e }) + } + + /// Get multiple box values decoded according to an ABI type. + pub async fn get_box_values_from_abi_type( + &self, + abi_type: &ABIType, + filter_func: Option, + ) -> Result, AppClientError> { + let names = self.get_box_names().await?; + let filtered_names = if let Some(filter) = filter_func { + names.into_iter().filter(|name| filter(name)).collect() + } else { + names + }; + + let box_names: Vec = filtered_names + .iter() + .map(|name| name.name_raw.clone()) + .collect(); + + let values = self + .algorand + .app() + .get_box_values_from_abi_type(self.app_id, &box_names, abi_type) + .await + .map_err(|e| AppClientError::AppManagerError { source: e })?; + + Ok(filtered_names + .into_iter() + .zip(values.into_iter()) + .map(|(name, value)| BoxABIValue { name, value }) + .collect()) + } + + /// Get a parameter builder for creating transaction parameters. + pub fn params(&self) -> ParamsBuilder<'_> { + ParamsBuilder { client: self } + } + /// Get a transaction builder for creating unsigned transactions. + pub fn create_transaction(&self) -> TransactionBuilder<'_> { + TransactionBuilder { client: self } + } + /// Get a transaction sender for executing transactions. + pub fn send(&self) -> TransactionSender<'_> { + TransactionSender { client: self } + } + /// Get a state accessor for reading application state with ABI decoding. + pub fn state(&self) -> StateAccessor<'_> { + StateAccessor::new(self) + } +} diff --git a/crates/algokit_utils/src/applications/app_client/params_builder.rs b/crates/algokit_utils/src/applications/app_client/params_builder.rs new file mode 100644 index 000000000..e163607d7 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/params_builder.rs @@ -0,0 +1,557 @@ +use super::AppClient; +use super::types::{ + AppClientBareCallParams, AppClientMethodCallParams, CompilationParams, FundAppAccountParams, +}; +use crate::AppClientError; +use crate::clients::app_manager::AppState; +use crate::transactions::{ + AppCallMethodCallParams, AppCallParams, AppDeleteMethodCallParams, AppDeleteParams, + AppMethodCallArg, AppUpdateMethodCallParams, AppUpdateParams, PaymentParams, +}; +use algokit_abi::abi_method::ABIDefaultValue; +use algokit_abi::{ABIMethod, ABIMethodArgType, ABIType, ABIValue, DefaultValueSource}; +use algokit_transact::{Address, OnApplicationComplete}; +use base64::Engine; +use std::str::FromStr; + +enum StateSource<'app_client> { + Global, + Local(&'app_client str), +} + +pub struct ParamsBuilder<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +pub struct BareParamsBuilder<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +impl<'app_client> ParamsBuilder<'app_client> { + /// Get the bare call params builder. + pub fn bare(&self) -> BareParamsBuilder<'app_client> { + BareParamsBuilder { + client: self.client, + } + } + + /// Build parameters for an ABI method call with the specified on-complete action. + pub async fn call( + &self, + params: AppClientMethodCallParams, + on_complete: Option, + ) -> Result { + self.get_method_call_params(¶ms, on_complete.unwrap_or(OnApplicationComplete::NoOp)) + .await + } + + /// Build parameters for an ABI method call with OptIn on-complete action. + pub async fn opt_in( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.get_method_call_params(¶ms, OnApplicationComplete::OptIn) + .await + } + + /// Build parameters for an ABI method call with CloseOut on-complete action. + pub async fn close_out( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.get_method_call_params(¶ms, OnApplicationComplete::CloseOut) + .await + } + + /// Build parameters for an ABI method call with ClearState on-complete action. + pub async fn clear_state( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.get_method_call_params(¶ms, OnApplicationComplete::ClearState) + .await + } + + /// Build parameters for an ABI method call with Delete on-complete action. + pub async fn delete( + &self, + params: AppClientMethodCallParams, + ) -> Result { + let abi_method = self.get_abi_method(¶ms.method)?; + let sender = self.client.get_sender_address(¶ms.sender)?.as_str(); + let resolved_args = self + .resolve_args(&abi_method, ¶ms.args, &sender) + .await?; + + Ok(AppDeleteMethodCallParams { + sender: self.client.get_sender_address(¶ms.sender)?, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to: get_optional_address(¶ms.rekey_to)?, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + app_id: self.client.app_id, + method: abi_method, + args: resolved_args, + account_references: super::utils::parse_account_refs_to_addresses( + ¶ms.account_references, + )?, + app_references: params.app_references.clone(), + asset_references: params.asset_references.clone(), + box_references: params.box_references.clone(), + }) + } + + /// Build parameters for updating the application using an ABI method call. + pub async fn update( + &self, + params: AppClientMethodCallParams, + compilation_params: Option, + ) -> Result { + // Compile programs (and populate AppManager cache/source maps) + let compilation_params = compilation_params.unwrap_or_default(); + let (approval_program, clear_state_program) = + self.client.compile(&compilation_params).await?; + + let abi_method = self.get_abi_method(¶ms.method)?; + let sender = self.client.get_sender_address(¶ms.sender)?.as_str(); + let resolved_args = self + .resolve_args(&abi_method, ¶ms.args, &sender) + .await?; + + Ok(AppUpdateMethodCallParams { + sender: self.client.get_sender_address(¶ms.sender)?, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to: get_optional_address(¶ms.rekey_to)?, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + app_id: self.client.app_id, + method: abi_method, + args: resolved_args, + account_references: super::utils::parse_account_refs_to_addresses( + ¶ms.account_references, + )?, + app_references: params.app_references.clone(), + asset_references: params.asset_references.clone(), + box_references: params.box_references.clone(), + approval_program, + clear_state_program, + }) + } + + /// Build parameters for funding the application's account. + pub fn fund_app_account( + &self, + params: &FundAppAccountParams, + ) -> Result { + let sender = self.client.get_sender_address(¶ms.sender)?; + let receiver = self.client.app_address(); + let rekey_to = get_optional_address(¶ms.rekey_to)?; + + Ok(PaymentParams { + sender, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + receiver, + amount: params.amount, + }) + } + + async fn get_method_call_params( + &self, + params: &AppClientMethodCallParams, + on_complete: OnApplicationComplete, + ) -> Result { + let abi_method = self.get_abi_method(¶ms.method)?; + let sender = self.client.get_sender_address(¶ms.sender)?.as_str(); + let resolved_args = self + .resolve_args(&abi_method, ¶ms.args, &sender) + .await?; + + Ok(AppCallMethodCallParams { + sender: self.client.get_sender_address(¶ms.sender)?, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to: get_optional_address(¶ms.rekey_to)?, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + app_id: self.client.app_id, + method: abi_method, + args: resolved_args, + account_references: super::utils::parse_account_refs_to_addresses( + ¶ms.account_references, + )?, + app_references: params.app_references.clone(), + asset_references: params.asset_references.clone(), + box_references: params.box_references.clone(), + on_complete, + }) + } + + fn get_abi_method(&self, method_name_or_signature: &str) -> Result { + self.client + .app_spec + .find_abi_method(method_name_or_signature) + .map_err(|e| AppClientError::ABIError { source: e }) + } + + async fn resolve_args( + &self, + method: &ABIMethod, + provided: &Vec, + sender: &str, + ) -> Result, AppClientError> { + let mut resolved: Vec = Vec::with_capacity(method.args.len()); + + if method.args.len() != provided.len() { + return Err(AppClientError::ValidationError { + message: format!( + "The number of provided arguments is {} while the method expects {} arguments", + provided.len(), + method.args.len() + ), + }); + } + + for (index, (method_arg, provided_arg)) in method.args.iter().zip(provided).enumerate() { + let method_arg_name = method_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", index + 1)); + match (&method_arg.arg_type, provided_arg) { + (ABIMethodArgType::Value(value_type), AppMethodCallArg::DefaultValue) => { + let default_value = method_arg.default_value.as_ref().ok_or_else(|| { + AppClientError::ParamsBuilderError { + message: format!( + "No default value defined for argument {} in call to method {}", + method_arg_name, method.name + ), + } + })?; + + let value = self + .resolve_default_value(default_value, value_type, sender) + .await + .map_err(|e| AppClientError::ParamsBuilderError { + message: format!( + "Failed to resolve default value for arg {}: {:?}", + method_arg_name, e + ), + })?; + resolved.push(AppMethodCallArg::ABIValue(value)); + } + (_, AppMethodCallArg::DefaultValue) => { + return Err(AppClientError::ParamsBuilderError { + message: format!( + "Default value is not supported by argument {} in call to method {}", + method_arg_name, method.name + ), + }); + } + // Intentionally defer type compatibility and structural validation to ABI + // encoding/composer (consistent with TS/Py). Here we only enforce arg count and + // default value handling; encoding will surface any mismatches. + (_, value) => { + resolved.push(value.clone()); + } + } + } + + Ok(resolved) + } + + async fn resolve_state_value( + &self, + default: &ABIDefaultValue, + value_type: &ABIType, + source: StateSource<'_>, + ) -> Result { + let key = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| AppClientError::ParamsBuilderError { + message: format!( + "Failed to decode {} key: {}", + match source { + StateSource::Global => "global", + StateSource::Local(_) => "local", + }, + e + ), + })?; + + let state = match source { + StateSource::Global => self.client.get_global_state().await?, + StateSource::Local(sender) => self.client.get_local_state(sender).await?, + }; + + let app_state = state + .values() + .find(|value| match value { + AppState::Uint(uint_value) => uint_value.key_raw == key, + AppState::Bytes(bytes_value) => bytes_value.key_raw == key, + }) + .ok_or_else(|| AppClientError::ParamsBuilderError { + message: format!( + "The key {} could not be found in {} storage", + default.data, + match source { + StateSource::Global => "global", + StateSource::Local(_) => "local", + } + ), + })?; + + match app_state { + AppState::Uint(uint_value) => Ok(ABIValue::from(uint_value.value)), + AppState::Bytes(bytes_value) => Ok(value_type + .decode(&bytes_value.value_raw) + .map_err(|e| AppClientError::ABIError { source: e })?), + } + } + + /// Resolve a default value from various sources (method call, literal, state, or box). + pub async fn resolve_default_value( + &self, + default: &ABIDefaultValue, + value_type: &ABIType, + sender: &str, + ) -> Result { + let value_type = default.value_type.clone().unwrap_or(value_type.clone()); + + match default.source { + DefaultValueSource::Method => { + let method_signature = default.data.clone(); + let arc56_method = self + .client + .app_spec + .get_method(&method_signature) + .map_err(|e| AppClientError::ABIError { source: e })?; + + let method_call_params = AppClientMethodCallParams { + method: method_signature.clone(), + args: vec![AppMethodCallArg::DefaultValue; arc56_method.args.len()], + sender: Some(sender.to_string()), + ..Default::default() + }; + + let app_call_result = + Box::pin(self.client.send().call(method_call_params, None, None)).await?; + let abi_return = app_call_result.abi_return.ok_or_else(|| { + AppClientError::ParamsBuilderError { + message: "Method call did not return a value".to_string(), + } + })?; + + match abi_return.return_value { + None => Err(AppClientError::ParamsBuilderError { + message: "Method call did not return a value".to_string(), + }), + Some(return_value) => Ok(return_value), + } + } + DefaultValueSource::Literal => { + let value_bytes = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| AppClientError::ParamsBuilderError { + message: format!("Failed to decode base64 literal: {}", e), + })?; + Ok(value_type + .decode(&value_bytes) + .map_err(|e| AppClientError::ABIError { source: e })?) + } + DefaultValueSource::Global => { + self.resolve_state_value(default, &value_type, StateSource::Global) + .await + } + DefaultValueSource::Local => { + self.resolve_state_value(default, &value_type, StateSource::Local(sender)) + .await + } + DefaultValueSource::Box => { + let box_key = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| AppClientError::ParamsBuilderError { + message: format!("Failed to decode box key: {}", e), + })?; + let box_value = self.client.get_box_value(&box_key).await?; + Ok(value_type + .decode(&box_value) + .map_err(|e| AppClientError::ABIError { source: e })?) + } + } + } +} + +impl BareParamsBuilder<'_> { + /// Build parameters for a bare application call with the specified on-complete action. + pub fn call( + &self, + params: AppClientBareCallParams, + on_complete: Option, + ) -> Result { + self.build_bare_app_call_params(params, on_complete.unwrap_or(OnApplicationComplete::NoOp)) + } + + /// Build parameters for a bare application call with OptIn on-complete action. + pub fn opt_in(&self, params: AppClientBareCallParams) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::OptIn) + } + + /// Build parameters for a bare application call with CloseOut on-complete action. + pub fn close_out( + &self, + params: AppClientBareCallParams, + ) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::CloseOut) + } + + /// Build parameters for a bare application call with Delete on-complete action. + pub fn delete( + &self, + params: AppClientBareCallParams, + ) -> Result { + Ok(AppDeleteParams { + sender: self.client.get_sender_address(¶ms.sender)?, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to: get_optional_address(¶ms.rekey_to)?, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + app_id: self.client.app_id, + args: params.args, + account_references: super::utils::parse_account_refs_to_addresses( + ¶ms.account_references, + )?, + app_references: params.app_references, + asset_references: params.asset_references, + box_references: params.box_references, + }) + } + + /// Build parameters for a bare application call with ClearState on-complete action. + pub fn clear_state( + &self, + params: AppClientBareCallParams, + ) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::ClearState) + } + + /// Build parameters for updating the application using a bare application call. + pub async fn update( + &self, + params: AppClientBareCallParams, + compilation_params: Option, + ) -> Result { + // Compile programs (and populate AppManager cache/source maps) + let compilation_params = compilation_params.unwrap_or_default(); + let (approval_program, clear_state_program) = + self.client.compile(&compilation_params).await?; + + Ok(AppUpdateParams { + sender: self.client.get_sender_address(¶ms.sender)?, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to: get_optional_address(¶ms.rekey_to)?, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + app_id: self.client.app_id, + args: params.args, + account_references: super::utils::parse_account_refs_to_addresses( + ¶ms.account_references, + )?, + app_references: params.app_references, + asset_references: params.asset_references, + box_references: params.box_references, + approval_program, + clear_state_program, + }) + } + + fn build_bare_app_call_params( + &self, + params: AppClientBareCallParams, + on_complete: OnApplicationComplete, + ) -> Result { + Ok(AppCallParams { + sender: self.client.get_sender_address(¶ms.sender)?, + signer: self + .client + .resolve_signer(params.sender.clone(), params.signer.clone()), + rekey_to: get_optional_address(¶ms.rekey_to)?, + note: params.note.clone(), + lease: params.lease, + static_fee: params.static_fee, + extra_fee: params.extra_fee, + max_fee: params.max_fee, + validity_window: params.validity_window, + first_valid_round: params.first_valid_round, + last_valid_round: params.last_valid_round, + app_id: self.client.app_id, + on_complete, + args: params.args, + account_references: super::utils::parse_account_refs_to_addresses( + ¶ms.account_references, + )?, + app_references: params.app_references, + asset_references: params.asset_references, + box_references: params.box_references, + }) + } +} + +fn get_optional_address(value: &Option) -> Result, AppClientError> { + match value { + Some(s) => { + Ok(Some(Address::from_str(s).map_err(|e| { + AppClientError::TransactError { source: e } + })?)) + } + None => Ok(None), + } +} diff --git a/crates/algokit_utils/src/applications/app_client/sender.rs b/crates/algokit_utils/src/applications/app_client/sender.rs new file mode 100644 index 000000000..68bd44ad4 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/sender.rs @@ -0,0 +1,302 @@ +use crate::transactions::SendTransactionResult; +use crate::transactions::composer::SimulateParams; +use crate::{AppClientError, SendAppCallResult, SendParams}; +use algokit_transact::{MAX_SIMULATE_OPCODE_BUDGET, OnApplicationComplete}; + +use super::types::{AppClientBareCallParams, AppClientMethodCallParams, CompilationParams}; +use super::{AppClient, FundAppAccountParams}; + +pub struct TransactionSender<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +pub struct BareTransactionSender<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +impl<'app_client> TransactionSender<'app_client> { + /// Get the bare transaction sender. + pub fn bare(&self) -> BareTransactionSender<'app_client> { + BareTransactionSender { + client: self.client, + } + } + + /// Execute an ABI method call with the specified on-complete action. + pub async fn call( + &self, + params: AppClientMethodCallParams, + on_complete: Option, + send_params: Option, + ) -> Result { + let arc56_method = self + .client + .app_spec + .get_method(¶ms.method) + .map_err(|e| AppClientError::ABIError { source: e })?; + + let mut method_params = self.client.params().call(params, on_complete).await?; + + if method_params.on_complete == OnApplicationComplete::NoOp + && arc56_method.readonly == Some(true) + { + let transaction_composer_config = self.client.transaction_composer_config.clone(); + + let mut composer = self + .client + .algorand() + .new_group(transaction_composer_config.clone()); + + if transaction_composer_config + .clone() + .is_some_and(|c| c.cover_app_call_inner_transaction_fees) + && method_params.max_fee.is_some() + { + method_params.static_fee = method_params.max_fee; + method_params.extra_fee = None; + } + + let _ = composer + .add_app_call_method_call(method_params) + .map_err(|e| AppClientError::ComposerError { source: e }); + + let simulate_params = SimulateParams { + allow_unnamed_resources: Some( + transaction_composer_config + .map(|c| c.populate_app_call_resources.is_enabled()) + .unwrap_or(true), + ), + skip_signatures: true, + extra_opcode_budget: Some(MAX_SIMULATE_OPCODE_BUDGET), + ..Default::default() + }; + + let transactions_with_signers = composer + .build() + .await + .map_err(|e| AppClientError::ComposerError { source: e })?; + let transactions = transactions_with_signers + .iter() + .map(|tx_with_signer| tx_with_signer.transaction.clone()) + .collect(); + + let simulate_results = composer + .simulate(Some(simulate_params)) + .await + .map_err(|e| AppClientError::ComposerError { source: e })?; + + let group_id = simulate_results + .group + .map(hex::encode) + .unwrap_or_else(|| "".to_string()); + let abi_returns = simulate_results.abi_returns; + let last_abi_return = abi_returns.last().cloned(); + + let send_transaction_result = SendTransactionResult::new( + group_id, + simulate_results.transaction_ids, + transactions, + simulate_results.confirmations, + Some(abi_returns), + ) + .map_err(|e| AppClientError::TransactionResultError { source: e })?; + + let send_app_call_result = + SendAppCallResult::new(send_transaction_result, last_abi_return); + + Ok(send_app_call_result) + } else { + self.client + .algorand + .send() + .app_call_method_call(method_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + } + + /// Execute an ABI method call with OptIn on-complete action. + pub async fn opt_in( + &self, + params: AppClientMethodCallParams, + send_params: Option, + ) -> Result { + let method_params = self.client.params().opt_in(params).await?; + + self.client + .algorand + .send() + .app_call_method_call(method_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Execute an ABI method call with CloseOut on-complete action. + pub async fn close_out( + &self, + params: AppClientMethodCallParams, + send_params: Option, + ) -> Result { + let method_params = self.client.params().close_out(params).await?; + + self.client + .algorand + .send() + .app_call_method_call(method_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Execute an ABI method call with Delete on-complete action. + pub async fn delete( + &self, + params: AppClientMethodCallParams, + send_params: Option, + ) -> Result { + let delete_params = self.client.params().delete(params).await?; + + self.client + .algorand + .send() + .app_delete_method_call(delete_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Update the application using an ABI method call. + pub async fn update( + &self, + params: AppClientMethodCallParams, + compilation_params: Option, + send_params: Option, + ) -> Result { + let update_params = self + .client + .params() + .update(params, compilation_params) + .await?; + + self.client + .algorand + .send() + .app_update_method_call(update_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Send payment to fund the application's account. + pub async fn fund_app_account( + &self, + params: FundAppAccountParams, + send_params: Option, + ) -> Result { + let payment = self.client.params().fund_app_account(¶ms)?; + + self.client + .algorand + .send() + .payment(payment, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } +} + +impl BareTransactionSender<'_> { + /// Execute a bare application call with the specified on-complete action. + pub async fn call( + &self, + params: AppClientBareCallParams, + on_complete: Option, + send_params: Option, + ) -> Result { + let params = self.client.params().bare().call(params, on_complete)?; + self.client + .algorand + .send() + .app_call(params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Execute a bare application call with OptIn on-complete action. + pub async fn opt_in( + &self, + params: AppClientBareCallParams, + send_params: Option, + ) -> Result { + let app_call = self.client.params().bare().opt_in(params)?; + self.client + .algorand + .send() + .app_call(app_call, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Execute a bare application call with CloseOut on-complete action. + pub async fn close_out( + &self, + params: AppClientBareCallParams, + send_params: Option, + ) -> Result { + let app_call = self.client.params().bare().close_out(params)?; + self.client + .algorand + .send() + .app_call(app_call, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Execute a bare application call with Delete on-complete action. + pub async fn delete( + &self, + params: AppClientBareCallParams, + send_params: Option, + ) -> Result { + let delete_params = self.client.params().bare().delete(params)?; + self.client + .algorand + .send() + .app_delete(delete_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Execute a bare application call with ClearState on-complete action. + pub async fn clear_state( + &self, + params: AppClientBareCallParams, + send_params: Option, + ) -> Result { + let app_call = self.client.params().bare().clear_state(params)?; + self.client + .algorand + .send() + .app_call(app_call, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, true)) + } + + /// Update the application using a bare application call. + pub async fn update( + &self, + params: AppClientBareCallParams, + compilation_params: Option, + send_params: Option, + ) -> Result { + let update_params = self + .client + .params() + .bare() + .update(params, compilation_params) + .await?; + + self.client + .algorand + .send() + .app_update(update_params, send_params) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } +} diff --git a/crates/algokit_utils/src/applications/app_client/state_accessor.rs b/crates/algokit_utils/src/applications/app_client/state_accessor.rs new file mode 100644 index 000000000..1dabbbcbe --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/state_accessor.rs @@ -0,0 +1,417 @@ +use super::{AppClient, AppClientError}; +use crate::clients::app_manager::AppState; +use algokit_abi::arc56_contract::{ABIStorageKey, ABIStorageMap}; +use algokit_abi::{ABIType, ABIValue}; +use async_trait::async_trait; +use base64::Engine; +use num_bigint::BigUint; +use std::collections::HashMap; + +pub struct BoxStateAccessor<'app_client> { + client: &'app_client AppClient, +} + +pub struct StateAccessor<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +impl<'app_client> StateAccessor<'app_client> { + /// Create a new state accessor for the given app client. + pub fn new(client: &'app_client AppClient) -> Self { + Self { client } + } + + /// Get an accessor for the application's global state. + pub fn global_state(&self) -> AppStateAccessor<'_> { + let provider = GlobalStateProvider { + client: self.client, + }; + AppStateAccessor::new("global".to_string(), Box::new(provider)) + } + + /// Get an accessor for an account's local state with this application. + pub fn local_state(&self, address: &str) -> AppStateAccessor<'_> { + let provider = LocalStateProvider { + client: self.client, + address: address.to_string(), + }; + AppStateAccessor::new("local".to_string(), Box::new(provider)) + } + + /// Get an accessor for the application's box storage. + pub fn box_storage(&self) -> BoxStateAccessor<'app_client> { + BoxStateAccessor { + client: self.client, + } + } +} + +type GetStateResult = Result, AppState>, AppClientError>; + +#[async_trait(?Send)] +pub trait StateProvider { + async fn get_app_state(&self) -> GetStateResult; + fn get_storage_keys(&self) -> Result, AppClientError>; + fn get_storage_maps(&self) -> Result, AppClientError>; +} + +struct GlobalStateProvider<'app_client> { + client: &'app_client AppClient, +} + +#[async_trait(?Send)] +impl StateProvider for GlobalStateProvider<'_> { + async fn get_app_state(&self) -> GetStateResult { + self.client.get_global_state().await + } + + fn get_storage_keys(&self) -> Result, AppClientError> { + self.client + .app_spec + .get_global_abi_storage_keys() + .map_err(|e| AppClientError::ABIError { source: e }) + } + + fn get_storage_maps(&self) -> Result, AppClientError> { + self.client + .app_spec + .get_global_abi_storage_maps() + .map_err(|e| AppClientError::ABIError { source: e }) + } +} + +struct LocalStateProvider<'app_client> { + client: &'app_client AppClient, + address: String, +} + +#[async_trait(?Send)] +impl StateProvider for LocalStateProvider<'_> { + async fn get_app_state(&self) -> GetStateResult { + self.client.get_local_state(&self.address).await + } + + fn get_storage_keys(&self) -> Result, AppClientError> { + self.client + .app_spec + .get_local_abi_storage_keys() + .map_err(|e| AppClientError::ABIError { source: e }) + } + + fn get_storage_maps(&self) -> Result, AppClientError> { + self.client + .app_spec + .get_local_abi_storage_maps() + .map_err(|e| AppClientError::ABIError { source: e }) + } +} + +pub struct AppStateAccessor<'provider> { + name: String, + provider: Box, +} + +impl<'provider> AppStateAccessor<'provider> { + /// Create a new app state accessor with the given name and provider. + pub fn new(name: String, provider: Box) -> Self { + Self { name, provider } + } + + /// Get all ABI-decoded state values for this storage type. + pub async fn get_all(&self) -> Result>, AppClientError> { + let state = self.provider.get_app_state().await?; + let storage_key_map = self.provider.get_storage_keys()?; + + let mut result = HashMap::new(); + for (key_name, storage_key) in storage_key_map { + let abi_value = self.decode_storage_key(&key_name, &storage_key, &state)?; + result.insert(key_name, abi_value); + } + Ok(result) + } + + /// Get a specific ABI-decoded state value by key name. + pub async fn get_value(&self, key_name: &str) -> Result, AppClientError> { + let state = self.provider.get_app_state().await?; + let storage_key_map = self.provider.get_storage_keys()?; + + let storage_key = + storage_key_map + .get(key_name) + .ok_or_else(|| AppClientError::AppStateError { + message: format!("{} state key '{}' not found", self.name, key_name), + })?; + + self.decode_storage_key(key_name, storage_key, &state) + } + + fn decode_storage_key( + &self, + key_name: &str, + storage_key: &ABIStorageKey, + state: &HashMap, AppState>, + ) -> Result, AppClientError> { + let key_bytes = base64::engine::general_purpose::STANDARD + .decode(&storage_key.key) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode {} key '{}': {}", self.name, key_name, e), + })?; + + let value = state.get(&key_bytes); + + match value { + None => Ok(None), + Some(app_state) => Ok(Some(decode_app_state(&storage_key.value_type, app_state)?)), + } + } + + /// Get all key-value pairs from an ABI-defined state map. + pub async fn get_map( + &self, + map_name: &str, + ) -> Result, AppClientError> { + let state = self.provider.get_app_state().await?; + let storage_map_map = self.provider.get_storage_maps()?; + let storage_map = + storage_map_map + .get(map_name) + .ok_or_else(|| AppClientError::AppStateError { + message: format!("{} state map '{}' not found", self.name, map_name), + })?; + let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode map prefix: {}", e), + })? + } else { + Vec::new() + }; + + let mut result = HashMap::new(); + for (key, app_state) in state.iter() { + if !key.starts_with(&prefix_bytes) { + continue; + } + + let tail = &key[prefix_bytes.len()..]; + let decoded_key = storage_map + .key_type + .decode(tail) + .map_err(|e| AppClientError::ABIError { source: e })?; + + let decoded_value = decode_app_state(&storage_map.value_type, app_state)?; + result.insert(decoded_key, decoded_value); + } + + Ok(result) + } + + /// Get a specific value from an ABI-defined state map by key. + pub async fn get_map_value( + &self, + map_name: &str, + key: ABIValue, + ) -> Result, AppClientError> { + let state = self.provider.get_app_state().await?; + let storage_map_map = self.provider.get_storage_maps()?; + let storage_map = + storage_map_map + .get(map_name) + .ok_or_else(|| AppClientError::AppStateError { + message: format!("{} state map '{}' not found", self.name, map_name), + })?; + + let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode map prefix: {}", e), + })? + } else { + Vec::new() + }; + let encoded_key = storage_map + .key_type + .encode(&key) + .map_err(|e| AppClientError::ABIError { source: e })?; + let full_key = [prefix_bytes, encoded_key].concat(); + + let value = state.get(&full_key); + + match value { + None => Ok(None), + Some(app_state) => Ok(Some(decode_app_state(&storage_map.value_type, app_state)?)), + } + } +} + +impl BoxStateAccessor<'_> { + /// Get all ABI-decoded box values for this application. + pub async fn get_all(&self) -> Result, AppClientError> { + let box_storage_keys = self + .client + .app_spec + .get_box_abi_storage_keys() + .map_err(|e| AppClientError::ABIError { source: e })?; + let mut results: HashMap = HashMap::new(); + + for (box_name, storage_key) in box_storage_keys { + let box_name_bytes = base64::engine::general_purpose::STANDARD + .decode(&storage_key.key) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode box key '{}': {}", box_name, e), + })?; + + // TODO: what to do when it failed to fetch the box? + let box_value = self.client.get_box_value(&box_name_bytes).await?; + let abi_value = storage_key + .value_type + .decode(&box_value) + .map_err(|e| AppClientError::ABIError { source: e })?; + results.insert(box_name, abi_value); + } + + Ok(results) + } + + /// Get a specific ABI-decoded box value by name. + pub async fn get_value(&self, name: &str) -> Result { + let box_storage_keys = self + .client + .app_spec + .get_box_abi_storage_keys() + .map_err(|e| AppClientError::ABIError { source: e })?; + + let storage_key = + box_storage_keys + .get(name) + .ok_or_else(|| AppClientError::AppStateError { + message: format!("Box key '{}' not found", name), + })?; + + let box_name_bytes = base64::engine::general_purpose::STANDARD + .decode(&storage_key.key) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode box key '{}': {}", name, e), + })?; + + // TODO: what to do when it failed to fetch the box? + let box_value = self.client.get_box_value(&box_name_bytes).await?; + storage_key + .value_type + .decode(&box_value) + .map_err(|e| AppClientError::ABIError { source: e }) + } + + /// Get all key-value pairs from an ABI-defined box map. + pub async fn get_map( + &self, + map_name: &str, + ) -> Result, AppClientError> { + let storage_map_map = self + .client + .app_spec + .get_box_abi_storage_maps() + .map_err(|e| AppClientError::ABIError { source: e })?; + let storage_map = + storage_map_map + .get(map_name) + .ok_or_else(|| AppClientError::AppStateError { + message: format!("Box map '{}' not found", map_name), + })?; + + let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode map prefix: {}", e), + })? + } else { + Vec::new() + }; + + let box_names = self.client.get_box_names().await?; + let box_names = box_names + .iter() + .filter(|box_name| box_name.name_raw.starts_with(&prefix_bytes)) + .collect::>(); + + let mut results: HashMap = HashMap::new(); + for box_name in box_names { + let tail = &box_name.name_raw[prefix_bytes.len()..]; + let decoded_key = storage_map + .key_type + .decode(tail) + .map_err(|e| AppClientError::ABIError { source: e })?; + + let box_value = self.client.get_box_value(&box_name.name_raw).await?; + let decoded_value = storage_map + .value_type + .decode(&box_value) + .map_err(|e| AppClientError::ABIError { source: e })?; + results.insert(decoded_key, decoded_value); + } + + Ok(results) + } + + /// Get a specific value from an ABI-defined box map by key. + pub async fn get_map_value( + &self, + map_name: &str, + key: &ABIValue, + ) -> Result, AppClientError> { + let storage_map_map = self + .client + .app_spec + .get_box_abi_storage_maps() + .map_err(|e| AppClientError::ABIError { source: e })?; + let storage_map = + storage_map_map + .get(map_name) + .ok_or_else(|| AppClientError::AppStateError { + message: format!("Box map '{}' not found", map_name), + })?; + + let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| AppClientError::AppStateError { + message: format!("Failed to decode map prefix: {}", e), + })? + } else { + Vec::new() + }; + + let encoded_key = storage_map + .key_type + .encode(key) + .map_err(|e| AppClientError::ABIError { source: e })?; + let full_key = [prefix_bytes, encoded_key].concat(); + + let box_value = match self.client.get_box_value(&full_key).await { + Ok(val) => val, + Err(AppClientError::AppStateError { .. }) => return Ok(None), + Err(e) => return Err(e), + }; + + let decoded = storage_map + .value_type + .decode(&box_value) + .map_err(|e| AppClientError::ABIError { source: e })?; + Ok(Some(decoded)) + } +} + +fn decode_app_state( + value_type: &ABIType, + app_state: &AppState, +) -> Result { + match &app_state { + AppState::Uint(uint_app_state) => Ok(ABIValue::Uint(BigUint::from(uint_app_state.value))), + AppState::Bytes(bytes_app_state) => Ok(value_type + .decode(&bytes_app_state.value_raw) + .map_err(|e| AppClientError::ABIError { source: e })?), + } +} diff --git a/crates/algokit_utils/src/applications/app_client/transaction_builder.rs b/crates/algokit_utils/src/applications/app_client/transaction_builder.rs new file mode 100644 index 000000000..ce0f2c2db --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/transaction_builder.rs @@ -0,0 +1,232 @@ +use crate::AppClientError; +use algokit_transact::OnApplicationComplete; +use futures::TryFutureExt; + +use super::types::{AppClientBareCallParams, AppClientMethodCallParams, CompilationParams}; +use super::{AppClient, FundAppAccountParams}; + +pub struct TransactionBuilder<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +pub struct BareTransactionBuilder<'app_client> { + pub(crate) client: &'app_client AppClient, +} + +impl TransactionBuilder<'_> { + /// Get the bare transaction builder. + pub fn bare(&self) -> BareTransactionBuilder<'_> { + BareTransactionBuilder { + client: self.client, + } + } + + /// Create an unsigned ABI method call transaction with the specified on-complete action. + pub async fn call( + &self, + params: AppClientMethodCallParams, + on_complete: Option, + ) -> Result { + let params = self.client.params().call(params, on_complete).await?; + let trasactions = self + .client + .algorand + .create() + .app_call_method_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await?; + Ok(trasactions[0].clone()) + } + + /// Create an unsigned ABI method call transaction with OptIn on-complete action. + pub async fn opt_in( + &self, + params: AppClientMethodCallParams, + ) -> Result { + let params = self.client.params().opt_in(params).await?; + let trasactions = self + .client + .algorand + .create() + .app_call_method_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await?; + Ok(trasactions[0].clone()) + } + + /// Create an unsigned ABI method call transaction with CloseOut on-complete action. + pub async fn close_out( + &self, + params: AppClientMethodCallParams, + ) -> Result { + let params = self.client.params().close_out(params).await?; + let trasactions = self + .client + .algorand + .create() + .app_call_method_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await?; + Ok(trasactions[0].clone()) + } + + /// Create an unsigned ABI method call transaction with ClearState on-complete action. + pub async fn clear_state( + &self, + params: AppClientMethodCallParams, + ) -> Result { + let params = self.client.params().clear_state(params).await?; + let trasactions = self + .client + .algorand + .create() + .app_call_method_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await?; + Ok(trasactions[0].clone()) + } + + /// Create an unsigned ABI method call transaction with Delete on-complete action. + pub async fn delete( + &self, + params: AppClientMethodCallParams, + ) -> Result { + let params = self.client.params().delete(params).await?; + let trasactions = self + .client + .algorand + .create() + .app_delete_method_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await?; + Ok(trasactions[0].clone()) + } + + /// Create an unsigned application update transaction using an ABI method call. + pub async fn update( + &self, + params: AppClientMethodCallParams, + compilation_params: Option, + ) -> Result { + let params = self + .client + .params() + .update(params, compilation_params) + .await?; + let trasactions = self + .client + .algorand + .create() + .app_update_method_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await?; + Ok(trasactions[0].clone()) + } + + /// Create an unsigned payment transaction to fund the application's account. + pub async fn fund_app_account( + &self, + params: FundAppAccountParams, + ) -> Result { + let params = self.client.params().fund_app_account(¶ms)?; + self.client + .algorand + .create() + .payment(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } +} + +impl BareTransactionBuilder<'_> { + /// Create an unsigned bare application call transaction with the specified on-complete action. + pub async fn call( + &self, + params: AppClientBareCallParams, + on_complete: Option, + ) -> Result { + let params = self.client.params().bare().call(params, on_complete)?; + self.client + .algorand + .create() + .app_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } + + /// Create an unsigned bare application call transaction with OptIn on-complete action. + pub async fn opt_in( + &self, + params: AppClientBareCallParams, + ) -> Result { + let params = self.client.params().bare().opt_in(params)?; + self.client + .algorand + .create() + .app_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } + + /// Create an unsigned bare application call transaction with CloseOut on-complete action. + pub async fn close_out( + &self, + params: AppClientBareCallParams, + ) -> Result { + let params = self.client.params().bare().close_out(params)?; + self.client + .algorand + .create() + .app_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } + + /// Create an unsigned bare application call transaction with Delete on-complete action. + pub async fn delete( + &self, + params: AppClientBareCallParams, + ) -> Result { + let params = self.client.params().bare().delete(params)?; + self.client + .algorand + .create() + .app_delete(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } + + /// Create an unsigned bare application call transaction with ClearState on-complete action. + pub async fn clear_state( + &self, + params: AppClientBareCallParams, + ) -> Result { + let params = self.client.params().bare().clear_state(params)?; + self.client + .algorand + .create() + .app_call(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } + + /// Create an unsigned application update transaction using a bare application call. + pub async fn update( + &self, + params: AppClientBareCallParams, + compilation_params: Option, + ) -> Result { + let params: crate::AppUpdateParams = self + .client + .params() + .bare() + .update(params, compilation_params) + .await?; + self.client + .algorand + .create() + .app_update(params) + .map_err(|e| AppClientError::ComposerError { source: e }) + .await + } +} diff --git a/crates/algokit_utils/src/applications/app_client/types.rs b/crates/algokit_utils/src/applications/app_client/types.rs new file mode 100644 index 000000000..ad9f25175 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/types.rs @@ -0,0 +1,159 @@ +use crate::AlgorandClient; +use crate::clients::app_manager::TealTemplateValue; +use crate::transactions::TransactionComposerConfig; +use crate::transactions::TransactionSigner; +use crate::transactions::app_call::AppMethodCallArg; +use algokit_abi::Arc56Contract; +use algokit_transact::BoxReference; +use derive_more::Debug; +use std::collections::HashMap; +use std::sync::Arc; + +/// Container for source maps captured during compilation/simulation. +#[derive(Debug, Clone, Default)] +pub struct AppSourceMaps { + pub approval_source_map: Option, + pub clear_source_map: Option, +} + +/// Parameters required to construct an AppClient instance. +// Important: do NOT derive Clone for this struct while it contains `AlgorandClient`. +// `AlgorandClient` is intentionally non-Clone: it owns live HTTP clients, internal caches, +// and shared mutable state (e.g., signer registry via Arc>). Forcing Clone here +// would either require making `AlgorandClient` Clone or wrapping it in Arc implicitly, +// which encourages accidental copying of a process-wide client and confusing ownership/ +// lifetime semantics. If you need to share the client, wrap it in Arc at the call site +// and pass that explicitly, rather than deriving Clone on this params type. +pub struct AppClientParams { + pub app_id: u64, + pub app_spec: Arc56Contract, + pub algorand: AlgorandClient, + pub app_name: Option, + pub default_sender: Option, + pub default_signer: Option>, + pub source_maps: Option, + pub transaction_composer_config: Option, +} + +/// Parameters for funding an application's account. +#[derive(Debug, Clone, Default)] +pub struct FundAppAccountParams { + pub amount: u64, + pub sender: Option, + #[debug(skip)] + pub signer: Option>, + pub rekey_to: Option, + pub note: Option>, + pub lease: Option<[u8; 32]>, + pub static_fee: Option, + pub extra_fee: Option, + pub max_fee: Option, + pub validity_window: Option, + pub first_valid_round: Option, + pub last_valid_round: Option, + pub close_remainder_to: Option, +} + +/// Parameters for ABI method call operations +#[derive(Debug, Clone, Default)] +pub struct AppClientMethodCallParams { + pub method: String, + pub args: Vec, + pub sender: Option, + #[debug(skip)] + pub signer: Option>, + pub rekey_to: Option, + pub note: Option>, + pub lease: Option<[u8; 32]>, + pub static_fee: Option, + pub extra_fee: Option, + pub max_fee: Option, + pub validity_window: Option, + pub first_valid_round: Option, + pub last_valid_round: Option, + pub account_references: Option>, + pub app_references: Option>, + pub asset_references: Option>, + pub box_references: Option>, +} + +/// Parameters for bare (non-ABI) app call operations +#[derive(Debug, Clone, Default)] +pub struct AppClientBareCallParams { + pub args: Option>>, + pub sender: Option, + #[debug(skip)] + pub signer: Option>, + pub rekey_to: Option, + pub note: Option>, + pub lease: Option<[u8; 32]>, + pub static_fee: Option, + pub extra_fee: Option, + pub max_fee: Option, + pub validity_window: Option, + pub first_valid_round: Option, + pub last_valid_round: Option, + pub account_references: Option>, + pub app_references: Option>, + pub asset_references: Option>, + pub box_references: Option>, +} + +/// Enriched logic error details with source map information. +#[derive(Debug, Clone, Default)] +pub struct LogicError { + pub message: String, + pub program: Option>, + pub source_map: Option, + pub transaction_id: Option, + pub pc: Option, + pub line_no: Option, + pub lines: Option>, + pub traces: Option>, + /// Original logic error string if parsed + pub logic_error_str: Option, +} + +impl std::fmt::Display for LogicError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let tx = self.transaction_id.as_deref().unwrap_or("N/A"); + let pc = self + .pc + .map(|p| p.to_string()) + .unwrap_or_else(|| "N/A".to_string()); + let mut base = format!("Txn {} had error '{}' at PC {}", tx, self.message, pc); + if let Some(line) = self.line_no { + base.push_str(&format!(" and Source Line {}", line)); + } + writeln!(f, "{}", base)?; + if let Some(trace) = self.annotated_trace() { + write!(f, "{}", trace)?; + } + Ok(()) + } +} + +impl LogicError { + /// Build a simple annotated snippet string from stored lines and line number. + pub fn annotated_trace(&self) -> Option { + let lines = self.lines.as_ref()?; + let line_no = self.line_no? as usize; + let mut out = String::new(); + for entry in lines { + out.push_str(entry); + if entry.starts_with(&format!("{:>4} |", line_no)) { + out.push_str("\t<--- Error"); + } + out.push('\n'); + } + if out.is_empty() { None } else { Some(out) } + } +} + +/// Compilation configuration for update/compile flows +#[derive(Debug, Clone, Default)] +pub struct CompilationParams { + pub deploy_time_params: Option>, + pub updatable: Option, + pub deletable: Option, +} diff --git a/crates/algokit_utils/src/applications/app_client/utils.rs b/crates/algokit_utils/src/applications/app_client/utils.rs new file mode 100644 index 000000000..65ac9c069 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/utils.rs @@ -0,0 +1,56 @@ +use super::AppClient; +use super::error_transformation::extract_logic_error_data; +use crate::AppClientError; +use crate::transactions::TransactionSenderError; +use std::str::FromStr; + +fn contains_logic_error(s: &str) -> bool { + extract_logic_error_data(s).is_some() +} + +/// Transform transaction errors to include enhanced logic error details when applicable. +pub fn transform_transaction_error( + client: &AppClient, + err: TransactionSenderError, + is_clear_state_program: bool, +) -> AppClientError { + let err_str = err.to_string(); + if contains_logic_error(&err_str) { + // Only transform errors that are for this app (when app_id is known) + if client.app_id() != 0 { + let app_tag = format!("app={}", client.app_id()); + if !err_str.contains(&app_tag) { + return AppClientError::TransactionSenderError { source: err }; + } + } + let tx_err = crate::transactions::TransactionResultError::ParsingError { + message: err_str.clone(), + }; + let logic = client.expose_logic_error(&tx_err, is_clear_state_program); + return AppClientError::LogicError { + message: logic.message.clone(), + logic: Box::new(logic), + }; + } + + AppClientError::TransactionSenderError { source: err } +} + +/// Parse optional account reference strings into Address objects. +pub fn parse_account_refs_to_addresses( + account_refs: &Option>, +) -> Result>, AppClientError> { + match account_refs { + None => Ok(None), + Some(refs) => { + let mut result = Vec::with_capacity(refs.len()); + for s in refs { + result.push( + algokit_transact::Address::from_str(s) + .map_err(|e| AppClientError::TransactError { source: e })?, + ); + } + Ok(Some(result)) + } + } +} diff --git a/crates/algokit_utils/src/applications/mod.rs b/crates/algokit_utils/src/applications/mod.rs index f12da40ae..928796a90 100644 --- a/crates/algokit_utils/src/applications/mod.rs +++ b/crates/algokit_utils/src/applications/mod.rs @@ -1,3 +1,4 @@ +pub mod app_client; pub mod app_deployer; // Re-export commonly used client types diff --git a/crates/algokit_utils/src/clients/app_manager.rs b/crates/algokit_utils/src/clients/app_manager.rs index cc86b6087..41a23d176 100644 --- a/crates/algokit_utils/src/clients/app_manager.rs +++ b/crates/algokit_utils/src/clients/app_manager.rs @@ -31,22 +31,29 @@ pub struct CompiledTeal { pub compiled: String, pub compiled_hash: String, pub compiled_base64_to_bytes: Vec, - pub source_map: Option, + pub source_map: Option, // TODO: review this, relying on serde doesn't seem right } #[derive(Debug, Clone)] -pub struct AppState { +pub enum AppState { + Uint(UintAppState), + Bytes(BytesAppState), +} + +#[derive(Debug, Clone)] +pub struct UintAppState { pub key_raw: Vec, pub key_base64: String, - pub value_raw: Option>, - pub value_base64: Option, - pub value: AppStateValue, + pub value: u64, } #[derive(Debug, Clone)] -pub enum AppStateValue { - Uint(u64), - Bytes(String), +pub struct BytesAppState { + pub key_raw: Vec, + pub key_base64: String, + pub value_raw: Vec, + pub value_base64: String, + pub value: String, } #[derive(Debug, Clone)] @@ -160,8 +167,21 @@ impl AppManager { ) -> Result { let mut teal_code = Self::strip_teal_comments(teal_template_code); + // When deployment metadata is provided, avoid replacing + // TMPL_UPDATABLE/TMPL_DELETABLE via generic template variables; let the + // deploy-time control function handle them. if let Some(params) = template_params { - teal_code = Self::replace_template_variables(&teal_code, params)?; + let filtered_params: TealTemplateParams = if deployment_metadata.is_some() { + let mut clone = params.clone(); + clone.remove("UPDATABLE"); + clone.remove("DELETABLE"); + clone.remove("TMPL_UPDATABLE"); + clone.remove("TMPL_DELETABLE"); + clone + } else { + params.clone() + }; + teal_code = Self::replace_template_variables(&teal_code, &filtered_params)?; } if let Some(metadata) = deployment_metadata { @@ -282,19 +302,18 @@ impl AppManager { box_name: &BoxIdentifier, ) -> Result, AppManagerError> { let (_, name_bytes) = Self::get_box_reference(box_name); - let name_base64 = Base64.encode(&name_bytes); + // Algod expects goal-arg style encoding for box name query param in 'encoding:value'. + // However our HTTP client decodes base64 automatically into bytes for the Box model fields. + // The API still requires 'b64:' for the query parameter value. + let name_goal = format!("b64:{}", Base64.encode(&name_bytes)); let box_result = self .algod_client - .get_application_box_by_name(app_id, &name_base64) + .get_application_box_by_name(app_id, &name_goal) .await .map_err(|e| AppManagerError::AlgodClientError { source: e })?; - Base64 - .decode(&box_result.value) - .map_err(|e| AppManagerError::DecodingError { - message: e.to_string(), - }) + Ok(box_result.value) } /// Get values for multiple boxes. @@ -397,6 +416,42 @@ impl AppManager { (0, box_id.clone()) } + /// Helper function to ensure bytes are decoded from base64 if needed. + /// When using `Bytes` deserializer with JSON, base64 strings are not decoded + /// but kept as ASCII bytes of the base64 string. This function detects and fixes that. + fn ensure_decoded_bytes(bytes: &[u8]) -> Vec { + // Check if bytes could be a base64 string + if let Ok(s) = std::str::from_utf8(bytes) { + // Base64 strings have specific characteristics: + // - Length is multiple of 4 (with padding) or would be after padding + // - Contains base64 chars + // - Successfully decodes to different bytes than the original + if !s.is_empty() + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=') + { + // Additional check: base64 strings typically have = padding or specific length + let looks_like_base64 = s.contains('=') + || s.contains('+') + || s.contains('/') + || (s.len() % 4 == 0 && s.len() >= 8); // Reasonable minimum for base64 + + if looks_like_base64 { + // Try to decode as base64 + if let Ok(decoded) = Base64.decode(s) { + // Only return decoded if it's actually different + // (prevents treating plain text as base64) + if decoded != bytes { + return decoded; + } + } + } + } + } + // Already raw bytes or not valid base64 + bytes.to_vec() + } + /// Decode application state from raw format. /// Keys are decoded from base64 to Vec for binary data support, matching TypeScript UInt8Array typing. pub fn decode_app_state( @@ -413,20 +468,26 @@ impl AppManager { })?; // TODO(stabilization): Consider r#type pattern consistency across API vs ABI types (PR #229 comment) - let (value_raw, value_base64, value) = match state_val.value.r#type { + let app_state = match state_val.value.r#type { 1 => { - // Bytes - now already decoded from base64 by serde - let value_raw = state_val.value.bytes.clone(); + // Handle both cases: raw bytes (from msgpack) or base64 string bytes (from JSON with Bytes deserializer) + let value_raw = Self::ensure_decoded_bytes(&state_val.value.bytes); let value_base64 = Base64.encode(&value_raw); let value_str = String::from_utf8(value_raw.clone()) .unwrap_or_else(|_| hex::encode(&value_raw)); - ( - Some(value_raw), - Some(value_base64), - AppStateValue::Bytes(value_str), - ) + AppState::Bytes(BytesAppState { + key_raw: key_raw.clone(), + key_base64: Base64.encode(&key_raw), + value_raw, + value_base64, + value: value_str, + }) } - 2 => (None, None, AppStateValue::Uint(state_val.value.uint)), + 2 => AppState::Uint(UintAppState { + key_raw: key_raw.clone(), + key_base64: Base64.encode(&key_raw), + value: state_val.value.uint, + }), _ => { return Err(AppManagerError::DecodingError { message: format!("Unknown state data type: {}", state_val.value.r#type), @@ -434,16 +495,7 @@ impl AppManager { } }; - state_values.insert( - key_raw.clone(), - AppState { - key_raw: key_raw.clone(), - key_base64: Base64.encode(&key_raw), - value_raw, - value_base64, - value, - }, - ); + state_values.insert(key_raw.clone(), app_state); } Ok(state_values) diff --git a/crates/algokit_utils/src/config.rs b/crates/algokit_utils/src/config.rs new file mode 100644 index 000000000..4a04ab41e --- /dev/null +++ b/crates/algokit_utils/src/config.rs @@ -0,0 +1,85 @@ +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::sync::broadcast; + +/// Minimal lifecycle event types +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EventType { + /// Emitted when an app is compiled (for source map capture) + AppCompiled, + /// Emitted when a transaction group is simulated (for AVM traces) + TxnGroupSimulated, +} + +/// Minimal event payloads +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppCompiledEventData { + pub app_name: Option, + pub approval_source_map: Option, + pub clear_source_map: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxnGroupSimulatedEventData { + pub simulate_response: serde_json::Value, +} + +#[derive(Debug, Clone)] +pub enum EventData { + AppCompiled(AppCompiledEventData), + TxnGroupSimulated(TxnGroupSimulatedEventData), +} + +/// Async event emitter using Tokio broadcast +#[derive(Clone)] +pub struct AsyncEventEmitter { + sender: broadcast::Sender<(EventType, EventData)>, +} + +impl AsyncEventEmitter { + pub fn new(buffer: usize) -> Self { + let (sender, _receiver) = broadcast::channel(buffer); + Self { sender } + } + + pub fn subscribe(&self) -> broadcast::Receiver<(EventType, EventData)> { + self.sender.subscribe() + } + + pub async fn emit(&self, event_type: EventType, data: EventData) { + // Ignore error if there are no subscribers + let _ = self.sender.send((event_type, data)); + } +} + +/// Global flags and event emitter +static DEBUG: AtomicBool = AtomicBool::new(false); +static TRACE_ALL: AtomicBool = AtomicBool::new(false); +static EVENTS: Lazy = Lazy::new(|| AsyncEventEmitter::new(32)); + +/// Global runtime config singleton +pub struct Config; + +impl Config { + pub fn debug() -> bool { + DEBUG.load(Ordering::Relaxed) + } + + pub fn trace_all() -> bool { + TRACE_ALL.load(Ordering::Relaxed) + } + + pub fn events() -> AsyncEventEmitter { + EVENTS.clone() + } + + pub fn configure(new_debug: Option, new_trace_all: Option) { + if let Some(d) = new_debug { + DEBUG.store(d, Ordering::Relaxed); + } + if let Some(t) = new_trace_all { + TRACE_ALL.store(t, Ordering::Relaxed); + } + } +} diff --git a/crates/algokit_utils/src/lib.rs b/crates/algokit_utils/src/lib.rs index 6abdeaf06..14f966455 100644 --- a/crates/algokit_utils/src/lib.rs +++ b/crates/algokit_utils/src/lib.rs @@ -1,5 +1,6 @@ pub mod applications; pub mod clients; +pub mod config; pub mod transactions; // Re-exports for clean UniFFI surface @@ -23,3 +24,6 @@ pub use transactions::{ TransactionResultError, TransactionSender, TransactionSenderError, TransactionSigner, TransactionWithSigner, }; + +pub use applications::app_client::{AppClient, AppClientError, AppClientParams, AppSourceMaps}; +pub use config::{Config, EventType}; diff --git a/crates/algokit_utils/src/transactions/app_call.rs b/crates/algokit_utils/src/transactions/app_call.rs index 8f155bbd3..68986f81b 100644 --- a/crates/algokit_utils/src/transactions/app_call.rs +++ b/crates/algokit_utils/src/transactions/app_call.rs @@ -21,7 +21,10 @@ use std::str::FromStr; pub enum AppMethodCallArg { ABIValue(ABIValue), ABIReference(ABIReferenceValue), - // TODO: default value will be handled in another PR when ARC56 is fully supported + /// Sentinel to request ARC-56 default resolution for this argument (handled by AppClient params builder) + DefaultValue, + /// Placeholder for a transaction-typed argument. Not encoded; satisfied by a transaction + /// included in the same group (extracted from other method call arguments). TransactionPlaceholder, Transaction(Transaction), TransactionWithSigner(TransactionWithSigner), diff --git a/crates/algokit_utils/src/transactions/composer.rs b/crates/algokit_utils/src/transactions/composer.rs index a0fe13bdb..48c08f4b0 100644 --- a/crates/algokit_utils/src/transactions/composer.rs +++ b/crates/algokit_utils/src/transactions/composer.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::{ genesis_id_is_localnet, transactions::{ @@ -8,6 +9,7 @@ use crate::{ payment::{build_account_close, build_payment}, }, }; +use algod_client::models::SimulateTransaction; use algod_client::{ AlgodClient, apis::{Error as AlgodError, Format}, @@ -157,6 +159,26 @@ pub struct SendTransactionComposerResults { pub abi_returns: Vec, } +#[derive(Debug, Clone, Default)] +pub struct SimulateParams { + pub allow_more_logging: Option, + pub allow_empty_signatures: Option, + pub allow_unnamed_resources: Option, + pub extra_opcode_budget: Option, + pub exec_trace_config: Option, + pub simulation_round: Option, + pub skip_signatures: bool, +} + +#[derive(Debug, Clone)] +pub struct SimulateComposerResults { + pub group: Option, + pub transaction_ids: Vec, + pub confirmations: Vec, + pub abi_returns: Vec, + pub simulate_response: SimulateTransaction, +} + #[derive(Debug, Clone, Default)] pub struct TransactionComposerConfig { pub cover_app_call_inner_transaction_fees: bool, @@ -621,6 +643,7 @@ impl Composer { args: &[AppMethodCallArg], transaction: ComposerTransaction, ) -> Result<(), ComposerError> { + let starting_index = self.transactions.len(); let mut composer_transactions = Self::extract_composer_transactions_from_app_method_call_params(args); composer_transactions.push(transaction); @@ -629,7 +652,65 @@ impl Composer { return Err(ComposerError::GroupSizeError); } - for composer_transaction in composer_transactions { + for (offset, composer_transaction) in composer_transactions.into_iter().enumerate() { + // If this is a method call with a signer set, attach it directly to preceding bare txn args + let maybe_signer: Option> = match &composer_transaction { + ComposerTransaction::AppCallMethodCall(p) => p.signer.clone(), + ComposerTransaction::AppCreateMethodCall(p) => p.signer.clone(), + ComposerTransaction::AppUpdateMethodCall(p) => p.signer.clone(), + ComposerTransaction::AppDeleteMethodCall(p) => p.signer.clone(), + _ => None, + }; + if let Some(signer) = maybe_signer { + let end_exclusive = starting_index + offset; + for idx in starting_index..end_exclusive { + match self.transactions.get_mut(idx) { + Some(ComposerTransaction::Transaction(tx)) => { + // Upgrade to TransactionWithSigner if not already signed + let tx_clone = tx.clone(); + *self.transactions.get_mut(idx).unwrap() = + ComposerTransaction::TransactionWithSigner(TransactionWithSigner { + transaction: tx_clone, + signer: signer.clone(), + }); + } + Some(other) => { + let signer_slot = match other { + ComposerTransaction::Payment(p) => Some(&mut p.signer), + ComposerTransaction::AccountClose(p) => Some(&mut p.signer), + ComposerTransaction::AssetTransfer(p) => Some(&mut p.signer), + ComposerTransaction::AssetOptIn(p) => Some(&mut p.signer), + ComposerTransaction::AssetOptOut(p) => Some(&mut p.signer), + ComposerTransaction::AssetClawback(p) => Some(&mut p.signer), + ComposerTransaction::AssetCreate(p) => Some(&mut p.signer), + ComposerTransaction::AssetConfig(p) => Some(&mut p.signer), + ComposerTransaction::AssetDestroy(p) => Some(&mut p.signer), + ComposerTransaction::AssetFreeze(p) => Some(&mut p.signer), + ComposerTransaction::AssetUnfreeze(p) => Some(&mut p.signer), + ComposerTransaction::AppCall(p) => Some(&mut p.signer), + ComposerTransaction::AppCreateCall(p) => Some(&mut p.signer), + ComposerTransaction::AppUpdateCall(p) => Some(&mut p.signer), + ComposerTransaction::AppDeleteCall(p) => Some(&mut p.signer), + ComposerTransaction::OnlineKeyRegistration(p) => { + Some(&mut p.signer) + } + ComposerTransaction::OfflineKeyRegistration(p) => { + Some(&mut p.signer) + } + ComposerTransaction::NonParticipationKeyRegistration(p) => { + Some(&mut p.signer) + } + _ => None, + }; + if let Some(slot) = signer_slot { + slot.get_or_insert_with(|| signer.clone()); + } + } + _ => {} + } + } + } + self.push(composer_transaction)?; } @@ -1858,11 +1939,18 @@ impl Composer { .enumerate() .map(|(group_index, txn)| { let ctxn = &self.transactions[group_index]; - let signer = if let Some(transaction_signer) = ctxn.signer() { - transaction_signer - } else { - let sender_address = txn.header().sender.clone(); - (self.signer_getter)(sender_address.clone())? + let signer = match ctxn { + ComposerTransaction::TransactionWithSigner(tx_with_signer) => { + tx_with_signer.signer.clone() + } + _ => { + if let Some(transaction_signer) = ctxn.signer() { + transaction_signer + } else { + let sender_address = txn.header().sender.clone(); + (self.signer_getter)(sender_address.clone())? + } + } }; Ok(TransactionWithSigner { transaction: txn, @@ -2101,6 +2189,103 @@ impl Composer { pub fn count(&self) -> usize { self.transactions.len() } + + pub async fn simulate( + &mut self, + simulate_params: Option, + ) -> Result { + let simulate_params = simulate_params.unwrap_or_default(); + + self.build().await?; + + let transactions_with_signers = + self.built_group.as_ref().ok_or(ComposerError::StateError { + message: "No transactions available".to_string(), + })?; + + let group = transactions_with_signers[0].transaction.header().group; + + let signed_transactions: Vec = match simulate_params.skip_signatures { + true => transactions_with_signers + .iter() + .map(|txn_with_signer| SignedTransaction { + transaction: txn_with_signer.transaction.clone(), + signature: Some(EMPTY_SIGNATURE), + auth_address: None, + multisignature: None, + }) + .collect(), + false => self.gather_signatures().await?.to_vec(), + }; + + let transaction_ids: Vec = signed_transactions + .iter() + .map(|txn| txn.id()) + .collect::, _>>()?; + + let txn_group = SimulateRequestTransactionGroup { + txns: signed_transactions, + }; + let simulate_request = SimulateRequest { + txn_groups: vec![txn_group], + round: simulate_params.simulation_round, + allow_empty_signatures: if Config::debug() || simulate_params.skip_signatures { + Some(true) + } else { + simulate_params.allow_empty_signatures + }, + allow_more_logging: simulate_params.allow_more_logging, + allow_unnamed_resources: simulate_params.allow_unnamed_resources, + extra_opcode_budget: simulate_params.extra_opcode_budget, + exec_trace_config: simulate_params.exec_trace_config, + fix_signers: Some(true), + }; + + // Call simulate endpoint + let simulate_response = self + .algod_client + .simulate_transaction(simulate_request, Some(Format::Msgpack)) + .await + .map_err(|e| ComposerError::AlgodClientError { source: e })?; + + let simulated_group_result = &simulate_response.txn_groups[0]; + + if let Some(failure_message) = &simulated_group_result.failure_message { + let failed_at = simulated_group_result + .failed_at + .as_ref() + .map(|v| { + v.iter() + .map(|i| i.to_string()) + .collect::>() + .join(", ") + }) + .unwrap_or_else(|| "unknown".to_string()); + return Err(ComposerError::TransactionError { + message: format!( + "Transaction failed at transaction(s) {} in the group. {}", + failed_at, failure_message + ), + }); + } + + // Collect confirmations and ABI returns similar to send() + let confirmations: Vec = simulated_group_result + .txn_results + .iter() + .map(|r| r.txn_result.clone()) + .collect(); + + let abi_returns = self.parse_abi_return_values(&confirmations); + + Ok(SimulateComposerResults { + group, + transaction_ids, + confirmations, + abi_returns, + simulate_response, + }) + } } #[cfg(test)] diff --git a/crates/algokit_utils/src/transactions/sender.rs b/crates/algokit_utils/src/transactions/sender.rs index 2480027d9..8751648f4 100644 --- a/crates/algokit_utils/src/transactions/sender.rs +++ b/crates/algokit_utils/src/transactions/sender.rs @@ -20,7 +20,7 @@ use crate::{ transactions::TransactionComposerConfig, }; use algod_client::apis::AlgodApiError; -use algokit_abi::{ABIMethod, ABIReturn}; +use algokit_abi::ABIMethod; use algokit_transact::Address; use snafu::Snafu; @@ -197,9 +197,9 @@ impl TransactionSender { mut composer: Composer, send_params: Option, ) -> Result { - let built_transactions = composer.build().await?; + let transactions_with_signers = composer.build().await?; - let raw_transactions: Vec = built_transactions + let transactions: Vec = transactions_with_signers .iter() .map(|tx_with_signer| tx_with_signer.transaction.clone()) .collect(); @@ -221,7 +221,7 @@ impl TransactionSender { let result = SendTransactionResult::new( group_id, composer_results.transaction_ids, - raw_transactions, + transactions, composer_results.confirmations, abi_returns, )?; @@ -263,47 +263,6 @@ impl TransactionSender { transform_result(base_result) } - /// Extract ABI return from transaction result using app manager for enhanced processing. - /// - /// This method takes a transaction result and method parameter to extract and parse - /// ABI return values with proper type information from the app manager. - /// - /// # Arguments - /// * `result` - The transaction result containing potential ABI returns - /// * `params` - Parameters containing the method definition for ABI processing - /// - /// # Returns - /// * `Option` - The processed ABI return if available and valid - fn extract_abi_return_from_result( - &self, - result: &SendTransactionResult, - params: &impl HasMethod, - ) -> Option { - // Get the last ABI return from the result (most recent transaction) - let abi_return = result.abi_returns.as_ref()?.last()?.clone(); - - // Use app manager to enhance the ABI return processing - let method = params.method(); - - // If the method has a return type, validate and enhance the return - if method.returns.is_some() { - // Use app manager static method to parse the return value with proper method information - match AppManager::get_abi_return(&abi_return.raw_return_value, method) { - Some(parsed) => { - // Return enhanced ABIReturn with validated parsing - Some(parsed) - } - None => { - // Method has no return type - Some(abi_return) - } - } - } else { - // Method has no return type, return as-is - Some(abi_return) - } - } - /// Extract compilation metadata for TEAL programs using app manager caching. fn extract_compilation_metadata( &self, @@ -581,13 +540,15 @@ impl TransactionSender { params: AppCallMethodCallParams, send_params: Option, ) -> Result { - let params_clone = params.clone(); self.send_single_transaction_with_result( send_params, |composer| composer.add_app_call_method_call(params), |base_result| { - // Extract ABI return using helper method for enhanced processing - let abi_return = self.extract_abi_return_from_result(&base_result, ¶ms_clone); + let abi_return = base_result + .abi_returns + .as_ref() + .and_then(|returns| returns.last()) + .cloned(); Ok(SendAppCallResult::new(base_result, abi_return)) }, ) @@ -602,14 +563,16 @@ impl TransactionSender { ) -> Result { // Extract compilation metadata using helper method let (compiled_approval, compiled_clear) = self.extract_compilation_metadata(¶ms); - let params_clone = params.clone(); self.send_single_transaction_with_result( send_params, |composer| composer.add_app_create_method_call(params), |base_result| { - // Extract ABI return using helper method for enhanced processing - let abi_return = self.extract_abi_return_from_result(&base_result, ¶ms_clone); + let abi_return = base_result + .abi_returns + .as_ref() + .and_then(|returns| returns.last()) + .cloned(); // Convert CompiledTeal to Vec for the result let approval_bytes = compiled_approval.map(|ct| ct.compiled_base64_to_bytes); @@ -630,15 +593,16 @@ impl TransactionSender { ) -> Result { // Extract compilation metadata using helper method let (compiled_approval, compiled_clear) = self.extract_compilation_metadata(¶ms); - let params_clone = params.clone(); self.send_single_transaction_with_result( send_params, |composer| composer.add_app_update_method_call(params), |base_result| { - // Extract ABI return using helper method for enhanced processing - let abi_return = self.extract_abi_return_from_result(&base_result, ¶ms_clone); - + let abi_return = base_result + .abi_returns + .as_ref() + .and_then(|returns| returns.last()) + .cloned(); // Convert CompiledTeal to Vec for the result let approval_bytes = compiled_approval.map(|ct| ct.compiled_base64_to_bytes); let clear_bytes = compiled_clear.map(|ct| ct.compiled_base64_to_bytes); @@ -660,13 +624,15 @@ impl TransactionSender { params: AppDeleteMethodCallParams, send_params: Option, ) -> Result { - let params_clone = params.clone(); self.send_single_transaction_with_result( send_params, |composer| composer.add_app_delete_method_call(params), |base_result| { - // Extract ABI return using helper method for enhanced processing - let abi_return = self.extract_abi_return_from_result(&base_result, ¶ms_clone); + let abi_return = base_result + .abi_returns + .as_ref() + .and_then(|returns| returns.last()) + .cloned(); Ok(SendAppCallResult::new(base_result, abi_return)) }, ) diff --git a/crates/algokit_utils/tests/applications/app_client/client_management.rs b/crates/algokit_utils/tests/applications/app_client/client_management.rs new file mode 100644 index 000000000..eaba54898 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/client_management.rs @@ -0,0 +1,146 @@ +use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture, deploy_arc56_contract}; +use algokit_abi::{ABIValue, Arc56Contract}; +use algokit_transact::{OnApplicationComplete, StateSchema}; +use algokit_utils::applications::app_client::{AppClient, AppClientMethodCallParams}; +use algokit_utils::clients::app_manager::AppManager; +use algokit_utils::{AlgorandClient as RootAlgorandClient, AppCreateParams, AppMethodCallArg}; +use rstest::*; +use std::collections::HashMap; +use std::sync::Arc; + +fn get_sandbox_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::sandbox::APPLICATION_ARC56) + .expect("valid arc56") +} + +fn get_hello_world_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::hello_world::APPLICATION_ARC56) + .expect("valid arc56") +} + +#[rstest] +#[tokio::test] +async fn from_network_resolves_id(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let spec = get_hello_world_spec(); + let app_id = deploy_arc56_contract(&fixture, &sender, &spec, None, None, None).await?; + + let mut spec_with_networks = spec.clone(); + spec_with_networks.networks = Some(HashMap::from([( + "localnet".to_string(), + algokit_abi::arc56_contract::Network { app_id }, + )])); + + let client = AppClient::from_network( + spec_with_networks, + RootAlgorandClient::default_localnet(None), + None, + None, + None, + None, + None, + ) + .await?; + + assert_eq!(client.app_id(), app_id); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn from_creator_and_name_resolves_and_can_call( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let spec = get_sandbox_spec(); + let src = spec.source.as_ref().expect("source expected"); + let approval_teal = src.get_decoded_approval().expect("approval"); + let clear_teal = src.get_decoded_clear().expect("clear"); + + let app_manager: &AppManager = fixture.algorand_client.app(); + let compiled_approval = app_manager.compile_teal(&approval_teal).await?; + let compiled_clear = app_manager.compile_teal(&clear_teal).await?; + + let app_name = "MY_APP".to_string(); + let deploy_note = format!( + "{}:j{}", + "ALGOKIT_DEPLOYER", + serde_json::json!({ + "name": app_name, + "version": "1.0", + "updatable": false, + "deletable": false + }) + ); + + let create_params = AppCreateParams { + sender: sender.clone(), + on_complete: OnApplicationComplete::NoOp, + approval_program: compiled_approval.compiled_base64_to_bytes.clone(), + clear_state_program: compiled_clear.compiled_base64_to_bytes.clone(), + global_state_schema: Some(StateSchema { + num_uints: spec.state.schema.global_state.ints, + num_byte_slices: spec.state.schema.global_state.bytes, + }), + local_state_schema: Some(StateSchema { + num_uints: spec.state.schema.local_state.ints, + num_byte_slices: spec.state.schema.local_state.bytes, + }), + note: Some(deploy_note.into_bytes()), + ..Default::default() + }; + + let mut composer = fixture.algorand_client.new_group(None); + composer.add_app_create(create_params)?; + let create_group = composer.send(None).await?; + let app_id = create_group.confirmations[0] + .app_id + .expect("No app ID returned"); + + fixture + .wait_for_indexer_transaction(&create_group.transaction_ids[0]) + .await?; + + let algorand = RootAlgorandClient::default_localnet(None); + let client = AppClient::from_creator_and_name( + &sender.to_string(), + &app_name, + spec.clone(), + algorand, + Some(sender.to_string()), + Some(Arc::new(fixture.test_account.clone())), + None, + None, + None, + ) + .await?; + + assert_eq!(client.app_id(), app_id); + assert_eq!(client.app_name(), Some(&app_name)); + + let res = client + .send() + .call( + AppClientMethodCallParams { + method: "hello_world".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from("test"))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let abi_ret = res.abi_return.as_ref().expect("abi return"); + match &abi_ret.return_value { + Some(ABIValue::String(s)) => assert_eq!(s, "Hello, test"), + _ => return Err("expected string return".into()), + } + + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/app_client/compilation.rs b/crates/algokit_utils/tests/applications/app_client/compilation.rs new file mode 100644 index 000000000..39426d78e --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/compilation.rs @@ -0,0 +1,52 @@ +use crate::common::TestResult; +use crate::common::app_fixture::testing_app_fixture; +use algokit_utils::applications::app_client::CompilationParams; +use algokit_utils::clients::app_manager::TealTemplateValue; +use algokit_utils::config::{AppCompiledEventData, EventData, EventType}; +use rstest::*; + +#[rstest] +#[tokio::test] +async fn compile_applies_template_params_and_emits_event( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + // Use an app name to assert AppCompiled event has a name + let f = testing_app_fixture.await?; + algokit_utils::config::Config::configure(Some(true), None); + let mut events = algokit_utils::config::Config::events().subscribe(); + let client = f.client; + + let compilation_params = CompilationParams { + deploy_time_params: Some( + [("VALUE", 1), ("UPDATABLE", 0), ("DELETABLE", 0)] + .into_iter() + .map(|(k, v)| (k.to_string(), TealTemplateValue::Int(v))) + .collect(), + ), + updatable: Some(false), + deletable: Some(false), + }; + client.compile(&compilation_params).await?; + + if let Ok((event_type, data)) = + tokio::time::timeout(std::time::Duration::from_millis(5000), events.recv()).await? + { + assert_eq!(event_type, EventType::AppCompiled); + match data { + EventData::AppCompiled(AppCompiledEventData { + app_name, + approval_source_map, + clear_source_map, + }) => { + assert!(app_name.is_none() || app_name.as_deref() == Some("TestingApp")); + assert!(approval_source_map.is_some()); + assert!(clear_source_map.is_some()); + } + _ => return Err("unexpected event data".into()), + } + } else { + return Err("expected AppCompiled event".into()); + } + + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/app_client/default_values.rs b/crates/algokit_utils/tests/applications/app_client/default_values.rs new file mode 100644 index 000000000..70c363f8c --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/default_values.rs @@ -0,0 +1,292 @@ +use crate::common::TestResult; +use crate::common::app_fixture::testing_app_fixture; +use algokit_abi::ABIValue; +use algokit_utils::AppMethodCallArg; +use algokit_utils::applications::app_client::AppClientMethodCallParams; +use num_bigint::BigUint; +use rstest::*; + +#[rstest] +#[tokio::test] +async fn test_default_value_from_literal( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let client = f.client; + let sender = f.sender_address; + + let defined = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from("defined value"))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let defined_ret = defined + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match defined_ret { + ABIValue::String(s) => assert_eq!(s, "defined value"), + _ => return Err("Expected string return".into()), + } + + let defaulted = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value".to_string(), + args: vec![AppMethodCallArg::DefaultValue], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let default_ret = defaulted + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match default_ret { + ABIValue::String(s) => assert_eq!(s, "default value"), + _ => return Err("Expected string return".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_default_value_from_method( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let client = f.client; + let sender = f.sender_address; + + let defined = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value_from_abi".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from("defined value"))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let defined_ret = defined + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match defined_ret { + ABIValue::String(s) => assert_eq!(s, "ABI, defined value"), + _ => return Err("Expected string return".into()), + } + + let defaulted = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value_from_abi".to_string(), + args: vec![AppMethodCallArg::DefaultValue], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let default_ret = defaulted + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match default_ret { + ABIValue::String(s) => assert_eq!(s, "ABI, default value"), + _ => return Err("Expected string return".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_default_value_from_global_state( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let client = f.client; + let sender = f.sender_address; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_global".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(456u64)), + AppMethodCallArg::ABIValue(ABIValue::from(2u64)), + AppMethodCallArg::ABIValue(ABIValue::from("asdf")), + AppMethodCallArg::ABIValue(ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ])), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let defined = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value_from_global_state".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from(123u64))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let defined_ret = defined + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match defined_ret { + ABIValue::Uint(v) => assert_eq!(v, BigUint::from(123u64)), + _ => return Err("Expected uint return".into()), + } + + let defaulted = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value_from_global_state".to_string(), + args: vec![AppMethodCallArg::DefaultValue], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let default_ret = defaulted + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match default_ret { + ABIValue::Uint(v) => assert_eq!(v, BigUint::from(456u64)), + _ => return Err("Expected uint return".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_default_value_from_local_state( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let client = f.client; + let sender = f.sender_address; + + client + .send() + .opt_in( + AppClientMethodCallParams { + method: "opt_in".to_string(), + args: vec![], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_local".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(1u64)), + AppMethodCallArg::ABIValue(ABIValue::from(2u64)), + AppMethodCallArg::ABIValue(ABIValue::from("banana")), + AppMethodCallArg::ABIValue(ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ])), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let defined = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value_from_local_state".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from("defined value"))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let defined_ret = defined + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match defined_ret { + ABIValue::String(s) => assert_eq!(s, "Local state, defined value"), + _ => return Err("Expected string return".into()), + } + + let defaulted = client + .send() + .call( + AppClientMethodCallParams { + method: "default_value_from_local_state".to_string(), + args: vec![AppMethodCallArg::DefaultValue], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + let default_ret = defaulted + .abi_return + .and_then(|r| r.return_value) + .expect("Expected ABI return value"); + match default_ret { + ABIValue::String(s) => assert_eq!(s, "Local state, banana"), + _ => return Err("Expected string return".into()), + } + + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/app_client/error_handling.rs b/crates/algokit_utils/tests/applications/app_client/error_handling.rs new file mode 100644 index 000000000..e87319b69 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/error_handling.rs @@ -0,0 +1,36 @@ +use crate::common::TestResult; +use crate::common::app_fixture::testing_app_fixture; +use algokit_utils::applications::app_client::{AppClientError, AppClientMethodCallParams}; +use rstest::*; + +#[rstest] +#[tokio::test] +async fn test_exposing_logic_error_without_sourcemaps( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + let error_response = client + .send() + .call( + AppClientMethodCallParams { + method: "error".to_string(), + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await + .expect_err("expected logic error"); + + if let AppClientError::LogicError { logic, .. } = &error_response { + assert!(logic.message.contains("assert failed pc=885")); + } + + Ok(()) +} + +// NOTE: more comprehensive version with source maps will be added in app factory pr diff --git a/crates/algokit_utils/tests/applications/app_client/mod.rs b/crates/algokit_utils/tests/applications/app_client/mod.rs new file mode 100644 index 000000000..2b1242fe9 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/mod.rs @@ -0,0 +1,8 @@ +pub mod client_management; +pub mod compilation; +pub mod default_values; +pub mod error_handling; +pub mod params; +pub mod send; +pub mod state; +pub mod structs; diff --git a/crates/algokit_utils/tests/applications/app_client/params.rs b/crates/algokit_utils/tests/applications/app_client/params.rs new file mode 100644 index 000000000..cd27e08bf --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/params.rs @@ -0,0 +1,216 @@ +use crate::common::TestResult; +use crate::common::app_fixture::{sandbox_app_fixture, testing_app_fixture}; +use algokit_abi::ABIValue; +use algokit_transact::BoxReference; +use algokit_utils::applications::app_client::AppClientBareCallParams; +use algokit_utils::applications::app_client::{AppClientMethodCallParams, FundAppAccountParams}; +use algokit_utils::{AppMethodCallArg, PaymentParams}; +use rstest::*; + +#[rstest] +#[tokio::test] +async fn params_build_method_call_and_defaults( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_global".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(999u64)), + AppMethodCallArg::ABIValue(ABIValue::from(2u64)), + AppMethodCallArg::ABIValue(ABIValue::from("seed")), + AppMethodCallArg::ABIValue(ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ])), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + client + .send() + .opt_in( + AppClientMethodCallParams { + method: "opt_in".to_string(), + args: vec![], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + client + .send() + .call( + AppClientMethodCallParams { + method: "set_local".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(1u64)), + AppMethodCallArg::ABIValue(ABIValue::from(2u64)), + AppMethodCallArg::ABIValue(ABIValue::from("bananas")), + AppMethodCallArg::ABIValue(ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ])), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let built = client + .params() + .call( + AppClientMethodCallParams { + method: "default_value_from_local_state".to_string(), + args: vec![AppMethodCallArg::DefaultValue], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + assert_eq!(built.method.name, "default_value_from_local_state"); + assert_eq!(built.args.len(), 1); + match &built.args[0] { + AppMethodCallArg::ABIValue(ABIValue::String(s)) => assert_eq!(s, "bananas"), + _ => return Err("expected string arg resolved from local state".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn params_build_includes_foreign_references_from_args( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let mut f = testing_app_fixture.await?; + let sender = f.sender_address.clone(); + let client = &f.client; + let extra = f.algorand_fixture.generate_account(None).await?; + let extra_addr = extra.account().address().to_string(); + + let built = client + .params() + .call( + AppClientMethodCallParams { + method: "call_abi_foreign_refs".to_string(), + args: vec![], + sender: Some(sender.to_string()), + account_references: Some(vec![extra_addr.clone()]), + app_references: Some(vec![345]), + asset_references: Some(vec![567]), + ..Default::default() + }, + None, + ) + .await?; + + assert!(built.account_references.as_ref().unwrap().len() >= 1); + assert!(built.app_references.as_ref().unwrap().contains(&345)); + assert!(built.asset_references.as_ref().unwrap().contains(&567)); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn params_build_bare_and_fund_payment( + #[future] sandbox_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = sandbox_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + let bare = client.params().bare().call( + AppClientBareCallParams { + args: None, + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: b"1".to_vec(), + }]), + ..Default::default() + }, + None, + )?; + assert_eq!(bare.box_references.as_ref().unwrap()[0].name, b"1".to_vec()); + + let pay: PaymentParams = client.params().fund_app_account(&FundAppAccountParams { + amount: 200_000, + sender: Some(sender.to_string()), + ..Default::default() + })?; + assert_eq!(pay.amount, 200_000); + assert_eq!(pay.receiver, client.app_address()); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn params_construct_txn_with_abi_tx_arg_and_return( + #[future] sandbox_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = sandbox_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + let payment = PaymentParams { + sender: sender.clone(), + signer: None, + rekey_to: None, + note: None, + lease: None, + static_fee: None, + extra_fee: None, + max_fee: None, + validity_window: None, + first_valid_round: None, + last_valid_round: None, + receiver: sender.clone(), + amount: 123, + }; + + let result = client + .send() + .call( + AppClientMethodCallParams { + method: "get_pay_txn_amount".to_string(), + args: vec![AppMethodCallArg::Payment(payment)], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + assert_eq!(result.common_params.transactions.len(), 2); + let abi_ret = result.abi_return.as_ref().expect("abi return expected"); + match &abi_ret.return_value { + Some(ABIValue::Uint(u)) => assert_eq!(*u, num_bigint::BigUint::from(123u32)), + _ => return Err("expected uint return".into()), + } + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/app_client/send.rs b/crates/algokit_utils/tests/applications/app_client/send.rs new file mode 100644 index 000000000..7b0ad86e0 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/send.rs @@ -0,0 +1,422 @@ +use crate::common::app_fixture::{sandbox_app_fixture, testing_app_fixture, testing_app_spec}; +use crate::common::{TestResult, nested_contract_fixture}; +use algokit_abi::{ABIMethod, ABIValue}; +use algokit_transact::{BoxReference, SignedTransaction, Transaction}; +use algokit_utils::applications::app_client::AppClientMethodCallParams; +use algokit_utils::transactions::{PaymentParams, TransactionSigner, TransactionWithSigner}; +use algokit_utils::{AppCallMethodCallParams, AppManager, AppMethodCallArg}; +use async_trait::async_trait; +use num_bigint::BigUint; +use rand::Rng; +use rstest::*; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +#[rstest] +#[tokio::test] +async fn test_create_then_call_app( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let client = f.client; + let sender = f.sender_address; + + let result = client + .send() + .call( + AppClientMethodCallParams { + method: "call_abi".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from("test"))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let abi_return = result.abi_return.expect("Expected ABI return"); + match abi_return.return_value { + Some(ABIValue::String(s)) => assert_eq!(s, "Hello, test"), + _ => return Err("Expected string ABI return".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_construct_transaction_with_abi_encoding_including_transaction( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let mut f = testing_app_fixture.await?; + let funded_account = f.algorand_fixture.generate_account(None).await?; + let funded_addr = funded_account.account().address(); + + let mut rng = rand::thread_rng(); + let amount: u64 = rng.gen_range(1..=10000); + + let payment_txn = f + .algorand_fixture + .algorand_client + .create() + .payment(PaymentParams { + sender: funded_addr.clone(), + receiver: funded_addr.clone(), + amount, + ..Default::default() + }) + .await?; + let client = f.client; + + let result = client + .send() + .call( + AppClientMethodCallParams { + method: "call_abi_txn".to_string(), + args: vec![ + AppMethodCallArg::Transaction(payment_txn), + AppMethodCallArg::ABIValue(ABIValue::from("test")), + ], + sender: Some(funded_addr.to_string()), + signer: Some(Arc::new(funded_account.clone())), + ..Default::default() + }, + None, + None, + ) + .await?; + + assert_eq!(result.common_params.transactions.len(), 2); + + let abi_return = result.abi_return.as_ref().expect("Expected ABI return"); + let expected_return = format!("Sent {}. {}", amount, "test"); + match &abi_return.return_value { + Some(ABIValue::String(s)) => assert_eq!(s, &expected_return), + _ => return Err("Expected string ABI return".into()), + } + + let method = testing_app_spec() + .find_abi_method("call_abi_txn") + .expect("ABI method"); + let decoded = AppManager::get_abi_return(&abi_return.raw_return_value, &method) + .expect("Decoded ABI return"); + match decoded.return_value { + Some(ABIValue::String(s)) => assert_eq!(s, expected_return), + _ => return Err("Expected string ABI return from AppManager decoding".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_call_app_with_too_many_args( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + let err = client + .send() + .call( + AppClientMethodCallParams { + method: "call_abi".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from("one")), + AppMethodCallArg::ABIValue(ABIValue::from("two")), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await + .expect_err("Expected validation error due to too many args"); + + assert!( + err.to_string() + .contains("The number of provided arguments is"), + "Unexpected error message: {}", + err + ); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_call_app_with_rekey( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let mut f = testing_app_fixture.await?; + let sender = f.sender_address; + + let rekey_to_account = f.algorand_fixture.generate_account(None).await?; + let rekey_to_addr = rekey_to_account.account().address(); + let client = f.client; + + client + .send() + .opt_in( + AppClientMethodCallParams { + method: "opt_in".to_string(), + args: vec![], + sender: Some(sender.to_string()), + rekey_to: Some(rekey_to_addr.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + let _payment_result = client + .algorand() + .send() + .payment( + PaymentParams { + sender: sender.clone(), + signer: Some(Arc::new(rekey_to_account.clone())), + receiver: sender.clone(), + amount: 0, + ..Default::default() + }, + None, + ) + .await?; + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let mut f = testing_app_fixture.await?; + let _sender = f.sender_address; + + let funded_account = f.algorand_fixture.generate_account(None).await?; + let funded_addr = funded_account.account().address(); + + let mut rng = rand::thread_rng(); + let amount = rng.gen_range(1..=10000); + + let payment_txn = f + .algorand_fixture + .algorand_client + .create() + .payment(PaymentParams { + sender: funded_addr.clone(), + receiver: funded_addr.clone(), + amount, + ..Default::default() + }) + .await?; + + let called_indexes = Arc::new(Mutex::new(Vec::new())); + + struct IndexCapturingSigner { + original_signer: Arc, + called_indexes: Arc>>, + } + + #[async_trait] + impl TransactionSigner for IndexCapturingSigner { + async fn sign_transactions( + &self, + transactions: &[Transaction], + indices: &[usize], + ) -> Result, String> { + { + let mut indexes = self.called_indexes.lock().unwrap(); + indexes.extend_from_slice(indices); + } + self.original_signer + .sign_transactions(transactions, indices) + .await + } + } + + let client = f.client; + + client + .send() + .call( + AppClientMethodCallParams { + method: "call_abi_txn".to_string(), + args: vec![ + AppMethodCallArg::Transaction(payment_txn), + AppMethodCallArg::ABIValue(ABIValue::from("test")), + ], + sender: Some(funded_addr.to_string()), + signer: Some(Arc::new(IndexCapturingSigner { + original_signer: Arc::new(funded_account.clone()), + called_indexes: called_indexes.clone(), + })), + ..Default::default() + }, + None, + None, + ) + .await?; + + let indexes = called_indexes.lock().unwrap().clone(); + + assert_eq!(indexes, vec![0, 1], "Expected indexes 0 and 1 to be signed"); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_sign_transaction_in_group_with_different_signer_if_provided( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let mut f = testing_app_fixture.await?; + let sender = f.sender_address; + + let new_account = f.algorand_fixture.generate_account(None).await?; + let new_addr = new_account.account().address(); + + let payment_txn = f + .algorand_fixture + .algorand_client + .create() + .payment(PaymentParams { + sender: new_addr.clone(), + receiver: new_addr.clone(), + amount: 2_000, + ..Default::default() + }) + .await?; + let client = f.client; + + client + .send() + .call( + AppClientMethodCallParams { + method: "call_abi_txn".to_string(), + args: vec![ + AppMethodCallArg::TransactionWithSigner(TransactionWithSigner { + transaction: payment_txn, + signer: Arc::new(new_account.clone()), + }), + AppMethodCallArg::ABIValue(ABIValue::from("test")), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_sign_nested_transactions_in_group_with_different_signers( + #[future] nested_contract_fixture: crate::common::AppFixtureResult, +) -> TestResult { + eprintln!("=== Starting test_sign_transaction_in_group_with_different_signer_if_provided2 ==="); + let mut f = nested_contract_fixture.await?; + let bob_account = f.algorand_fixture.generate_account(None).await?; + let bob_addr = bob_account.account().address(); + + let alice_account = f.algorand_fixture.generate_account(None).await?; + let alice_addr = alice_account.account().address(); + + let payment_txn = f + .algorand_fixture + .algorand_client + .create() + .payment(PaymentParams { + sender: bob_addr.clone(), + signer: Some(Arc::new(bob_account.clone())), + receiver: bob_addr.clone(), + amount: 2_000, + ..Default::default() + }) + .await?; + let client = f.client; + + let result = client + .send() + .call( + AppClientMethodCallParams { + method: "nestedTxnArg".to_string(), + args: vec![ + AppMethodCallArg::TransactionPlaceholder, + AppMethodCallArg::AppCallMethodCall(AppCallMethodCallParams { + sender: bob_addr.clone(), + signer: Some(Arc::new(bob_account.clone())), + app_id: f.app_id, + method: ABIMethod::from_str("txnArg(pay)address").unwrap(), + args: vec![AppMethodCallArg::Transaction(payment_txn)], + ..Default::default() + }), + ], + sender: Some(alice_addr.to_string()), + signer: Some(Arc::new(alice_account.clone())), + ..Default::default() + }, + None, + None, + ) + .await?; + + assert_eq!( + result.abi_return.as_ref().unwrap().return_value, + Some(ABIValue::Uint(BigUint::from(client.app_id()))) + ); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn bare_call_with_box_reference_builds_and_sends( + #[future] sandbox_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = sandbox_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + let result = client + .send() + .call( + AppClientMethodCallParams { + method: "hello_world".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from("test"))], + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: b"1".to_vec(), + }]), + ..Default::default() + }, + None, + None, + ) + .await?; + + match &result.common_params.transaction { + algokit_transact::Transaction::AppCall(fields) => { + assert_eq!(fields.app_id, f.app_id); + assert_eq!( + fields.box_references.as_ref().unwrap(), + &vec![BoxReference { + app_id: 0, + name: b"1".to_vec() + }] + ); + } + _ => return Err("expected app call".into()), + } + + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/app_client/state.rs b/crates/algokit_utils/tests/applications/app_client/state.rs new file mode 100644 index 000000000..e11b23bf6 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/state.rs @@ -0,0 +1,609 @@ +use crate::common::TestResult; +use crate::common::app_fixture::{ + boxmap_app_fixture, testing_app_fixture, testing_app_puya_fixture, +}; +use algokit_abi::{ABIType, ABIValue}; +use algokit_transact::BoxReference; +// client params not needed with fixtures +use algokit_utils::AppMethodCallArg; +use algokit_utils::applications::app_client::{AppClientMethodCallParams, FundAppAccountParams}; +use algokit_utils::clients::app_manager::{AppState, BoxName}; +use base64::{Engine, engine::general_purpose::STANDARD as Base64}; +use num_bigint::BigUint; +use rstest::*; +use std::collections::HashMap; +use std::str::FromStr; + +#[rstest] +#[tokio::test] +async fn test_global_state_retrieval( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_global".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(1u64)), + AppMethodCallArg::ABIValue(ABIValue::from(2u64)), + AppMethodCallArg::ABIValue(ABIValue::from("asdf")), + AppMethodCallArg::ABIValue(ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ])), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let global_state = client.get_global_state().await?; + + assert!(global_state.contains_key("int1".as_bytes())); + assert!(global_state.contains_key("int2".as_bytes())); + assert!(global_state.contains_key("bytes1".as_bytes())); + assert!(global_state.contains_key("bytes2".as_bytes())); + + let mut keys: Vec = global_state + .keys() + .map(|k| String::from_utf8_lossy(k).to_string()) + .collect(); + keys.sort(); + assert_eq!(keys, vec!["bytes1", "bytes2", "int1", "int2", "value"]); + + match global_state.get("int1".as_bytes()).unwrap() { + AppState::Uint(state) => assert_eq!(state.value, 1), + _ => return Err("Expected uint state".into()), + } + + match global_state.get("int2".as_bytes()).unwrap() { + AppState::Uint(state) => assert_eq!(state.value, 2), + _ => return Err("Expected uint state".into()), + } + + match global_state.get("bytes1".as_bytes()).unwrap() { + AppState::Bytes(state) => { + assert_eq!(String::from_utf8(state.value_raw.clone()).unwrap(), "asdf"); + } + _ => return Err("Expected bytes state".into()), + } + + match global_state.get("bytes2".as_bytes()).unwrap() { + AppState::Bytes(state) => { + assert_eq!(state.value_raw, vec![1, 2, 3, 4]); + } + _ => return Err("Expected bytes state".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_local_state_retrieval( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + client + .send() + .opt_in( + AppClientMethodCallParams { + method: "opt_in".to_string(), + args: vec![], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_local".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(1u64)), + AppMethodCallArg::ABIValue(ABIValue::from(2u64)), + AppMethodCallArg::ABIValue(ABIValue::from("asdf")), + AppMethodCallArg::ABIValue(ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ])), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let local_state = client.get_local_state(&sender.to_string()).await?; + + assert!(local_state.contains_key("local_int1".as_bytes())); + assert!(local_state.contains_key("local_int2".as_bytes())); + assert!(local_state.contains_key("local_bytes1".as_bytes())); + assert!(local_state.contains_key("local_bytes2".as_bytes())); + + let mut keys: Vec = local_state + .keys() + .map(|k| String::from_utf8_lossy(k).to_string()) + .collect(); + keys.sort(); + assert_eq!( + keys, + vec!["local_bytes1", "local_bytes2", "local_int1", "local_int2"] + ); + + match local_state.get("local_int1".as_bytes()).unwrap() { + AppState::Uint(state) => assert_eq!(state.value, 1), + _ => return Err("Expected uint state".into()), + } + + match local_state.get("local_int2".as_bytes()).unwrap() { + AppState::Uint(state) => assert_eq!(state.value, 2), + _ => return Err("Expected uint state".into()), + } + + match local_state.get("local_bytes1".as_bytes()).unwrap() { + AppState::Bytes(state) => { + assert_eq!(String::from_utf8(state.value_raw.clone()).unwrap(), "asdf"); + } + _ => return Err("Expected bytes state".into()), + } + + match local_state.get("local_bytes2".as_bytes()).unwrap() { + AppState::Bytes(state) => { + assert_eq!(state.value_raw, vec![1, 2, 3, 4]); + } + _ => return Err("Expected bytes state".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_box_retrieval( + #[future] testing_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + let box_name1: Vec = vec![0, 0, 0, 1]; + let box_name2: Vec = vec![0, 0, 0, 2]; + + client + .fund_app_account( + FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_box".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::Array( + box_name1.iter().copied().map(ABIValue::from_byte).collect(), + )), + AppMethodCallArg::ABIValue(ABIValue::from("value1")), + ], + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_name1.clone(), + }]), + ..Default::default() + }, + None, + None, + ) + .await?; + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_box".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::Array( + box_name2.iter().copied().map(ABIValue::from_byte).collect(), + )), + AppMethodCallArg::ABIValue(ABIValue::from("value2")), + ], + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_name2.clone(), + }]), + ..Default::default() + }, + None, + None, + ) + .await?; + + let box_names = client.get_box_names().await?; + let names: Vec> = box_names.iter().map(|n| n.name_raw.clone()).collect(); + assert!(names.contains(&box_name1)); + assert!(names.contains(&box_name2)); + + let box_values = client.get_box_values().await?; + let box1_value = client.get_box_value(&box_name1).await?; + + let box_name1_base64 = Base64.encode(&box_name1); + let box_name2_base64 = Base64.encode(&box_name2); + + let mut box_names_base64: Vec<_> = box_values.iter().map(|b| &b.name.name_base64).collect(); + box_names_base64.sort(); + let mut expected_names = vec![&box_name1_base64, &box_name2_base64]; + expected_names.sort(); + assert_eq!(box_names_base64, expected_names); + + let box1 = box_values + .iter() + .find(|b| b.name.name_base64 == box_name1_base64) + .expect("box1 should exist"); + assert_eq!(box1.value, b"value1"); + assert_eq!(box1_value, box1.value); + + let box2 = box_values + .iter() + .find(|b| b.name.name_base64 == box_name2_base64) + .expect("box2 should exist"); + assert_eq!(box2.value, b"value2"); + + let expected_value_decoded = "1234524352"; + let expected_value = format!("\x00\n{}", expected_value_decoded); + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_box".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::Array( + box_name1.iter().copied().map(ABIValue::from_byte).collect(), + )), + AppMethodCallArg::ABIValue(ABIValue::from(expected_value.as_str())), + ], + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_name1.clone(), + }]), + ..Default::default() + }, + None, + None, + ) + .await?; + + let abi_string_type = "string".parse::().unwrap(); + let box_name1_base64_for_filter = box_name1_base64.clone(); + let boxes_abi = client + .get_box_values_from_abi_type( + &abi_string_type, + Some(Box::new(move |name: &BoxName| { + name.name_base64 == box_name1_base64_for_filter + })), + ) + .await?; + + let box1_abi_value = client + .get_box_value_from_abi_type(&box_name1, &abi_string_type) + .await?; + + assert_eq!(boxes_abi.len(), 1); + if let ABIValue::String(decoded_str) = &boxes_abi[0].value { + assert_eq!(decoded_str, expected_value_decoded); + } else { + return Err("Expected string ABIValue".into()); + } + + if let ABIValue::String(decoded_str) = &box1_abi_value { + assert_eq!(decoded_str, expected_value_decoded); + } else { + return Err("Expected string ABIValue".into()); + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_box_maps( + #[future] boxmap_app_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = boxmap_app_fixture.await?; + let sender = f.sender_address; + let app_client = f.client; + + app_client + .fund_app_account( + FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + let _result = app_client + .send() + .call( + AppClientMethodCallParams { + method: "setValue".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(1u64))), + AppMethodCallArg::ABIValue(ABIValue::String("foo".to_string())), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await; + + let box_map = app_client.state().box_storage().get_map("bMap").await?; + assert_eq!(box_map.len(), 1); + + let key = ABIValue::Uint(BigUint::from(1u64)); + let expected_value = ABIValue::String("foo".to_string()); + assert_eq!(box_map.get(&key), Some(&expected_value)); + + let box_map_value = app_client + .state() + .box_storage() + .get_map_value("bMap", &ABIValue::Uint(BigUint::from(1u64))) + .await?; + assert_eq!(box_map_value, Some(expected_value)); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn box_methods_with_manually_encoded_abi_args( + #[future] testing_app_puya_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_puya_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + client + .fund_app_account( + FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + let box_prefix = b"box_bytes".to_vec(); + let name_type = ABIType::String; + let box_name = "asdf"; + let box_name_encoded = name_type.encode(&ABIValue::from(box_name)).unwrap(); + let box_identifier = { + let mut v = box_prefix.clone(); + v.extend_from_slice(&box_name_encoded); + v + }; + + let value_type = ABIType::DynamicArray(Box::new(ABIType::Byte)); + let encoded = value_type + .encode(&ABIValue::from(vec![ + ABIValue::from_byte(116), + ABIValue::from_byte(101), + ABIValue::from_byte(115), + ABIValue::from_byte(116), + ABIValue::from_byte(95), + ABIValue::from_byte(98), + ABIValue::from_byte(121), + ABIValue::from_byte(116), + ABIValue::from_byte(101), + ABIValue::from_byte(115), + ])) + .unwrap(); + + client + .send() + .call( + AppClientMethodCallParams { + method: "set_box_bytes".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from("asdf")), + AppMethodCallArg::ABIValue(ABIValue::Array( + encoded.into_iter().map(ABIValue::from_byte).collect(), + )), + ], + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_identifier.clone(), + }]), + ..Default::default() + }, + None, + None, + ) + .await?; + + let retrieved = client + .algorand() + .app() + .get_box_value_from_abi_type(client.app_id(), &box_identifier, &value_type) + .await?; + assert_eq!( + retrieved, + ABIValue::Array(vec![ + ABIValue::from_byte(116), + ABIValue::from_byte(101), + ABIValue::from_byte(115), + ABIValue::from_byte(116), + ABIValue::from_byte(95), + ABIValue::from_byte(98), + ABIValue::from_byte(121), + ABIValue::from_byte(116), + ABIValue::from_byte(101), + ABIValue::from_byte(115), + ]) + ); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn box_methods_with_arc4_returns_parametrized( + #[future] testing_app_puya_fixture: crate::common::AppFixtureResult, +) -> TestResult { + let f = testing_app_puya_fixture.await?; + let sender = f.sender_address; + let client = f.client; + + client + .fund_app_account( + FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + ) + .await?; + + let mut big = num_bigint::BigUint::from(1u64); + big <<= 256u32; + let cases: Vec<(Vec, &str, &str, ABIValue)> = vec![ + ( + b"box_str".to_vec(), + "set_box_str", + "string", + ABIValue::from("string"), + ), + ( + b"box_int".to_vec(), + "set_box_int", + "uint32", + ABIValue::from(123u32), + ), + ( + b"box_int512".to_vec(), + "set_box_int512", + "uint512", + ABIValue::from(big), + ), + ( + b"box_static".to_vec(), + "set_box_static", + "byte[4]", + ABIValue::Array(vec![ + ABIValue::from_byte(1), + ABIValue::from_byte(2), + ABIValue::from_byte(3), + ABIValue::from_byte(4), + ]), + ), + ( + b"".to_vec(), + "set_struct", + "(string,uint64)", + ABIValue::Array(vec![ABIValue::from("box1"), ABIValue::from(123u64)]), + ), + ]; + + for (box_prefix, method_sig, value_type_str, arg_val) in cases { + let name_type = ABIType::String; + let name_encoded = name_type.encode(&ABIValue::from("box1")).unwrap(); + let mut box_reference = box_prefix.clone(); + box_reference.extend_from_slice(&name_encoded); + + let method_arg_val = if method_sig == "set_struct" { + ABIValue::Struct(HashMap::from([ + ("name".to_string(), ABIValue::from("box1")), + ("id".to_string(), ABIValue::from(123u64)), + ])) + } else { + arg_val.clone() + }; + + client + .send() + .call( + AppClientMethodCallParams { + method: method_sig.to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from("box1")), + AppMethodCallArg::ABIValue(method_arg_val), + ], + sender: Some(sender.to_string()), + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_reference.clone(), + }]), + ..Default::default() + }, + None, + None, + ) + .await?; + + let expected_raw = algokit_abi::ABIType::from_str(value_type_str) + .unwrap() + .encode(&arg_val) + .unwrap(); + let actual_raw = client.get_box_value(&box_reference).await?; + assert_eq!(actual_raw, expected_raw); + + let decoded = client + .get_box_value_from_abi_type( + &box_reference, + &ABIType::from_str(value_type_str).unwrap(), + ) + .await?; + assert_eq!(decoded, arg_val); + + let box_name_for_filter = box_reference.clone(); + let values = client + .get_box_values_from_abi_type( + &ABIType::from_str(value_type_str).unwrap(), + Some(Box::new(move |name: &BoxName| { + name.name_raw == box_name_for_filter + })), + ) + .await?; + assert_eq!(values.len(), 1); + assert_eq!(values[0].value, decoded); + } + + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/app_client/structs.rs b/crates/algokit_utils/tests/applications/app_client/structs.rs new file mode 100644 index 000000000..4e4213746 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client/structs.rs @@ -0,0 +1,211 @@ +use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture, deploy_arc56_contract}; +use algokit_abi::{ABIValue, Arc56Contract}; +use algokit_utils::applications::app_client::AppClientMethodCallParams; +use algokit_utils::transactions::TransactionComposerConfig; +use algokit_utils::{AlgorandClient as RootAlgorandClient, AppMethodCallArg, ResourcePopulation}; +use rstest::*; +use std::collections::HashMap; +use std::sync::Arc; + +fn get_nested_struct_spec() -> Arc56Contract { + let json = algokit_test_artifacts::nested_struct_storage::APPLICATION_ARC56; + Arc56Contract::from_json(json).expect("valid arc56") +} + +fn get_nested_struct_create_application_args() -> Vec> { + vec![vec![184u8, 68u8, 123u8, 54u8]] +} + +#[rstest] +#[tokio::test] +async fn test_nested_structs_described_by_structure( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let spec = get_nested_struct_spec(); + let app_id = deploy_arc56_contract( + &fixture, + &sender, + &spec, + None, + None, + Some(get_nested_struct_create_application_args()), + ) + .await?; + + let mut algorand = RootAlgorandClient::default_localnet(None); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let app_client = algokit_utils::applications::app_client::AppClient::new( + algokit_utils::applications::app_client::AppClientParams { + app_id, + app_spec: spec, + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + default_signer: None, + source_maps: None, + transaction_composer_config: Some(TransactionComposerConfig { + populate_app_call_resources: ResourcePopulation::Enabled { + use_access_list: false, + }, + ..Default::default() + }), + }, + ); + + app_client + .send() + .call( + AppClientMethodCallParams { + method: "setValue".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(1u64)), + AppMethodCallArg::ABIValue(ABIValue::from("hello")), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let result = app_client + .send() + .call( + AppClientMethodCallParams { + method: "getValue".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from(1u64))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let abi_ret = result.abi_return.expect("abi return"); + let value = abi_ret.return_value.expect("decoded value"); + match value { + ABIValue::Struct(ref outer) => { + let x = match outer.get("x").expect("x") { + ABIValue::Struct(m) => m, + _ => return Err("x should be a struct".into()), + }; + match x.get("a").expect("a") { + ABIValue::String(s) => assert_eq!(s, "hello"), + _ => return Err("a should be string".into()), + } + } + _ => return Err("expected struct return".into()), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn test_nested_structs_referenced_by_name( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let mut spec = get_nested_struct_spec(); + spec.structs = HashMap::from([ + ( + "Struct1".to_string(), + vec![algokit_abi::arc56_contract::StructField { + name: "a".to_string(), + field_type: algokit_abi::arc56_contract::StructFieldType::Value( + "string".to_string(), + ), + }], + ), + ( + "Struct2".to_string(), + vec![algokit_abi::arc56_contract::StructField { + name: "x".to_string(), + field_type: algokit_abi::arc56_contract::StructFieldType::Value( + "Struct1".to_string(), + ), + }], + ), + ]); + + let app_id = deploy_arc56_contract( + &fixture, + &sender, + &spec, + None, + None, + Some(get_nested_struct_create_application_args()), + ) + .await?; + + let mut algorand = RootAlgorandClient::default_localnet(None); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let app_client = algokit_utils::applications::app_client::AppClient::new( + algokit_utils::applications::app_client::AppClientParams { + app_id, + app_spec: spec, + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + default_signer: None, + source_maps: None, + transaction_composer_config: None, + }, + ); + + app_client + .send() + .call( + AppClientMethodCallParams { + method: "setValue".to_string(), + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::from(1u64)), + AppMethodCallArg::ABIValue(ABIValue::from("hello")), + ], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let result = app_client + .send() + .call( + AppClientMethodCallParams { + method: "getValue".to_string(), + args: vec![AppMethodCallArg::ABIValue(ABIValue::from(1u64))], + sender: Some(sender.to_string()), + ..Default::default() + }, + None, + None, + ) + .await?; + + let abi_ret = result.abi_return.expect("abi return"); + let value = abi_ret.return_value.expect("decoded value"); + match value { + ABIValue::Struct(ref outer) => { + let x = match outer.get("x").expect("x") { + ABIValue::Struct(m) => m, + _ => return Err("x should be a struct".into()), + }; + match x.get("a").expect("a") { + ABIValue::String(s) => assert_eq!(s, "hello"), + _ => return Err("a should be string".into()), + } + } + _ => return Err("expected struct return".into()), + } + + Ok(()) +} diff --git a/crates/algokit_utils/tests/applications/mod.rs b/crates/algokit_utils/tests/applications/mod.rs index 923aa8d78..4d5af633a 100644 --- a/crates/algokit_utils/tests/applications/mod.rs +++ b/crates/algokit_utils/tests/applications/mod.rs @@ -1 +1,2 @@ +pub mod app_client; pub mod app_deployer; diff --git a/crates/algokit_utils/tests/clients/app_manager.rs b/crates/algokit_utils/tests/clients/app_manager.rs index 0427f716d..d0619940b 100644 --- a/crates/algokit_utils/tests/clients/app_manager.rs +++ b/crates/algokit_utils/tests/clients/app_manager.rs @@ -410,8 +410,15 @@ fn test_app_state_keys_as_vec_u8() { // Verify the actual data in AppState let app_state = &result[&key_raw]; - assert_eq!(app_state.key_raw, key_raw); - assert_eq!(app_state.key_base64, Base64.encode(&key_raw)); + match app_state { + AppState::Uint(uint_value) => { + assert_eq!(uint_value.key_raw, key_raw); + assert_eq!(uint_value.key_base64, Base64.encode(&key_raw)); + } + AppState::Bytes(_) => { + panic!("Expected AppState::Uint"); + } + } // Test with binary key data (non-UTF-8) let binary_key = vec![0xFF, 0xFE, 0xFD, 0x00]; @@ -420,7 +427,7 @@ fn test_app_state_keys_as_vec_u8() { let binary_state_val = TealKeyValue { key: binary_key_base64, value: TealValue { - r#type: 2, + r#type: 2, // Uint type bytes: Vec::new(), uint: 123, }, @@ -432,7 +439,15 @@ fn test_app_state_keys_as_vec_u8() { // Verify binary key works correctly assert!(binary_result.contains_key(&binary_key)); let binary_app_state = &binary_result[&binary_key]; - assert_eq!(binary_app_state.key_raw, binary_key); + match binary_app_state { + AppState::Uint(uint_app_state) => { + assert_eq!(uint_app_state.key_raw, binary_key); + assert_eq!(uint_app_state.value, 123); + } + AppState::Bytes(_) => { + panic!("Expected AppState::Uint"); + } + } // Test bytes value type with base64 deserialization let bytes_key = b"bytes_key".to_vec(); @@ -454,18 +469,16 @@ fn test_app_state_keys_as_vec_u8() { // Verify bytes value handling assert!(bytes_result.contains_key(&bytes_key)); let bytes_app_state = &bytes_result[&bytes_key]; - assert_eq!(bytes_app_state.key_raw, bytes_key); - assert_eq!(bytes_app_state.value_raw, Some(bytes_value.clone())); - assert_eq!( - bytes_app_state.value_base64, - Some(Base64.encode(&bytes_value)) - ); - - // Check that the bytes value is correctly decoded as UTF-8 string - if let AppStateValue::Bytes(ref value_str) = bytes_app_state.value { - assert_eq!(value_str, "Hello, World!"); - } else { - panic!("Expected AppStateValue::Bytes"); + match bytes_app_state { + AppState::Uint(_) => { + panic!("Expected AppState::Bytes"); + } + AppState::Bytes(value) => { + assert_eq!(value.key_raw, bytes_key); + assert_eq!(value.value_raw, bytes_value.clone()); + assert_eq!(value.value_base64, Base64.encode(&bytes_value)); + assert_eq!(value.value, "Hello, World!"); + } } } diff --git a/crates/algokit_utils/tests/common/app_fixture.rs b/crates/algokit_utils/tests/common/app_fixture.rs new file mode 100644 index 000000000..594e0f695 --- /dev/null +++ b/crates/algokit_utils/tests/common/app_fixture.rs @@ -0,0 +1,214 @@ +use crate::common::{ + AlgorandFixture, AlgorandFixtureResult, algorand_fixture, deploy_arc56_contract, +}; +use algokit_abi::Arc56Contract; +use algokit_transact::Address; +use algokit_utils::AlgorandClient; +use algokit_utils::ResourcePopulation; +use algokit_utils::applications::app_client::{AppClient, AppClientParams}; +use algokit_utils::clients::app_manager::{ + DeploymentMetadata, TealTemplateParams, TealTemplateValue, +}; +use algokit_utils::transactions::TransactionComposerConfig; +use rstest::fixture; +use std::sync::Arc; + +pub struct AppFixture { + pub algorand_fixture: AlgorandFixture, + pub sender_address: Address, + pub app_id: u64, + pub app_spec: Arc56Contract, + pub client: AppClient, +} + +pub type AppFixtureResult = Result>; + +#[derive(Default)] +pub struct AppFixtureOptions { + pub template_params: Option, + pub deploy_metadata: Option, + pub args: Option>>, + pub transaction_composer_config: Option, + pub default_sender_override: Option, + pub app_name: Option, +} + +pub async fn build_app_fixture( + fixture: AlgorandFixture, + spec: Arc56Contract, + opts: AppFixtureOptions, +) -> AppFixtureResult { + let sender = fixture.test_account.account().address(); + + let app_id = deploy_arc56_contract( + &fixture, + &sender, + &spec, + opts.template_params.clone(), + opts.deploy_metadata.clone(), + opts.args.clone(), + ) + .await?; + + let mut algorand = AlgorandClient::default_localnet(None); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id, + app_spec: spec.clone(), + algorand, + app_name: opts.app_name.clone(), + default_sender: Some( + opts.default_sender_override + .unwrap_or_else(|| sender.to_string()), + ), + default_signer: None, + source_maps: None, + transaction_composer_config: opts.transaction_composer_config, + }); + + Ok(AppFixture { + algorand_fixture: fixture, + sender_address: sender, + app_id, + app_spec: spec, + client, + }) +} + +pub fn default_teal_params(value: u64, updatable: bool, deletable: bool) -> TealTemplateParams { + let mut t = TealTemplateParams::default(); + t.insert("VALUE".to_string(), TealTemplateValue::Int(value)); + t.insert( + "UPDATABLE".to_string(), + TealTemplateValue::Int(if updatable { 1 } else { 0 }), + ); + t.insert( + "DELETABLE".to_string(), + TealTemplateValue::Int(if deletable { 1 } else { 0 }), + ); + t +} + +// ARC56 contract specs for test apps +pub fn testing_app_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::testing_app::APPLICATION_ARC56).unwrap() +} + +pub fn nested_contract_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::nested_contract::APPLICATION_ARC56).unwrap() +} + +pub fn sandbox_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::sandbox::APPLICATION_ARC56).unwrap() +} + +pub fn hello_world_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::hello_world::APPLICATION_ARC56).unwrap() +} + +pub fn boxmap_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::box_map_test::APPLICATION_ARC56).unwrap() +} + +pub fn testing_app_puya_spec() -> Arc56Contract { + Arc56Contract::from_json(algokit_test_artifacts::testing_app_puya::APPLICATION_ARC56).unwrap() +} + +// Common fixtures for app_client tests +#[fixture] +pub async fn testing_app_fixture( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> AppFixtureResult { + let f = algorand_fixture.await?; + let spec = testing_app_spec(); + build_app_fixture( + f, + spec, + AppFixtureOptions { + template_params: Some(default_teal_params(0, false, false)), + ..Default::default() + }, + ) + .await +} + +#[fixture] +pub async fn nested_contract_fixture( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> AppFixtureResult { + let f = algorand_fixture.await?; + let spec = nested_contract_spec(); + build_app_fixture( + f, + spec, + AppFixtureOptions { + args: Some(vec![vec![184u8, 68u8, 123u8, 54u8]]), + transaction_composer_config: Some(TransactionComposerConfig { + populate_app_call_resources: ResourcePopulation::Enabled { + use_access_list: false, + }, + ..Default::default() + }), + ..Default::default() + }, + ) + .await +} + +#[fixture] +pub async fn sandbox_app_fixture( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> AppFixtureResult { + let f = algorand_fixture.await?; + let spec = sandbox_spec(); + build_app_fixture( + f, + spec, + AppFixtureOptions { + template_params: Some(default_teal_params(0, false, false)), + ..Default::default() + }, + ) + .await +} + +#[fixture] +pub async fn hello_world_app_fixture( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> AppFixtureResult { + let f = algorand_fixture.await?; + let spec = hello_world_spec(); + build_app_fixture(f, spec, AppFixtureOptions::default()).await +} + +#[fixture] +pub async fn boxmap_app_fixture( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> AppFixtureResult { + let f = algorand_fixture.await?; + let spec = boxmap_spec(); + build_app_fixture( + f, + spec, + AppFixtureOptions { + args: Some(vec![vec![184u8, 68u8, 123u8, 54u8]]), + transaction_composer_config: Some(TransactionComposerConfig { + populate_app_call_resources: ResourcePopulation::Enabled { + use_access_list: false, + }, + ..Default::default() + }), + ..Default::default() + }, + ) + .await +} + +#[fixture] +pub async fn testing_app_puya_fixture( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> AppFixtureResult { + let f = algorand_fixture.await?; + let spec = testing_app_puya_spec(); + build_app_fixture(f, spec, AppFixtureOptions::default()).await +} diff --git a/crates/algokit_utils/tests/common/mod.rs b/crates/algokit_utils/tests/common/mod.rs index eff0382d1..8896fd7c2 100644 --- a/crates/algokit_utils/tests/common/mod.rs +++ b/crates/algokit_utils/tests/common/mod.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] #![allow(unused_imports)] +pub mod app_fixture; pub mod fixture; pub mod indexer_helpers; pub mod local_net_dispenser; @@ -8,10 +9,19 @@ pub mod mnemonic; pub mod test_account; use algokit_abi::Arc56Contract; -use algokit_transact::Address; use algokit_utils::AppCreateParams; +use algokit_utils::clients::app_manager::{ + AppManager, DeploymentMetadata, TealTemplateParams, TealTemplateValue, +}; +use algokit_utils::config::{AppCompiledEventData, Config, EventData, EventType}; use base64::prelude::*; +pub use app_fixture::{ + AppFixture, AppFixtureOptions, AppFixtureResult, boxmap_app_fixture, boxmap_spec, + build_app_fixture, default_teal_params, hello_world_app_fixture, hello_world_spec, + nested_contract_fixture, sandbox_app_fixture, sandbox_spec, testing_app_fixture, + testing_app_puya_fixture, testing_app_puya_spec, testing_app_spec, +}; pub use fixture::{AlgorandFixture, AlgorandFixtureResult, algorand_fixture}; pub use indexer_helpers::{ IndexerWaitConfig, IndexerWaitError, wait_for_indexer, wait_for_indexer_transaction, @@ -23,25 +33,53 @@ pub type TestResult = Result<(), Box>; pub async fn deploy_arc56_contract( fixture: &AlgorandFixture, - sender: &Address, + sender: &algokit_transact::Address, arc56_contract: &Arc56Contract, + template_params: Option, + deploy_metadata: Option, + args: Option>>, ) -> Result> { let teal_source = arc56_contract .source .clone() .expect("No source found in app spec"); - let approval_bytes = BASE64_STANDARD.decode(teal_source.approval)?; - - let clear_state_bytes = BASE64_STANDARD.decode(teal_source.clear)?; + // Decode TEAL source (templates) + let approval_src_bytes = BASE64_STANDARD.decode(teal_source.approval)?; + let clear_src_bytes = BASE64_STANDARD.decode(teal_source.clear)?; + let approval_teal = String::from_utf8(approval_src_bytes)?; + let clear_teal = String::from_utf8(clear_src_bytes)?; - let approval_compile_result = fixture.algod.teal_compile(approval_bytes, None).await?; - let clear_state_compile_result = fixture.algod.teal_compile(clear_state_bytes, None).await?; + // Compile via AppManager with substitution and source-map support + let app_manager = AppManager::new(fixture.algod.clone()); + let approval_compile = app_manager + .compile_teal_template( + &approval_teal, + template_params.as_ref(), + deploy_metadata.as_ref(), + ) + .await?; + let clear_compile = app_manager + .compile_teal_template( + &clear_teal, + template_params.as_ref(), + deploy_metadata.as_ref(), + ) + .await?; let app_create_params = AppCreateParams { sender: sender.clone(), - approval_program: approval_compile_result.result, - clear_state_program: clear_state_compile_result.result, + args: args, + approval_program: approval_compile.compiled_base64_to_bytes, + clear_state_program: clear_compile.compiled_base64_to_bytes, + global_state_schema: Some(algokit_transact::StateSchema { + num_uints: arc56_contract.state.schema.global_state.ints, + num_byte_slices: arc56_contract.state.schema.global_state.bytes, + }), + local_state_schema: Some(algokit_transact::StateSchema { + num_uints: arc56_contract.state.schema.local_state.ints, + num_byte_slices: arc56_contract.state.schema.local_state.bytes, + }), ..Default::default() }; diff --git a/crates/algokit_utils/tests/transactions/composer/app_call.rs b/crates/algokit_utils/tests/transactions/composer/app_call.rs index 532c65d27..8270719e2 100644 --- a/crates/algokit_utils/tests/transactions/composer/app_call.rs +++ b/crates/algokit_utils/tests/transactions/composer/app_call.rs @@ -8,6 +8,7 @@ use algokit_transact::{ Address, OnApplicationComplete, PaymentTransactionFields, StateSchema, Transaction, TransactionHeader, TransactionId, }; +use algokit_utils::transactions::composer::SimulateParams; use algokit_utils::{AppCallMethodCallParams, AssetCreateParams, ComposerError}; use algokit_utils::{ AppCallParams, AppCreateParams, AppDeleteParams, AppMethodCallArg, AppUpdateParams, @@ -814,6 +815,78 @@ async fn test_get_returned_value_of_nested_app_call_method_calls( } } +#[rstest] +#[tokio::test] +async fn group_simulate_matches_send( + #[future] arc56_algorand_fixture: Arc56AppFixtureResult, +) -> TestResult { + let Arc56AppFixture { + sender_address: sender, + app_id, + arc56_contract, + algorand_fixture, + } = arc56_algorand_fixture.await?; + + // Compose group: add(uint64,uint64)uint64 + payment + hello_world(string)string + let mut composer = algorand_fixture.algorand_client.new_group(None); + + // 1) add(uint64,uint64)uint64 + let method_add = get_abi_method(&arc56_contract, "add")?; + let add_params = AppCallMethodCallParams { + sender: sender.clone(), + app_id, + method: method_add, + args: vec![ + AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(1u64))), + AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(2u64))), + ], + ..Default::default() + }; + composer.add_app_call_method_call(add_params)?; + + // 2) payment + let payment = PaymentParams { + sender: sender.clone(), + receiver: sender.clone(), + amount: 10_000, + ..Default::default() + }; + composer.add_payment(payment)?; + + // 3) hello_world(string)string + let method_hello = get_abi_method(&arc56_contract, "hello_world")?; + let call_params = AppCallMethodCallParams { + sender: sender.clone(), + app_id, + method: method_hello, + args: vec![AppMethodCallArg::ABIValue(ABIValue::String( + "test".to_string(), + ))], + ..Default::default() + }; + composer.add_app_call_method_call(call_params)?; + + let simulate = composer + .simulate(Some(SimulateParams { + skip_signatures: true, + ..Default::default() + })) + .await?; + let send = composer.send(None).await?; + + assert_eq!(simulate.transaction_ids, send.transaction_ids); + // Compare all ABI returns in order where both sides have a value + for (simulate_abi_return, send_abi_return) in + simulate.abi_returns.iter().zip(send.abi_returns.iter()) + { + assert_eq!( + simulate_abi_return.return_value, + send_abi_return.return_value + ); + } + Ok(()) +} + struct Arc56AppFixture { sender_address: Address, app_id: u64, @@ -831,7 +904,15 @@ async fn arc56_algorand_fixture( let sender_address = algorand_fixture.test_account.account().address(); let arc56_contract: Arc56Contract = serde_json::from_str(sandbox::APPLICATION_ARC56)?; - let app_id = deploy_arc56_contract(&algorand_fixture, &sender_address, &arc56_contract).await?; + let app_id = deploy_arc56_contract( + &algorand_fixture, + &sender_address, + &arc56_contract, + None, + None, + None, + ) + .await?; Ok(Arc56AppFixture { sender_address, @@ -1179,14 +1260,9 @@ fn get_abi_method( arc56_contract: &Arc56Contract, name: &str, ) -> Result> { - let method = arc56_contract - .methods - .iter() - .find(|m| m.name == name) - .ok_or_else(|| format!("Failed to find {} method", name))? - .try_into() - .map_err(|e| format!("Failed to convert ARC56 method to ABI method: {}", e))?; - Ok(method) + Ok(arc56_contract + .find_abi_method(name) + .map_err(|e| format!("Failed to convert ARC56 method to ABI method: {}", e))?) } fn get_abi_return( diff --git a/crates/algokit_utils/tests/transactions/sender.rs b/crates/algokit_utils/tests/transactions/sender.rs index 46f0c35e9..e9d6688cc 100644 --- a/crates/algokit_utils/tests/transactions/sender.rs +++ b/crates/algokit_utils/tests/transactions/sender.rs @@ -159,14 +159,17 @@ async fn test_abi_method_returns_enhanced_processing( // Deploy ABI app using existing pattern let arc56_contract: Arc56Contract = serde_json::from_str(sandbox::APPLICATION_ARC56)?; - let app_id = deploy_arc56_contract(&algorand_fixture, &sender_address, &arc56_contract).await?; - - let method = arc56_contract - .methods - .iter() - .find(|m| m.name == "hello_world") - .expect("Failed to find hello_world method") - .try_into()?; + let app_id = deploy_arc56_contract( + &algorand_fixture, + &sender_address, + &arc56_contract, + None, + None, + None, + ) + .await?; + + let method = arc56_contract.find_abi_method("hello_world")?; let params = AppCallMethodCallParams { sender: sender_address,