Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4ccf619
feat: app client and initial batch of tests transpiled from py
aorumbayev Sep 2, 2025
07d6863
chore: fix merge conflicts against main; refine common fixture usage
aorumbayev Sep 2, 2025
46871f7
feat: config singleton and event emitter
aorumbayev Sep 2, 2025
0c04a92
chore: minor comments
aorumbayev Sep 2, 2025
78abbb6
chore: pr comments
aorumbayev Sep 2, 2025
9c577ae
refactor: addressing pr comments
aorumbayev Sep 3, 2025
9406350
refactor: add native struct support to abi crate; refine abi handling…
aorumbayev Sep 4, 2025
e626e98
refactor: PR feedback (#252)
PatrickDinh Sep 11, 2025
092606d
chore: merge conflicts and clippy warning fixes
aorumbayev Sep 11, 2025
d33f36d
tests: app client state access tests;
aorumbayev Sep 12, 2025
18323a0
fix(composer): propagate method-level signer to transaction args and …
aorumbayev Sep 14, 2025
0253ba6
fix: add missing signer fields to app call params
aorumbayev Sep 14, 2025
06e8ec6
chore: wip more tests
aorumbayev Sep 14, 2025
b69acb1
chore: wip
aorumbayev Sep 15, 2025
4646499
chore: refining logic error handling; additional app client tests
aorumbayev Sep 15, 2025
e57c4e8
refactor: async trait for sate accessor; descriptive lifetime names
aorumbayev Sep 15, 2025
e459ca4
chore: moving remaining tests from transpiled ref tests; minor refine…
aorumbayev Sep 15, 2025
03fdf8b
some PR feedback (#258)
PatrickDinh Sep 16, 2025
d5dce36
chore: err pattern instead of panic statements in tests
aorumbayev Sep 16, 2025
d3a64b8
chore: generic app client fixtures
aorumbayev Sep 16, 2025
3752822
chore: logic error tweaks
aorumbayev Sep 16, 2025
17887bd
chore: addressing pr comments
aorumbayev Sep 16, 2025
4f497f8
chore: simplifying match statement in composer
aorumbayev Sep 16, 2025
b866165
core: add extra bare txn types
aorumbayev Sep 17, 2025
83e5972
chore: pr comments
aorumbayev Sep 17, 2025
c061310
Merge remote-tracking branch 'origin/main' into feat/application-client
aorumbayev Sep 17, 2025
eeb67a1
chore: additional comments and extra edge case test for nested app ca…
aorumbayev Sep 17, 2025
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use crate::DefaultValueSource;
use crate::abi_type::ABIType;
use crate::abi_value::ABIValue;
use crate::constants::VOID_RETURN_TYPE;
use crate::error::ABIError;
use sha2::{Digest, Sha512_256};
use std::fmt::Display;
use std::str::FromStr;

/// Constant for void return type in method signatures.
const VOID_RETURN_TYPE: &str = "void";

/// Represents a transaction type that can be used as an ABI method argument.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ABITransactionType {
Expand Down Expand Up @@ -158,7 +157,7 @@ impl FromStr for ABIMethodArgType {
}

/// Represents a parsed ABI method, including its name, arguments, and return type.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone)]
pub struct ABIMethod {
/// The name of the method.
pub name: String,
Expand All @@ -170,57 +169,7 @@ pub struct ABIMethod {
pub description: Option<String>,
}

/// Represents an argument in an ABI method.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ABIMethodArg {
/// The type of the argument.
pub arg_type: ABIMethodArgType,
/// An optional name for the argument.
pub name: Option<String>,
/// An optional description of the argument.
pub description: Option<String>,
}

impl ABIMethod {
/// Creates a new ABI method.
pub fn new(
name: String,
args: Vec<ABIMethodArg>,
returns: Option<ABIType>,
description: Option<String>,
) -> Self {
Self {
name,
args,
returns,
description,
}
}

/// Returns the number of transaction arguments in the method.
pub fn transaction_arg_count(&self) -> usize {
self.args
.iter()
.filter(|arg| arg.arg_type.is_transaction())
.count()
}

/// Returns the number of reference arguments in the method.
pub fn reference_arg_count(&self) -> usize {
self.args
.iter()
.filter(|arg| arg.arg_type.is_reference())
.count()
}

/// Returns the number of value-type arguments in the method.
pub fn value_arg_count(&self) -> usize {
self.args
.iter()
.filter(|arg| arg.arg_type.is_value_type())
.count()
}

/// Returns the method selector, which is the first 4 bytes of the SHA-512/256 hash of the method signature.
pub fn selector(&self) -> Result<Vec<u8>, ABIError> {
let signature = self.signature()?;
Expand Down Expand Up @@ -279,6 +228,71 @@ impl ABIMethod {
}
}

/// Default value information for ABI method arguments.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ABIDefaultValue {
/// Base64 encoded bytes, base64 ARC4 encoded uint64, or UTF-8 method selector
pub data: String,
/// Where the default value is coming from
pub source: DefaultValueSource,
/// How the data is encoded. This is the encoding for the data provided here, not the arg type
pub value_type: Option<ABIType>,
}

/// Represents an argument in an ABI method.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ABIMethodArg {
/// The type of the argument.
pub arg_type: ABIMethodArgType,
/// An optional name for the argument.
pub name: Option<String>,
/// An optional description of the argument.
pub description: Option<String>,
/// An optional default value for the argument.
pub default_value: Option<ABIDefaultValue>,
}

impl ABIMethod {
/// Creates a new ABI method.
pub fn new(
name: String,
args: Vec<ABIMethodArg>,
returns: Option<ABIType>,
description: Option<String>,
) -> Self {
Self {
name,
args,
returns,
description,
}
}

/// Returns the number of transaction arguments in the method.
pub fn transaction_arg_count(&self) -> usize {
self.args
.iter()
.filter(|arg| arg.arg_type.is_transaction())
.count()
}

/// Returns the number of reference arguments in the method.
pub fn reference_arg_count(&self) -> usize {
self.args
.iter()
.filter(|arg| arg.arg_type.is_reference())
.count()
}

/// Returns the number of value-type arguments in the method.
pub fn value_arg_count(&self) -> usize {
self.args
.iter()
.filter(|arg| arg.arg_type.is_value_type())
.count()
}
}

impl FromStr for ABIMethod {
type Err = ABIError;

Expand Down Expand Up @@ -323,7 +337,7 @@ impl FromStr for ABIMethod {
for (i, arg_type) in arguments.iter().enumerate() {
let _type = ABIMethodArgType::from_str(arg_type)?;
let arg_name = Some(format!("arg{}", i));
let arg = ABIMethodArg::new(_type, arg_name, None);
let arg = ABIMethodArg::new(_type, arg_name, None, None);
args.push(arg);
}

Expand All @@ -347,11 +361,13 @@ impl ABIMethodArg {
arg_type: ABIMethodArgType,
name: Option<String>,
description: Option<String>,
default_value: Option<ABIDefaultValue>,
) -> Self {
Self {
arg_type,
name,
description,
default_value,
}
}
}
Expand Down Expand Up @@ -424,7 +440,6 @@ fn split_arguments_by_comma(args_str: &str) -> Result<Vec<String>, ABIError> {
mod tests {
use super::*;
use crate::abi_type::parse_tuple_content;
use hex;
use rstest::rstest;

// Transaction type parsing with round-trip validation
Expand Down Expand Up @@ -517,19 +532,6 @@ mod tests {
assert!(ABIMethod::from_str(signature).is_err());
}

// Method selector verification - critical for hash correctness
#[rstest]
#[case("add(uint64,uint64)uint64", "fe6bdf69")]
#[case("optIn()void", "29314d95")]
#[case("deposit(pay,uint64)void", "f2355b55")]
#[case("bootstrap(pay,pay,application)void", "895c2a3b")]
fn method_selector(#[case] signature: &str, #[case] expected_hex: &str) {
let method = ABIMethod::from_str(signature).unwrap();
let selector = method.selector().unwrap();
assert_eq!(hex::encode(&selector), expected_hex);
assert_eq!(selector.len(), 4);
}

// ARC-4 tuple parsing - essential cases
#[rstest]
#[case("uint64,string,bool", vec!["uint64", "string", "bool"])]
Expand All @@ -549,15 +551,6 @@ mod tests {
assert!(parse_tuple_content(input).is_err());
}

// Signature round-trip
#[rstest]
#[case("add(uint64,uint64)uint64")]
#[case("optIn()void")]
fn signature_round_trip(#[case] signature: &str) {
let method = ABIMethod::from_str(signature).unwrap();
assert_eq!(method.signature().unwrap(), signature);
}

// Method argument type predicates
#[test]
fn method_arg_type_predicates() {
Expand All @@ -569,17 +562,4 @@ mod tests {
assert!(!ref_arg.is_transaction() && ref_arg.is_reference() && !ref_arg.is_value_type());
assert!(!val_arg.is_transaction() && !val_arg.is_reference() && val_arg.is_value_type());
}

// Edge cases
#[test]
fn empty_method_name_error() {
let method = ABIMethod::new("".to_string(), vec![], None, None);
assert!(method.signature().is_err());
}

#[test]
fn selector_length() {
let method = ABIMethod::new("test".to_string(), vec![], None, None);
assert_eq!(method.selector().unwrap().len(), 4);
}
}
52 changes: 49 additions & 3 deletions crates/algokit_abi/src/abi_type.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::{
ABIError, ABIValue,
ABIError, ABIValue, StructField,
constants::{
ALGORAND_PUBLIC_KEY_BYTE_LENGTH, BITS_PER_BYTE, MAX_BIT_SIZE, MAX_PRECISION,
STATIC_ARRAY_REGEX, UFIXED_REGEX,
},
types::collections::tuple::find_bool_sequence_end,
types::collections::{r#struct::ABIStruct, tuple::find_bool_sequence_end},
};
use std::{
collections::HashMap,
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
Expand Down Expand Up @@ -98,6 +99,14 @@ pub enum ABIType {
StaticArray(Box<ABIType>, usize),
/// A dynamic-length array of another ABI type.
DynamicArray(Box<ABIType>),
/// A struct type with named fields.
Struct(ABIStruct),
/// Raw byteslice without the length prefix that is specified in ARC-4.
AVMBytes,
/// A utf-8 string without the length prefix that is specified in ARC-4.
AVMString,
/// A 64-bit unsigned integer.
AVMUint64,
}

impl AsRef<ABIType> for ABIType {
Expand Down Expand Up @@ -125,6 +134,10 @@ impl ABIType {
ABIType::String => self.encode_string(value),
ABIType::Byte => self.encode_byte(value),
ABIType::Bool => self.encode_bool(value),
ABIType::Struct(struct_type) => struct_type.encode(value),
ABIType::AVMBytes => self.encode_avm_bytes(value),
ABIType::AVMString => self.encode_avm_string(value),
ABIType::AVMUint64 => self.encode_avm_uint64(value),
}
}

Expand All @@ -146,14 +159,21 @@ impl ABIType {
ABIType::Tuple(_) => self.decode_tuple(bytes),
ABIType::StaticArray(_, _size) => self.decode_static_array(bytes),
ABIType::DynamicArray(_) => self.decode_dynamic_array(bytes),
ABIType::Struct(struct_type) => struct_type.decode(bytes),
ABIType::AVMBytes => self.decode_avm_bytes(bytes),
ABIType::AVMString => self.decode_avm_string(bytes),
ABIType::AVMUint64 => self.decode_avm_uint64(bytes),
}
}

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::DynamicArray(_) | ABIType::String => true,
ABIType::DynamicArray(_) | ABIType::String | ABIType::AVMBytes | ABIType::AVMString => {
true
}
ABIType::Struct(struct_type) => struct_type.to_tuple_type().is_dynamic(),
_ => false,
}
}
Expand All @@ -165,6 +185,7 @@ impl ABIType {
ABIType::Address => Ok(ALGORAND_PUBLIC_KEY_BYTE_LENGTH),
ABIType::Bool => Ok(1),
ABIType::Byte => Ok(1),
ABIType::AVMUint64 => Ok(8),
ABIType::StaticArray(child_type, size) => match child_type.as_ref() {
ABIType::Bool => Ok((*size).div_ceil(BITS_PER_BYTE as usize)),
_ => Ok(Self::get_size(child_type)? * *size),
Expand All @@ -190,14 +211,32 @@ impl ABIType {
}
Ok(size)
}
ABIType::Struct(struct_type) => {
let tuple_type = struct_type.to_tuple_type();
Self::get_size(&tuple_type)
}
ABIType::String => Err(ABIError::DecodingError {
message: format!("Failed to get size, {} is a dynamic type", abi_type),
}),
ABIType::DynamicArray(_) => Err(ABIError::DecodingError {
message: format!("Failed to get size, {} is a dynamic type", abi_type),
}),
ABIType::AVMBytes => Err(ABIError::DecodingError {
message: format!("Failed to get size, {} is a dynamic type", abi_type),
}),
ABIType::AVMString => Err(ABIError::DecodingError {
message: format!("Failed to get size, {} is a dynamic type", abi_type),
}),
}
}

pub(crate) fn from_struct(
struct_name: &str,
structs: &HashMap<String, Vec<StructField>>,
) -> Result<Self, ABIError> {
let struct_type = ABIStruct::get_abi_struct_type(struct_name, structs)?;
Ok(Self::Struct(struct_type))
}
}

impl Display for ABIType {
Expand All @@ -221,6 +260,10 @@ impl Display for ABIType {
ABIType::DynamicArray(child_type) => {
write!(f, "{}[]", child_type)
}
ABIType::Struct(struct_type) => write!(f, "{}", struct_type), // TODO: test this to make sure the method selector is correct
ABIType::AVMBytes => write!(f, "AVMBytes"),
ABIType::AVMString => write!(f, "AVMString"),
ABIType::AVMUint64 => write!(f, "AVMUint64"),
}
}
}
Expand Down Expand Up @@ -321,6 +364,9 @@ impl FromStr for ABIType {
"bool" => Ok(ABIType::Bool),
"address" => Ok(ABIType::Address),
"string" => Ok(ABIType::String),
"AVMBytes" => Ok(ABIType::AVMBytes),
"AVMString" => Ok(ABIType::AVMString),
"AVMUint64" => Ok(ABIType::AVMUint64),
_ => Err(ABIError::ValidationError {
message: format!("Cannot convert string '{}' to an ABI type", s),
}),
Expand Down
Loading
Loading