diff --git a/crates/algokit_abi/src/abi_type.rs b/crates/algokit_abi/src/abi_type.rs index 424f429bd..79367f130 100644 --- a/crates/algokit_abi/src/abi_type.rs +++ b/crates/algokit_abi/src/abi_type.rs @@ -5,6 +5,7 @@ use crate::{ STATIC_ARRAY_REGEX, UFIXED_REGEX, }, types::collections::tuple::find_bool_sequence_end, + types::struct_type::StructType, }; use std::{ fmt::{Display, Formatter, Result as FmtResult}, @@ -98,6 +99,8 @@ pub enum ABIType { StaticArray(Box, usize), /// A dynamic-length array of another ABI type. DynamicArray(Box), + /// A named struct type with ordered fields + Struct(StructType), } impl AsRef for ABIType { @@ -125,6 +128,24 @@ impl ABIType { ABIType::String => self.encode_string(value), ABIType::Byte => self.encode_byte(value), ABIType::Bool => self.encode_bool(value), + ABIType::Struct(struct_type) => { + // Convert struct map -> tuple vec based on field order, encode with tuple encoder + let tuple_type = struct_type.to_tuple_type(); + let tuple_values = match value { + ABIValue::Struct(map) => struct_type.struct_to_tuple(map)?, + // Backwards-compatible: allow tuple-style array values for struct-typed args + ABIValue::Array(values) => values.clone(), + _ => { + return Err(ABIError::EncodingError { + message: format!( + "ABI value mismatch, expected struct for type {}, got {:?}", + self, value + ), + }); + } + }; + tuple_type.encode(&ABIValue::Array(tuple_values)) + } } } @@ -146,6 +167,22 @@ 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) => { + let tuple_type = struct_type.to_tuple_type(); + let decoded = tuple_type.decode(bytes)?; + match decoded { + ABIValue::Array(values) => { + let map = struct_type.tuple_to_struct(values)?; + Ok(ABIValue::Struct(map)) + } + other => Err(ABIError::DecodingError { + message: format!( + "Expected tuple decode for struct {}, got {:?}", + struct_type.name, other + ), + }), + } + } } } @@ -153,6 +190,7 @@ 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::Struct(struct_type) => struct_type.to_tuple_type().as_ref().is_dynamic(), ABIType::DynamicArray(_) | ABIType::String => true, _ => false, } @@ -190,6 +228,7 @@ impl ABIType { } Ok(size) } + ABIType::Struct(struct_type) => Self::get_size(&struct_type.to_tuple_type()), ABIType::String => Err(ABIError::DecodingError { message: format!("Failed to get size, {} is a dynamic type", abi_type), }), @@ -221,6 +260,9 @@ impl Display for ABIType { ABIType::DynamicArray(child_type) => { write!(f, "{}[]", child_type) } + ABIType::Struct(struct_type) => { + write!(f, "{}", struct_type.to_tuple_type()) + } } } } diff --git a/crates/algokit_abi/src/abi_value.rs b/crates/algokit_abi/src/abi_value.rs index 9faa39d09..3c0dbc99c 100644 --- a/crates/algokit_abi/src/abi_value.rs +++ b/crates/algokit_abi/src/abi_value.rs @@ -1,4 +1,5 @@ use num_bigint::BigUint; +use std::collections::HashMap; /// Represents a value that can be encoded or decoded as an ABI type. #[derive(Debug, Clone, PartialEq)] @@ -15,6 +16,8 @@ pub enum ABIValue { Array(Vec), /// An Algorand address. Address(String), + /// A struct value represented as a map of field name to value. + Struct(HashMap), } impl From for ABIValue { diff --git a/crates/algokit_abi/src/arc56_contract.rs b/crates/algokit_abi/src/arc56_contract.rs index 2eda68b96..0e7e68314 100644 --- a/crates/algokit_abi/src/arc56_contract.rs +++ b/crates/algokit_abi/src/arc56_contract.rs @@ -1,9 +1,9 @@ use crate::abi_type::ABIType; use crate::error::ABIError; use crate::method::{ABIMethod, ABIMethodArg, ABIMethodArgType}; +use crate::types::struct_type as abi_struct; use base64::{Engine as _, engine::general_purpose}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::HashMap; use std::str::FromStr; @@ -609,44 +609,94 @@ 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, resolving struct types into ABIType::Struct + pub 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)?; + Ok(ABIMethodArg::new( + arg_type, + arg.name.clone(), + arg.desc.clone(), + )) + }) + .collect(); - result.insert(key, value); + // Resolve return type + let returns = if method.returns.return_type == "void" { + None + } else if let Some(struct_name) = &method.returns.struct_name { + Some(ABIType::Struct(self.build_struct_type(struct_name)?)) + } else { + Some(ABIType::from_str(&method.returns.return_type)?) + }; + + Ok(ABIMethod::new( + method.name.clone(), + args?, + returns, + method.desc.clone(), + )) + } + + fn resolve_method_arg_type(&self, arg: &MethodArg) -> Result { + if let Some(struct_name) = &arg.struct_name { + let struct_ty = self.build_struct_type(struct_name)?; + return Ok(ABIMethodArgType::Value(ABIType::Struct(struct_ty))); } + // Fallback to standard parsing for non-struct args (including refs/txns) + ABIMethodArgType::from_str(&arg.arg_type) + } + + fn build_struct_type(&self, struct_name: &str) -> Result { + let fields = self + .structs + .get(struct_name) + .ok_or_else(|| ABIError::ValidationError { + message: format!("Unknown struct '{}' in ARC-56 spec", struct_name), + })?; + Ok(self.build_struct_type_from_fields(struct_name, fields)) + } - result + fn build_struct_type_from_fields( + &self, + full_name: &str, + fields: &[StructField], + ) -> abi_struct::StructType { + let abi_fields: Vec = fields + .iter() + .map(|f| { + let abi_ty = self.struct_field_type_to_abi_type(full_name, &f.name, &f.field_type); + abi_struct::StructField::new(f.name.clone(), abi_ty) + }) + .collect(); + abi_struct::StructType::new(full_name.to_string(), abi_fields) + } + + fn struct_field_type_to_abi_type( + &self, + parent_name: &str, + field_name: &str, + field_type: &StructFieldType, + ) -> ABIType { + match field_type { + StructFieldType::Value(type_name) => { + if let Some(nested_fields) = self.structs.get(type_name) { + let nested_name = format!("{}.{}", parent_name, field_name); + let nested = self.build_struct_type_from_fields(&nested_name, nested_fields); + ABIType::Struct(nested) + } else { + ABIType::from_str(type_name).unwrap_or(ABIType::String) + } + } + StructFieldType::Nested(nested_fields) => { + let nested_name = format!("{}.{}", parent_name, field_name); + let nested = self.build_struct_type_from_fields(&nested_name, nested_fields); + ABIType::Struct(nested) + } + } } } diff --git a/crates/algokit_abi/src/lib.rs b/crates/algokit_abi/src/lib.rs index b6a13b658..4a1097cf2 100644 --- a/crates/algokit_abi/src/lib.rs +++ b/crates/algokit_abi/src/lib.rs @@ -12,6 +12,7 @@ pub use abi_type::ABIType; pub use abi_value::ABIValue; pub use arc56_contract::*; pub use error::ABIError; +pub use types::struct_type::{StructField as ABIStructField, StructType as ABIStructType}; pub use method::{ ABIMethod, ABIMethodArg, ABIMethodArgType, ABIReferenceType, ABIReferenceValue, ABIReturn, diff --git a/crates/algokit_abi/src/types/mod.rs b/crates/algokit_abi/src/types/mod.rs index 386170e55..5efbc0ea8 100644 --- a/crates/algokit_abi/src/types/mod.rs +++ b/crates/algokit_abi/src/types/mod.rs @@ -1,2 +1,3 @@ pub mod collections; pub mod primitives; +pub mod struct_type; diff --git a/crates/algokit_abi/src/types/struct_type.rs b/crates/algokit_abi/src/types/struct_type.rs new file mode 100644 index 000000000..fbd3373ef --- /dev/null +++ b/crates/algokit_abi/src/types/struct_type.rs @@ -0,0 +1,253 @@ +use crate::{ABIError, ABIType, ABIValue}; +use std::collections::HashMap; + +/// Represents an ABI struct type with named fields +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StructType { + /// The name of the struct type + pub name: String, + /// The fields of the struct in order + pub fields: Vec, +} + +/// Represents a field in a struct +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StructField { + /// The name of the field + pub name: String, + /// The ABI type of the field + pub abi_type: Box, +} + +impl StructType { + /// Create a new struct type + pub fn new(name: impl Into, fields: Vec) -> Self { + Self { + name: name.into(), + fields, + } + } + + /// Convert this struct type to an equivalent tuple type + pub fn to_tuple_type(&self) -> ABIType { + let tuple_types: Vec = self + .fields + .iter() + .map(|field| (*field.abi_type).clone()) + .collect(); + ABIType::Tuple(tuple_types) + } + + /// Convert a struct value (HashMap) to a tuple value (Vec) for encoding + pub fn struct_to_tuple( + &self, + struct_map: &HashMap, + ) -> Result, ABIError> { + let mut tuple_values = Vec::with_capacity(self.fields.len()); + + for field in &self.fields { + let value = struct_map + .get(&field.name) + .ok_or_else(|| ABIError::ValidationError { + message: format!("Missing field '{}' in struct '{}'", field.name, self.name), + })?; + + // If the field is itself a struct, it should already be in the correct ABIValue form + tuple_values.push(value.clone()); + } + + Ok(tuple_values) + } + + /// Convert a tuple value (Vec) to a struct value (HashMap) after decoding + pub fn tuple_to_struct( + &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() + ), + }); + } + + let mut struct_map = HashMap::with_capacity(self.fields.len()); + + for (field, value) in self.fields.iter().zip(tuple_values.into_iter()) { + struct_map.insert(field.name.clone(), value); + } + + Ok(struct_map) + } +} + +impl StructField { + /// Create a new struct field + pub fn new(name: impl Into, abi_type: ABIType) -> Self { + Self { + name: name.into(), + abi_type: Box::new(abi_type), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::abi_type::BitSize; + use num_bigint::BigUint; + + #[test] + fn test_struct_to_tuple_type() { + let struct_type = StructType::new( + "Person", + vec![ + StructField::new("name", ABIType::String), + StructField::new("age", ABIType::Uint(BitSize::new(64).unwrap())), + StructField::new("active", ABIType::Bool), + ], + ); + + let tuple_type = struct_type.to_tuple_type(); + match tuple_type { + ABIType::Tuple(types) => { + assert_eq!(types.len(), 3); + assert_eq!(types[0], ABIType::String); + assert_eq!(types[1], ABIType::Uint(BitSize::new(64).unwrap())); + assert_eq!(types[2], ABIType::Bool); + } + _ => panic!("Expected tuple type"), + } + } + + #[test] + fn test_struct_to_tuple_conversion() { + let struct_type = StructType::new( + "Point", + vec![ + StructField::new("x", ABIType::Uint(BitSize::new(32).unwrap())), + StructField::new("y", ABIType::Uint(BitSize::new(32).unwrap())), + ], + ); + + let mut struct_map = HashMap::new(); + struct_map.insert("x".to_string(), ABIValue::Uint(BigUint::from(10u32))); + struct_map.insert("y".to_string(), ABIValue::Uint(BigUint::from(20u32))); + + let tuple_values = struct_type.struct_to_tuple(&struct_map).unwrap(); + assert_eq!(tuple_values.len(), 2); + assert_eq!(tuple_values[0], ABIValue::Uint(BigUint::from(10u32))); + assert_eq!(tuple_values[1], ABIValue::Uint(BigUint::from(20u32))); + } + + #[test] + fn test_tuple_to_struct_conversion() { + let struct_type = StructType::new( + "Point", + vec![ + StructField::new("x", ABIType::Uint(BitSize::new(32).unwrap())), + StructField::new("y", ABIType::Uint(BitSize::new(32).unwrap())), + ], + ); + + let tuple_values = vec![ + ABIValue::Uint(BigUint::from(10u32)), + ABIValue::Uint(BigUint::from(20u32)), + ]; + + let struct_map = struct_type.tuple_to_struct(tuple_values).unwrap(); + assert_eq!(struct_map.len(), 2); + assert_eq!( + struct_map.get("x"), + Some(&ABIValue::Uint(BigUint::from(10u32))) + ); + assert_eq!( + struct_map.get("y"), + Some(&ABIValue::Uint(BigUint::from(20u32))) + ); + } + + #[test] + fn test_missing_field_error() { + let struct_type = StructType::new( + "Point", + vec![ + StructField::new("x", ABIType::Uint(BitSize::new(32).unwrap())), + StructField::new("y", ABIType::Uint(BitSize::new(32).unwrap())), + ], + ); + + let mut struct_map = HashMap::new(); + struct_map.insert("x".to_string(), ABIValue::Uint(BigUint::from(10u32))); + // Missing "y" field + + let result = struct_type.struct_to_tuple(&struct_map); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("Missing field 'y'") + ); + } + + #[test] + fn test_nested_struct() { + // Create inner struct type + let inner_struct = StructType::new( + "Address", + vec![ + StructField::new("street", ABIType::String), + StructField::new("city", ABIType::String), + ], + ); + + // Create outer struct with nested struct + let outer_struct = StructType::new( + "Person", + vec![ + StructField::new("name", ABIType::String), + StructField::new("address", ABIType::Struct(inner_struct.clone())), + ], + ); + + // Create nested struct value + let mut address_map = HashMap::new(); + address_map.insert( + "street".to_string(), + ABIValue::String("123 Main St".to_string()), + ); + address_map.insert( + "city".to_string(), + ABIValue::String("Springfield".to_string()), + ); + + let mut person_map = HashMap::new(); + person_map.insert("name".to_string(), ABIValue::String("Alice".to_string())); + person_map.insert("address".to_string(), ABIValue::Struct(address_map)); + + // Convert to tuple + let tuple_values = outer_struct.struct_to_tuple(&person_map).unwrap(); + assert_eq!(tuple_values.len(), 2); + assert_eq!(tuple_values[0], ABIValue::String("Alice".to_string())); + + // The nested struct should remain as a struct in the tuple + match &tuple_values[1] { + ABIValue::Struct(map) => { + assert_eq!( + map.get("street"), + Some(&ABIValue::String("123 Main St".to_string())) + ); + assert_eq!( + map.get("city"), + Some(&ABIValue::String("Springfield".to_string())) + ); + } + _ => panic!("Expected nested struct"), + } + } +} 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/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..86e083037 100644 --- a/crates/algokit_test_artifacts/src/lib.rs +++ b/crates/algokit_test_artifacts/src/lib.rs @@ -155,11 +155,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 diff --git a/crates/algokit_utils/Cargo.toml b/crates/algokit_utils/Cargo.toml index f8f88b169..1520da0d6 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"] } diff --git a/crates/algokit_utils/src/applications/app_client/abi_integration.rs b/crates/algokit_utils/src/applications/app_client/abi_integration.rs new file mode 100644 index 000000000..2862b2353 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/abi_integration.rs @@ -0,0 +1,347 @@ +/// This module is to be later integrated into the abi crate to further simplify app client logic +/// For now, it consolidates main functionality not covered by the abi crate and required in app client +use algokit_abi::ABIMethod; +use algokit_abi::{ABIType, ABIValue}; +use base64::Engine; +use std::collections::HashMap; +use std::str::FromStr; + +use super::AppClient; +use super::error::AppClientError; +use crate::transactions::AppMethodCallArg; + +impl AppClient { + async fn resolve_default_value_for_arg_base( + &self, + default: &algokit_abi::arc56_contract::DefaultValue, + abi_type_str: &str, + sender: Option<&str>, + ) -> Result { + use algokit_abi::arc56_contract::DefaultValueSource as Src; + let abi_type = ABIType::from_str(abi_type_str).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", abi_type_str, e)) + })?; + match default.source { + Src::Literal => { + let raw = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode base64 literal: {}", + e + )) + })?; + if let Some(ref vt) = default.value_type { + if vt == algokit_abi::arc56_contract::AVM_STRING { + let s = String::from_utf8_lossy(&raw).to_string(); + return Ok(ABIValue::from(s)); + } + if vt == algokit_abi::arc56_contract::AVM_BYTES { + let arr = raw.into_iter().map(ABIValue::from_byte).collect(); + return Ok(ABIValue::Array(arr)); + } + } + let decode_type = if let Some(ref vt) = default.value_type { + ABIType::from_str(vt).map_err(|e| { + AppClientError::AbiError(format!( + "Invalid default value ABI type '{}': {}", + vt, e + )) + })? + } else { + abi_type.clone() + }; + decode_type.decode(&raw).map_err(|e| { + AppClientError::AbiError(format!("Failed to decode default literal: {}", e)) + }) + } + Src::Global => { + let key = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode global key: {}", + e + )) + })?; + let state = self + .algorand + .app() + .get_global_state(self.app_id.ok_or(AppClientError::ValidationError( + "Missing app_id".to_string(), + ))?) + .await + .map_err(|e| AppClientError::Network(e.to_string()))?; + self.get_abi_decoded_value( + &key, + &state, + abi_type_str, + default.value_type.as_deref(), + ) + .await + } + Src::Local => { + let sender_addr = sender.ok_or_else(|| { + AppClientError::ValidationError( + "Sender is required to resolve local state default".to_string(), + ) + })?; + let key = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode local key: {}", + e + )) + })?; + let state = self + .algorand + .app() + .get_local_state( + self.app_id.ok_or(AppClientError::ValidationError( + "Missing app_id".to_string(), + ))?, + sender_addr, + ) + .await + .map_err(|e| AppClientError::Network(e.to_string()))?; + self.get_abi_decoded_value( + &key, + &state, + abi_type_str, + default.value_type.as_deref(), + ) + .await + } + Src::Box => { + let box_key = base64::engine::general_purpose::STANDARD + .decode(&default.data) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode box key: {}", e)) + })?; + let raw = self + .algorand + .app() + .get_box_value( + self.app_id.ok_or(AppClientError::ValidationError( + "Missing app_id".to_string(), + ))?, + &box_key, + ) + .await + .map_err(|e| AppClientError::Network(e.to_string()))?; + let effective_type = default.value_type.as_deref().unwrap_or(abi_type_str); + if effective_type == algokit_abi::arc56_contract::AVM_STRING { + return Ok(ABIValue::from(String::from_utf8_lossy(&raw).to_string())); + } + if effective_type == algokit_abi::arc56_contract::AVM_BYTES { + let arr = raw.into_iter().map(ABIValue::from_byte).collect(); + return Ok(ABIValue::Array(arr)); + } + let decode_type = ABIType::from_str(effective_type).map_err(|e| { + AppClientError::AbiError(format!( + "Invalid ABI type '{}': {}", + effective_type, e + )) + })?; + decode_type.decode(&raw).map_err(|e| { + AppClientError::AbiError(format!("Failed to decode box default: {}", e)) + }) + } + Src::Method => Err(AppClientError::ValidationError( + "Nested method default values are not supported".to_string(), + )), + } + } + async fn get_abi_decoded_value( + &self, + key: &[u8], + state: &HashMap, crate::clients::app_manager::AppState>, + abi_type_str: &str, + default_value_type: Option<&str>, + ) -> Result { + let app_state = state.get(key).ok_or_else(|| { + AppClientError::ValidationError(format!("State key not found: {:?}", key)) + })?; + let effective_type = default_value_type.unwrap_or(abi_type_str); + super::state_accessor::decode_app_state_value(effective_type, app_state) + } + + /// Resolve a single ARC-56 default value entry to an ABIValue for a value-type arg + pub async fn resolve_default_value_for_arg( + &self, + default: &algokit_abi::arc56_contract::DefaultValue, + abi_type_str: &str, + sender: Option<&str>, + ) -> Result { + use algokit_abi::arc56_contract::DefaultValueSource as Src; + match default.source { + Src::Method => { + let method_signature = default.data.clone(); + let arc56_method = self + .app_spec + .get_arc56_method(&method_signature) + .map_err(|e| AppClientError::MethodNotFound(e.to_string()))?; + + // Resolve all defaults for the method's value-type args + let mut resolved_args: Vec = + Vec::with_capacity(arc56_method.args.len()); + for arg in &arc56_method.args { + if let Some(def) = &arg.default_value { + let val = self + .resolve_default_value_for_arg_base(def, &arg.arg_type, sender) + .await?; + resolved_args.push(AppMethodCallArg::ABIValue(val)); + } else { + return Err(AppClientError::ValidationError(format!( + "Method default for '{}' refers to method '{}' which has a required argument without a default", + abi_type_str, arc56_method.name + ))); + } + } + + // Build params via params layer and inject resolved args + let method_call_params = super::types::AppClientMethodCallParams { + method: method_signature.clone(), + args: Some(resolved_args), + sender: sender.map(|s| s.to_string()), + ..Default::default() + }; + let params = self + .params() + .method_call_no_defaults(&method_call_params) + .map_err(AppClientError::ValidationError)?; + + // Prefer simulate for readonly + let is_readonly = arc56_method.readonly.unwrap_or(false); + if is_readonly { + let mut composer = self.algorand().new_group(); + composer + .add_app_call_method_call(params) + .map_err(|e| AppClientError::TransactionError(e.to_string()))?; + let sim = composer + .simulate(Some(crate::transactions::composer::SimulateParams { + allow_empty_signatures: Some(true), + skip_signatures: true, + ..Default::default() + })) + .await + .map_err(|e| AppClientError::TransactionError(e.to_string()))?; + let ret = sim.returns.last().cloned().ok_or_else(|| { + AppClientError::ValidationError( + "No ABI return found in simulate result".to_string(), + ) + })?; + return Ok(ret.return_value); + } + + let res = self + .algorand() + .send() + .app_call_method_call(params, None) + .await + .map_err(|e| AppClientError::TransactionError(e.to_string()))?; + let ret = res.abi_return.ok_or_else(|| { + AppClientError::ValidationError( + "Default value method call did not return a value".to_string(), + ) + })?; + Ok(ret.return_value) + } + _ => { + // Non-method sources use shared base resolver + self.resolve_default_value_for_arg_base(default, abi_type_str, sender) + .await + } + } + } + /// Resolve ARC-56 default arguments for a method. Provided args may be fewer than required. + pub async fn resolve_default_arguments( + &self, + method_name_or_sig: &str, + provided_args: &Option>, + sender: Option<&str>, + ) -> Result, AppClientError> { + let method = self + .app_spec + .get_arc56_method(method_name_or_sig) + .map_err(|e| AppClientError::MethodNotFound(e.to_string()))?; + + let mut resolved: Vec = Vec::with_capacity(method.args.len()); + + for (i, m_arg) in method.args.iter().enumerate() { + if let Some(p) = provided_args.as_ref().and_then(|v| v.get(i)).cloned() { + resolved.push(p); + continue; + } + + if let Some(default) = &m_arg.default_value { + let value = self + .resolve_default_value_for_arg(default, &m_arg.arg_type, sender) + .await?; + resolved.push(value); + } else { + return Err(AppClientError::ValidationError(format!( + "No value provided and no default for argument {} of method {}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name + ))); + } + } + + Ok(resolved) + } + + pub fn is_readonly_method(&self, method: &ABIMethod) -> bool { + if let Ok(signature) = method.signature() { + if let Ok(m) = self.app_spec.get_arc56_method(&signature) { + if let Some(ro) = m.readonly { + return ro; + } + } + } + false + } + + /// Simulate a read-only method call for cost-free execution. + pub async fn simulate_readonly_call( + &self, + params: super::types::AppClientMethodCallParams, + ) -> Result { + // Build full method params (resolve defaults) via params layer + let method_params = self + .params() + .method_call(¶ms) + .await + .map_err(AppClientError::ValidationError)?; + + // If debug enabled, reuse shared debug simulate helper to emit traces + if crate::config::Config::debug() { + self.send() + .simulate_readonly_with_tracing_for_debug(¶ms, false) + .await + .map_err(|e| AppClientError::TransactionError(e.to_string()))?; + } + + // Always prefer simulate for readonly method calls + let mut composer = self.algorand().new_group(); + composer + .add_app_call_method_call(method_params) + .map_err(|e| AppClientError::TransactionError(e.to_string()))?; + let sim = composer + .simulate(Some(crate::transactions::composer::SimulateParams { + allow_empty_signatures: Some(true), + skip_signatures: true, + ..Default::default() + })) + .await + .map_err(|e| AppClientError::TransactionError(e.to_string()))?; + let ret = sim.returns.last().cloned().ok_or_else(|| { + AppClientError::ValidationError("No ABI return found in simulate result".to_string()) + })?; + Ok(ret) + } +} 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..9f43d9ba4 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/compilation.rs @@ -0,0 +1,118 @@ +use super::{AppClient, AppClientError}; +use crate::clients::app_manager::DeploymentMetadata; + +impl AppClient { + pub async fn compile_with_params( + &self, + compilation_params: &super::types::CompilationParams, + ) -> Result<(Vec, Vec), AppClientError> { + let approval = self + .compile_approval_with_params(compilation_params) + .await?; + let clear = self.compile_clear_with_params(compilation_params).await?; + + // Emit AppCompiled event when debug flag is enabled + if crate::config::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 = crate::config::AppCompiledEventData { + app_name, + approval_source_map: approval_map, + clear_source_map: clear_map, + }; + crate::config::Config::events() + .emit( + crate::config::EventType::AppCompiled, + crate::config::EventData::AppCompiled(event), + ) + .await; + } + + Ok((approval, clear)) + } + + pub async fn compile_approval_with_params( + &self, + compilation_params: &super::types::CompilationParams, + ) -> Result, AppClientError> { + let source = self.app_spec.source.as_ref().ok_or_else(|| { + AppClientError::CompilationError("Missing source in app spec".to_string()) + })?; + + // 1) Decode TEAL from ARC-56 source + let mut teal = source + .get_decoded_approval() + .map_err(|e| AppClientError::CompilationError(e.to_string()))?; + + // 2) Apply template variables if provided + if let Some(params) = &compilation_params.deploy_time_params { + teal = + crate::clients::app_manager::AppManager::replace_template_variables(&teal, params) + .map_err(|e| AppClientError::CompilationError(e.to_string()))?; + } + + // 3) Apply deploy-time controls + if compilation_params.updatable.is_some() || compilation_params.deletable.is_some() { + let metadata = DeploymentMetadata { + updatable: compilation_params.updatable, + deletable: compilation_params.deletable, + }; + teal = crate::clients::app_manager::AppManager::replace_teal_template_deploy_time_control_params(&teal, &metadata) + .map_err(|e| AppClientError::CompilationError(e.to_string()))?; + } + + // 4) Compile to populate AppManager cache and source maps + let _compiled = self + .algorand() + .app() + .compile_teal(&teal) + .await + .map_err(|e| AppClientError::AppManagerError(e.to_string()))?; + + // Return TEAL source bytes (TransactionSender will pull compiled bytes from cache) + Ok(teal.into_bytes()) + } + + pub async fn compile_clear_with_params( + &self, + compilation_params: &super::types::CompilationParams, + ) -> Result, AppClientError> { + let source = self.app_spec.source.as_ref().ok_or_else(|| { + AppClientError::CompilationError("Missing source in app spec".to_string()) + })?; + + // 1) Decode TEAL from ARC-56 source + let mut teal = source + .get_decoded_clear() + .map_err(|e| AppClientError::CompilationError(e.to_string()))?; + + // 2) Apply template variables if provided + if let Some(params) = &compilation_params.deploy_time_params { + teal = + crate::clients::app_manager::AppManager::replace_template_variables(&teal, params) + .map_err(|e| AppClientError::CompilationError(e.to_string()))?; + } + + // 3) NOTE: Deploy-time controls don't apply to clear program; skip + + // 4) Compile to populate AppManager cache and source maps + let _compiled = self + .algorand() + .app() + .compile_teal(&teal) + .await + .map_err(|e| AppClientError::AppManagerError(e.to_string()))?; + + Ok(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..05423cd0e --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/error.rs @@ -0,0 +1,62 @@ +use crate::clients::app_manager::AppManagerError; +use crate::transactions::TransactionSenderError; +use algokit_abi::error::ABIError; + +#[derive(Debug)] +pub enum AppClientError { + AppIdNotFound { + network_names: Vec, + available: Vec, + }, + Network(String), + Lookup(String), + MethodNotFound(String), + AbiError(String), + TransactionError(String), + AppManagerError(String), + CompilationError(String), + ValidationError(String), +} + +impl std::fmt::Display for AppClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AppIdNotFound { + network_names, + available, + } => write!( + f, + "No app ID found for network {:?}. Available keys in spec: {:?}", + network_names, available + ), + Self::Network(msg) => write!(f, "Network error: {}", msg), + Self::Lookup(msg) => write!(f, "Lookup error: {}", msg), + Self::MethodNotFound(msg) => write!(f, "Method not found: {}", msg), + Self::AbiError(msg) => write!(f, "ABI error: {}", msg), + Self::TransactionError(msg) => write!(f, "Transaction error: {}", msg), + Self::AppManagerError(msg) => write!(f, "App manager error: {}", msg), + Self::CompilationError(msg) => write!(f, "Compilation error: {}", msg), + Self::ValidationError(msg) => write!(f, "Validation error: {}", msg), + } + } +} + +impl std::error::Error for AppClientError {} + +impl From for AppClientError { + fn from(e: ABIError) -> Self { + Self::AbiError(e.to_string()) + } +} + +impl From for AppClientError { + fn from(e: TransactionSenderError) -> Self { + Self::TransactionError(e.to_string()) + } +} + +impl From for AppClientError { + fn from(e: AppManagerError) -> Self { + Self::AppManagerError(e.to_string()) + } +} 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..db9190781 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/error_transformation.rs @@ -0,0 +1,132 @@ +use super::types::LogicError; +use super::{AppClient, AppSourceMaps}; +use crate::transactions::TransactionResultError; +use serde_json::Value as JsonValue; + +impl AppClient { + pub fn import_source_maps(&mut self, source_maps: AppSourceMaps) { + self.source_maps = Some(source_maps); + } + + 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: bool) -> LogicError { + let err_str = format!("{}", error); + let (line_no_opt, listing) = self.apply_source_map_for_message(&err_str, is_clear); + let source_map = self.get_source_map(is_clear).cloned(); + let transaction_id = Self::extract_transaction_id(&err_str); + + let logic = LogicError { + logic_error_str: err_str.clone(), + program: None, + source_map, + transaction_id, + pc: Self::extract_pc(&err_str), + line_no: line_no_opt, + lines: if listing.is_empty() { + None + } else { + Some(listing) + }, + traces: None, + }; + + if crate::config::Config::debug() { + // TODO: Add traces to LogicError + } + + logic + } + + fn extract_transaction_id(error_str: &str) -> Option { + // Look for transaction ID pattern in error message + if let Some(idx) = error_str.find("transaction ") { + let start = idx + "transaction ".len(); + let remaining = &error_str[start..]; + if let Some(end) = remaining.find(' ') { + return Some(remaining[..end].to_string()); + } + } + None + } + + fn apply_source_map_for_message( + &self, + error_str: &str, + is_clear: 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) { + return (Some(line_no), listing); + } + } + (None, Vec::new()) + } + + 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) = v.trim_start_matches('=').parse::() { + return Some(parsed); + } + } + } + } + None + } + + fn apply_source_map(&self, pc: u64, is_clear: bool) -> Option<(u64, Vec)> { + let map = self.get_source_map(is_clear)?; + let line_no = Self::map_pc_to_line(map, pc)?; + let listing = Self::truncate_listing(map, line_no, 3); + Some((line_no, listing)) + } + + fn get_source_map(&self, is_clear: bool) -> Option<&JsonValue> { + let maps = self.source_maps.as_ref()?; + if is_clear { + maps.clear_source_map.as_ref() + } else { + maps.approval_source_map.as_ref() + } + } + + 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 + } + + 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 + } +} 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..f75db338a --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/mod.rs @@ -0,0 +1,399 @@ +use algokit_abi::Arc56Contract; +use std::collections::HashMap; + +use crate::AlgorandClient; +use crate::applications::AppDeployer; +use crate::clients::network_client::NetworkDetails; +use algokit_transact::Address; +use std::str::FromStr; +mod abi_integration; +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, AppClientJsonParams, AppClientMethodCallParams, AppClientParams, + AppSourceMaps, FundAppAccountParams, +}; + +/// A client for interacting with an Algorand smart contract application (ARC-56 focused). +pub struct AppClient { + app_id: Option, + app_spec: Arc56Contract, + algorand: AlgorandClient, + default_sender: Option, + source_maps: Option, + app_name: 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, + source_maps: params.source_maps, + app_name: params.app_name, + } + } + + /// Create a new client from JSON parameters. + /// Accepts a JSON string and normalizes into a typed ARC-56 contract. + pub fn from_json(params: types::AppClientJsonParams) -> Result { + let app_spec = Arc56Contract::from_json(params.app_spec_json) + .map_err(|e| AppClientError::ValidationError(e.to_string()))?; + Ok(Self::new(AppClientParams { + app_id: params.app_id, + app_spec, + algorand: params.algorand, + app_name: params.app_name, + default_sender: params.default_sender, + source_maps: params.source_maps, + })) + } + + /// 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, + source_maps: Option, + ) -> Result { + let network = algorand + .client() + .network() + .await + .map_err(|e| AppClientError::Network(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: Some(app_id), + app_spec, + algorand, + app_name, + default_sender, + source_maps, + })) + } + + /// Construct from creator address and application name via indexer lookup. + pub async fn from_creator_and_name( + creator_address: &str, + app_name: &str, + app_spec: Arc56Contract, + algorand: AlgorandClient, + default_sender: Option, + source_maps: Option, + ignore_cache: Option, + ) -> Result { + let address = Address::from_str(creator_address) + .map_err(|e| AppClientError::Lookup(format!("Invalid creator address: {}", e)))?; + + let indexer_client = algorand.client().indexer(); + 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(e.to_string()))?; + + let app_metadata = lookup.apps.get(app_name).ok_or_else(|| { + AppClientError::Lookup(format!( + "App not found for creator {} and name {}", + creator_address, app_name + )) + })?; + + Ok(Self::new(AppClientParams { + app_id: Some(app_metadata.app_id), + app_spec, + algorand, + app_name: Some(app_name.to_string()), + default_sender, + source_maps, + })) + } + + 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 + } + + pub fn app_id(&self) -> Option { + self.app_id + } + pub fn app_spec(&self) -> &Arc56Contract { + &self.app_spec + } + pub fn algorand(&self) -> &AlgorandClient { + &self.algorand + } + pub fn app_name(&self) -> Option<&String> { + self.app_name.as_ref() + } + pub fn default_sender(&self) -> Option<&String> { + self.default_sender.as_ref() + } + + /// Get the application address if app_id is set. + pub fn app_address(&self) -> Option
{ + self.app_id.map(|id| Address::from_app_id(&id)) + } + + fn get_sender_address(&self, sender: &Option) -> Result { + let sender_str = sender + .as_ref() + .or(self.default_sender.as_ref()) + .ok_or_else(|| { + 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| format!("Invalid sender address: {}", e)) + } + + fn get_optional_address(value: &Option) -> Result, String> { + match value { + Some(s) => Ok(Some( + Address::from_str(s).map_err(|e| format!("Invalid address: {}", e))?, + )), + None => Ok(None), + } + } + + fn get_app_address(&self) -> Result { + let app_id = self.app_id.ok_or_else(|| "Missing app_id".to_string())?; + Ok(Address::from_app_id(&app_id)) + } + + /// Direct method: fund the application's account + pub async fn fund_app_account( + &self, + params: FundAppAccountParams, + ) -> Result< + crate::transactions::SendTransactionResult, + crate::transactions::TransactionSenderError, + > { + let payment = self.params().fund_app_account(¶ms).map_err(|e| { + crate::transactions::TransactionSenderError::ValidationError { message: e } + })?; + + self.algorand.send().payment(payment, None).await + } + + /// Get raw global state as HashMap, AppState> + pub async fn get_global_state( + &self, + ) -> Result< + std::collections::HashMap, crate::clients::app_manager::AppState>, + AppClientError, + > { + self.algorand + .app() + .get_global_state( + self.app_id + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + ) + .await + .map_err(AppClientError::from) + } + + /// Get raw local state for an address + pub async fn get_local_state( + &self, + address: &str, + ) -> Result< + std::collections::HashMap, crate::clients::app_manager::AppState>, + AppClientError, + > { + self.algorand + .app() + .get_local_state( + self.app_id + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + address, + ) + .await + .map_err(AppClientError::from) + } + + /// 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 + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + ) + .await + .map_err(AppClientError::from) + } + + /// Get the value of a box by raw identifier + pub async fn get_box_value( + &self, + name: &crate::clients::app_manager::BoxIdentifier, + ) -> Result, AppClientError> { + self.algorand + .app() + .get_box_value( + self.app_id + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + name, + ) + .await + .map_err(AppClientError::from) + } + + /// Get a box value decoded using an ABI type + pub async fn get_box_value_from_abi_type( + &self, + name: &crate::clients::app_manager::BoxIdentifier, + abi_type: &algokit_abi::ABIType, + ) -> Result { + self.algorand + .app() + .get_box_value_from_abi_type( + self.app_id + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + name, + abi_type, + ) + .await + .map_err(AppClientError::from) + } + + /// Get values for multiple boxes + pub async fn get_box_values( + &self, + names: &[crate::clients::app_manager::BoxIdentifier], + ) -> Result>, AppClientError> { + self.algorand + .app() + .get_box_values( + self.app_id + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + names, + ) + .await + .map_err(AppClientError::from) + } + + /// Get multiple box values decoded using an ABI type + pub async fn get_box_values_from_abi_type( + &self, + names: &[crate::clients::app_manager::BoxIdentifier], + abi_type: &algokit_abi::ABIType, + ) -> Result, AppClientError> { + self.algorand + .app() + .get_box_values_from_abi_type( + self.app_id + .ok_or_else(|| AppClientError::ValidationError("Missing app_id".to_string()))?, + names, + abi_type, + ) + .await + .map_err(AppClientError::from) + } +} + +// -------- Minimal fluent API scaffolding (to be expanded incrementally) -------- + +impl AppClient { + pub fn params(&self) -> ParamsBuilder<'_> { + ParamsBuilder { client: self } + } + pub fn create_transaction(&self) -> TransactionBuilder<'_> { + TransactionBuilder { client: self } + } + pub fn send(&self) -> TransactionSender<'_> { + TransactionSender { client: self } + } + pub fn state(&self) -> StateAccessor<'_> { + StateAccessor::new(self) + } +} + +// Method call parameter building is implemented in params_builder.rs + +impl TransactionBuilder<'_> { + pub async fn call_method( + &self, + params: types::AppClientMethodCallParams, + ) -> Result + { + let method_params = self + .client + .params() + .method_call(¶ms) + .await + .map_err( + |e| crate::transactions::composer::ComposerError::TransactionError { message: e }, + )?; + self.client + .algorand + .create() + .app_call_method_call(method_params) + .await + } +} + +impl TransactionSender<'_> {} 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..864fa20d0 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/params_builder.rs @@ -0,0 +1,492 @@ +use algokit_abi::ABIMethod; +use algokit_transact::OnApplicationComplete; + +use crate::transactions::{ + AppCallMethodCallParams, AppCallParams, AppDeleteMethodCallParams, AppDeleteParams, + AppMethodCallArg, AppUpdateMethodCallParams, AppUpdateParams, CommonTransactionParams, + PaymentParams, +}; + +use super::AppClient; +use super::types::{ + AppClientBareCallParams, AppClientMethodCallParams, CompilationParams, FundAppAccountParams, +}; + +pub struct ParamsBuilder<'a> { + pub(crate) client: &'a AppClient, +} + +pub struct BareParamsBuilder<'a> { + pub(crate) client: &'a AppClient, +} + +impl<'a> ParamsBuilder<'a> { + /// Get the bare call params builder. + pub fn bare(&self) -> BareParamsBuilder<'a> { + BareParamsBuilder { + client: self.client, + } + } + + /// Call a method with NoOp. + pub async fn call( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::NoOp) + .await + } + + /// Call a method with OptIn. + pub async fn opt_in( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::OptIn) + .await + } + + /// Call a method with CloseOut. + pub async fn close_out( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::CloseOut) + .await + } + + /// Call a method with Delete. + pub async fn delete( + &self, + params: AppClientMethodCallParams, + ) -> Result { + let method_params = self + .method_call_with_on_complete(params, OnApplicationComplete::DeleteApplication) + .await?; + + Ok(AppDeleteMethodCallParams { + common_params: method_params.common_params, + app_id: method_params.app_id, + method: method_params.method, + args: method_params.args, + account_references: method_params.account_references, + app_references: method_params.app_references, + asset_references: method_params.asset_references, + box_references: method_params.box_references, + }) + } + + /// Update the application with a method call. + pub async fn update( + &self, + params: AppClientMethodCallParams, + compilation_params: Option, + ) -> Result { + // Compile programs (and populate AppManager cache/source maps) + let cp = compilation_params.unwrap_or_default(); + let (approval_program, clear_state_program) = self + .client + .compile_with_params(&cp) + .await + .map_err(|e| e.to_string())?; + + // Reuse method_call to resolve method + args + common params + let method_params = self.method_call(¶ms).await?; + + Ok(AppUpdateMethodCallParams { + common_params: method_params.common_params, + app_id: method_params.app_id, + approval_program, + clear_state_program, + method: method_params.method, + args: method_params.args, + account_references: method_params.account_references, + app_references: method_params.app_references, + asset_references: method_params.asset_references, + box_references: method_params.box_references, + }) + } + + /// Fund the application account. + pub fn fund_app_account(&self, params: &FundAppAccountParams) -> Result { + let sender = self.client.get_sender_address(¶ms.sender)?; + let receiver = self.client.get_app_address()?; + let rekey_to = AppClient::get_optional_address(¶ms.rekey_to)?; + + Ok(PaymentParams { + common_params: CommonTransactionParams { + sender, + 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, + ..Default::default() + }, + receiver, + amount: params.amount, + }) + } + + async fn method_call_with_on_complete( + &self, + mut params: AppClientMethodCallParams, + on_complete: OnApplicationComplete, + ) -> Result { + params.on_complete = Some(on_complete); + self.method_call(¶ms).await + } + + pub async fn method_call( + &self, + params: &AppClientMethodCallParams, + ) -> Result { + let abimethod = self.to_abimethod(¶ms.method)?; + let provided_len = params.args.as_ref().map(|v| v.len()).unwrap_or(0); + let expected = abimethod.args.len(); + if provided_len > expected { + return Err(format!( + "Unexpected arg at position {}. {} only expects {} args", + expected + 1, + abimethod.name, + expected + )); + } + + let resolved_args = self + .resolve_args_with_defaults(&abimethod, ¶ms.args, params.sender.as_deref()) + .await?; + + Ok(AppCallMethodCallParams { + common_params: self.build_common_params_from_method(params)?, + app_id: self + .client + .app_id + .ok_or_else(|| "Missing app_id".to_string())?, + method: abimethod, + args: resolved_args, + account_references: super::utils::parse_account_refs_strs(¶ms.account_references)?, + app_references: params.app_references.clone(), + asset_references: params.asset_references.clone(), + box_references: params.box_references.clone(), + on_complete: params.on_complete.unwrap_or(OnApplicationComplete::NoOp), + }) + } + + /// Build method call params without performing default resolution. + /// Provided arguments are used as-is, only basic validation is applied. + pub fn method_call_no_defaults( + &self, + params: &AppClientMethodCallParams, + ) -> Result { + let abimethod = self.to_abimethod(¶ms.method)?; + let provided_args = params.args.clone().unwrap_or_default(); + let provided_len = provided_args.len(); + let expected = abimethod.args.len(); + if provided_len > expected { + return Err(format!( + "Unexpected arg at position {}. {} only expects {} args", + expected + 1, + abimethod.name, + expected + )); + } + + Ok(AppCallMethodCallParams { + common_params: self.build_common_params_from_method(params)?, + app_id: self + .client + .app_id + .ok_or_else(|| "Missing app_id".to_string())?, + method: abimethod, + args: provided_args, + account_references: super::utils::parse_account_refs_strs(¶ms.account_references)?, + app_references: params.app_references.clone(), + asset_references: params.asset_references.clone(), + box_references: params.box_references.clone(), + on_complete: params.on_complete.unwrap_or(OnApplicationComplete::NoOp), + }) + } + + fn build_common_params_from_method( + &self, + params: &AppClientMethodCallParams, + ) -> Result { + Ok(CommonTransactionParams { + sender: self.client.get_sender_address(¶ms.sender)?, + rekey_to: AppClient::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, + ..Default::default() + }) + } + + fn to_abimethod(&self, method_name_or_sig: &str) -> Result { + let m = self + .client + .app_spec + .get_arc56_method(method_name_or_sig) + .map_err(|e| e.to_string())?; + self.client + .app_spec + .to_abi_method(m) + .map_err(|e| e.to_string()) + } + + async fn resolve_args_with_defaults( + &self, + method: &ABIMethod, + provided: &Option>, + sender: Option<&str>, + ) -> Result, String> { + use algokit_abi::ABIMethodArgType; + let mut resolved: Vec = Vec::with_capacity(method.args.len()); + + // Pre-fetch ARC-56 method once if available + let arc56_method = method + .signature() + .ok() + .and_then(|sig| self.client.app_spec().get_arc56_method(&sig).ok()); + + for (i, m_arg) in method.args.iter().enumerate() { + let provided_arg = provided.as_ref().and_then(|v| v.get(i)).cloned(); + + match (&m_arg.arg_type, provided_arg) { + // Value-type arguments + (ABIMethodArgType::Value(value_type), Some(AppMethodCallArg::ABIValue(v))) => { + // Provided concrete ABI value + // (we don't type-check here; encoder will validate) + let _ = value_type; // silence unused variable warning if any + resolved.push(AppMethodCallArg::ABIValue(v)); + } + (ABIMethodArgType::Value(value_type), Some(AppMethodCallArg::DefaultValue)) => { + // Explicit request to use ARC-56 default + let def = arc56_method + .as_ref() + .and_then(|m| m.args.get(i)) + .and_then(|a| a.default_value.clone()) + .ok_or_else(|| { + format!( + "No default value defined for argument {} in call to method {}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name + ) + })?; + let abi_type_string = value_type.to_string(); + let value = self + .client + .resolve_default_value_for_arg(&def, &abi_type_string, sender) + .await + .map_err(|e| e.to_string())?; + resolved.push(AppMethodCallArg::ABIValue(value)); + } + (ABIMethodArgType::Value(_), Some(other)) => { + return Err(format!( + "Invalid argument type for value argument {} in call to method {}: {:?}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name, + other + )); + } + (ABIMethodArgType::Value(value_type), None) => { + // No provided value; try default, else error + if let Some(def) = arc56_method + .as_ref() + .and_then(|m| m.args.get(i)) + .and_then(|a| a.default_value.clone()) + { + let abi_type_string = value_type.to_string(); + let value = self + .client + .resolve_default_value_for_arg(&def, &abi_type_string, sender) + .await + .map_err(|e| e.to_string())?; + resolved.push(AppMethodCallArg::ABIValue(value)); + } else { + return Err(format!( + "No value provided for required argument {} in call to method {}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name + )); + } + } + + // Reference-type arguments must be provided explicitly as ABIReference + (ABIMethodArgType::Reference(_), Some(AppMethodCallArg::ABIReference(r))) => { + resolved.push(AppMethodCallArg::ABIReference(r)); + } + (ABIMethodArgType::Reference(_), Some(AppMethodCallArg::DefaultValue)) => { + return Err(format!( + "DefaultValue sentinel not supported for reference argument {} in call to method {}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name + )); + } + (ABIMethodArgType::Reference(_), Some(other)) => { + return Err(format!( + "Invalid argument type for reference argument {} in call to method {}: {:?}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name, + other + )); + } + (ABIMethodArgType::Reference(_), None) => { + return Err(format!( + "No value provided for required reference argument {} in call to method {}", + m_arg + .name + .clone() + .unwrap_or_else(|| format!("arg{}", i + 1)), + method.name + )); + } + + // Transaction-type arguments: allow omission or DefaultValue -> placeholder + (ABIMethodArgType::Transaction(_), Some(AppMethodCallArg::DefaultValue)) => { + resolved.push(AppMethodCallArg::TransactionPlaceholder); + } + (ABIMethodArgType::Transaction(_), Some(arg)) => { + // Any transaction-bearing variant or explicit placeholder is accepted + resolved.push(arg); + } + (ABIMethodArgType::Transaction(_), None) => { + resolved.push(AppMethodCallArg::TransactionPlaceholder); + } + } + } + + Ok(resolved) + } +} + +impl BareParamsBuilder<'_> { + /// Call with NoOp. + pub fn call(&self, params: AppClientBareCallParams) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::NoOp) + } + + /// Call with OptIn. + pub fn opt_in(&self, params: AppClientBareCallParams) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::OptIn) + } + + /// Call with CloseOut. + pub fn close_out(&self, params: AppClientBareCallParams) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::CloseOut) + } + + /// Call with Delete. + pub fn delete(&self, params: AppClientBareCallParams) -> Result { + let app_call = + self.build_bare_app_call_params(params, OnApplicationComplete::DeleteApplication)?; + Ok(AppDeleteParams { + common_params: app_call.common_params, + app_id: app_call.app_id, + args: app_call.args, + account_references: app_call.account_references, + app_references: app_call.app_references, + asset_references: app_call.asset_references, + box_references: app_call.box_references, + }) + } + + /// Call with ClearState. + pub fn clear_state(&self, params: AppClientBareCallParams) -> Result { + self.build_bare_app_call_params(params, OnApplicationComplete::ClearState) + } + + /// Update with bare call. + pub async fn update( + &self, + params: AppClientBareCallParams, + compilation_params: Option, + ) -> Result { + // Compile programs (and populate AppManager cache/source maps) + let cp = compilation_params.unwrap_or_default(); + let (approval_program, clear_state_program) = self + .client + .compile_with_params(&cp) + .await + .map_err(|e| e.to_string())?; + + // Resolve common/bare fields + let app_call = + self.build_bare_app_call_params(params, OnApplicationComplete::UpdateApplication)?; + + Ok(AppUpdateParams { + common_params: app_call.common_params, + app_id: app_call.app_id, + approval_program, + clear_state_program, + args: app_call.args, + account_references: app_call.account_references, + app_references: app_call.app_references, + asset_references: app_call.asset_references, + box_references: app_call.box_references, + }) + } + + fn build_bare_app_call_params( + &self, + params: AppClientBareCallParams, + default_on_complete: OnApplicationComplete, + ) -> Result { + Ok(AppCallParams { + common_params: self.build_common_params_from_bare(¶ms)?, + app_id: self + .client + .app_id + .ok_or_else(|| "Missing app_id".to_string())?, + on_complete: params.on_complete.unwrap_or(default_on_complete), + args: params.args, + account_references: super::utils::parse_account_refs_strs(¶ms.account_references)?, + app_references: params.app_references, + asset_references: params.asset_references, + box_references: params.box_references, + }) + } + + fn build_common_params_from_bare( + &self, + params: &AppClientBareCallParams, + ) -> Result { + Ok(CommonTransactionParams { + sender: self.client.get_sender_address(¶ms.sender)?, + rekey_to: AppClient::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, + ..Default::default() + }) + } +} 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..4231e7911 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/sender.rs @@ -0,0 +1,349 @@ +use crate::transactions::{SendTransactionResult, TransactionSenderError}; +use algokit_transact::OnApplicationComplete; + +use super::types::{AppClientBareCallParams, AppClientMethodCallParams, CompilationParams}; +use super::{AppClient, FundAppAccountParams}; +// use std::str::FromStr; // no longer needed after refactor + +pub struct TransactionSender<'a> { + pub(crate) client: &'a AppClient, +} + +pub struct BareTransactionSender<'a> { + pub(crate) client: &'a AppClient, +} + +impl<'a> TransactionSender<'a> { + /// Get the bare transaction sender. + pub fn bare(&self) -> BareTransactionSender<'a> { + BareTransactionSender { + client: self.client, + } + } + + /// Call a method with NoOp. + pub async fn call( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::NoOp) + .await + } + + /// Call a method with OptIn. + pub async fn opt_in( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::OptIn) + .await + } + + /// Call a method with CloseOut. + pub async fn close_out( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::CloseOut) + .await + } + + /// Call a method with Delete. + pub async fn delete( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::DeleteApplication) + .await + } + + /// Update the application with a method call. + pub async fn update( + &self, + params: AppClientMethodCallParams, + compilation_params: Option, + ) -> Result { + let update_params = self + .client + .params() + .update(params, compilation_params) + .await + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + + self.client + .algorand + .send() + .app_update_method_call(update_params, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Fund the application account. + pub async fn fund_app_account( + &self, + params: FundAppAccountParams, + ) -> Result { + let payment = self + .client + .params() + .fund_app_account(¶ms) + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + self.client + .algorand + .send() + .payment(payment, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + async fn method_call_with_on_complete( + &self, + mut params: AppClientMethodCallParams, + on_complete: OnApplicationComplete, + ) -> Result { + params.on_complete = Some(on_complete); + let method_params = self + .client + .params() + .method_call(¶ms) + .await + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + let is_delete = matches!( + method_params.on_complete, + OnApplicationComplete::DeleteApplication + ); + // If debug enabled and readonly method, simulate with tracing + let is_readonly = self.client.is_readonly_method(&method_params.method); + if crate::config::Config::debug() && is_readonly { + self.simulate_readonly_with_tracing_for_debug(¶ms, is_delete) + .await?; + } + + let result = if is_delete { + let delete_params = crate::transactions::AppDeleteMethodCallParams { + common_params: method_params.common_params.clone(), + app_id: method_params.app_id, + method: method_params.method.clone(), + args: method_params.args.clone(), + account_references: method_params.account_references.clone(), + app_references: method_params.app_references.clone(), + asset_references: method_params.asset_references.clone(), + box_references: method_params.box_references.clone(), + }; + self.client + .algorand + .send() + .app_delete_method_call(delete_params, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false))? + } else { + self.client + .algorand + .send() + .app_call_method_call(method_params, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false))? + }; + + // Returns are already ABI-decoded; expose as-is + Ok(result) + } + + // Simulate a readonly call when debug is enabled, emitting traces if configured. + pub(crate) async fn simulate_readonly_with_tracing_for_debug( + &self, + params: &AppClientMethodCallParams, + is_delete: bool, + ) -> Result<(), TransactionSenderError> { + let mut composer = self.client.algorand().new_group(); + if is_delete { + let method_params_for_composer = self + .client + .params() + .method_call(params) + .await + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + let delete_params = crate::transactions::AppDeleteMethodCallParams { + common_params: method_params_for_composer.common_params.clone(), + app_id: method_params_for_composer.app_id, + method: method_params_for_composer.method.clone(), + args: method_params_for_composer.args.clone(), + account_references: method_params_for_composer.account_references.clone(), + app_references: method_params_for_composer.app_references.clone(), + asset_references: method_params_for_composer.asset_references.clone(), + box_references: method_params_for_composer.box_references.clone(), + }; + composer + .add_app_delete_method_call(delete_params) + .map_err(|e| TransactionSenderError::ValidationError { + message: e.to_string(), + })?; + } else { + let method_params_for_composer = self + .client + .params() + .method_call(params) + .await + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + composer + .add_app_call_method_call(method_params_for_composer) + .map_err(|e| TransactionSenderError::ValidationError { + message: e.to_string(), + })?; + } + + let sim_params = crate::transactions::composer::SimulateParams { + allow_more_logging: Some(true), + allow_empty_signatures: Some(true), + exec_trace_config: Some(algod_client::models::SimulateTraceConfig { + enable: Some(true), + scratch_change: Some(true), + stack_change: Some(true), + state_change: Some(true), + }), + skip_signatures: true, + ..Default::default() + }; + + let sim = composer.simulate(Some(sim_params)).await.map_err(|e| { + TransactionSenderError::ValidationError { + message: e.to_string(), + } + })?; + + if crate::config::Config::trace_all() { + let json = serde_json::to_value(&sim.confirmations) + .unwrap_or(serde_json::json!({"error":"failed to serialize confirmations"})); + let event = crate::config::TxnGroupSimulatedEventData { + simulate_response: json, + }; + crate::config::Config::events() + .emit( + crate::config::EventType::TxnGroupSimulated, + crate::config::EventData::TxnGroupSimulated(event), + ) + .await; + } + + Ok(()) + } +} + +impl BareTransactionSender<'_> { + /// Call with NoOp. + pub async fn call( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .call(params) + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + self.client + .algorand + .send() + .app_call(app_call, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Call with OptIn. + pub async fn opt_in( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .opt_in(params) + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + self.client + .algorand + .send() + .app_call(app_call, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Call with CloseOut. + pub async fn close_out( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .close_out(params) + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + self.client + .algorand + .send() + .app_call(app_call, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Call with Delete. + pub async fn delete( + &self, + params: AppClientBareCallParams, + ) -> Result { + let delete_params = self + .client + .params() + .bare() + .delete(params) + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + self.client + .algorand + .send() + .app_delete(delete_params, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, false)) + } + + /// Call with ClearState. + pub async fn clear_state( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .clear_state(params) + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + self.client + .algorand + .send() + .app_call(app_call, None) + .await + .map_err(|e| super::utils::transform_transaction_error(self.client, e, true)) + } + + /// Update with bare call. + pub async fn update( + &self, + params: AppClientBareCallParams, + compilation_params: Option, + ) -> Result { + let update_params = self + .client + .params() + .bare() + .update(params, compilation_params) + .await + .map_err(|e| TransactionSenderError::ValidationError { message: e })?; + + self.client + .algorand + .send() + .app_update(update_params, None) + .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..5b85ead4e --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/state_accessor.rs @@ -0,0 +1,558 @@ +use super::{AppClient, AppClientError}; +use algokit_abi::arc56_contract::{AVM_BYTES, AVM_STRING}; +use algokit_abi::{ABIType, ABIValue}; +use base64::Engine; +use std::collections::HashMap; +use std::str::FromStr; + +pub struct GlobalStateAccessor<'a> { + client: &'a AppClient, +} + +pub struct LocalStateAccessor<'a> { + client: &'a AppClient, + address: String, +} + +pub struct BoxStateAccessor<'a> { + client: &'a AppClient, +} + +pub struct StateAccessor<'a> { + pub(crate) client: &'a AppClient, +} + +impl<'a> StateAccessor<'a> { + pub fn new(client: &'a AppClient) -> Self { + Self { client } + } + + pub fn global_state(&self) -> GlobalStateAccessor<'a> { + GlobalStateAccessor { + client: self.client, + } + } + pub fn local_state(&self, address: &str) -> LocalStateAccessor<'a> { + LocalStateAccessor { + client: self.client, + address: address.to_string(), + } + } + pub fn box_storage(&self) -> BoxStateAccessor<'a> { + BoxStateAccessor { + client: self.client, + } + } +} + +impl GlobalStateAccessor<'_> { + pub async fn get_all(&self) -> Result, AppClientError> { + let state = self.client.get_global_state().await?; + let mut result = HashMap::new(); + for (name, metadata) in &self.client.app_spec.state.keys.global_state { + // decode key and fetch value + let key_bytes = base64::engine::general_purpose::STANDARD + .decode(&metadata.key) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode global key '{}': {}", + name, e + )) + })?; + let app_state = state.get(&key_bytes).ok_or_else(|| { + AppClientError::ValidationError(format!( + "Global state key '{}' not found in app state", + name + )) + })?; + let abi_value = decode_app_state_value(&metadata.value_type, app_state)?; + result.insert(name.clone(), abi_value); + } + Ok(result) + } + + pub async fn get_value(&self, name: &str) -> Result { + let metadata = self + .client + .app_spec + .state + .keys + .global_state + .get(name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown global state key: {}", name)) + })?; + let key_bytes = base64::engine::general_purpose::STANDARD + .decode(&metadata.key) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode global key '{}': {}", + name, e + )) + })?; + let state = self.client.get_global_state().await?; + let app_state = state.get(&key_bytes).ok_or_else(|| { + AppClientError::ValidationError(format!( + "Global state key '{}' not found in app state", + name + )) + })?; + decode_app_state_value(&metadata.value_type, app_state) + } + + pub async fn get_map_value( + &self, + map_name: &str, + key: &ABIValue, + ) -> Result { + let map = self + .client + .app_spec + .state + .maps + .global_state + .get(map_name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown global map: {}", map_name)) + })?; + let key_type = ABIType::from_str(&map.key_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.key_type, e)) + })?; + let key_bytes = key_type.encode(key).map_err(|e| { + AppClientError::ValidationError(format!("Failed to encode map key: {}", e)) + })?; + let mut full_key = if let Some(prefix_b64) = &map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode map prefix: {}", e)) + })? + } else { + Vec::new() + }; + full_key.extend_from_slice(&key_bytes); + + let state = self.client.get_global_state().await?; + let app_state = state.get(&full_key).ok_or_else(|| { + AppClientError::ValidationError(format!("Global map '{}' key not found", map_name)) + })?; + decode_app_state_value(&map.value_type, app_state) + } + + pub async fn get_map( + &self, + map_name: &str, + ) -> Result, AppClientError> { + let map = self + .client + .app_spec + .state + .maps + .global_state + .get(map_name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown global map: {}", map_name)) + })?; + let prefix_bytes = if let Some(prefix_b64) = &map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode map prefix: {}", e)) + })? + } else { + Vec::new() + }; + let key_type = ABIType::from_str(&map.key_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.key_type, e)) + })?; + + let mut result = HashMap::new(); + let state = self.client.get_global_state().await?; + for (key_raw, app_state) in state.iter() { + if !key_raw.starts_with(&prefix_bytes) { + continue; + } + let tail = &key_raw[prefix_bytes.len()..]; + // Decode the map key tail according to ABI type, error if invalid + let decoded_key = key_type.decode(tail).map_err(|e| { + AppClientError::AbiError(format!( + "Failed to decode key for map '{}': {}", + map_name, e + )) + })?; + let key_str = abi_value_to_string(&decoded_key); + let value = decode_app_state_value(&map.value_type, app_state)?; + result.insert(key_str, value); + } + Ok(result) + } +} + +impl LocalStateAccessor<'_> { + pub async fn get_all(&self) -> Result, AppClientError> { + let state = self.client.get_local_state(&self.address).await?; + let mut result = HashMap::new(); + for (name, metadata) in &self.client.app_spec.state.keys.local_state { + let key_bytes = base64::engine::general_purpose::STANDARD + .decode(&metadata.key) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode local key '{}': {}", + name, e + )) + })?; + let app_state = state.get(&key_bytes).ok_or_else(|| { + AppClientError::ValidationError(format!( + "Local state key '{}' not found for address {}", + name, self.address + )) + })?; + let abi_value = decode_app_state_value(&metadata.value_type, app_state)?; + result.insert(name.clone(), abi_value); + } + Ok(result) + } + + pub async fn get_value(&self, name: &str) -> Result { + let metadata = self + .client + .app_spec + .state + .keys + .local_state + .get(name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown local state key: {}", name)) + })?; + let key_bytes = base64::engine::general_purpose::STANDARD + .decode(&metadata.key) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode local key '{}': {}", + name, e + )) + })?; + let state = self.client.get_local_state(&self.address).await?; + let app_state = state.get(&key_bytes).ok_or_else(|| { + AppClientError::ValidationError(format!( + "Local state key '{}' not found for address {}", + name, self.address + )) + })?; + decode_app_state_value(&metadata.value_type, app_state) + } + + pub async fn get_map_value( + &self, + map_name: &str, + key: &ABIValue, + ) -> Result { + let map = self + .client + .app_spec + .state + .maps + .local_state + .get(map_name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown local map: {}", map_name)) + })?; + let key_type = ABIType::from_str(&map.key_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.key_type, e)) + })?; + let key_bytes = key_type.encode(key).map_err(|e| { + AppClientError::ValidationError(format!("Failed to encode map key: {}", e)) + })?; + let mut full_key = if let Some(prefix_b64) = &map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode map prefix: {}", e)) + })? + } else { + Vec::new() + }; + full_key.extend_from_slice(&key_bytes); + + let state = self.client.get_local_state(&self.address).await?; + let app_state = state.get(&full_key).ok_or_else(|| { + AppClientError::ValidationError(format!("Local map '{}' key not found", map_name)) + })?; + decode_app_state_value(&map.value_type, app_state) + } + + pub async fn get_map( + &self, + map_name: &str, + ) -> Result, AppClientError> { + let map = self + .client + .app_spec + .state + .maps + .local_state + .get(map_name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown local map: {}", map_name)) + })?; + let prefix_bytes = if let Some(prefix_b64) = &map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode map prefix: {}", e)) + })? + } else { + Vec::new() + }; + let key_type = ABIType::from_str(&map.key_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.key_type, e)) + })?; + + let mut result = HashMap::new(); + let state = self.client.get_local_state(&self.address).await?; + for (key_raw, app_state) in state.iter() { + if !key_raw.starts_with(&prefix_bytes) { + continue; + } + let tail = &key_raw[prefix_bytes.len()..]; + let decoded_key = key_type.decode(tail).map_err(|e| { + AppClientError::AbiError(format!( + "Failed to decode key for map '{}': {}", + map_name, e + )) + })?; + let key_str = abi_value_to_string(&decoded_key); + let value = decode_app_state_value(&map.value_type, app_state)?; + result.insert(key_str, value); + } + Ok(result) + } +} + +impl BoxStateAccessor<'_> { + pub async fn get_value(&self, name: &str) -> Result { + let metadata = self + .client + .app_spec + .state + .keys + .box_keys + .get(name) + .ok_or_else(|| AppClientError::ValidationError(format!("Unknown box key: {}", name)))?; + let box_name = base64::engine::general_purpose::STANDARD + .decode(&metadata.key) + .map_err(|e| { + AppClientError::ValidationError(format!( + "Failed to decode box key '{}': {}", + name, e + )) + })?; + let abi_type = ABIType::from_str(&metadata.value_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", metadata.value_type, e)) + })?; + self.client + .algorand() + .app() + .get_box_value_from_abi_type( + self.client.app_id().ok_or(AppClientError::ValidationError( + "Missing app_id".to_string(), + ))?, + &box_name, + &abi_type, + ) + .await + .map_err(|e| AppClientError::AppManagerError(e.to_string())) + } + + pub async fn get_map_value( + &self, + map_name: &str, + key: &ABIValue, + ) -> Result { + let map = self + .client + .app_spec + .state + .maps + .box_maps + .get(map_name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown box map: {}", map_name)) + })?; + let key_type = ABIType::from_str(&map.key_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.key_type, e)) + })?; + let key_bytes = key_type.encode(key).map_err(|e| { + AppClientError::ValidationError(format!("Failed to encode map key: {}", e)) + })?; + let mut full_key = if let Some(prefix_b64) = &map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode map prefix: {}", e)) + })? + } else { + Vec::new() + }; + full_key.extend_from_slice(&key_bytes); + let value_type = ABIType::from_str(&map.value_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.value_type, e)) + })?; + self.client + .algorand() + .app() + .get_box_value_from_abi_type( + self.client.app_id().ok_or(AppClientError::ValidationError( + "Missing app_id".to_string(), + ))?, + &full_key, + &value_type, + ) + .await + .map_err(|e| AppClientError::AppManagerError(e.to_string())) + } + + pub async fn get_map( + &self, + map_name: &str, + ) -> Result, AppClientError> { + let map = self + .client + .app_spec + .state + .maps + .box_maps + .get(map_name) + .ok_or_else(|| { + AppClientError::ValidationError(format!("Unknown box map: {}", map_name)) + })?; + let prefix_bytes = if let Some(prefix_b64) = &map.prefix { + base64::engine::general_purpose::STANDARD + .decode(prefix_b64) + .map_err(|e| { + AppClientError::ValidationError(format!("Failed to decode map prefix: {}", e)) + })? + } else { + Vec::new() + }; + + let key_type = ABIType::from_str(&map.key_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.key_type, e)) + })?; + let value_type = ABIType::from_str(&map.value_type).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", map.value_type, e)) + })?; + + let mut result = HashMap::new(); + let box_names = self.client.get_box_names().await?; + for box_name in box_names { + if !box_name.name_raw.starts_with(&prefix_bytes) { + continue; + } + let tail = &box_name.name_raw[prefix_bytes.len()..]; + let decoded_key = key_type.decode(tail).map_err(|e| { + AppClientError::AbiError(format!( + "Failed to decode key for map '{}': {}", + map_name, e + )) + })?; + let key_str = abi_value_to_string(&decoded_key); + let val = self + .client + .algorand() + .app() + .get_box_value_from_abi_type( + self.client.app_id().ok_or(AppClientError::ValidationError( + "Missing app_id".to_string(), + ))?, + &box_name.name_raw, + &value_type, + ) + .await + .map_err(|e| AppClientError::AppManagerError(e.to_string()))?; + result.insert(key_str, val); + } + Ok(result) + } +} + +pub(crate) fn decode_app_state_value( + value_type_str: &str, + app_state: &crate::clients::app_manager::AppState, +) -> Result { + match &app_state.value { + crate::clients::app_manager::AppStateValue::Uint(u) => { + // For integer types, convert to ABIValue::Uint directly + let big = num_bigint::BigUint::from(*u); + Ok(ABIValue::Uint(big)) + } + crate::clients::app_manager::AppStateValue::Bytes(_) => { + // Special-case AVM native types + let raw = app_state.value_raw.clone().ok_or_else(|| { + AppClientError::ValidationError( + "Missing raw bytes for bytes state value".to_string(), + ) + })?; + + if value_type_str == AVM_STRING { + let s = String::from_utf8_lossy(&raw).to_string(); + // Attempt to treat ASCII as base64-encoded string then fall back + if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(s.trim()) { + if let Ok(decoded_str) = String::from_utf8(decoded.clone()) { + return Ok(ABIValue::from(decoded_str)); + } + } + return Ok(ABIValue::from(s)); + } + if value_type_str == AVM_BYTES { + // Try to interpret raw as base64 string first, then fall back. + if let Ok(ascii) = String::from_utf8(raw.clone()) { + if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(&ascii) { + if let Ok(decoded_str) = String::from_utf8(decoded.clone()) { + return Ok(ABIValue::from(decoded_str)); + } else { + let arr = decoded.into_iter().map(ABIValue::from_byte).collect(); + return Ok(ABIValue::Array(arr)); + } + } + // Not base64; treat UTF-8 bytes as string + return Ok(ABIValue::from(ascii)); + } + let arr = raw.into_iter().map(ABIValue::from_byte).collect(); + return Ok(ABIValue::Array(arr)); + } + + // Fallback to ABI decoding for declared ARC-4 types (includes structs) + let abi_type = ABIType::from_str(value_type_str).map_err(|e| { + AppClientError::AbiError(format!("Invalid ABI type '{}': {}", value_type_str, e)) + })?; + abi_type.decode(&raw).map_err(|e| { + AppClientError::AbiError(format!("Failed to decode state value: {}", e)) + }) + } + } +} + +fn abi_value_to_string(value: &ABIValue) -> String { + match value { + ABIValue::Bool(b) => b.to_string(), + ABIValue::Uint(u) => u.to_string(), + ABIValue::String(s) => s.clone(), + ABIValue::Byte(b) => b.to_string(), + ABIValue::Address(addr) => addr.clone(), + ABIValue::Array(arr) => { + let inner: Vec = arr.iter().map(abi_value_to_string).collect(); + format!("[{}]", inner.join(",")) + } + ABIValue::Struct(map) => { + // Render deterministic order by key for stability + let mut keys: Vec<&String> = map.keys().collect(); + keys.sort(); + let inner: Vec = keys + .into_iter() + .map(|k| format!("{}:{}", k, abi_value_to_string(&map[k]))) + .collect(); + format!("{{{}}}", inner.join(",")) + } + } +} 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..825c0d5d9 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/transaction_builder.rs @@ -0,0 +1,243 @@ +use crate::transactions::composer::ComposerError; +use algokit_transact::OnApplicationComplete; + +use super::types::{AppClientBareCallParams, AppClientMethodCallParams, CompilationParams}; +use super::{AppClient, FundAppAccountParams}; + +pub struct TransactionBuilder<'a> { + pub(crate) client: &'a AppClient, +} + +pub struct BareTransactionBuilder<'a> { + pub(crate) client: &'a AppClient, +} + +impl TransactionBuilder<'_> { + /// Get the bare transaction builder. + pub fn bare(&self) -> BareTransactionBuilder<'_> { + BareTransactionBuilder { + client: self.client, + } + } + + /// Creates an ABI method call with NoOp. + pub async fn call( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::NoOp) + .await + } + + /// Creates an ABI method call with OptIn. + pub async fn opt_in( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::OptIn) + .await + } + + /// Creates an ABI method call with CloseOut. + pub async fn close_out( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::CloseOut) + .await + } + + /// Creates an ABI method call with Delete. + pub async fn delete( + &self, + params: AppClientMethodCallParams, + ) -> Result { + self.method_call_with_on_complete(params, OnApplicationComplete::DeleteApplication) + .await + } + + /// Update the application with method call. + pub async fn update( + &self, + params: AppClientMethodCallParams, + compilation_params: Option, + ) -> Result { + // Build update params via params builder (includes compilation) + let update_params = self + .client + .params() + .update(params, compilation_params) + .await + .map_err(|e| ComposerError::TransactionError { message: e })?; + + // Create transactions directly using update params + let built = self + .client + .algorand + .create() + .app_update_method_call(update_params) + .await?; + Ok(built) + } + + /// Fund the application account. + pub async fn fund_app_account( + &self, + params: FundAppAccountParams, + ) -> Result { + let payment = self + .client + .params() + .fund_app_account(¶ms) + .map_err(|e| ComposerError::TransactionError { message: e })?; + self.client.algorand.create().payment(payment).await + } + + async fn method_call_with_on_complete( + &self, + mut params: AppClientMethodCallParams, + on_complete: OnApplicationComplete, + ) -> Result { + params.on_complete = Some(on_complete); + let method_params = self + .client + .params() + .method_call(¶ms) + .await + .map_err(|e| ComposerError::TransactionError { message: e })?; + let is_delete = matches!( + method_params.on_complete, + OnApplicationComplete::DeleteApplication + ); + let built = if is_delete { + // Route delete on-complete to delete-specific API + let delete_params = crate::transactions::AppDeleteMethodCallParams { + common_params: method_params.common_params.clone(), + app_id: method_params.app_id, + method: method_params.method.clone(), + args: method_params.args.clone(), + account_references: method_params.account_references.clone(), + app_references: method_params.app_references.clone(), + asset_references: method_params.asset_references.clone(), + box_references: method_params.box_references.clone(), + }; + self.client + .algorand + .create() + .app_delete_method_call(delete_params) + .await? + } else { + self.client + .algorand + .create() + .app_call_method_call(method_params) + .await? + }; + Ok(built.transactions[0].clone()) + } +} + +impl BareTransactionBuilder<'_> { + /// Call with NoOp. + pub async fn call( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .call(params) + .map_err(|e| ComposerError::TransactionError { message: e })?; + self.client.algorand.create().app_call(app_call).await + } + + /// Call with OptIn. + pub async fn opt_in( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .opt_in(params) + .map_err(|e| ComposerError::TransactionError { message: e })?; + self.client.algorand.create().app_call(app_call).await + } + + /// Call with CloseOut. + pub async fn close_out( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .close_out(params) + .map_err(|e| ComposerError::TransactionError { message: e })?; + self.client.algorand.create().app_call(app_call).await + } + + /// Call with Delete. + pub async fn delete( + &self, + params: AppClientBareCallParams, + ) -> Result { + let delete_params = self + .client + .params() + .bare() + .delete(params) + .map_err(|e| ComposerError::TransactionError { message: e })?; + // Use delete-specific API for bare delete + self.client + .algorand + .create() + .app_delete(delete_params) + .await + } + + /// Call with ClearState. + pub async fn clear_state( + &self, + params: AppClientBareCallParams, + ) -> Result { + let app_call = self + .client + .params() + .bare() + .clear_state(params) + .map_err(|e| ComposerError::TransactionError { message: e })?; + self.client.algorand.create().app_call(app_call).await + } + + /// Update with bare call. + pub async fn update( + &self, + params: AppClientBareCallParams, + compilation_params: Option, + ) -> Result { + // Build update params via params builder (includes compilation) + let update_params = self + .client + .params() + .bare() + .update(params, compilation_params) + .await + .map_err(|e| ComposerError::TransactionError { message: e })?; + + let built = self + .client + .algorand + .create() + .app_update(update_params) + .await?; + Ok(crate::transactions::BuiltTransactions { + transactions: vec![built], + method_calls: std::collections::HashMap::new(), + signers: std::collections::HashMap::new(), + }) + } +} 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..6b723d4d3 --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/types.rs @@ -0,0 +1,123 @@ +use crate::AlgorandClient; +use crate::clients::app_manager::TealTemplateValue; +use crate::transactions::app_call::AppMethodCallArg; +use algokit_abi::Arc56Contract; +use algokit_transact::{BoxReference, OnApplicationComplete}; +use std::collections::HashMap; + +/// 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: Option, + pub app_spec: Arc56Contract, + pub algorand: AlgorandClient, + pub app_name: Option, + pub default_sender: Option, + pub source_maps: Option, +} + +/// Parameters for constructing an AppClient from a JSON app spec. +/// The JSON must be a valid ARC-56 contract specification string. +// See note above on not deriving Clone while this contains `AlgorandClient`. +pub struct AppClientJsonParams<'a> { + pub app_id: Option, + pub app_spec_json: &'a str, + pub algorand: AlgorandClient, + pub app_name: Option, + pub default_sender: Option, + pub source_maps: Option, +} + +/// Parameters for funding an application's account. +#[derive(Debug, Clone, Default)] +pub struct FundAppAccountParams { + pub amount: u64, + pub sender: 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: Option>, + pub sender: 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>, + pub on_complete: Option, +} + +/// Parameters for bare (non-ABI) app call operations +#[derive(Debug, Clone, Default)] +pub struct AppClientBareCallParams { + pub args: Option>>, + pub sender: 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>, + pub on_complete: Option, +} + +/// Enriched logic error details with source map information. +#[derive(Debug, Clone, Default)] +pub struct LogicError { + pub logic_error_str: 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>, +} + +/// 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..0628d81de --- /dev/null +++ b/crates/algokit_utils/src/applications/app_client/utils.rs @@ -0,0 +1,62 @@ +use super::AppClient; +use crate::transactions::TransactionSenderError; +use crate::transactions::composer::ComposerError; + +use std::str::FromStr; + +/// Format a logic error message with details. +pub fn format_logic_error_message(error: &super::types::LogicError) -> String { + let mut parts = vec![error.logic_error_str.clone()]; + if let Some(line) = error.line_no { + parts.push(format!("at line {}", line)); + } + if let Some(pc) = error.pc { + parts.push(format!("(pc={})", pc)); + } + if let Some(lines) = &error.lines { + parts.push("\n--- program listing ---".to_string()); + parts.extend(lines.iter().cloned()); + parts.push("--- end listing ---".to_string()); + } + parts.join(" ") +} + +/// Transform a transaction error with logic error enhancement. +pub fn transform_transaction_error( + client: &AppClient, + err: TransactionSenderError, + is_clear: bool, +) -> TransactionSenderError { + match &err { + TransactionSenderError::ComposerError { + source: ComposerError::PoolError { message }, + } => { + let tx_err = crate::transactions::TransactionResultError::ParsingError { + message: message.clone(), + }; + let logic = client.expose_logic_error(&tx_err, is_clear); + let msg = format_logic_error_message(&logic); + TransactionSenderError::ValidationError { message: msg } + } + _ => err, + } +} + +/// Parse account reference strings to addresses. +pub fn parse_account_refs_strs( + account_refs: &Option>, +) -> Result>, String> { + 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| format!("Invalid address: {}", 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 4e0245746..b16bdaa39 100644 --- a/crates/algokit_utils/src/clients/app_manager.rs +++ b/crates/algokit_utils/src/clients/app_manager.rs @@ -282,19 +282,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. 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 34a1d585d..23b867885 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 9fcf83f7e..38a488cfe 100644 --- a/crates/algokit_utils/src/transactions/app_call.rs +++ b/crates/algokit_utils/src/transactions/app_call.rs @@ -22,7 +22,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 1cc8a223e..e05dc7548 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::{ @@ -159,6 +160,24 @@ pub struct SendTransactionComposerResults { pub abi_returns: Vec, ComposerError>>, } +#[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 transactions: Vec, + pub confirmations: Vec, + pub returns: Vec, +} + #[derive(Debug, Clone)] pub struct SendParams { pub max_rounds_to_wait_for_confirmation: Option, @@ -2112,6 +2131,109 @@ impl Composer { method_calls } + + pub async fn simulate( + &mut self, + params: Option, + ) -> Result { + let params = params.unwrap_or_default(); + + // Build transactions (this also runs analysis for resource population/fees as configured) + self.build(None).await?; + + // Prepare transactions for simulation: drop group field and use empty signatures or gather signatures + let transactions_with_signers = + self.built_group.as_ref().ok_or(ComposerError::StateError { + message: "No transactions built".to_string(), + })?; + + // Prepare transactions for simulate by using empty signatures without re-grouping or signing + let signed_for_sim: Vec = 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(); + + let txn_group = SimulateRequestTransactionGroup { + txns: signed_for_sim, + }; + let simulate_request = SimulateRequest { + txn_groups: vec![txn_group], + round: params.simulation_round, + allow_empty_signatures: if Config::debug() || params.skip_signatures { + Some(true) + } else { + params.allow_empty_signatures + }, + allow_more_logging: params.allow_more_logging, + allow_unnamed_resources: params.allow_unnamed_resources, + extra_opcode_budget: params.extra_opcode_budget, + exec_trace_config: params.exec_trace_config, + fix_signers: Some(true), + }; + + // Call simulate endpoint + let response = self + .algod_client + .simulate_transaction(simulate_request, Some(Format::Msgpack)) + .await + .map_err(|e| ComposerError::AlgodClientError { source: e })?; + + let group = &response.txn_groups[0]; + + if let Some(failure_message) = &group.failure_message { + let failed_at = group + .failed_at + .as_ref() + .map(|v| { + v.iter() + .map(|i| i.to_string()) + .collect::>() + .join(", ") + }) + .unwrap_or_else(|| "unknown".to_string()); + return Err(ComposerError::StateError { + message: format!( + "Error analyzing group requirements via simulate in transaction {}: {}", + failed_at, failure_message + ), + }); + } + + // Collect confirmations and ABI returns similar to send() + let confirmations: Vec = group + .txn_results + .iter() + .map(|r| r.txn_result.clone()) + .collect(); + + let transactions: Vec = self + .built_group + .as_ref() + .unwrap() + .iter() + .map(|tw| tw.transaction.clone()) + .collect(); + + let abi_returns = self.parse_abi_return_values(&confirmations); + let returns: Vec = abi_returns + .into_iter() + .filter_map(|r| match r { + Ok(Some(v)) => Some(v), + _ => None, + }) + .collect(); + + Ok(SimulateComposerResults { + transactions, + confirmations, + returns, + }) + } } #[cfg(test)] diff --git a/crates/algokit_utils/tests/applications/app_client.rs b/crates/algokit_utils/tests/applications/app_client.rs new file mode 100644 index 000000000..84007d367 --- /dev/null +++ b/crates/algokit_utils/tests/applications/app_client.rs @@ -0,0 +1,1289 @@ +use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture, deploy_arc56_contract}; +use algokit_abi::Arc56Contract; +use algokit_transact::BoxReference; +use algokit_utils::AlgorandClient as RootAlgorandClient; +use algokit_utils::applications::app_client::AppClientMethodCallParams; +use algokit_utils::applications::app_client::{AppClient, AppClientJsonParams, AppClientParams}; +use algokit_utils::clients::app_manager::{TealTemplateParams, TealTemplateValue}; +use rstest::*; +use std::str::FromStr; +use std::sync::Arc; + +fn get_testing_app_spec() -> Arc56Contract { + let json = algokit_test_artifacts::testing_app::APPLICATION_ARC56; + Arc56Contract::from_json(json).expect("valid arc56") +} + +#[test] +fn app_client_from_network_works() { + let algorand = RootAlgorandClient::default_localnet(); + // JSON constructor + let json = algokit_test_artifacts::state_management_demo::APPLICATION_ARC56; + let client = AppClient::from_json(AppClientJsonParams { + app_id: None, + app_spec_json: json, + algorand, + app_name: None, + default_sender: None, + source_maps: None, + }) + .expect("app client from json"); + assert!(!client.app_spec().methods.is_empty()); +} + +fn get_sandbox_spec() -> Arc56Contract { + let json = algokit_test_artifacts::sandbox::APPLICATION_ARC56; + Arc56Contract::from_json(json).expect("valid arc56") +} + +#[rstest] +#[tokio::test] +async fn retrieve_state(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + // Deploy testing_app + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = + deploy_arc56_contract(&fixture, &sender, &get_testing_app_spec(), Some(tmpl), None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Global state: set and verify + client + .send() + .call(AppClientMethodCallParams { + method: "set_global".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(1u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(2u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("asdf")), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array(vec![ + algokit_abi::ABIValue::from_byte(1), + algokit_abi::ABIValue::from_byte(2), + algokit_abi::ABIValue::from_byte(3), + algokit_abi::ABIValue::from_byte(4), + ])), + ]), + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + + let global_state = client.state().global_state().get_all().await?; + assert!(global_state.contains_key("int1")); + assert!(global_state.contains_key("int2")); + assert!(global_state.contains_key("bytes1")); + assert!(global_state.contains_key("bytes2")); + assert_eq!( + global_state.get("int1").unwrap(), + &algokit_abi::ABIValue::from(1u64) + ); + assert_eq!( + global_state.get("int2").unwrap(), + &algokit_abi::ABIValue::from(2u64) + ); + assert_eq!( + global_state.get("bytes1").unwrap(), + &algokit_abi::ABIValue::from("asdf") + ); + + // Local: opt-in and set; verify + client + .send() + .opt_in(AppClientMethodCallParams { + method: "opt_in".to_string(), + args: None, + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + + client + .send() + .call(AppClientMethodCallParams { + method: "set_local".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(1u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(2u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("asdf")), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array(vec![ + algokit_abi::ABIValue::from_byte(1), + algokit_abi::ABIValue::from_byte(2), + algokit_abi::ABIValue::from_byte(3), + algokit_abi::ABIValue::from_byte(4), + ])), + ]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + + let local_state = client + .state() + .local_state(&sender.to_string()) + .get_all() + .await?; + assert_eq!( + local_state.get("local_int1").unwrap(), + &algokit_abi::ABIValue::from(1u64) + ); + assert_eq!( + local_state.get("local_int2").unwrap(), + &algokit_abi::ABIValue::from(2u64) + ); + assert_eq!( + local_state.get("local_bytes1").unwrap(), + &algokit_abi::ABIValue::from("asdf") + ); + + // Boxes + let box_name1: Vec = vec![0, 0, 0, 1]; + let box_name2: Vec = vec![0, 0, 0, 2]; + + // Fund app account to enable box writes + client + .fund_app_account( + algokit_utils::applications::app_client::FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + ) + .await?; + + client + .send() + .call(AppClientMethodCallParams { + method: "set_box".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array( + box_name1 + .iter() + .copied() + .map(algokit_abi::ABIValue::from_byte) + .collect(), + )), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("value1")), + ]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_name1.clone(), + }]), + on_complete: None, + }) + .await?; + + client + .send() + .call(AppClientMethodCallParams { + method: "set_box".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array( + box_name2 + .iter() + .copied() + .map(algokit_abi::ABIValue::from_byte) + .collect(), + )), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("value2")), + ]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_name2.clone(), + }]), + on_complete: None, + }) + .await?; + + let box_names = client.get_box_names().await?; + let names: Vec> = box_names.into_iter().map(|n| n.name_raw).collect(); + assert!(names.contains(&box_name1)); + assert!(names.contains(&box_name2)); + + let box1_value = client.get_box_value(&box_name1).await?; + assert_eq!(box1_value, b"value1"); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn logic_error_exposure_with_source_maps( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + use algokit_utils::applications::app_client::AppSourceMaps; + use algokit_utils::transactions::sender_results::TransactionResultError; + + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + // Deploy testing_app with template params + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = deploy_arc56_contract( + &fixture, + &sender, + &get_testing_app_spec(), + Some(tmpl.clone()), + None, + ) + .await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let mut client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Compile TEAL to get source maps and import + let src = client.app_spec().source.as_ref().expect("source expected"); + let approval_teal = src.get_decoded_approval().unwrap(); + let clear_teal = src.get_decoded_clear().unwrap(); + let app_manager = fixture.algorand_client.app(); + let compiled_approval = app_manager + .compile_teal_template(&approval_teal, Some(&tmpl), None) + .await?; + let compiled_clear = app_manager + .compile_teal_template(&clear_teal, Some(&tmpl), None) + .await?; + client.import_source_maps(AppSourceMaps { + approval_source_map: compiled_approval.source_map, + clear_source_map: compiled_clear.source_map, + }); + + // Trigger logic error + let err = client + .send() + .call(AppClientMethodCallParams { + method: "error".to_string(), + args: None, + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await + .expect_err("expected error"); + + let logic = client.expose_logic_error( + &TransactionResultError::ParsingError { + message: err.to_string(), + }, + false, + ); + assert!(logic.pc.is_some()); + assert!(logic.logic_error_str.contains("assert failed")); + if let Some(id) = &logic.transaction_id { + assert!(id.len() >= 52); + } + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn box_methods_with_manually_encoded_abi_args( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + // Deploy testing_app_puya + let json = algokit_test_artifacts::testing_app_puya::APPLICATION_ARC56; + let spec = Arc56Contract::from_json(json).expect("valid arc56"); + let app_id = deploy_arc56_contract(&fixture, &sender, &spec, None, None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: spec, + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Fund app account + client + .fund_app_account( + algokit_utils::applications::app_client::FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + ) + .await?; + + // Prepare box name and encoded value + let box_prefix = b"box_bytes".to_vec(); + let name_type = algokit_abi::ABIType::from_str("string").unwrap(); + let box_name = "asdf"; + let box_name_encoded = name_type + .encode(&algokit_abi::ABIValue::from(box_name)) + .unwrap(); + let box_identifier = { + let mut v = box_prefix.clone(); + v.extend_from_slice(&box_name_encoded); + v + }; + + // byte[] value + let value_type = algokit_abi::ABIType::from_str("byte[]").unwrap(); + let encoded = value_type + .encode(&algokit_abi::ABIValue::from(vec![ + algokit_abi::ABIValue::from_byte(116), + algokit_abi::ABIValue::from_byte(101), + algokit_abi::ABIValue::from_byte(115), + algokit_abi::ABIValue::from_byte(116), + algokit_abi::ABIValue::from_byte(95), + algokit_abi::ABIValue::from_byte(98), + algokit_abi::ABIValue::from_byte(121), + algokit_abi::ABIValue::from_byte(116), + algokit_abi::ABIValue::from_byte(101), + algokit_abi::ABIValue::from_byte(115), + ])) + .unwrap(); + + client + .send() + .call(AppClientMethodCallParams { + method: "set_box_bytes".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("asdf")), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array( + encoded + .into_iter() + .map(algokit_abi::ABIValue::from_byte) + .collect(), + )), + ]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_identifier.clone(), + }]), + on_complete: None, + }) + .await?; + + let retrieved = client + .get_box_value_from_abi_type(&box_identifier, &value_type) + .await?; + assert_eq!( + retrieved, + algokit_abi::ABIValue::Array(vec![ + algokit_abi::ABIValue::from_byte(116), + algokit_abi::ABIValue::from_byte(101), + algokit_abi::ABIValue::from_byte(115), + algokit_abi::ABIValue::from_byte(116), + algokit_abi::ABIValue::from_byte(95), + algokit_abi::ABIValue::from_byte(98), + algokit_abi::ABIValue::from_byte(121), + algokit_abi::ABIValue::from_byte(116), + algokit_abi::ABIValue::from_byte(101), + algokit_abi::ABIValue::from_byte(115), + ]) + ); + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn construct_transaction_with_abi_encoding_including_foreign_references_not_in_signature( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + // Deploy testing_app which has call_abi_foreign_refs()string + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = + deploy_arc56_contract(&fixture, &sender, &get_testing_app_spec(), Some(tmpl), None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Create a secondary account for account_references + let mut new_fixture = fixture; // reuse underlying clients for funding convenience + let new_account = new_fixture.generate_account(None).await?; + let new_addr = new_account.account().address(); + + let send_res = client + .send() + .call(AppClientMethodCallParams { + method: "call_abi_foreign_refs".to_string(), + args: None, + sender: Some(sender.to_string()), + 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, + account_references: Some(vec![new_addr.to_string()]), + app_references: Some(vec![345]), + asset_references: Some(vec![567]), + box_references: None, + on_complete: None, + }) + .await?; + + let abi_ret = send_res.abi_return.as_ref().expect("abi return expected"); + if let algokit_abi::ABIValue::String(s) = &abi_ret.return_value { + assert!(s.contains("App: 345")); + assert!(s.contains("Asset: 567")); + } else { + panic!("expected string return"); + } + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn abi_with_default_arg_from_local_state( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = + deploy_arc56_contract(&fixture, &sender, &get_testing_app_spec(), Some(tmpl), None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Opt-in and set local state + + client + .send() + .opt_in(AppClientMethodCallParams { + method: "opt_in".to_string(), + args: None, + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + + client + .send() + .call(AppClientMethodCallParams { + method: "set_local".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(1u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(2u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("bananas")), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array(vec![ + algokit_abi::ABIValue::from_byte(1), + algokit_abi::ABIValue::from_byte(2), + algokit_abi::ABIValue::from_byte(3), + algokit_abi::ABIValue::from_byte(4), + ])), + ]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + + // Call with explicit value first; expect echo prefix + defined value + let defined = client + .send() + .call(AppClientMethodCallParams { + method: "default_value_from_local_state".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( + algokit_abi::ABIValue::from("defined value"), + )]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + let defined_ret = defined.abi_return.as_ref().expect("abi return expected"); + match &defined_ret.return_value { + algokit_abi::ABIValue::String(s) => { + assert_eq!(s, "Local state, defined value"); + } + _ => panic!("expected string return"), + } + + // Call method without providing arg; expect default from local state ("bananas") + let res = client + .send() + .call(AppClientMethodCallParams { + method: "default_value_from_local_state".to_string(), + args: None, // missing arg to trigger default resolver + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + + let abi_ret = res.abi_return.as_ref().expect("abi return expected"); + if let algokit_abi::ABIValue::String(s) = &abi_ret.return_value { + assert_eq!(s, "Local state, bananas"); + } else { + panic!("expected string return"); + } + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn abi_with_default_arg_from_literal( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + // Mirrors TS: default_value(string)string + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = + deploy_arc56_contract(&fixture, &sender, &get_testing_app_spec(), Some(tmpl), None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Call with explicit value + let defined = client + .send() + .call(AppClientMethodCallParams { + method: "default_value".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( + algokit_abi::ABIValue::from("defined value"), + )]), + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + let defined_ret = defined.abi_return.as_ref().expect("abi return expected"); + match &defined_ret.return_value { + algokit_abi::ABIValue::String(s) => { + assert_eq!(s, "defined value"); + } + _ => panic!("expected string return"), + } + + // Call with default (no arg) + let defaulted = client + .send() + .call(AppClientMethodCallParams { + method: "default_value".to_string(), + args: None, + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + let default_ret = defaulted.abi_return.as_ref().expect("abi return expected"); + match &default_ret.return_value { + algokit_abi::ABIValue::String(s) => { + assert_eq!(s, "default value"); + } + _ => panic!("expected string return"), + } + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn abi_with_default_arg_from_method( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + // Mirrors TS: default_value_from_abi(string)string + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = + deploy_arc56_contract(&fixture, &sender, &get_testing_app_spec(), Some(tmpl), None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Call with explicit value + let defined = client + .send() + .call(AppClientMethodCallParams { + method: "default_value_from_abi".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( + algokit_abi::ABIValue::from("defined value"), + )]), + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + let defined_ret = defined.abi_return.as_ref().expect("abi return expected"); + match &defined_ret.return_value { + algokit_abi::ABIValue::String(s) => { + assert_eq!(s, "ABI, defined value"); + } + _ => panic!("expected string return"), + } + + // Call with default (no arg) + let defaulted = client + .send() + .call(AppClientMethodCallParams { + method: "default_value_from_abi".to_string(), + args: None, + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + let default_ret = defaulted.abi_return.as_ref().expect("abi return expected"); + match &default_ret.return_value { + algokit_abi::ABIValue::String(s) => { + assert_eq!(s, "ABI, default value"); + } + _ => panic!("expected string return"), + } + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn abi_with_default_arg_from_global_state( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + // Mirrors TS: default_value_from_global_state(uint64)uint64 + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let mut tmpl: TealTemplateParams = Default::default(); + tmpl.insert("VALUE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("UPDATABLE".to_string(), TealTemplateValue::Int(0)); + tmpl.insert("DELETABLE".to_string(), TealTemplateValue::Int(0)); + let app_id = + deploy_arc56_contract(&fixture, &sender, &get_testing_app_spec(), Some(tmpl), None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_testing_app_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Seed global state (int1) via set_global + let seeded_val: u64 = 456; + client + .send() + .call(AppClientMethodCallParams { + method: "set_global".to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(seeded_val)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(2u64)), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("asdf")), + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::Array(vec![ + algokit_abi::ABIValue::from_byte(1), + algokit_abi::ABIValue::from_byte(2), + algokit_abi::ABIValue::from_byte(3), + algokit_abi::ABIValue::from_byte(4), + ])), + ]), + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + + // Call with explicit value + let defined = client + .send() + .call(AppClientMethodCallParams { + method: "default_value_from_global_state".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( + algokit_abi::ABIValue::from(123u64), + )]), + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + let defined_ret = defined.abi_return.as_ref().expect("abi return expected"); + match &defined_ret.return_value { + algokit_abi::ABIValue::Uint(u) => { + assert_eq!(*u, num_bigint::BigUint::from(123u64)); + } + _ => panic!("expected uint return"), + } + + // Call with default (no arg) -> should read seeded global state + let defaulted = client + .send() + .call(AppClientMethodCallParams { + method: "default_value_from_global_state".to_string(), + args: None, + sender: Some(sender.to_string()), + ..Default::default() + }) + .await?; + let default_ret = defaulted.abi_return.as_ref().expect("abi return expected"); + match &default_ret.return_value { + algokit_abi::ABIValue::Uint(u) => { + assert_eq!(*u, num_bigint::BigUint::from(seeded_val)); + } + _ => panic!("expected uint return"), + } + Ok(()) +} +#[rstest] +#[tokio::test] +async fn bare_call_with_box_reference_builds_and_sends( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let app_id = deploy_arc56_contract(&fixture, &sender, &get_sandbox_spec(), None, None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_sandbox_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Use method call (sandbox does not allow bare NoOp) + + let result = client + .send() + .call(AppClientMethodCallParams { + method: "hello_world".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( + algokit_abi::ABIValue::from("test"), + )]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: Some(vec![BoxReference { + app_id: 0, + name: b"1".to_vec(), + }]), + on_complete: None, + }) + .await?; + + match &result.common_params.transaction { + algokit_transact::Transaction::AppCall(fields) => { + assert_eq!(fields.app_id, app_id); + assert_eq!( + fields.box_references.as_ref().unwrap(), + &vec![BoxReference { + app_id: 0, + name: b"1".to_vec() + }] + ); + } + _ => panic!("expected app call"), + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn construct_transaction_with_boxes( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + let app_id = deploy_arc56_contract(&fixture, &sender, &get_sandbox_spec(), None, None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: get_sandbox_spec(), + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Build transaction with a box reference + + let built = client + .create_transaction() + .call(AppClientMethodCallParams { + method: "hello_world".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( + algokit_abi::ABIValue::from("test"), + )]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: Some(vec![BoxReference { + app_id: 0, + name: b"1".to_vec(), + }]), + on_complete: None, + }) + .await?; + match built { + algokit_transact::Transaction::AppCall(fields) => { + assert_eq!(fields.app_id, app_id); + assert_eq!( + fields.box_references.as_ref().unwrap(), + &vec![BoxReference { + app_id: 0, + name: b"1".to_vec() + }] + ); + } + _ => panic!("expected app call"), + } + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn construct_transaction_with_abi_encoding_including_transaction( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + // Use sandbox which has get_pay_txn_amount(pay)uint64 + let spec = get_sandbox_spec(); + let app_id = deploy_arc56_contract(&fixture, &sender, &spec, None, None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: spec, + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Prepare a payment as an ABI transaction argument + let payment = algokit_utils::PaymentParams { + common_params: algokit_utils::CommonTransactionParams { + sender: sender.clone(), + ..Default::default() + }, + receiver: sender.clone(), + amount: 12345, + }; + + let send_res = client + .send() + .call(AppClientMethodCallParams { + method: "get_pay_txn_amount".to_string(), + args: Some(vec![algokit_utils::AppMethodCallArg::Payment(payment)]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: None, + on_complete: None, + }) + .await?; + + // Expect a group of 2 transactions: payment + app call + assert_eq!(send_res.common_params.transactions.len(), 2); + // ABI return should be present and decode to expected value + let abi_ret = send_res.abi_return.as_ref().expect("abi return expected"); + let ret_val = match &abi_ret.return_value { + algokit_abi::ABIValue::Uint(u) => u.clone(), + _ => panic!("expected uint64 return"), + }; + assert_eq!(ret_val, num_bigint::BigUint::from(12345u32)); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn box_methods_with_arc4_returns_parametrized( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + // Deploy testing_app_puya + let spec = + Arc56Contract::from_json(algokit_test_artifacts::testing_app_puya::APPLICATION_ARC56) + .expect("valid arc56"); + let app_id = deploy_arc56_contract(&fixture, &sender, &spec, None, None).await?; + + let mut algorand = RootAlgorandClient::default_localnet(); + algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); + let client = AppClient::new(AppClientParams { + app_id: Some(app_id), + app_spec: spec, + algorand, + app_name: None, + default_sender: Some(sender.to_string()), + source_maps: None, + }); + + // Fund app account to allow box writes + client + .fund_app_account( + algokit_utils::applications::app_client::FundAppAccountParams { + amount: 1_000_000, + sender: Some(sender.to_string()), + ..Default::default() + }, + ) + .await?; + + // Parametrized ARC-4 return cases + let mut big = num_bigint::BigUint::from(1u64); + big <<= 256u32; + let cases: Vec<(Vec, &str, &str, algokit_abi::ABIValue)> = vec![ + ( + b"box_str".to_vec(), + "set_box_str", + "string", + algokit_abi::ABIValue::from("string"), + ), + ( + b"box_int".to_vec(), + "set_box_int", + "uint32", + algokit_abi::ABIValue::from(123u32), + ), + ( + b"box_int512".to_vec(), + "set_box_int512", + "uint512", + algokit_abi::ABIValue::from(big), + ), + ( + b"box_static".to_vec(), + "set_box_static", + "byte[4]", + algokit_abi::ABIValue::Array(vec![ + algokit_abi::ABIValue::from_byte(1), + algokit_abi::ABIValue::from_byte(2), + algokit_abi::ABIValue::from_byte(3), + algokit_abi::ABIValue::from_byte(4), + ]), + ), + // TODO: restore struct case after app factory is merged + ]; + + for (box_prefix, method_sig, value_type_str, arg_val) in cases { + // Encode the box name using ABIType "string" + let name_type = algokit_abi::ABIType::from_str("string").unwrap(); + let name_encoded = name_type + .encode(&algokit_abi::ABIValue::from("box1")) + .unwrap(); + let mut box_reference = box_prefix.clone(); + box_reference.extend_from_slice(&name_encoded); + + // Send method call + client + .send() + .call(AppClientMethodCallParams { + method: method_sig.to_string(), + args: Some(vec![ + algokit_utils::AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from("box1")), + algokit_utils::AppMethodCallArg::ABIValue(arg_val.clone()), + ]), + sender: Some(sender.to_string()), + 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, + account_references: None, + app_references: None, + asset_references: None, + box_references: Some(vec![BoxReference { + app_id: 0, + name: box_reference.clone(), + }]), + on_complete: None, + }) + .await?; + + // Verify raw equals ABI-encoded expected + 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); + + // Decode via ABI type and verify + let decoded = client + .get_box_value_from_abi_type( + &box_reference, + &algokit_abi::ABIType::from_str(value_type_str).unwrap(), + ) + .await?; + assert_eq!(decoded, arg_val); + + // TODO: restore struct and nested struct tests after app factory is merged + } + + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn app_client_from_network_resolves_id( + #[future] algorand_fixture: AlgorandFixtureResult, +) -> TestResult { + // Deploy hello_world and write networks mapping into spec, then call from_network + let fixture = algorand_fixture.await?; + let sender = fixture.test_account.account().address(); + + let spec = Arc56Contract::from_json(algokit_test_artifacts::hello_world::APPLICATION_ARC56) + .expect("valid arc56"); + let app_id = deploy_arc56_contract(&fixture, &sender, &spec, None, None).await?; + + let mut spec_with_networks = spec.clone(); + spec_with_networks.networks = Some(std::collections::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, + ) + .await + .expect("from_network"); + assert_eq!(client.app_id(), Some(app_id)); + 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/common/mod.rs b/crates/algokit_utils/tests/common/mod.rs index 37fd61562..b72ac4e3b 100644 --- a/crates/algokit_utils/tests/common/mod.rs +++ b/crates/algokit_utils/tests/common/mod.rs @@ -9,6 +9,9 @@ pub mod test_account; use algokit_abi::Arc56Contract; use algokit_transact::Address; +use algokit_utils::clients::app_manager::{ + AppManager, DeploymentMetadata, TealTemplateParams, TealTemplateValue, +}; use algokit_utils::{AppCreateParams, CommonTransactionParams}; use base64::prelude::*; @@ -25,26 +28,52 @@ pub async fn deploy_arc56_contract( fixture: &AlgorandFixture, sender: &Address, arc56_contract: &Arc56Contract, + template_params: Option, + deploy_metadata: 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 { common_params: CommonTransactionParams { sender: sender.clone(), ..Default::default() }, - approval_program: approval_compile_result.result, - clear_state_program: clear_state_compile_result.result, + 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 485a5dd7b..2ed58f7ef 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, CommonTransactionParams, ComposerError, }; @@ -873,6 +874,92 @@ 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(); + + // 1) add(uint64,uint64)uint64 + let method_add = get_abi_method(&arc56_contract, "add")?; + let add_params = AppCallMethodCallParams { + common_params: CommonTransactionParams { + sender: sender.clone(), + ..Default::default() + }, + 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 { + common_params: CommonTransactionParams { + sender: sender.clone(), + ..Default::default() + }, + receiver: sender.clone(), + amount: 10_000, + }; + composer.add_payment(payment)?; + + // 3) hello_world(string)string + let method_hello = get_abi_method(&arc56_contract, "hello_world")?; + let call_params = AppCallMethodCallParams { + common_params: CommonTransactionParams { + sender: sender.clone(), + ..Default::default() + }, + 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.transactions.len(), send.transaction_ids.len()); + // Compare all ABI returns in order where both sides have a value + let mut sim_iter = simulate.returns.iter(); + let mut send_iter = send.abi_returns.iter(); + loop { + let sim_next = sim_iter.next(); + let send_next = send_iter.next(); + match (sim_next, send_next) { + (Some(sim_ret), Some(send_ret)) => { + if let Ok(Some(send_val)) = send_ret { + assert_eq!(sim_ret.return_value, send_val.return_value); + } + } + _ => break, + } + } + Ok(()) +} + struct Arc56AppFixture { sender_address: Address, app_id: u64, @@ -890,7 +977,14 @@ 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, + ) + .await?; Ok(Arc56AppFixture { sender_address, diff --git a/crates/algokit_utils/tests/transactions/sender.rs b/crates/algokit_utils/tests/transactions/sender.rs index cbeba5b94..8168ed9b6 100644 --- a/crates/algokit_utils/tests/transactions/sender.rs +++ b/crates/algokit_utils/tests/transactions/sender.rs @@ -169,7 +169,14 @@ 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 app_id = deploy_arc56_contract( + &algorand_fixture, + &sender_address, + &arc56_contract, + None, + None, + ) + .await?; let method = arc56_contract .methods