Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions crates/algokit_abi/src/abi_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -98,6 +99,8 @@ pub enum ABIType {
StaticArray(Box<ABIType>, usize),
/// A dynamic-length array of another ABI type.
DynamicArray(Box<ABIType>),
/// A named struct type with ordered fields
Struct(StructType),
}

impl AsRef<ABIType> for ABIType {
Expand Down Expand Up @@ -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))
}
}
}

Expand All @@ -146,13 +167,30 @@ 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
),
}),
}
}
}
}

pub(crate) fn is_dynamic(&self) -> bool {
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,
}
Expand Down Expand Up @@ -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),
}),
Expand Down Expand Up @@ -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())
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/algokit_abi/src/abi_value.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -15,6 +16,8 @@ pub enum ABIValue {
Array(Vec<ABIValue>),
/// An Algorand address.
Address(String),
/// A struct value represented as a map of field name to value.
Struct(HashMap<String, ABIValue>),
}

impl From<bool> for ABIValue {
Expand Down
124 changes: 87 additions & 37 deletions crates/algokit_abi/src/arc56_contract.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<String, Vec<StructField>>,
) -> HashMap<String, Value> {
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<ABIMethod, ABIError> {
// Resolve argument types
let args: Result<Vec<ABIMethodArg>, 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<ABIMethodArgType, ABIError> {
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<abi_struct::StructType, ABIError> {
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<abi_struct::StructField> = 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)
}
}
}
}
1 change: 1 addition & 0 deletions crates/algokit_abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/algokit_abi/src/types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod collections;
pub mod primitives;
pub mod struct_type;
Loading
Loading