From 98279100daead69971f5f69f2a596f8a69c062de Mon Sep 17 00:00:00 2001 From: Ivan Schuetz Date: Mon, 11 Apr 2022 16:45:46 +0200 Subject: [PATCH] Atomic Transaction Composer --- Cargo.toml | 11 +- algonaut_abi/src/abi_encode.rs | 15 +- algonaut_abi/src/abi_encode_test.rs | 81 +- algonaut_abi/src/abi_interactions.rs | 114 +-- algonaut_abi/src/abi_type.rs | 210 ++--- algonaut_abi/src/abi_type_test.rs | 175 ++-- algonaut_abi/src/lib.rs | 2 +- algonaut_core/src/address.rs | 2 +- algonaut_core/src/lib.rs | 8 + algonaut_model/src/algod/v2/mod.rs | 15 +- algonaut_transaction/Cargo.toml | 2 + algonaut_transaction/src/account.rs | 14 + algonaut_transaction/src/api_model.rs | 3 +- algonaut_transaction/src/builder.rs | 10 +- algonaut_transaction/src/contract_account.rs | 4 +- algonaut_transaction/src/error.rs | 4 + algonaut_transaction/src/lib.rs | 2 +- algonaut_transaction/src/transaction.rs | 2 +- algonaut_transaction/src/tx_group.rs | 4 +- examples/app_call.rs | 3 +- examples/logic_sig_contract_account.rs | 2 +- src/atomic_transaction_composer/mod.rs | 790 +++++++++++++++++ .../transaction_signer.rs | 71 ++ src/error.rs | 7 +- src/lib.rs | 2 + src/util/mod.rs | 1 + src/util/wait_for_pending_tx.rs | 34 + tests/docker/run_docker.sh | 6 +- tests/features_runner.rs | 25 +- tests/step_defs/integration/abi.rs | 831 ++++++++++++++++++ tests/step_defs/integration/applications.rs | 227 +---- tests/step_defs/integration/general.rs | 161 ++++ tests/step_defs/integration/mod.rs | 3 + tests/step_defs/integration/world.rs | 55 ++ tests/step_defs/util.rs | 13 +- 35 files changed, 2383 insertions(+), 526 deletions(-) create mode 100644 src/atomic_transaction_composer/mod.rs create mode 100644 src/atomic_transaction_composer/transaction_signer.rs create mode 100644 src/util/mod.rs create mode 100644 src/util/wait_for_pending_tx.rs create mode 100644 tests/step_defs/integration/abi.rs create mode 100644 tests/step_defs/integration/general.rs create mode 100644 tests/step_defs/integration/world.rs diff --git a/Cargo.toml b/Cargo.toml index 872e94eb..0c8cfe75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,13 +32,22 @@ algonaut_transaction = { path = "algonaut_transaction", version = "0.3.0" } algonaut_abi = { path = "algonaut_abi", version = "0.3.0" } thiserror = "1.0.23" rmp-serde = "1.0.0" +num-traits = "0.2.14" +num-bigint = "0.4.3" +futures-timer = "3.0.2" +instant = { version = "0.1", features = [ "now" ] } +data-encoding = "2.3.1" +sha2 = "0.10.1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +gloo-timers = { version = "=0.2.1", features = ["futures"] } +instant = { version = "0.1", features = [ "now", "wasm-bindgen" ] } [dev-dependencies] dotenv = "0.15.0" tokio = { version = "1.6.0", features = ["rt-multi-thread", "macros"] } rand = "0.8.3" getrandom = { version = "0.2.2", features = ["js"] } -data-encoding = "2.3.1" cucumber = "0.13.0" async-trait = "0.1.51" diff --git a/algonaut_abi/src/abi_encode.rs b/algonaut_abi/src/abi_encode.rs index 68839946..9566858d 100644 --- a/algonaut_abi/src/abi_encode.rs +++ b/algonaut_abi/src/abi_encode.rs @@ -1,9 +1,6 @@ use crate::{ abi_error::AbiError, - abi_type::{ - make_byte_type, make_tuple_type, AbiType, AbiValue, ADDRESS_BYTE_SIZE, - LENGTH_ENCODE_BYTE_SIZE, SINGLE_BYTE_SIZE, - }, + abi_type::{AbiType, AbiValue, ADDRESS_BYTE_SIZE, LENGTH_ENCODE_BYTE_SIZE, SINGLE_BYTE_SIZE}, biguint_ext::BigUintExt, }; use algonaut_core::Address; @@ -298,7 +295,7 @@ impl AbiType { AbiType::Address => { let mut child_types = Vec::with_capacity(ADDRESS_BYTE_SIZE); for _ in 0..ADDRESS_BYTE_SIZE { - child_types.push(make_byte_type()) + child_types.push(AbiType::byte()) } child_types } @@ -330,7 +327,7 @@ impl AbiType { let mut child_types = Vec::with_capacity(tup_len[0]); for _ in 0..tup_len[0] { - child_types.push(make_byte_type()) + child_types.push(AbiType::byte()) } child_types } @@ -341,7 +338,7 @@ impl AbiType { } }; - make_tuple_type(child_types) + AbiType::tuple(child_types) } /// ByteLen method calculates the byte length of a static ABI type. @@ -389,7 +386,7 @@ impl AbiType { } _ => Err(AbiError::Msg(format!( "Can't pre-compute byte length: {} is a dynamic type", - self.string()?, + self, ))), } } @@ -524,7 +521,7 @@ fn decode_tuple(encoded: &[u8], children: &[AbiType]) -> Result, A Ok(values) } -pub fn find_bool_lr(types: &[AbiType], index: usize, delta: i32) -> Result { +pub(crate) fn find_bool_lr(types: &[AbiType], index: usize, delta: i32) -> Result { let mut until: usize = 0; loop { let current_index: usize = (index as i32 + delta * until as i32) diff --git a/algonaut_abi/src/abi_encode_test.rs b/algonaut_abi/src/abi_encode_test.rs index f8c0c3bc..564ae0f3 100644 --- a/algonaut_abi/src/abi_encode_test.rs +++ b/algonaut_abi/src/abi_encode_test.rs @@ -1,11 +1,7 @@ #[cfg(test)] mod test_encode { use crate::{ - abi_type::{ - make_address_type, make_bool_type, make_byte_type, make_dynamic_array_type, - make_static_array_type, make_string_type, make_ufixed_type, make_uint_type, AbiType, - AbiValue, - }, + abi_type::{AbiType, AbiValue}, biguint_ext::BigUintExt, }; use algonaut_core::Address; @@ -19,7 +15,7 @@ mod test_encode { for size in (8..512).step_by(8) { let upper_limit: BigUint = BigUint::from(1u8).shl(size); - let uint_type = make_uint_type(size).unwrap(); + let uint_type = AbiType::uint(size).unwrap(); for _ in 0..1000 { let random_int: BigUint = rng.gen_biguint(size.clone().try_into().unwrap()); @@ -58,7 +54,7 @@ mod test_encode { let upper_limit: BigUint = BigUint::from(1u8).shl(size); for precision in 1..160 { - let ufixed_type_res = make_ufixed_type(size, precision); + let ufixed_type_res = AbiType::ufixed(size, precision); assert!(ufixed_type_res.is_ok(), "make ufixed type fail"); let ufixed_type = ufixed_type_res.unwrap(); @@ -106,7 +102,7 @@ mod test_encode { rand = rng.gen_biguint(256); let addr_encode = rand.to_bytes_be_padded(32).unwrap(); assert_eq!( - make_address_type() + AbiType::address() .encode(AbiValue::Address(Address( addr_encode.clone().try_into().unwrap() ))) @@ -115,7 +111,7 @@ mod test_encode { ); } assert_eq!( - make_address_type() + AbiType::address() .encode(AbiValue::Address(Address( upper_encoded.clone().try_into().unwrap() ))) @@ -128,11 +124,11 @@ mod test_encode { #[test] fn test_encode_bool() { assert_eq!( - make_bool_type().encode(AbiValue::Bool(false)).unwrap(), + AbiType::bool().encode(AbiValue::Bool(false)).unwrap(), &[0x00] ); assert_eq!( - make_bool_type().encode(AbiValue::Bool(true)).unwrap(), + AbiType::bool().encode(AbiValue::Bool(true)).unwrap(), &[0x80] ); } @@ -140,7 +136,7 @@ mod test_encode { #[test] fn test_encode_byte() { for i in 0..=u8::MAX { - assert_eq!(make_byte_type().encode(AbiValue::Byte(i)).unwrap(), &[i]); + assert_eq!(AbiType::byte().encode(AbiValue::Byte(i)).unwrap(), &[i]); } } @@ -165,7 +161,7 @@ mod test_encode { assert_eq!( gen_bytes, - make_string_type() + AbiType::string() .encode(AbiValue::String(gen_string)) .unwrap(), ); @@ -182,7 +178,7 @@ mod test_encode { // assert_eq!(format!("{:b}", x), "101010"); assert_eq!( - make_static_array_type(make_bool_type(), 5) + AbiType::static_array(AbiType::bool(), 5) .encode(AbiValue::Array(input_values)) .unwrap(), expected @@ -200,7 +196,7 @@ mod test_encode { assert_eq!( expected, - make_static_array_type(make_bool_type(), 11) + AbiType::static_array(AbiType::bool(), 11) .encode(AbiValue::Array(input_values)) .unwrap(), ); @@ -217,7 +213,7 @@ mod test_encode { assert_eq!( expected, - make_dynamic_array_type(make_bool_type()) + AbiType::dynamic_array(AbiType::bool()) .encode(AbiValue::Array(input_values)) .unwrap(), ); @@ -345,10 +341,7 @@ mod test_decode { use rand::Rng; use crate::{ - abi_type::{ - make_address_type, make_bool_type, make_byte_type, make_string_type, make_ufixed_type, - make_uint_type, AbiType, AbiValue, - }, + abi_type::{AbiType, AbiValue}, biguint_ext::BigUintExt, }; @@ -359,14 +352,14 @@ mod test_decode { for size in (8..512).step_by(8) { for _ in 0..1000 { let random_int: BigUint = rng.gen_biguint(size.clone().try_into().unwrap()); - let encoded_int = make_uint_type(size) + let encoded_int = AbiType::uint(size) .unwrap() .encode(AbiValue::Int(random_int.clone())) .unwrap(); assert_eq!( AbiValue::Int(random_int), - make_uint_type(size).unwrap().decode(&encoded_int).unwrap(), + AbiType::uint(size).unwrap().decode(&encoded_int).unwrap(), ); } @@ -380,7 +373,7 @@ mod test_decode { assert_eq!( AbiValue::Int(largest), - make_uint_type(size).unwrap().decode(&expected).unwrap(), + AbiType::uint(size).unwrap().decode(&expected).unwrap(), ); } } @@ -393,13 +386,13 @@ mod test_decode { for precision in 1..160 { for _ in 0..20 { let random_int: BigUint = rng.gen_biguint(size.clone().try_into().unwrap()); - let encoded_int = make_ufixed_type(size, precision) + let encoded_int = AbiType::ufixed(size, precision) .unwrap() .encode(AbiValue::Int(random_int.clone())) .unwrap(); assert_eq!( AbiValue::Int(random_int), - make_ufixed_type(size, precision) + AbiType::ufixed(size, precision) .unwrap() .decode(&encoded_int) .unwrap(), @@ -414,7 +407,7 @@ mod test_decode { assert_eq!( AbiValue::Int(largest), - make_ufixed_type(size, precision) + AbiType::ufixed(size, precision) .unwrap() .decode(&expected) .unwrap(), @@ -439,13 +432,13 @@ mod test_decode { assert_eq!( AbiValue::Address(Address(addr_encode.clone().try_into().unwrap())), - make_address_type().decode(&addr_encode).unwrap(), + AbiType::address().decode(&addr_encode).unwrap(), ); } assert_eq!( AbiValue::Address(Address(upper_encoded.clone().try_into().unwrap())), - make_address_type().decode(&upper_encoded).unwrap(), + AbiType::address().decode(&upper_encoded).unwrap(), ); } } @@ -453,12 +446,12 @@ mod test_decode { #[test] fn test_decode_bool() { assert_eq!( - make_bool_type().decode(&[0x00]).unwrap(), + AbiType::bool().decode(&[0x00]).unwrap(), AbiValue::Bool(false) ); assert_eq!( - make_bool_type().decode(&[0x80]).unwrap(), + AbiType::bool().decode(&[0x80]).unwrap(), AbiValue::Bool(true) ); } @@ -466,7 +459,7 @@ mod test_decode { #[test] fn test_decode_byte() { for i in 0..=u8::MAX { - assert_eq!(make_byte_type().decode(&[i]).unwrap(), AbiValue::Byte(i)); + assert_eq!(AbiType::byte().decode(&[i]).unwrap(), AbiValue::Byte(i)); } } @@ -491,7 +484,7 @@ mod test_decode { assert_eq!( AbiValue::String(gen_string), - make_string_type().decode(&gen_bytes).unwrap(), + AbiType::string().decode(&gen_bytes).unwrap(), ); } } @@ -780,11 +773,7 @@ mod test_roundrip { use std::convert::TryInto; use crate::{ - abi_type::{ - make_address_type, make_bool_type, make_byte_type, make_dynamic_array_type, - make_static_array_type, make_string_type, make_tuple_type, make_ufixed_type, - make_uint_type, AbiType, AbiValue, - }, + abi_type::{AbiType, AbiValue}, biguint_ext::BigUintExt, }; use algonaut_core::Address; @@ -822,7 +811,7 @@ mod test_roundrip { } let type_ = test_value_pool[0][i].type_str.clone().parse().unwrap(); test_value_pool[6].push(RawValueWithAbiType::new( - &make_static_array_type(type_, 20).string().unwrap(), + &AbiType::static_array(type_, 20).to_string(), AbiValue::Array(value_arr), )); } @@ -875,7 +864,7 @@ mod test_roundrip { } let type_ = test_value_pool[0][i].type_str.clone().parse().unwrap(); test_value_pool[6].push(RawValueWithAbiType::new( - &make_dynamic_array_type(type_).string().unwrap(), + &AbiType::dynamic_array(type_).to_string(), AbiValue::Array(value_arr), )); } @@ -938,7 +927,7 @@ mod test_roundrip { ); } - let abi_str = make_tuple_type(tuple_types).unwrap().string().unwrap(); + let abi_str = AbiType::tuple(tuple_types).unwrap().to_string(); test_value_pool[8].push(RawValueWithAbiType::new( &abi_str, AbiValue::Array(tuple_values), @@ -959,13 +948,13 @@ mod test_roundrip { for i in (8..512).step_by(8) { for _ in 0..200 { test_value_pool[0].push(RawValueWithAbiType::new( - &make_uint_type(i).unwrap().string().unwrap(), + &AbiType::uint(i).unwrap().to_string(), AbiValue::Int(generate_random_int(i as u64)), )); } for j in 1..160 { test_value_pool[1].push(RawValueWithAbiType::new( - &make_ufixed_type(i, j).unwrap().string().unwrap(), + &AbiType::ufixed(i, j).unwrap().to_string(), AbiValue::Int(generate_random_int(i as u64)), )); } @@ -973,14 +962,14 @@ mod test_roundrip { for i in 0..=u8::MAX { test_value_pool[2].push(RawValueWithAbiType::new( - &make_byte_type().string().unwrap(), + &AbiType::byte().to_string(), AbiValue::Byte(i), )); } for i in 0..2 { test_value_pool[3].push(RawValueWithAbiType::new( - &make_bool_type().string().unwrap(), + &AbiType::bool().to_string(), AbiValue::Bool(i == 0), )); } @@ -991,7 +980,7 @@ mod test_roundrip { let address = Address(address_encode.try_into().unwrap()); test_value_pool[4].push(RawValueWithAbiType::new( - &make_address_type().string().unwrap(), + &AbiType::address().to_string(), AbiValue::Address(address), )); } @@ -1005,7 +994,7 @@ mod test_roundrip { .collect(); test_value_pool[5].push(RawValueWithAbiType::new( - &make_string_type().string().unwrap(), + &AbiType::string().to_string(), AbiValue::String(gen_string), )); } diff --git a/algonaut_abi/src/abi_interactions.rs b/algonaut_abi/src/abi_interactions.rs index f1b4dc35..47c054da 100644 --- a/algonaut_abi/src/abi_interactions.rs +++ b/algonaut_abi/src/abi_interactions.rs @@ -77,7 +77,7 @@ pub struct AbiMethodArg { /// The type of the argument as a string. /// See [get_type_object](get_type_object) to obtain the ABI type object #[serde(rename = "type")] - pub type_: String, + pub(crate) type_: String, /// User-friendly description for the argument #[serde(rename = "desc", skip_serializing_if = "Option::is_none")] @@ -85,7 +85,14 @@ pub struct AbiMethodArg { /// Cache that holds the parsed type object #[serde(skip)] - pub parsed: Option, + pub(crate) parsed: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AbiArgType { + Tx(TransactionArgType), + Ref(ReferenceArgType), + AbiObj(AbiType), } impl PartialEq for AbiMethodArg { @@ -99,46 +106,38 @@ impl PartialEq for AbiMethodArg { impl Eq for AbiMethodArg {} impl AbiMethodArg { - pub fn is_transaction_arg(&self) -> bool { - self.transaction_arg().is_some() + pub fn type_(&mut self) -> Result { + Ok(if let Some(tx_arg) = self.transaction_arg() { + AbiArgType::Tx(tx_arg) + } else if let Some(ref_arg) = self.reference_arg() { + AbiArgType::Ref(ref_arg) + } else { + let type_ = self.type_.parse::()?; + self.parsed = Some(type_.clone()); + AbiArgType::AbiObj(type_) + }) } - pub fn transaction_arg(&self) -> Option { - TransactionArgType::from_api_str(&self.type_).ok() + pub fn abi_obj_or_err(&mut self) -> Result { + let type_ = self.type_()?; + match type_ { + AbiArgType::AbiObj(obj) => Ok(obj), + _ => Err(AbiError::Msg(format!( + "The arg: {type_:?} is not an ABI object." + ))), + } } - pub fn is_reference_arg(&self) -> bool { - self.reference_arg().is_some() + fn is_transaction_arg(&self) -> bool { + self.transaction_arg().is_some() } - pub fn reference_arg(&self) -> Option { - ReferenceArgType::from_api_str(&self.type_).ok() + fn transaction_arg(&self) -> Option { + TransactionArgType::from_api_str(&self.type_).ok() } - /// parses and returns the ABI type object for this argument's - /// type. An error will be returned if this argument's type is a transaction or - /// reference type - pub fn get_type_object(&mut self) -> Result { - if self.is_transaction_arg() { - return Err(AbiError::Msg(format!( - "Invalid operation on transaction type: {}", - self.type_ - ))); - } - if self.is_reference_arg() { - return Err(AbiError::Msg(format!( - "Invalid operation on reference type: {}", - self.type_ - ))); - } - if let Some(parsed) = &self.parsed { - return Ok(parsed.clone()); - } - - let type_obj = self.type_.parse::()?; - self.parsed = Some(type_obj.clone()); - - Ok(type_obj) + fn reference_arg(&self) -> Option { + ReferenceArgType::from_api_str(&self.type_).ok() } } @@ -148,7 +147,7 @@ pub struct AbiReturn { /// The type of the argument as a string. See the [get_type_object](get_type_object) to /// obtain the ABI type object #[serde(rename = "type")] - pub type_: String, + pub(crate) type_: String, /// User-friendly description for the argument #[serde(rename = "desc", skip_serializing_if = "Option::is_none")] @@ -156,7 +155,7 @@ pub struct AbiReturn { /// Cache that holds the parsed type object #[serde(skip)] - pub parsed: Option, + pub(crate) parsed: Option, } impl PartialEq for AbiReturn { @@ -168,27 +167,36 @@ impl PartialEq for AbiReturn { impl Eq for AbiReturn {} impl AbiReturn { - fn is_void(&self) -> bool { - self.type_ == "void" + pub fn is_void(&self) -> bool { + Self::is_void_str(&self.type_) + } + + pub fn is_void_str(s: &str) -> bool { + s == "void" } - fn get_type_object(&mut self) -> Result { + pub fn type_(&mut self) -> Result { if self.is_void() { - return Err(AbiError::Msg( - "Invalid operation on void return type".to_owned(), - )); - } - if let Some(parsed) = &self.parsed { - return Ok(parsed.clone()); - } + Ok(AbiReturnType::Void) + } else { + if let Some(parsed) = &self.parsed { + return Ok(AbiReturnType::Some(parsed.clone())); + } - let type_obj = self.type_.parse::()?; - self.parsed = Some(type_obj.clone()); + let type_obj = self.type_.parse::()?; + self.parsed = Some(type_obj.clone()); - Ok(type_obj) + Ok(AbiReturnType::Some(type_obj)) + } } } +#[derive(Debug, Clone)] +pub enum AbiReturnType { + Some(AbiType), + Void, +} + /// Represents an ABI method return value #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AbiMethod { @@ -259,10 +267,8 @@ impl AbiMethod { parsed: None, }; - if !return_type.is_void() { - // fill type object cache - return_type.get_type_object()?; - } + // fill type object cache + return_type.type_()?; let mut args: Vec = Vec::with_capacity(arg_types.len()); @@ -282,7 +288,7 @@ impl AbiMethod { } // fill type object cache - args[i].get_type_object()?; + args[i].type_()?; } Ok(AbiMethod { diff --git a/algonaut_abi/src/abi_type.rs b/algonaut_abi/src/abi_type.rs index 02db5851..ede5cfe0 100644 --- a/algonaut_abi/src/abi_type.rs +++ b/algonaut_abi/src/abi_type.rs @@ -3,7 +3,7 @@ use algonaut_core::Address; use lazy_static::lazy_static; use num_bigint::BigUint; use regex::Regex; -use std::{convert::TryInto, str::FromStr}; +use std::{convert::TryInto, fmt::Display, str::FromStr}; pub const ADDRESS_BYTE_SIZE: usize = 32; pub const LENGTH_ENCODE_BYTE_SIZE: usize = 2; @@ -43,6 +43,7 @@ impl AbiType { _ => false, } } + /// Returns references to element's children. Variants that don't specify children return an empty vector. pub fn children(&self) -> &[AbiType] { match self { @@ -53,9 +54,7 @@ impl AbiType { _ => &[], } } -} -impl AbiType { /// Determines whether the ABI type is dynamic or static. pub fn is_dynamic(&self) -> bool { match self { @@ -63,31 +62,114 @@ impl AbiType { _ => self.has_dynamic_child(), } } +} +impl Display for AbiType { /// Serialize an ABI Type to a string in ABI encoding. - pub fn string(&self) -> Result { - match self { - AbiType::UInt { bit_size } => Ok(format!("uint{}", bit_size)), - AbiType::Byte => Ok("byte".to_owned()), + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + AbiType::UInt { bit_size } => format!("uint{}", bit_size), + AbiType::Byte => "byte".to_owned(), AbiType::UFixed { bit_size, precision, - } => Ok(format!("ufixed{}x{}", bit_size, precision)), - AbiType::Bool => Ok("bool".to_owned()), + } => format!("ufixed{}x{}", bit_size, precision), + AbiType::Bool => "bool".to_owned(), AbiType::StaticArray { len, child_type } => { - Ok(format!("{}[{}]", child_type.string()?, len)) + format!("{}[{}]", child_type, len) } - AbiType::DynamicArray { child_type } => Ok(format!("{}[]", child_type.string()?)), - AbiType::String => Ok("string".to_owned()), - AbiType::Address => Ok("address".to_owned()), + AbiType::DynamicArray { child_type } => format!("{}[]", child_type), + AbiType::String => "string".to_owned(), + AbiType::Address => "address".to_owned(), AbiType::Tuple { child_types, .. } => { let mut type_strings = Vec::with_capacity(child_types.len()); for child_type in child_types { - type_strings.push(child_type.string()?) + type_strings.push(child_type.to_string()) } - Ok(format!("({})", type_strings.join(","))) + format!("({})", type_strings.join(",")) } + }; + write!(f, "{}", str) + } +} + +impl AbiType { + pub fn dynamic_array(arg_type: AbiType) -> AbiType { + AbiType::DynamicArray { + child_type: Box::new(arg_type), + } + } + + pub fn static_array(arg_type: AbiType, array_len: u16) -> AbiType { + AbiType::StaticArray { + len: array_len, + child_type: Box::new(arg_type), + } + } + + /// Makes `Uint` ABI type by taking a type bitSize argument. + /// The range of type bitSize is [8, 512] and type bitSize % 8 == 0. + pub fn uint(type_size: usize) -> Result { + if type_size % 8 != 0 || type_size < 8 || type_size > 512 { + return Err(AbiError::Msg(format!( + "unsupported uint type bitSize: {type_size}" + ))); + } + + Ok(AbiType::UInt { + bit_size: type_size as u16, + }) + } + + pub fn address() -> AbiType { + AbiType::Address + } + + pub fn byte() -> AbiType { + AbiType::Byte + } + + pub fn bool() -> AbiType { + AbiType::Bool + } + + pub fn string() -> AbiType { + AbiType::String + } + + /// Makes `UFixed` ABI type by taking type bitSize and type precision as arguments. + /// The range of type bitSize is [8, 512] and type bitSize % 8 == 0. + /// The range of type precision is [1, 160]. + pub fn ufixed(type_size: usize, type_precision: usize) -> Result { + if type_size % 8 != 0 || !(8..=512).contains(&type_size) { + return Err(AbiError::Msg(format!( + "unsupported ufixed type bitSize: {type_size}" + ))); + } + if !(1..=160).contains(&type_precision) { + return Err(AbiError::Msg(format!( + "unsupported ufixed type precision: {type_precision}" + ))); } + + Ok(AbiType::UFixed { + bit_size: type_size as u16, // cast: safe bounds checked in this fn + precision: type_precision as u16, // cast: safe bounds checked in this fn + }) + } + + /// Makes tuple ABI type with argument types + pub fn tuple(argument_types: Vec) -> Result { + if argument_types.len() >= u16::MAX as usize { + return Err(AbiError::Msg( + "tuple type child type number larger than maximum uint16 error".to_owned(), + )); + } + + Ok(AbiType::Tuple { + len: argument_types.len() as u16, // cast: safe bounds checked in this fn + child_types: argument_types, + }) } } @@ -99,7 +181,7 @@ impl FromStr for AbiType { fn from_str(s: &str) -> Result { if let Some(stripped) = s.strip_suffix("[]") { let array_arg_type = stripped.parse()?; - Ok(make_dynamic_array_type(array_arg_type)) + Ok(AbiType::dynamic_array(array_arg_type)) } else if s.ends_with(']') { lazy_static! { static ref RE: Regex = Regex::new(r"^([a-z\d\[\](),]+)\[([1-9][\d]*)]$").unwrap(); @@ -118,7 +200,7 @@ impl FromStr for AbiType { AbiError::Msg(format!("Error parsing array len: {array_len_s}: {e:?}")) })?; - Ok(make_static_array_type( + Ok(AbiType::static_array( array_type, array_len.try_into().map_err(|_| { AbiError::Msg("Couldn't convert array_len: {array_len} in u16".to_owned()) @@ -129,9 +211,9 @@ impl FromStr for AbiType { .parse() .map_err(|e| AbiError::Msg(format!("Ill formed uint type: {s}: {e:?}")))?; - make_uint_type(type_size) + AbiType::uint(type_size) } else if s == "byte" { - Ok(make_byte_type()) + Ok(AbiType::byte()) } else if s.starts_with("ufixed") { lazy_static! { static ref RE: Regex = Regex::new(r"^ufixed([1-9][\d]*)x([1-9][\d]*)$").unwrap(); @@ -155,13 +237,13 @@ impl FromStr for AbiType { )) })?; - make_ufixed_type(ufixed_size, ufixed_precision) + AbiType::ufixed(ufixed_size, ufixed_precision) } else if s == "bool" { - Ok(make_bool_type()) + Ok(AbiType::bool()) } else if s == "address" { - Ok(make_address_type()) + Ok(AbiType::address()) } else if s == "string" { - Ok(make_string_type()) + Ok(AbiType::string()) } else if s.len() >= 2 && s.starts_with('(') && s.ends_with(')') { let tuple_content = parse_tuple_content(&s[1..s.len() - 1])?; let mut tuple_types = Vec::with_capacity(tuple_content.len()); @@ -171,7 +253,7 @@ impl FromStr for AbiType { tuple_types.push(ti); } - make_tuple_type(tuple_types) + AbiType::tuple(tuple_types) } else { Err(AbiError::Msg(format!( "cannot convert string: `{s}` to ABI type" @@ -180,86 +262,6 @@ impl FromStr for AbiType { } } -// TODO make* -> associated fns from enum probably - -pub fn make_dynamic_array_type(arg_type: AbiType) -> AbiType { - AbiType::DynamicArray { - child_type: Box::new(arg_type), - } -} - -pub fn make_static_array_type(arg_type: AbiType, array_len: u16) -> AbiType { - AbiType::StaticArray { - len: array_len, - child_type: Box::new(arg_type), - } -} - -/// Makes `Uint` ABI type by taking a type bitSize argument. -/// The range of type bitSize is [8, 512] and type bitSize % 8 == 0. -pub fn make_uint_type(type_size: usize) -> Result { - if type_size % 8 != 0 || type_size < 8 || type_size > 512 { - return Err(AbiError::Msg(format!( - "unsupported uint type bitSize: {type_size}" - ))); - } - - Ok(AbiType::UInt { - bit_size: type_size as u16, - }) -} - -pub fn make_address_type() -> AbiType { - AbiType::Address -} - -pub fn make_byte_type() -> AbiType { - AbiType::Byte -} - -pub fn make_bool_type() -> AbiType { - AbiType::Bool -} - -pub fn make_string_type() -> AbiType { - AbiType::String -} - -/// Makes `UFixed` ABI type by taking type bitSize and type precision as arguments. -/// The range of type bitSize is [8, 512] and type bitSize % 8 == 0. -/// The range of type precision is [1, 160]. -pub fn make_ufixed_type(type_size: usize, type_precision: usize) -> Result { - if type_size % 8 != 0 || !(8..=512).contains(&type_size) { - return Err(AbiError::Msg(format!( - "unsupported ufixed type bitSize: {type_size}" - ))); - } - if !(1..=160).contains(&type_precision) { - return Err(AbiError::Msg(format!( - "unsupported ufixed type precision: {type_precision}" - ))); - } - - Ok(AbiType::UFixed { - bit_size: type_size as u16, // cast: safe bounds checked in this fn - precision: type_precision as u16, // cast: safe bounds checked in this fn - }) -} - -/// Makes tuple ABI type with argument types -pub fn make_tuple_type(argument_types: Vec) -> Result { - if argument_types.len() >= u16::MAX as usize { - return Err(AbiError::Msg( - "tuple type child type number larger than maximum uint16 error".to_owned(), - )); - } - - Ok(AbiType::Tuple { - len: argument_types.len() as u16, // cast: safe bounds checked in this fn - child_types: argument_types, - }) -} - /// Keeps track of the start and end of a segment in a string. struct Segment { left: usize, diff --git a/algonaut_abi/src/abi_type_test.rs b/algonaut_abi/src/abi_type_test.rs index b115e143..bafefdf3 100644 --- a/algonaut_abi/src/abi_type_test.rs +++ b/algonaut_abi/src/abi_type_test.rs @@ -6,14 +6,7 @@ mod test { type_testpool: Vec, tuple_testpool: Vec, } - use crate::{ - abi_encode::find_bool_lr, - abi_type::{ - make_address_type, make_bool_type, make_byte_type, make_dynamic_array_type, - make_static_array_type, make_string_type, make_tuple_type, make_ufixed_type, - make_uint_type, AbiType, - }, - }; + use crate::{abi_encode::find_bool_lr, abi_type::AbiType}; fn generate_random_tuple_type( type_testpool: &mut [AbiType], @@ -32,31 +25,31 @@ mod test { tuple_elems.push(type_testpool[rng.gen_range(0..type_testpool.len())].clone()); } } - make_tuple_type(tuple_elems).unwrap() + AbiType::tuple(tuple_elems).unwrap() } fn setup() -> SetupRes { let mut type_testpool = vec![ - make_bool_type(), - make_address_type(), - make_string_type(), - make_byte_type(), + AbiType::bool(), + AbiType::address(), + AbiType::string(), + AbiType::byte(), ]; for i in (8..512).step_by(8) { - type_testpool.push(make_uint_type(i).unwrap()); + type_testpool.push(AbiType::uint(i).unwrap()); } for i in (8..512).step_by(8) { for j in 1..=160 { - type_testpool.push(make_ufixed_type(i, j).unwrap()); + type_testpool.push(AbiType::ufixed(i, j).unwrap()); } } for i in 0..type_testpool.len() { - type_testpool.push(make_dynamic_array_type(type_testpool[i].clone())); - type_testpool.push(make_static_array_type(type_testpool[i].clone(), 10)); - type_testpool.push(make_static_array_type(type_testpool[i].clone(), 20)); + type_testpool.push(AbiType::dynamic_array(type_testpool[i].clone())); + type_testpool.push(AbiType::static_array(type_testpool[i].clone(), 10)); + type_testpool.push(AbiType::static_array(type_testpool[i].clone(), 20)); } let mut tuple_testpool = vec![]; @@ -75,8 +68,8 @@ mod test { #[test] fn test_uint_valid() { for i in (8..512).step_by(8) { - let type_ = make_uint_type(i).unwrap(); - assert_eq!(type_.string().unwrap(), format!("uint{}", i)) + let type_ = AbiType::uint(i).unwrap(); + assert_eq!(type_.to_string(), format!("uint{}", i)) } } @@ -92,7 +85,7 @@ mod test { } let final_size_rand = size_rand; - assert!(make_uint_type(final_size_rand).is_err()) + assert!(AbiType::uint(final_size_rand).is_err()) } } @@ -100,8 +93,8 @@ mod test { fn test_ufixed_valid() { for i in (8..512).step_by(8) { for j in 1..160 { - let type_ = make_ufixed_type(i, j).unwrap(); - assert_eq!(type_.string().unwrap(), format!("ufixed{}x{}", i, j)) + let type_ = AbiType::ufixed(i, j).unwrap(); + assert_eq!(type_.to_string(), format!("ufixed{}x{}", i, j)) } } } @@ -124,68 +117,59 @@ mod test { let final_rand_precision = precision_rand; let final_size_rand = size_rand; - assert!(make_ufixed_type(final_size_rand, final_rand_precision).is_err()) + assert!(AbiType::ufixed(final_size_rand, final_rand_precision).is_err()) } } #[test] fn test_simple_types_valid() { - assert_eq!(&make_byte_type().string().unwrap(), "byte"); - assert_eq!(&make_string_type().string().unwrap(), "string"); - assert_eq!(&make_address_type().string().unwrap(), "address"); - assert_eq!(&make_bool_type().string().unwrap(), "bool"); + assert_eq!(&AbiType::byte().to_string(), "byte"); + assert_eq!(&AbiType::string().to_string(), "string"); + assert_eq!(&AbiType::address().to_string(), "address"); + assert_eq!(&AbiType::bool().to_string(), "bool"); } #[test] fn test_type_to_string_valid() { assert_eq!( - &make_dynamic_array_type(make_uint_type(32).unwrap()) - .string() - .unwrap(), + &AbiType::dynamic_array(AbiType::uint(32).unwrap()).to_string(), "uint32[]" ); assert_eq!( - &make_dynamic_array_type(make_dynamic_array_type(make_byte_type())) - .string() - .unwrap(), + &AbiType::dynamic_array(AbiType::dynamic_array(AbiType::byte())).to_string(), "byte[][]" ); assert_eq!( - &make_static_array_type(make_ufixed_type(128, 10).unwrap(), 100) - .string() - .unwrap(), + &AbiType::static_array(AbiType::ufixed(128, 10).unwrap(), 100).to_string(), "ufixed128x10[100]" ); assert_eq!( - &make_static_array_type(make_static_array_type(make_bool_type(), 128), 256) - .string() - .unwrap(), + &AbiType::static_array(AbiType::static_array(AbiType::bool(), 128), 256).to_string(), "bool[128][256]" ); assert_eq!( - &make_tuple_type(vec![ - make_uint_type(32).unwrap(), - make_tuple_type(vec![ - make_address_type(), - make_byte_type(), - make_static_array_type(make_bool_type(), 10), - make_dynamic_array_type(make_ufixed_type(256, 10).unwrap()), + &AbiType::tuple(vec![ + AbiType::uint(32).unwrap(), + AbiType::tuple(vec![ + AbiType::address(), + AbiType::byte(), + AbiType::static_array(AbiType::bool(), 10), + AbiType::dynamic_array(AbiType::ufixed(256, 10).unwrap()), ]) .unwrap() ]) .unwrap() - .string() - .unwrap(), + .to_string(), "(uint32,(address,byte,bool[10],ufixed256x10[]))" ); - assert_eq!(&make_tuple_type(vec![]).unwrap().string().unwrap(), "()"); + assert_eq!(&AbiType::tuple(vec![]).unwrap().to_string(), "()"); } #[test] fn test_uint_from_string_valid() { for i in (8..512).step_by(8) { let encoded = format!("uint{}", i); - let uint_type = make_uint_type(i).unwrap(); + let uint_type = AbiType::uint(i).unwrap(); assert_eq!(encoded.parse::().unwrap(), uint_type) } } @@ -211,7 +195,7 @@ mod test { for i in (8..512).step_by(8) { for j in 1..160 { let encoded = format!("ufixed{}x{}", i, j); - let ufixed_t = make_ufixed_type(i, j).unwrap(); + let ufixed_t = AbiType::ufixed(i, j).unwrap(); assert_eq!(encoded.parse::().unwrap(), ufixed_t); } } @@ -240,54 +224,54 @@ mod test { #[test] fn test_simple_type_from_string_valid() { - assert_eq!("address".parse::().unwrap(), make_address_type()); - assert_eq!("byte".parse::().unwrap(), make_byte_type()); - assert_eq!("bool".parse::().unwrap(), make_bool_type()); - assert_eq!("string".parse::().unwrap(), make_string_type()); + assert_eq!("address".parse::().unwrap(), AbiType::address()); + assert_eq!("byte".parse::().unwrap(), AbiType::byte()); + assert_eq!("bool".parse::().unwrap(), AbiType::bool()); + assert_eq!("string".parse::().unwrap(), AbiType::string()); } #[test] fn test_type_from_string_valid() { assert_eq!( "uint256[]".parse::().unwrap(), - make_dynamic_array_type(make_uint_type(256).unwrap()) + AbiType::dynamic_array(AbiType::uint(256).unwrap()) ); assert_eq!( "ufixed256x64[]".parse::().unwrap(), - make_dynamic_array_type(make_ufixed_type(256, 64).unwrap()) + AbiType::dynamic_array(AbiType::ufixed(256, 64).unwrap()) ); assert_eq!( "byte[][][][]".parse::().unwrap(), - make_dynamic_array_type(make_dynamic_array_type(make_dynamic_array_type( - make_dynamic_array_type(make_byte_type()) + AbiType::dynamic_array(AbiType::dynamic_array(AbiType::dynamic_array( + AbiType::dynamic_array(AbiType::byte()) ))) ); assert_eq!( "address[100]".parse::().unwrap(), - make_static_array_type(make_address_type(), 100) + AbiType::static_array(AbiType::address(), 100) ); assert_eq!( "uint64[][100]".parse::().unwrap(), - make_static_array_type(make_dynamic_array_type(make_uint_type(64).unwrap()), 100) + AbiType::static_array(AbiType::dynamic_array(AbiType::uint(64).unwrap()), 100) ); assert_eq!( "()".parse::().unwrap(), - make_tuple_type(vec![]).unwrap() + AbiType::tuple(vec![]).unwrap() ); assert_eq!( "(uint32,(address,byte,bool[10],ufixed256x10[]),byte[])" .parse::() .unwrap(), - make_tuple_type(vec![ - make_uint_type(32).unwrap(), - make_tuple_type(vec![ - make_address_type(), - make_byte_type(), - make_static_array_type(make_bool_type(), 10), - make_dynamic_array_type(make_ufixed_type(256, 10).unwrap()) + AbiType::tuple(vec![ + AbiType::uint(32).unwrap(), + AbiType::tuple(vec![ + AbiType::address(), + AbiType::byte(), + AbiType::static_array(AbiType::bool(), 10), + AbiType::dynamic_array(AbiType::ufixed(256, 10).unwrap()) ]) .unwrap(), - make_dynamic_array_type(make_byte_type()) + AbiType::dynamic_array(AbiType::byte()) ]) .unwrap() ); @@ -295,14 +279,14 @@ mod test { "(uint32,(address,byte,bool[10],(ufixed256x10[])))" .parse::() .unwrap(), - make_tuple_type(vec![ - make_uint_type(32).unwrap(), - make_tuple_type(vec![ - make_address_type(), - make_byte_type(), - make_static_array_type(make_bool_type(), 10), - make_tuple_type(vec![make_dynamic_array_type( - make_ufixed_type(256, 10).unwrap() + AbiType::tuple(vec![ + AbiType::uint(32).unwrap(), + AbiType::tuple(vec![ + AbiType::address(), + AbiType::byte(), + AbiType::static_array(AbiType::bool(), 10), + AbiType::tuple(vec![AbiType::dynamic_array( + AbiType::ufixed(256, 10).unwrap() )]) .unwrap() ]) @@ -314,14 +298,14 @@ mod test { "((uint32),(address,(byte,bool[10],ufixed256x10[])))" .parse::() .unwrap(), - make_tuple_type(vec![ - make_tuple_type(vec![make_uint_type(32).unwrap()]).unwrap(), - make_tuple_type(vec![ - make_address_type(), - make_tuple_type(vec![ - make_byte_type(), - make_static_array_type(make_bool_type(), 10), - make_dynamic_array_type(make_ufixed_type(256, 10).unwrap()) + AbiType::tuple(vec![ + AbiType::tuple(vec![AbiType::uint(32).unwrap()]).unwrap(), + AbiType::tuple(vec![ + AbiType::address(), + AbiType::tuple(vec![ + AbiType::byte(), + AbiType::static_array(AbiType::bool(), 10), + AbiType::dynamic_array(AbiType::ufixed(256, 10).unwrap()) ]) .unwrap() ]) @@ -382,7 +366,7 @@ mod test { fn test_tuple_roundtrip() { let tuple_testpool = setup().tuple_testpool; for t in tuple_testpool { - let encoded = t.string().unwrap(); + let encoded = t.to_string(); let decoded = encoded.parse::().unwrap(); assert_eq!(decoded, t.clone()); } @@ -409,8 +393,7 @@ mod test { let index0 = rng.gen_range(0..type_testpool.len()); let mut index1 = rng.gen_range(0..type_testpool.len()); - while type_testpool[index0].string().unwrap() == type_testpool[index1].string().unwrap() - { + while type_testpool[index0].to_string() == type_testpool[index1].to_string() { index1 = rng.gen_range(0..type_testpool.len()); } @@ -421,9 +404,7 @@ mod test { let index0 = rng.gen_range(0..tuple_testpool.len()); let mut index1 = rng.gen_range(0..tuple_testpool.len()); - while tuple_testpool[index0].string().unwrap() - == tuple_testpool[index1].string().unwrap() - { + while tuple_testpool[index0].to_string() == tuple_testpool[index1].to_string() { index1 = rng.gen_range(0..tuple_testpool.len()); } @@ -439,13 +420,13 @@ mod test { } = setup(); for t in &type_testpool { - let encoded = t.string().unwrap(); + let encoded = t.to_string(); let infer_from_string = encoded.contains("[]") || encoded.contains("string"); assert_eq!(infer_from_string, t.is_dynamic()); } for t in &tuple_testpool { - let encoded = t.string().unwrap(); + let encoded = t.to_string(); let infer_from_string = encoded.contains("[]") || encoded.contains("string"); assert_eq!(infer_from_string, t.is_dynamic()); } @@ -458,8 +439,8 @@ mod test { tuple_testpool, } = setup(); - assert_eq!(make_address_type().byte_len().unwrap(), 32); - assert_eq!(make_byte_type().byte_len().unwrap(), 1); + assert_eq!(AbiType::address().byte_len().unwrap(), 32); + assert_eq!(AbiType::byte().byte_len().unwrap(), 1); for t in type_testpool { if t.is_dynamic() { diff --git a/algonaut_abi/src/lib.rs b/algonaut_abi/src/lib.rs index 3a867e8e..65c18a54 100644 --- a/algonaut_abi/src/lib.rs +++ b/algonaut_abi/src/lib.rs @@ -27,7 +27,7 @@ pub fn make_tuple_type(argument_types: &[AbiType]) -> Result let mut strs = vec![]; for arg in argument_types { - strs.push(arg.string()?) + strs.push(arg.to_string()) } let str_tuple = format!("({})", strs.join(",")); diff --git a/algonaut_core/src/address.rs b/algonaut_core/src/address.rs index 9877fcb6..a370a42e 100644 --- a/algonaut_core/src/address.rs +++ b/algonaut_core/src/address.rs @@ -98,7 +98,7 @@ impl<'de> Deserialize<'de> for Address { } /// Convenience struct for handling multisig public identities -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MultisigAddress { /// the version of this multisig pub version: u8, diff --git a/algonaut_core/src/lib.rs b/algonaut_core/src/lib.rs index 3d48a3b9..4ca50276 100644 --- a/algonaut_core/src/lib.rs +++ b/algonaut_core/src/lib.rs @@ -321,3 +321,11 @@ mod tests { ); } } + +/// Returns the address corresponding to an application's escrow account. +pub fn to_app_address(app_id: u64) -> Address { + let bytes = app_id.to_be_bytes(); + let all_bytes = ["appID".as_bytes(), &bytes].concat(); + let hash = sha2::Sha512_256::digest(all_bytes); + Address(hash.into()) +} diff --git a/algonaut_model/src/algod/v2/mod.rs b/algonaut_model/src/algod/v2/mod.rs index 0e729a1c..5db949c0 100644 --- a/algonaut_model/src/algod/v2/mod.rs +++ b/algonaut_model/src/algod/v2/mod.rs @@ -536,7 +536,10 @@ pub struct GenesisBlock { /// A transaction. #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Transaction {} +pub struct Transaction { + // #[serde(rename = "txId")] +// pub tx_id: String, +} /// A potentially truncated list of transactions currently in the node's transaction pool. /// You can compute whether or not the list is truncated if the number of elements in the @@ -607,6 +610,16 @@ pub struct PendingTransaction { #[serde(rename = "sender-rewards")] pub sender_rewards: Option, + /// InnerTxns inner transactions produced by application execution. + #[serde(default, rename = "inner-txns", skip_serializing_if = "Vec::is_empty")] + pub inner_txs: Vec, + + /// Logs for the application being executed by this transaction. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + // #[serde(default)] + // pub logs: Vec>, + pub logs: Vec, + // pub logs: Option, /// The raw signed transaction. pub txn: Transaction, } diff --git a/algonaut_transaction/Cargo.toml b/algonaut_transaction/Cargo.toml index 8470fdcd..8f170461 100644 --- a/algonaut_transaction/Cargo.toml +++ b/algonaut_transaction/Cargo.toml @@ -13,6 +13,7 @@ algonaut_core = {path = "../algonaut_core", version = "0.3.0"} algonaut_crypto = {path = "../algonaut_crypto", version = "0.3.0"} algonaut_encoding = {path = "../algonaut_encoding", version = "0.3.0"} algonaut_model = {path = "../algonaut_model", version = "0.3.0"} +algonaut_abi = {path = "../algonaut_abi", version = "0.3.0"} data-encoding = "2.3.1" derive_more = "0.99.13" rand = "0.8.3" @@ -26,3 +27,4 @@ thiserror = "1.0.23" url = "2.2.0" urlencoding = "2.0.0-alpha.1" num-traits = "0.2.14" +num-bigint = "0.4.3" diff --git a/algonaut_transaction/src/account.rs b/algonaut_transaction/src/account.rs index f9f05b7e..493875a5 100644 --- a/algonaut_transaction/src/account.rs +++ b/algonaut_transaction/src/account.rs @@ -18,6 +18,20 @@ pub struct Account { key_pair: Ed25519KeyPair, } +impl Clone for Account { + fn clone(&self) -> Self { + Self::from_seed(self.seed) + } +} + +impl PartialEq for Account { + fn eq(&self, other: &Self) -> bool { + // TODO verify that this is correct - do we always get the same key pair for a seed? or is it otherwise still correct? + self.seed == other.seed && self.address == other.address + } +} +impl Eq for Account {} + impl Account { pub fn generate() -> Account { let seed: [u8; 32] = OsRng.gen(); diff --git a/algonaut_transaction/src/api_model.rs b/algonaut_transaction/src/api_model.rs index c40ed680..8d0bcc16 100644 --- a/algonaut_transaction/src/api_model.rs +++ b/algonaut_transaction/src/api_model.rs @@ -71,7 +71,7 @@ pub struct ApiTransaction { pub accounts: Option>, #[serde(rename = "apep", skip_serializing_if = "Option::is_none")] - pub extra_pages: Option, + pub extra_pages: Option, #[serde(rename = "apfa", skip_serializing_if = "Option::is_none")] pub foreign_apps: Option>, @@ -553,6 +553,7 @@ impl From for AssetParams { } } +// TODO move this somewhere else and make api_model non pub again pub fn to_tx_type_enum(type_: &TransactionType) -> TransactionTypeEnum { match type_ { TransactionType::Payment(_) => TransactionTypeEnum::Payment, diff --git a/algonaut_transaction/src/builder.rs b/algonaut_transaction/src/builder.rs index e85cb0bf..e1b20968 100644 --- a/algonaut_transaction/src/builder.rs +++ b/algonaut_transaction/src/builder.rs @@ -624,7 +624,7 @@ pub struct CreateApplication { foreign_assets: Option>, global_state_schema: Option, local_state_schema: Option, - extra_pages: u64, + extra_pages: u32, } impl CreateApplication { @@ -669,7 +669,7 @@ impl CreateApplication { self } - pub fn extra_pages(mut self, extra_pages: u64) -> Self { + pub fn extra_pages(mut self, extra_pages: u32) -> Self { self.extra_pages = extra_pages; self } @@ -769,10 +769,11 @@ pub struct CallApplication { app_arguments: Option>>, foreign_apps: Option>, foreign_assets: Option>, + on_complete: ApplicationCallOnComplete, } impl CallApplication { - pub fn new(sender: Address, app_id: u64) -> Self { + pub fn new(sender: Address, app_id: u64, on_complete: ApplicationCallOnComplete) -> Self { CallApplication { sender, app_id, @@ -780,6 +781,7 @@ impl CallApplication { app_arguments: None, foreign_apps: None, foreign_assets: None, + on_complete, } } @@ -807,7 +809,7 @@ impl CallApplication { TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { sender: self.sender, app_id: Some(self.app_id), - on_complete: ApplicationCallOnComplete::NoOp, + on_complete: self.on_complete, accounts: self.accounts, approval_program: None, app_arguments: self.app_arguments, diff --git a/algonaut_transaction/src/contract_account.rs b/algonaut_transaction/src/contract_account.rs index b1fe61d6..fc71496b 100644 --- a/algonaut_transaction/src/contract_account.rs +++ b/algonaut_transaction/src/contract_account.rs @@ -25,12 +25,12 @@ impl ContractAccount { pub fn sign( &self, - transaction: &Transaction, + transaction: Transaction, args: Vec>, ) -> Result { Ok(SignedTransaction { - transaction: transaction.clone(), transaction_id: transaction.id()?, + transaction, sig: TransactionSignature::Logic(SignedLogic { logic: self.program.clone(), args, diff --git a/algonaut_transaction/src/error.rs b/algonaut_transaction/src/error.rs index 0cd10230..bec695f5 100644 --- a/algonaut_transaction/src/error.rs +++ b/algonaut_transaction/src/error.rs @@ -26,4 +26,8 @@ pub enum TransactionError { MnemonicError(#[from] algonaut_crypto::error::CryptoError), #[error("Deserialization error: {0}")] Deserialization(String), + #[error("No accounts to sign the transaction.")] + NoAccountsToSign, + #[error("{}", 0)] + Msg(String), } diff --git a/algonaut_transaction/src/lib.rs b/algonaut_transaction/src/lib.rs index e150786b..c0edc1a5 100644 --- a/algonaut_transaction/src/lib.rs +++ b/algonaut_transaction/src/lib.rs @@ -1,5 +1,5 @@ pub mod account; -mod api_model; +pub mod api_model; // TODO revert pub pub mod auction; pub mod builder; pub mod contract_account; diff --git a/algonaut_transaction/src/transaction.rs b/algonaut_transaction/src/transaction.rs index 0ee2b2f6..af803b79 100644 --- a/algonaut_transaction/src/transaction.rs +++ b/algonaut_transaction/src/transaction.rs @@ -361,7 +361,7 @@ pub struct ApplicationCallTransaction { pub local_state_schema: Option, // Number of additional pages allocated to the application's approval and clear state programs. Each ExtraProgramPages is 2048 bytes. The sum of ApprovalProgram and ClearStateProgram may not exceed 2048*(1+ExtraProgramPages) bytes. - pub extra_pages: u64, + pub extra_pages: u32, } /// An application transaction must indicate the action to be taken following the execution of its approvalProgram or clearStateProgram. The variants below describe the available actions. diff --git a/algonaut_transaction/src/tx_group.rs b/algonaut_transaction/src/tx_group.rs index ff126118..2cdee5e4 100644 --- a/algonaut_transaction/src/tx_group.rs +++ b/algonaut_transaction/src/tx_group.rs @@ -26,7 +26,9 @@ impl TxGroup { Ok(()) } - fn compute_group_id(txns: &[&mut Transaction]) -> Result { + pub(crate) fn compute_group_id( + txns: &[&mut Transaction], + ) -> Result { if txns.is_empty() { return Err(TransactionError::EmptyTransactionListError); } diff --git a/examples/app_call.rs b/examples/app_call.rs index e1a6eea1..23197531 100644 --- a/examples/app_call.rs +++ b/examples/app_call.rs @@ -2,6 +2,7 @@ use algonaut::algod::v2::Algod; use algonaut::transaction::account::Account; use algonaut::transaction::builder::CallApplication; use algonaut::transaction::TxnBuilder; +use algonaut_transaction::transaction::ApplicationCallOnComplete; use dotenv::dotenv; use std::env; use std::error::Error; @@ -30,7 +31,7 @@ async fn main() -> Result<(), Box> { // int 1 let t = TxnBuilder::with( ¶ms, - CallApplication::new(sender.address(), 5) + CallApplication::new(sender.address(), 5, ApplicationCallOnComplete::NoOp) .app_arguments(vec![vec![1, 0], vec![255]]) .build(), ) diff --git a/examples/logic_sig_contract_account.rs b/examples/logic_sig_contract_account.rs index 4e9c7fbc..30a9982f 100644 --- a/examples/logic_sig_contract_account.rs +++ b/examples/logic_sig_contract_account.rs @@ -41,7 +41,7 @@ byte 0xFF ) .build()?; - let signed_t = contract_account.sign(&t, vec![vec![1, 0], vec![255]])?; + let signed_t = contract_account.sign(t, vec![vec![1, 0], vec![255]])?; let send_response = algod.broadcast_signed_transaction(&signed_t).await; println!("response {:?}", send_response); diff --git a/src/atomic_transaction_composer/mod.rs b/src/atomic_transaction_composer/mod.rs new file mode 100644 index 00000000..563d2c49 --- /dev/null +++ b/src/atomic_transaction_composer/mod.rs @@ -0,0 +1,790 @@ +pub mod transaction_signer; + +use algonaut_abi::{ + abi_error::AbiError, + abi_interactions::{ + AbiArgType, AbiMethod, AbiReturnType, ReferenceArgType, TransactionArgType, + }, + abi_type::{AbiType, AbiValue}, + make_tuple_type, +}; +use algonaut_core::{Address, CompiledTeal, SuggestedTransactionParams}; +use algonaut_crypto::HashDigest; +use algonaut_model::algod::v2::PendingTransaction; +use algonaut_transaction::{ + api_model::to_tx_type_enum, + builder::TxnFee, + error::TransactionError, + transaction::{ApplicationCallOnComplete, ApplicationCallTransaction, StateSchema}, + tx_group::TxGroup, + SignedTransaction, Transaction, TransactionType, TxnBuilder, +}; +use data_encoding::BASE64; +use num_bigint::BigUint; +use num_traits::ToPrimitive; +use std::collections::HashMap; + +use crate::{ + algod::v2::Algod, error::ServiceError, util::wait_for_pending_tx::wait_for_pending_transaction, +}; + +use self::transaction_signer::TransactionSigner; + +/// 4-byte prefix for logged return values, from https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#standard-format +const ABI_RETURN_HASH: [u8; 4] = [0x15, 0x1f, 0x7c, 0x75]; + +/// The maximum size of an atomic transaction group. +const MAX_ATOMIC_GROUP_SIZE: usize = 16; + +// if the abi type argument number > 15, then the abi types after 14th should be wrapped in a tuple +const MAX_ABI_ARG_TYPE_LEN: usize = 15; + +const FOREIGN_OBJ_ABI_UINT_SIZE: usize = 8; + +/// Represents an unsigned transactions and a signer that can authorize that transaction. +#[derive(Debug, Clone)] +pub struct TransactionWithSigner { + /// An unsigned transaction + pub tx: Transaction, + /// A transaction signer that can authorize the transaction + pub signer: TransactionSigner, +} + +/// Represents the output from a successful ABI method call. +#[derive(Debug, Clone)] +pub struct AbiMethodResult { + /// The TxID of the transaction that invoked the ABI method call. + pub tx_id: String, + /// Information about the confirmed transaction that invoked the ABI method call. + pub tx_info: PendingTransaction, + /// The method's return value + pub return_value: Result, +} + +#[derive(Debug, Clone)] +pub struct AbiReturnDecodeError(pub String); + +#[derive(Debug, Clone)] +pub enum AbiMethodReturnValue { + Some(AbiValue), + Void, +} + +/// Contains the parameters for the method AtomicTransactionComposer.AddMethodCall +pub struct AddMethodCallParams { + /// The ID of the smart contract to call. Set this to 0 to indicate an application creation call. + pub app_id: u64, + /// The method to call on the smart contract + pub method: AbiMethod, + /// The arguments to include in the method call. If omitted, no arguments will be passed to the method. + pub method_args: Vec, + /// Fee + pub fee: TxnFee, + /// The address of the sender of this application call + pub sender: Address, + /// Transactions params to use for this application call + pub suggested_params: SuggestedTransactionParams, + /// The OnComplete action to take for this application call + pub on_complete: ApplicationCallOnComplete, + /// The approval program for this application call. Only set this if this is an application + /// creation call, or if onComplete is UpdateApplicationOC. + pub approval_program: Option, + /// The clear program for this application call. Only set this if this is an application creation + /// call, or if onComplete is UpdateApplicationOC. + pub clear_program: Option, + /// The global schema sizes. Only set this if this is an application creation call. + pub global_schema: Option, + /// The local schema sizes. Only set this if this is an application creation call. + pub local_schema: Option, + /// The number of extra pages to allocate for the application's programs. Only set this if this + /// is an application creation call. + pub extra_pages: u32, + /// The note value for this application call + pub note: Option>, + /// The lease value for this application call + pub lease: Option, + /// If provided, the address that the sender will be rekeyed to at the conclusion of this application call + pub rekey_to: Option
, + /// A transaction Signer that can authorize this application call from sender + pub signer: TransactionSigner, +} + +#[derive(Debug, Clone)] +/// ExecuteResult contains the results of successfully calling the Execute method on an +/// AtomicTransactionComposer object. +pub struct ExecuteResult { + /// The round in which the executed transaction group was confirmed on chain + /// (optional, because the transaction's confirmed round is optional). + pub confirmed_round: Option, + /// A list of the TxIDs for each transaction in the executed group + pub tx_ids: Vec, + /// Return values for all the ABI method calls in the executed group + pub method_results: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum AtomicTransactionComposerStatus { + /// The atomic group is still under construction. + Building, + /// The atomic group has been finalized, but not yet signed. + Built, + /// The atomic group has been finalized and signed, but not yet submitted to the network. + Signed, + /// The atomic group has been finalized, signed, and submitted to the network. + Submitted, + /// The atomic group has been finalized, signed, submitted, and successfully committed to a block. + Committed, +} + +/// Helper used to construct and execute atomic transaction groups +#[derive(Debug)] +pub struct AtomicTransactionComposer { + /// The current status of the composer. The status increases monotonically. + status: AtomicTransactionComposerStatus, + + /// The transaction contexts in the group with their respective signers. + /// If status is greater than BUILDING then this slice cannot change. + method_map: HashMap, + + txs: Vec, + + signed_txs: Vec, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone)] +pub enum AbiArgValue { + TxWithSigner(TransactionWithSigner), + AbiValue(AbiValue), +} + +impl AbiArgValue { + fn address(&self) -> Option
{ + match self { + AbiArgValue::AbiValue(AbiValue::Address(address)) => Some(*address), + _ => None, + } + } + + fn int(&self) -> Option { + match self { + AbiArgValue::AbiValue(AbiValue::Int(int)) => Some(int.clone()), + _ => None, + } + } +} + +impl Default for AtomicTransactionComposer { + fn default() -> Self { + AtomicTransactionComposer { + status: AtomicTransactionComposerStatus::Building, + method_map: HashMap::new(), + txs: vec![], + signed_txs: vec![], + } + } +} + +impl AtomicTransactionComposer { + /// Returns the number of transactions currently in this atomic group. + pub fn len(&self) -> usize { + self.txs.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn status(&self) -> AtomicTransactionComposerStatus { + self.status + } + + /// Creates a new composer with the same underlying transactions. + /// The new composer's status will be BUILDING, so additional transactions may be added to it. + /// This probably can be named better, as it's not strictly a clone - for now keeping it like in the official SDKs. + pub fn clone_composer(&self) -> AtomicTransactionComposer { + let mut cloned = AtomicTransactionComposer { + status: AtomicTransactionComposerStatus::Building, + method_map: self.method_map.clone(), + txs: vec![], + signed_txs: vec![], + }; + + for tx_with_signer in &self.txs { + let mut tx = tx_with_signer.tx.clone(); + tx.group = None; + let new_tx_with_signer = TransactionWithSigner { + tx, + signer: tx_with_signer.signer.clone(), + }; + cloned.txs.push(new_tx_with_signer); + } + + cloned + } + + /// Adds a transaction to this atomic group. + /// + /// An error will be thrown if the composer's status is not Building, + /// or if adding this transaction causes the current group to exceed MaxAtomicGroupSize. + pub fn add_transaction( + &mut self, + txn_with_signer: TransactionWithSigner, + ) -> Result<(), ServiceError> { + if self.status != AtomicTransactionComposerStatus::Building { + return Err(ServiceError::Msg( + "status must be BUILDING in order to add transactions".to_owned(), + )); + } + + if self.len() == MAX_ATOMIC_GROUP_SIZE { + return Err(ServiceError::Msg(format!( + "reached max group size: {MAX_ATOMIC_GROUP_SIZE}" + ))); + } + + validate_tx(&txn_with_signer.tx, TransactionArgType::Any)?; + + self.txs.push(txn_with_signer); + + Ok(()) + } + + pub fn add_method_call( + &mut self, + params: &mut AddMethodCallParams, + ) -> Result<(), ServiceError> { + if self.status != AtomicTransactionComposerStatus::Building { + return Err(ServiceError::Msg( + "status must be BUILDING in order to add transactions".to_owned(), + )); + } + if params.method_args.len() != params.method.args.len() { + return Err(ServiceError::Msg(format!( + "incorrect number of arguments were provided: {} != {}", + params.method_args.len(), + params.method.args.len() + ))); + } + if self.len() + params.method.get_tx_count() > MAX_ATOMIC_GROUP_SIZE { + return Err(ServiceError::Msg(format!( + "reached max group size: {MAX_ATOMIC_GROUP_SIZE}" + ))); + } + + let mut method_types = vec![]; + let mut method_args: Vec = vec![]; + let mut txs_with_signer = vec![]; + let mut foreign_accounts = vec![]; + let mut foreign_assets = vec![]; + let mut foreign_apps = vec![]; + + for i in 0..params.method.args.len() { + let mut arg_type = params.method.args[i].clone(); + let arg_value = ¶ms.method_args[i]; + + match arg_type.type_()? { + AbiArgType::Tx(type_) => { + add_tx_arg_type_to_method_call(arg_value, type_, &mut txs_with_signer)? + } + AbiArgType::Ref(type_) => add_ref_arg_to_method_call( + &type_, + arg_value, + &mut foreign_accounts, + &mut foreign_assets, + &mut foreign_apps, + &mut method_types, + &mut method_args, + params.sender, + params.app_id, + )?, + AbiArgType::AbiObj(type_) => { + add_abi_obj_arg_to_method_call( + &type_, + arg_value, + &mut method_types, + &mut method_args, + )?; + } + }; + } + + if method_args.len() > MAX_ABI_ARG_TYPE_LEN { + let (type_, value) = wrap_overflowing_values(&method_types, &method_args)?; + method_types.push(type_); + method_args.push(value); + } + + let mut encoded_abi_args = vec![params.method.get_selector()?.into()]; + for i in 0..method_args.len() { + encoded_abi_args.push(method_types[i].encode(method_args[i].clone())?); + } + + let app_call = TransactionType::ApplicationCallTransaction(ApplicationCallTransaction { + sender: params.sender, + app_id: Some(params.app_id), + on_complete: params.on_complete.clone(), + accounts: Some(foreign_accounts), + approval_program: params.approval_program.clone(), + app_arguments: Some(encoded_abi_args), + clear_state_program: params.clear_program.clone(), + foreign_apps: Some(foreign_apps), + foreign_assets: Some(foreign_assets), + global_state_schema: params.global_schema.clone(), + local_state_schema: params.local_schema.clone(), + extra_pages: params.extra_pages, + }); + + let mut tx_builder = + TxnBuilder::with_fee(¶ms.suggested_params, params.fee.clone(), app_call); + if let Some(rekey_to) = params.rekey_to { + tx_builder = tx_builder.rekey_to(rekey_to); + } + if let Some(lease) = params.lease { + tx_builder = tx_builder.lease(lease); + } + if let Some(note) = params.note.clone() { + tx_builder = tx_builder.note(note); + } + + let tx = tx_builder.build()?; + + self.txs.append(&mut txs_with_signer); + self.txs.push(TransactionWithSigner { + tx, + signer: params.signer.clone(), + }); + self.method_map + .insert(self.txs.len() - 1, params.method.clone()); + + Ok(()) + } + + /// Finalize the transaction group and returned the finalized transactions. + /// The composer's status will be at least BUILT after executing this method. + pub fn build_group(&mut self) -> Result, ServiceError> { + if self.status >= AtomicTransactionComposerStatus::Built { + return Ok(self.txs.clone()); + } + + if self.txs.is_empty() { + return Err(ServiceError::Msg( + "should not build transaction group with 0 transactions in composer".to_owned(), + )); + } else if self.txs.len() > 1 { + let mut group_txs = vec![]; + for tx in self.txs.iter_mut() { + group_txs.push(&mut tx.tx); + } + TxGroup::assign_group_id(&mut group_txs)?; + } + + self.status = AtomicTransactionComposerStatus::Built; + Ok(self.txs.clone()) + } + + pub fn gather_signatures(&mut self) -> Result, ServiceError> { + if self.status >= AtomicTransactionComposerStatus::Signed { + return Ok(self.signed_txs.clone()); + } + + let tx_and_signers = self.build_group()?; + + let txs: Vec = self.txs.clone().into_iter().map(|t| t.tx).collect(); + + let mut visited = vec![false; txs.len()]; + let mut signed_txs = vec![]; + + for (i, tx_with_signer) in tx_and_signers.iter().enumerate() { + if visited[i] { + continue; + } + + let mut indices_to_sign = vec![]; + + for (j, other) in tx_and_signers.iter().enumerate() { + if !visited[j] && tx_with_signer.signer == other.signer { + indices_to_sign.push(j); + visited[j] = true; + } + } + + if indices_to_sign.is_empty() { + return Err(ServiceError::Msg( + "invalid tx signer provided, isn't equal to self".to_owned(), + )); + } + + let filtered_tx_group = indices_to_sign + .into_iter() + .map(|i| txs[i].clone()) + .collect(); + signed_txs = tx_with_signer.signer.sign_transactions(filtered_tx_group)?; + } + + self.signed_txs = signed_txs.clone(); + + self.status = AtomicTransactionComposerStatus::Signed; + + Ok(signed_txs) + } + + fn get_txs_ids(&self) -> Vec { + self.signed_txs + .iter() + .map(|t| t.transaction_id.clone()) + .collect() + } + + pub async fn submit(&mut self, algod: &Algod) -> Result, ServiceError> { + if self.status >= AtomicTransactionComposerStatus::Submitted { + return Err(ServiceError::Msg( + "Atomic Transaction Composer cannot submit committed transaction".to_owned(), + )); + } + + self.gather_signatures()?; + + algod + .broadcast_signed_transactions(&self.signed_txs) + .await?; + + self.status = AtomicTransactionComposerStatus::Submitted; + + Ok(self.get_txs_ids()) + } + + pub async fn execute(&mut self, algod: &Algod) -> Result { + if self.status >= AtomicTransactionComposerStatus::Committed { + return Err(ServiceError::Msg("status is already committed".to_owned())); + } + + self.submit(algod).await?; + + let mut index_to_wait = 0; + for i in 0..self.signed_txs.len() { + if self.method_map.contains_key(&i) { + index_to_wait = i; + break; + } + } + + let tx_id = &self.signed_txs[index_to_wait].transaction_id; + let pending_tx = wait_for_pending_transaction(algod, tx_id).await?; + + let mut return_list: Vec = vec![]; + + self.status = AtomicTransactionComposerStatus::Committed; + + for i in 0..self.txs.len() { + if !self.method_map.contains_key(&i) { + continue; + } + + let mut current_tx_id = tx_id.clone(); // this variable wouldn't be needed if our txn in PendingTransaction was complete / able to generate an id + let mut current_pending_tx = pending_tx.clone(); + + if i != index_to_wait { + let tx_id = self.signed_txs[i].transaction_id.clone(); + + match algod.pending_transaction_with_id(&tx_id).await { + Ok(p) => { + current_tx_id = tx_id; + current_pending_tx = p; + } + Err(e) => { + return_list.push(AbiMethodResult { + tx_id, + tx_info: pending_tx.clone(), + return_value: Err(AbiReturnDecodeError(format!("{e:?}"))), + }); + continue; + } + }; + } + + let return_type = self.method_map[&i].returns.clone().type_()?; + return_list.push(get_return_value_with_return_type( + ¤t_pending_tx, + ¤t_tx_id, + return_type, + )?); + } + + Ok(ExecuteResult { + confirmed_round: pending_tx.confirmed_round, + tx_ids: self.get_txs_ids(), + method_results: return_list, + }) + } +} + +fn get_return_value_with_return_type( + pending_tx: &PendingTransaction, + tx_id: &str, // our txn in PendingTransaction currently has no fields, so the tx id is passed separately + return_type: AbiReturnType, +) -> Result { + let return_value = match return_type { + AbiReturnType::Some(return_type) => { + get_return_value_with_abi_type(pending_tx, &return_type)? + } + AbiReturnType::Void => Ok(AbiMethodReturnValue::Void), + }; + + Ok(AbiMethodResult { + tx_id: tx_id.to_owned(), + tx_info: pending_tx.clone(), + return_value, + }) +} + +impl From for ServiceError { + fn from(e: TransactionError) -> Self { + Self::Msg(format!("{e:?}")) + } +} + +impl From for ServiceError { + fn from(e: AbiError) -> Self { + match e { + AbiError::Msg(msg) => Self::Msg(msg), + } + } +} + +fn validate_tx(tx: &Transaction, expected_type: TransactionArgType) -> Result<(), ServiceError> { + if tx.group.is_some() { + return Err(ServiceError::Msg("Expected empty group id".to_owned())); + } + + if expected_type != TransactionArgType::Any + && expected_type != TransactionArgType::One(to_tx_type_enum(&tx.txn_type)) + { + return Err(ServiceError::Msg(format!( + "expected transaction with type {expected_type:?}, but got type {:?}", + tx.txn_type + ))); + } + + Ok(()) +} + +fn add_tx_arg_type_to_method_call( + arg_value: &AbiArgValue, + expected_type: TransactionArgType, + txs_with_signer: &mut Vec, +) -> Result<(), ServiceError> { + let txn_and_signer = match arg_value { + AbiArgValue::TxWithSigner(tx_with_signer) => tx_with_signer, + _ => { + return Err(ServiceError::Msg( + "invalid arg value, expected transaction".to_owned(), + )); + } + }; + + validate_tx(&txn_and_signer.tx, expected_type)?; + txs_with_signer.push(txn_and_signer.to_owned()); + + Ok(()) +} + +fn add_abi_obj_arg_to_method_call( + abi_type: &AbiType, + arg_value: &AbiArgValue, + method_types: &mut Vec, + method_args: &mut Vec, +) -> Result<(), ServiceError> { + match arg_value { + AbiArgValue::AbiValue(value) => { + method_types.push(abi_type.clone()); + method_args.push(value.clone()); + } + AbiArgValue::TxWithSigner(_) => { + return Err(ServiceError::Msg( + "Invalid state: shouldn't be here with a tx with signer value type".to_owned(), + )); + } + } + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn add_ref_arg_to_method_call( + arg_type: &ReferenceArgType, + arg_value: &AbiArgValue, + + foreign_accounts: &mut Vec
, + foreign_assets: &mut Vec, + foreign_apps: &mut Vec, + + method_types: &mut Vec, + method_args: &mut Vec, + + sender: Address, + app_id: u64, +) -> Result<(), ServiceError> { + let index = add_to_foreign_array( + arg_type, + arg_value, + foreign_accounts, + foreign_assets, + foreign_apps, + sender, + app_id, + )?; + + method_types.push(AbiType::uint(FOREIGN_OBJ_ABI_UINT_SIZE)?); + method_args.push(AbiValue::Int(index.into())); + + Ok(()) +} + +/// Adds arg value to its respective foreign array +/// Returns index that can be used to reference `arg_value` in its foreign array (in TEAL). +fn add_to_foreign_array( + arg_type: &ReferenceArgType, + arg_value: &AbiArgValue, + foreign_accounts: &mut Vec
, + foreign_assets: &mut Vec, + foreign_apps: &mut Vec, + sender: Address, + app_id: u64, +) -> Result { + match arg_type { + ReferenceArgType::Account => match arg_value.address() { + Some(address) => Ok(populate_foreign_array( + address, + foreign_accounts, + Some(sender), + )), + _ => { + return Err(ServiceError::Msg(format!( + "Invalid value type: {arg_value:?} for arg type: {arg_type:?}" + ))); + } + }, + ReferenceArgType::Asset => match arg_value.int() { + Some(int) => { + let intu64 = int.to_u64().ok_or_else(|| { + AbiError::Msg(format!("big int: {int} couldn't be converted to u64")) + })?; + + Ok(populate_foreign_array(intu64, foreign_assets, None)) + } + _ => { + return Err(ServiceError::Msg(format!( + "Invalid value type: {arg_value:?} for arg type: {arg_type:?}" + ))); + } + }, + ReferenceArgType::Application => match arg_value.int() { + Some(int) => { + let intu64 = int.to_u64().ok_or_else(|| { + AbiError::Msg(format!("big int: {int} couldn't be converted to u64")) + })?; + + Ok(populate_foreign_array(intu64, foreign_apps, Some(app_id))) + } + _ => { + return Err(ServiceError::Msg(format!( + "Invalid value type: {arg_value:?} for arg type: {arg_type:?}" + ))); + } + }, + } +} + +fn wrap_overflowing_values( + method_types: &[AbiType], + method_args: &[AbiValue], +) -> Result<(AbiType, AbiValue), ServiceError> { + let mut wrapped_abi_types = vec![]; + let mut wrapped_value_list = vec![]; + + for i in (MAX_ABI_ARG_TYPE_LEN - 1)..method_args.len() { + wrapped_abi_types.push(method_types[i].clone()); + wrapped_value_list.push(method_args[i].clone()); + } + + let tuple_type = make_tuple_type(&wrapped_abi_types)?; + + Ok((tuple_type, AbiValue::Array(wrapped_value_list))) +} + +/// Add a value to an application call's foreign array. The addition will be as compact as possible, +/// and this function will return an index that can be used to reference `object_to_add` in `obj_array`. +/// +/// # Arguments +/// +/// * `obj_to_add` - The value to add to the array. If this value is already present in the array, +/// it will not be added again. Instead, the existing index will be returned. +/// * `obj_array` - The existing foreign array. This input may be modified to append `obj_to_add`. +/// * `zeroth_obj` - If provided, this value indicated two things: the 0 value is special for this +/// array, so all indexes into `obj_array` must start at 1; additionally, if `obj_to_add` equals +/// `zeroth_obj`, then `obj_to_add` will not be added to the array, and instead the 0 indexes will be returned. +/// +/// Returns an index that can be used to reference `obj_to_add` in `obj_array`. +fn populate_foreign_array( + obj_to_add: T, + obj_array: &mut Vec, + zeroth_obj: Option, +) -> usize { + if let Some(o) = &zeroth_obj { + if &obj_to_add == o { + return 0; + } + } + + let start_from: usize = zeroth_obj.map(|_| 1).unwrap_or(0); + let search_in_vec_index = obj_array.iter().position(|o| o == &obj_to_add); + if let Some(index) = search_in_vec_index { + start_from + index + } else { + obj_array.push(obj_to_add); + obj_array.len() - 1 + start_from + } +} + +fn get_return_value_with_abi_type( + pending_tx: &PendingTransaction, + abi_type: &AbiType, +) -> Result, ServiceError> { + if pending_tx.logs.is_empty() { + return Err(ServiceError::Msg( + "App call transaction did not log a return value".to_owned(), + )); + } + + let ret_line = &pending_tx.logs[pending_tx.logs.len() - 1]; + + let decoded_ret_line: Vec = BASE64 + .decode(ret_line.as_bytes()) + .map_err(|e| ServiceError::Msg(format!("BASE64 Decoding error: {e:?}")))?; + + if !check_log_ret(&decoded_ret_line) { + return Err(ServiceError::Msg( + "App call transaction did not log a return value(2)".to_owned(), + )); + } + + let abi_encoded = &decoded_ret_line[ABI_RETURN_HASH.len()..decoded_ret_line.len()]; + Ok(match abi_type.decode(abi_encoded) { + Ok(decoded) => Ok(AbiMethodReturnValue::Some(decoded)), + Err(e) => Err(AbiReturnDecodeError(format!("{e:?}"))), + }) +} + +fn check_log_ret(log_line: &[u8]) -> bool { + let abi_return_hash_len = ABI_RETURN_HASH.len(); + if log_line.len() < abi_return_hash_len { + return false; + } + for i in 0..abi_return_hash_len { + if log_line[i] != ABI_RETURN_HASH[i] { + return false; + } + } + true +} diff --git a/src/atomic_transaction_composer/transaction_signer.rs b/src/atomic_transaction_composer/transaction_signer.rs new file mode 100644 index 00000000..fdfd8dad --- /dev/null +++ b/src/atomic_transaction_composer/transaction_signer.rs @@ -0,0 +1,71 @@ +use algonaut_core::MultisigAddress; +use algonaut_transaction::{ + account::Account, contract_account::ContractAccount, error::TransactionError, + transaction::TransactionSignature, SignedTransaction, Transaction, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TransactionSigner { + BasicAccount(Account), + ContractAccount(ContractAccount), + MultisigAccount { + address: MultisigAddress, + accounts: Vec, + }, +} + +impl TransactionSigner { + pub fn sign_transactions( + &self, + tx_group: Vec, + ) -> Result, TransactionError> { + match self { + TransactionSigner::BasicAccount(account) => { + let mut signed_txs = vec![]; + for tx in tx_group { + signed_txs.push(account.sign_transaction(tx)?); + } + Ok(signed_txs) + } + + TransactionSigner::ContractAccount(account) => { + let mut signed_txs = vec![]; + for tx in tx_group { + signed_txs.push(account.sign(tx, vec![])?); + } + Ok(signed_txs) + } + + TransactionSigner::MultisigAccount { address, accounts } => { + let mut signed_txs = vec![]; + for tx in tx_group { + signed_txs.push(sign_msig_tx(address, accounts, tx)?); + } + Ok(signed_txs) + } + } + } +} + +fn sign_msig_tx( + address: &MultisigAddress, + accounts: &[Account], + tx: Transaction, +) -> Result { + if let Some(first_account) = accounts.first() { + let mut msig = first_account.init_transaction_msig(&tx, address)?; + for account in &accounts[1..accounts.len()] { + msig = account.append_to_transaction_msig(&tx, msig)?; + } + + let signed_t = SignedTransaction { + transaction_id: tx.id()?, + transaction: tx, + sig: TransactionSignature::Multi(msig), + }; + + Ok(signed_t) + } else { + Err(TransactionError::NoAccountsToSign) + } +} diff --git a/src/error.rs b/src/error.rs index 8dc34015..43947a86 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,7 +21,12 @@ pub enum ServiceError { /// HTTP calls errors #[error("http error: {0}")] Request(RequestError), - /// Internal errors (please open an [issue](https://github.com/manuelmauro/algonaut/issues)!) + + /// General text-only errors. Dedicated error variants can be created, if needed. + #[error("Msg: {0}")] + Msg(String), + /// Clearly SDK caused errors (please open an [issue](https://github.com/manuelmauro/algonaut/issues)!) + /// TODO rename in unexpected #[error("Internal error: {0}")] Internal(String), } diff --git a/src/lib.rs b/src/lib.rs index 3d09ee69..a11c4d94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub use algonaut_model as model; pub use algonaut_transaction as transaction; pub mod algod; +pub mod atomic_transaction_composer; pub mod error; pub mod indexer; pub mod kmd; +mod util; diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..81c879d4 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod wait_for_pending_tx; diff --git a/src/util/wait_for_pending_tx.rs b/src/util/wait_for_pending_tx.rs new file mode 100644 index 00000000..9baf9ed3 --- /dev/null +++ b/src/util/wait_for_pending_tx.rs @@ -0,0 +1,34 @@ +use crate::{algod::v2::Algod, error::ServiceError, model::algod::v2::PendingTransaction}; +use instant::Instant; +use std::time::Duration; + +/// Utility to wait for a transaction to be confirmed +pub async fn wait_for_pending_transaction( + algod: &Algod, + tx_id: &str, +) -> Result { + let timeout = Duration::from_secs(60); + let start = Instant::now(); + loop { + let pending_transaction = algod.pending_transaction_with_id(tx_id).await?; + // If the transaction has been confirmed or we time out, exit. + if pending_transaction.confirmed_round.is_some() { + return Ok(pending_transaction); + } else if start.elapsed() >= timeout { + return Err(ServiceError::Msg(format!( + "Pending transaction timed out ({timeout:?})" + ))); + } + sleep(250).await; + } +} + +#[cfg(target_arch = "wasm32")] +pub async fn sleep(ms: u32) { + gloo_timers::future::TimeoutFuture::new(ms).await; +} + +#[cfg(not(target_arch = "wasm32"))] +pub async fn sleep(ms: u32) { + futures_timer::Delay::new(std::time::Duration::from_millis(ms as u64)).await; +} diff --git a/tests/docker/run_docker.sh b/tests/docker/run_docker.sh index c5d69e8c..0c27a328 100755 --- a/tests/docker/run_docker.sh +++ b/tests/docker/run_docker.sh @@ -5,11 +5,7 @@ set -e rm -rf test-harness rm -rf tests/features -# fork with modified features, as cucumber-rs doesn't understand some syntax: -# https://github.com/cucumber-rs/cucumber/issues/174 -# https://github.com/cucumber-rs/cucumber/issues/175 -# git clone --single-branch --branch master https://github.com/algorand/algorand-sdk-testing.git test-harness -git clone --single-branch --branch master https://github.com/ivanschuetz/algorand-sdk-testing.git test-harness +git clone --single-branch --branch master https://github.com/algorand/algorand-sdk-testing.git test-harness # copy feature files into project mv test-harness/features tests/features diff --git a/tests/features_runner.rs b/tests/features_runner.rs index 1770e71c..2c8e2a6b 100644 --- a/tests/features_runner.rs +++ b/tests/features_runner.rs @@ -9,17 +9,28 @@ async fn main() { // features which depend completely on algod v1 are omitted // algod feature: omitted (algod v1) + // assets feature: omitted (algod v1) - // TODO abi feature: ABI not supported yet + // TODO use tags - so we don't have to create a new config per file (until the tests are complete) - integration::applications::World::cucumber() + integration::world::World::cucumber() .max_concurrent_scenarios(1) - .run(integration_path("applications")) + // show output (e.g. println! or dbg!) in terminal https://cucumber-rs.github.io/cucumber/current/output/terminal.html#manual-printing + // .with_writer( + // writer::Basic::raw(io::stdout(), writer::Coloring::Auto, 0) + // .summarized() + // .assert_normalized(), + // ) + .run("tests/features/integration/applications.feature") .await; - // assets feature: omitted (algod v1) -} + integration::world::World::cucumber() + .max_concurrent_scenarios(1) + .run("tests/features/integration/abi.feature") + .await; -fn integration_path(feature_name: &str) -> String { - format!("tests/features/integration/{}.feature", feature_name) + integration::world::World::cucumber() + .max_concurrent_scenarios(1) + .run("tests/features/integration/c2c.feature") + .await; } diff --git a/tests/step_defs/integration/abi.rs b/tests/step_defs/integration/abi.rs new file mode 100644 index 00000000..6e325e78 --- /dev/null +++ b/tests/step_defs/integration/abi.rs @@ -0,0 +1,831 @@ +use crate::step_defs::{ + integration::world::World, + util::{read_teal, wait_for_pending_transaction}, +}; +use algonaut::{ + atomic_transaction_composer::{ + transaction_signer::TransactionSigner, AbiArgValue, AbiMethodReturnValue, + AbiReturnDecodeError, AddMethodCallParams, AtomicTransactionComposer, + AtomicTransactionComposerStatus, TransactionWithSigner, + }, + error::ServiceError, +}; +use algonaut_abi::{ + abi_interactions::{AbiArgType, AbiMethod, AbiReturn, AbiReturnType, ReferenceArgType}, + abi_type::{AbiType, AbiValue}, +}; +use algonaut_core::{to_app_address, Address, MicroAlgos}; +use algonaut_model::algod::v2::PendingTransaction; +use algonaut_transaction::{ + builder::TxnFee, + transaction::{ApplicationCallOnComplete, StateSchema}, + Pay, TxnBuilder, +}; +use cucumber::{codegen::Regex, given, then, when}; +use data_encoding::BASE64; +use num_traits::ToPrimitive; +use sha2::Digest; +use std::convert::TryInto; +use std::error::Error; + +#[given(regex = r#"^I make a transaction signer for the ([^"]*) account\.$"#)] +#[when(regex = r#"^I make a transaction signer for the ([^"]*) account\.$"#)] +async fn i_make_a_transaction_signer_for_the_account(w: &mut World, account_str: String) { + let signer = TransactionSigner::BasicAccount(match account_str.as_ref() { + "transient" => w.transient_account.clone().unwrap(), + _ => panic!("Not handled account string: {}", account_str), + }); + + w.tx_signer = Some(signer); +} + +#[given(expr = "a new AtomicTransactionComposer")] +async fn a_new_atomic_transaction_composer(w: &mut World) { + w.tx_composer = Some(AtomicTransactionComposer::default()); + w.tx_composer_methods = Some(vec![]); +} + +#[when( + regex = r#"^I build a payment transaction with sender "([^"]*)", receiver "([^"]*)", amount (\d+), close remainder to "([^"]*)"$"# +)] +#[given( + regex = r#"^I build a payment transaction with sender "([^"]*)", receiver "([^"]*)", amount (\d+), close remainder to "([^"]*)"$"# +)] +async fn i_build_a_payment_transaction_with_sender_receiver_amount_close_remainder_to( + w: &mut World, + sender_str: String, + receiver_str: String, + amount: u64, + close_to: String, +) { + let transient_account = w.transient_account.clone().unwrap(); + let tx_params = w.tx_params.as_ref().unwrap(); + + let close_to = if close_to == "" { + None + } else { + Some(close_to.parse::
().unwrap()) + }; + + let sender = if sender_str == "transient" { + transient_account.address() + } else { + panic!("sender_str not supported: {}", sender_str); + }; + + let receiver = if receiver_str == "transient" { + transient_account.address() + } else { + panic!("receiver_str not supported: {}", receiver_str); + }; + + let mut payment = Pay::new(sender, receiver, MicroAlgos(amount)); + if let Some(close_to) = close_to { + payment = payment.close_remainder_to(close_to); + } + + let tx = TxnBuilder::with(tx_params, payment.build()) + .build() + .unwrap(); + + w.tx = Some(tx); +} + +#[when(expr = "I create a transaction with signer with the current transaction.")] +#[given(expr = "I create a transaction with signer with the current transaction.")] +async fn i_create_a_transaction_with_signer_with_the_current_transaction(w: &mut World) { + let tx = w.tx.clone().unwrap(); + let signer = w.tx_signer.clone().unwrap(); + + w.tx_with_signer = Some(TransactionWithSigner { tx, signer }); +} + +#[when(expr = "I add the current transaction with signer to the composer.")] +async fn i_add_the_current_transaction_with_signer_tothecomposer(w: &mut World) { + let tx_with_signer = w.tx_with_signer.clone().unwrap(); + let tx_composer = w.tx_composer.as_mut().unwrap(); + + tx_composer.add_transaction(tx_with_signer).unwrap(); +} + +#[then(expr = "I gather signatures with the composer.")] +async fn i_gather_signatures_with_the_composer(w: &mut World) { + let tx_composer = w.tx_composer.as_mut().unwrap(); + + w.signed_txs = Some(tx_composer.gather_signatures().unwrap()); +} + +#[then(regex = r#"^The composer should have a status of "([^"]*)"\.$"#)] +async fn the_composer_should_have_a_status_of(w: &mut World, status_str: String) { + let tx_composer = w.tx_composer.as_mut().unwrap(); + + let status = match status_str.as_ref() { + "BUILDING" => AtomicTransactionComposerStatus::Building, + "BUILT" => AtomicTransactionComposerStatus::Built, + "SIGNED" => AtomicTransactionComposerStatus::Signed, + "SUBMITTED" => AtomicTransactionComposerStatus::Submitted, + "COMMITTED" => AtomicTransactionComposerStatus::Committed, + _ => panic!("Not handled status string: {}", status_str), + }; + + if status != tx_composer.status() { + panic!("status doesn't match"); + } +} + +#[then(expr = "I clone the composer.")] +async fn i_clone_the_composer(w: &mut World) { + let tx_composer = w.tx_composer.as_mut().unwrap(); + + w.tx_composer = Some(tx_composer.clone_composer()); +} + +#[when(regex = r#"I create the Method object from method signature "([^"]*)"$"#)] +#[given(regex = r#"I create the Method object from method signature "([^"]*)"$"#)] +async fn create_method_object_from_signature(w: &mut World, method_sig: String) { + let abi_method = AbiMethod::from_signature(&method_sig).unwrap(); + w.abi_method = Some(abi_method); +} + +#[given(expr = "I create a new method arguments array.")] +#[when(expr = "I create a new method arguments array.")] +async fn i_create_a_new_method_arguments_array(w: &mut World) { + let abi_method = w.abi_method.as_ref().unwrap(); + let mut arg_types = vec![]; + for mut arg_type in abi_method.args.clone() { + match arg_type.type_().expect("no type") { + AbiArgType::Tx(_) => continue, + AbiArgType::Ref(rf) => match rf { + ReferenceArgType::Account => arg_types.push(AbiType::address()), + _ => arg_types.push(AbiType::uint(64).expect("couldn't create int type")), + }, + AbiArgType::AbiObj(obj) => { + arg_types.push(obj); + } + } + } + w.abi_method_arg_types = Some(arg_types); + // w.abi_method_args = Some(arg_types); + w.abi_method_arg_values = Some(vec![]); +} + +#[given(regex = r#"I append the encoded arguments "([^"]*)" to the method arguments array.$"#)] +#[when(regex = r#"I append the encoded arguments "([^"]*)" to the method arguments array.$"#)] +async fn i_append_the_encoded_arguments_to_the_method_arguments_array( + w: &mut World, + comma_separated_b64_args: String, +) -> Result<(), Box> { + let application_ids: &[u64] = w.app_ids.as_ref(); + let method_args = w.abi_method_arg_values.as_mut().expect("no method args"); + + let abi_method_arg_types = w + .abi_method_arg_types + .as_ref() + .expect("No method arg types"); + + if comma_separated_b64_args.is_empty() { + return Ok(()); + } + + let b64_args = comma_separated_b64_args.split(','); + for (arg_index, b64_arg) in b64_args.into_iter().enumerate() { + if b64_arg.contains(':') { + let parts: Vec<&str> = b64_arg.split(':').collect(); + if parts.len() != 2 || parts[0] != "ctxAppIdx" { + panic!("Cannot process argument: {}", b64_arg); + } + let parsed_index = parts[1].parse::().unwrap(); + if parsed_index >= application_ids.len() { + panic!( + "Application index out of bounds: {}, number of app IDs is {}", + parsed_index, + application_ids.len() + ); + } + + let arg = AbiValue::Int(application_ids[parsed_index].into()); + method_args.push(AbiArgValue::AbiValue(arg)); + } else { + let base64_decoded_arg = BASE64.decode(b64_arg.as_bytes()).unwrap(); + + let decoded = abi_method_arg_types[arg_index].decode(&base64_decoded_arg)?; + method_args.push(AbiArgValue::AbiValue(decoded)); + } + } + Ok(()) +} + +#[when( + regex = r#"^I add a method call with the ([^"]*) account, the current application, suggested params, on complete "([^"]*)", current transaction signer, current method arguments.$"# +)] +#[given( + regex = r#"^I add a method call with the ([^"]*) account, the current application, suggested params, on complete "([^"]*)", current transaction signer, current method arguments.$"# +)] +async fn i_add_a_method_call(w: &mut World, account_type: String, on_complete: String) { + add_method_call( + w, + account_type, + on_complete, + None, + None, + None, + None, + None, + None, + None, + false, + ) + .await; +} + +#[when( + regex = r#"^I add a method call with the ([^"]*) account, the current application, suggested params, on complete "([^"]*)", current transaction signer, current method arguments, approval-program "([^"]*)", clear-program "([^"]*)"\.$"# +)] +async fn i_add_a_method_call_for_update( + w: &mut World, + account_type: String, + on_complete: String, + approval_program: String, + clear_program: String, +) { + add_method_call( + w, + account_type, + on_complete, + Some(approval_program), + Some(clear_program), + None, + None, + None, + None, + None, + false, + ) + .await; +} + +#[when( + regex = r#"^I add a method call with the ([^"]*) account, the current application, suggested params, on complete "([^"]*)", current transaction signer, current method arguments, approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), extra-pages (\d+)\.$"# +)] +async fn i_add_a_method_call_for_create( + w: &mut World, + account_type: String, + on_complete: String, + approval_program: String, + clear_program: String, + global_bytes: u64, + global_ints: u64, + local_bytes: u64, + local_ints: u64, + extra_pages: u32, +) { + add_method_call( + w, + account_type, + on_complete, + Some(approval_program), + Some(clear_program), + Some(global_bytes), + Some(global_ints), + Some(local_bytes), + Some(local_ints), + Some(extra_pages), + false, + ) + .await; +} + +#[given( + regex = r#"^I add a nonced method call with the ([^"]*) account, the current application, suggested params, on complete "([^"]*)", current transaction signer, current method arguments\.$"# +)] +async fn i_add_method_call_with_nonce(w: &mut World, account_type: String, on_complete: String) { + add_method_call( + w, + account_type, + on_complete, + None, + None, + None, + None, + None, + None, + None, + true, + ) + .await; +} + +async fn add_method_call( + w: &mut World, + account_type: String, + on_complete: String, + approval_program: Option, + clear_program: Option, + global_bytes: Option, + global_ints: Option, + local_bytes: Option, + local_ints: Option, + extra_pages: Option, + use_nonce: bool, +) { + let algod = w.algod.as_ref().unwrap(); + let transient_account = w.transient_account.clone().unwrap(); + let abi_method = w.abi_method.as_ref().unwrap(); + let abi_method_args = w.abi_method_arg_values.as_mut().unwrap(); + let application_id = w.app_id.clone().unwrap(); + let tx_params = w.tx_params.clone().unwrap(); + let tx_signer = w.tx_signer.clone().unwrap(); + let tx_composer = w.tx_composer.as_mut().unwrap(); + let tx_composer_methods = w.tx_composer_methods.as_mut().unwrap(); + + let extra_pages = extra_pages.unwrap_or(0); + + let global_schema = match (global_ints, global_bytes) { + (Some(ints), Some(bytes)) => Some(StateSchema { + number_ints: ints, + number_byteslices: bytes, + }), + _ => None, + }; + + let local_schema = match (local_ints, local_bytes) { + (Some(ints), Some(bytes)) => Some(StateSchema { + number_ints: ints, + number_byteslices: bytes, + }), + _ => None, + }; + + let on_complete = match on_complete.as_ref() { + "crate" | "noop" | "call" => ApplicationCallOnComplete::NoOp, + "update" => ApplicationCallOnComplete::UpdateApplication, + "optin" => ApplicationCallOnComplete::OptIn, + "clear" => ApplicationCallOnComplete::ClearState, + "closeout" => ApplicationCallOnComplete::CloseOut, + "delete" => ApplicationCallOnComplete::DeleteApplication, + _ => panic!("invalid onComplete value"), + }; + + let use_account = match account_type.as_ref() { + "transient" => transient_account, + _ => panic!("Not handled account string: {}", account_type), + }; + + let approval = match approval_program { + Some(p) => Some(read_teal(algod, &p).await), + None => None, + }; + + let clear = match clear_program { + Some(p) => Some(read_teal(algod, &p).await), + None => None, + }; + + // populate args from methodArgs + + if abi_method_args.len() != abi_method.args.len() { + panic!( + "Provided argument count is incorrect. Expected {}, got {}", + abi_method_args.len(), + abi_method.args.len() + ); + }; + + let note_opt = if use_nonce { + Some( + w.note + .clone() + .expect("note should be set if using use_nonce"), + ) + } else { + None + }; + + let mut params = AddMethodCallParams { + app_id: application_id, + method: abi_method.to_owned(), + method_args: abi_method_args.to_owned(), + fee: TxnFee::Estimated { + fee_per_byte: tx_params.fee_per_byte, + min_fee: tx_params.min_fee, + }, + sender: use_account.address(), + suggested_params: tx_params, + on_complete, + approval_program: approval, + clear_program: clear, + global_schema, + local_schema, + extra_pages, + note: note_opt, + lease: None, + rekey_to: None, + signer: tx_signer, + }; + + tx_composer_methods.push(abi_method.to_owned()); + + tx_composer.add_method_call(&mut params).unwrap(); +} + +#[given(regex = r#"I add the nonce "([^"]*)"$"#)] +fn i_add_the_nonce(w: &mut World, nonce: String) { + w.note = Some( + format!("I should be unique thanks to this nonce: {nonce}") + .as_bytes() + .to_vec(), + ); +} + +#[given(expr = "I append the current transaction with signer to the method arguments array.")] +#[when(expr = "I append the current transaction with signer to the method arguments array.")] +fn i_append_the_current_transaction_with_signer_to_the_method_arguments_array(w: &mut World) { + let method_args = w.abi_method_arg_values.as_mut().expect("no method args"); + let tx_with_signer = w.tx_with_signer.clone().expect("no tx signer"); + + method_args.push(AbiArgValue::TxWithSigner(tx_with_signer)); +} + +#[when( + regex = r#"^I build the transaction group with the composer. If there is an error it is "([^"]*)".$"# +)] +#[then( + regex = r#"^I build the transaction group with the composer. If there is an error it is "([^"]*)".$"# +)] +#[given( + regex = r#"^I build the transaction group with the composer. If there is an error it is "([^"]*)".$"# +)] +fn i_build_the_transaction_group_with_the_composer(w: &mut World, error_type: String) { + let tx_composer = w.tx_composer.as_mut().unwrap(); + + let build_res = tx_composer.build_group(); + + match error_type.as_ref() { + "" => { + // no error expected + build_res.unwrap(); + } + "zero group size error" => { + let message = match build_res { + Ok(_) => None, + Err(e) => match e { + ServiceError::Msg(m) => Some(m), + _ => None, + }, + }; + + match message.as_deref() { + Some("attempting to build group with zero transactions") => {} + _ => panic!("expected error, but got: {:?}", message), + } + } + _ => panic!("Unknown error type: {}", error_type), + } +} + +#[then(expr = "I execute the current transaction group with the composer.")] +async fn i_execute_the_current_transaction_group_with_the_composer(w: &mut World) { + let algod = w.algod.as_ref().unwrap(); + let tx_composer = w.tx_composer.as_mut().unwrap(); + + let res = tx_composer.execute(algod).await; + + let res = res.expect("Failed executing"); + + w.tx_composer_res = Some(res) +} + +#[then(regex = r#"^The app should have returned "([^"]*)"\.$"#)] +async fn the_app_should_have_returned(w: &mut World, comma_separated_b64_results: String) { + let tx_composer_res = w.tx_composer_res.as_ref().unwrap(); + let tx_composer_methods = w.tx_composer_methods.as_ref().unwrap(); + + let b64_expected_results: Vec<&str> = comma_separated_b64_results.split(',').collect(); + + if b64_expected_results.len() != tx_composer_res.method_results.len() { + panic!( + "length of expected results doesn't match actual: {:?} != {}", + b64_expected_results, + tx_composer_res.method_results.len() + ); + } + + if tx_composer_methods.len() != tx_composer_res.method_results.len() { + panic!( + "length of composer's methods doesn't match results: {:?} != {}", + tx_composer_methods, + tx_composer_res.method_results.len() + ); + } + + for (i, b64_expected_result) in b64_expected_results.into_iter().enumerate() { + let expected_res_bytes = BASE64 + .decode(b64_expected_result.as_bytes()) + .expect("couldn't decode b64"); + match &tx_composer_res.method_results[i].return_value { + Ok(AbiMethodReturnValue::Some(value)) => { + let mut method = tx_composer_methods[i].clone(); + match method.returns.type_().expect("error retrieving type") { + AbiReturnType::Some(type_) => { + let expected_value = type_ + .decode(&expected_res_bytes) + .expect("the expected value doesn't match the actual result"); + + assert_eq!(&expected_value, value); + } + AbiReturnType::Void => panic!("unexpected void return type"), + } + } + Ok(AbiMethodReturnValue::Void) => { + if !expected_res_bytes.is_empty() { + panic!("Expected result should be empty") + } + } + Err(AbiReturnDecodeError(e)) => panic!("decode error: {:?}", e), + } + } +} + +#[then(regex = r#"^The app should have returned ABI types "([^"]*)"\.$"#)] +async fn the_app_should_have_returned_abi_types(w: &mut World, expected_type_strings_str: String) { + let tx_composer_res = w.tx_composer_res.as_ref().unwrap(); + + let expected_type_strings: Vec<&str> = expected_type_strings_str.split(':').collect(); + + if expected_type_strings.len() != tx_composer_res.method_results.len() { + panic!( + "length of expected results doesn't match actual: {} != {}", + expected_type_strings.len(), + tx_composer_res.method_results.len() + ); + } + + for (i, expected_type_string) in expected_type_strings.into_iter().enumerate() { + let actual_res = &tx_composer_res.method_results[i]; + + match &actual_res.return_value { + Ok(AbiMethodReturnValue::Some(value)) => { + let expected_type = expected_type_string.parse::().unwrap(); + + let encoded = expected_type + .encode(value.clone()) + .expect("couldn't encode value"); + + let decoded = expected_type + .decode(&encoded) + .expect("couldn't decode value"); + + assert_eq!( + &decoded, value, + "The round trip result does not match the original result" + ) + } + Ok(AbiMethodReturnValue::Void) => { + if !AbiReturn::is_void_str(expected_type_string) { + panic!("Not a void return type: {:?}", actual_res.return_value); + } + } + Err(e) => { + panic!("Decode error: {:?}", e) + } + } + } +} + +// The 1th atomic result for randomInt(1337) proves correct +// #[then(regex = r#"^The (\d+)th atomic result for randomInt\((\d+)\) proves correct$"#)] +#[then(regex = r#"^The (\d+)th atomic result for randomInt\((\d+)\) proves correct$"#)] +async fn check_random_int_result(w: &mut World, result_index: usize, input: u64) { + let tx_composers = w.tx_composer_res.as_ref().expect("No tx composer res"); + let tx_composer_res = &tx_composers.method_results[result_index]; + + let value = match &tx_composer_res.return_value { + Ok(AbiMethodReturnValue::Some(value)) => value, + _ => panic!("No decoded res"), + }; + + let (rand_int, witness) = match value { + AbiValue::Array(array) => match &array[0] { + AbiValue::Int(i) => match &array[1] { + AbiValue::Array(nested_array) => ( + i.to_u64().expect("couldn't convert bigint to int"), + nested_array.clone(), + ), + _ => panic!("nested abi value isn't an array"), + }, + _ => panic!("nested abi value isn't an int"), + }, + _ => panic!("abi value isn't an array"), + }; + + let mut witness_bytes = vec![]; + for (_, value) in witness.into_iter().enumerate() { + witness_bytes.push(match value { + AbiValue::Byte(b) => b, + _ => panic!("abi value isn't a byte"), + }); + } + + let x = sha2::Sha512_256::digest(&witness_bytes); + let int = u64::from_be_bytes(x[..8].try_into().expect("couldn't get slice from hash")); + let quotient = int % input as u64; + if quotient != rand_int { + panic!( + "Unexpected result: quotient is {} and randInt is {}", + quotient, rand_int + ); + } +} + +#[then(regex = r#"^The (\d+)th atomic result for randElement\("([^"]*)"\) proves correct$"#)] +async fn check_random_element_result(w: &mut World, result_index: usize, input: String) { + let tx_composers = w.tx_composer_res.as_ref().expect("No tx composer res"); + let tx_composer_res = &tx_composers.method_results[result_index]; + + let value = match &tx_composer_res.return_value { + Ok(value) => match value { + AbiMethodReturnValue::Some(value) => value, + AbiMethodReturnValue::Void => panic!("No decoded res"), + }, + _ => panic!("No decoded res"), + }; + + let (rand_el, witness) = match value { + AbiValue::Array(array) => match &array[0] { + AbiValue::Byte(b) => match &array[1] { + AbiValue::Array(nested_array) => (b.clone(), nested_array.clone()), + _ => panic!("nested abi value isn't an array"), + }, + _ => panic!("nested abi value isn't an byte"), + }, + _ => panic!("abi value isn't an array"), + }; + + let mut witness_bytes = vec![]; + for (_, value) in witness.into_iter().enumerate() { + witness_bytes.push(match value { + AbiValue::Byte(b) => b, + _ => panic!(), + }); + } + + let x = sha2::Sha512_256::digest(&witness_bytes); + let int = usize::from_be_bytes(x[..8].try_into().expect("couldn't get slice from hash")); + let quotient = int % input.len(); + if input.as_bytes()[quotient] != rand_el { + panic!( + "Unexpected result: quotient is {} and randInt is {}", + quotient, rand_el + ); + } +} + +#[then( + regex = r#"^I dig into the paths "([^"]*)" of the resulting atomic transaction tree I see group ids and they are all the same$"# +)] +async fn check_inner_txn_group_ids(w: &mut World, colon_separated_paths_string: String) { + let tx_composer_res = w.tx_composer_res.as_ref().expect("No tx composer res"); + + let mut paths: Vec> = vec![]; + + let comma_separated_path_strings = colon_separated_paths_string.split(':'); + for comma_separated_path_string in comma_separated_path_strings { + let path_of_strings = comma_separated_path_string.split(','); + let mut path = vec![]; + for string_component in path_of_strings { + let int_component = string_component.parse().unwrap(); + path.push(int_component) + } + paths.push(path) + } + + let mut tx_infos_to_check = vec![]; + + for path in paths { + let mut current: PendingTransaction = tx_composer_res.method_results[0].tx_info.clone(); + for path_index in 1..path.len() { + let inner_txn_index = path[path_index]; + if path_index == 0 { + current = tx_composer_res.method_results[inner_txn_index] + .tx_info + .clone(); + } else { + current = current.inner_txs[inner_txn_index].clone(); + } + } + + tx_infos_to_check.push(current); + } + + // TODO https://github.com/manuelmauro/algonaut/issues/156 + // let mut group; + // for (i, txInfo) in txInfosToCheck.into_iter().enumerate() { + // if i == 0 { + // group = txInfo.txn.group + // } + // if group != txInfo.txn.group { + // panic!("Group hashes differ: {} != {}", group, txInfo.txn.group); + // } + // } +} + +#[then( + regex = r#"^I can dig the (\d+)th atomic result with path "([^"]*)" and see the value "([^"]*)"$"# +)] +async fn check_atomic_result_against_value( + _w: &mut World, + _result_index: u64, + _path: String, + _expected_value: String, +) { + + // TODO https://github.com/manuelmauro/algonaut/issues/156 +} + +#[given(regex = r#"^an application id (\d+)$"#)] +async fn an_application_id(w: &mut World, app_id: u64) { + w.app_id = Some(app_id); +} + +#[then(regex = r#"^The (\d+)th atomic result for "([^"]*)" satisfies the regex "([^"]*)"$"#)] +async fn check_spin_result(w: &mut World, result_index: usize, method: String, r: String) { + let tx_composer_res = w.tx_composer_res.as_ref().expect("No tx composer res"); + + if method != "spin()" { + panic!("Incorrect method name, expected 'spin()', got '{}'", method); + } + + let result = &tx_composer_res.method_results[result_index]; + + let decoded_result = match &result.return_value { + Ok(AbiMethodReturnValue::Some(value)) => match value { + AbiValue::Array(array) => array, + _ => panic!("return value isn't an array"), + }, + _ => panic!("unexpected return value: {:?}", result.return_value), + }; + + let spin = match &decoded_result[0] { + AbiValue::Array(array) => array, + _ => panic!("first spin element isn't an array"), + }; + + let mut spin_bytes = vec![]; + for value in spin { + spin_bytes.push(match value { + AbiValue::Byte(b) => *b, + _ => panic!("non-byte in spin array"), + }); + } + + let regex: Regex = Regex::new(&r).expect(&format!("couldn't create regex for: {}", r)); + let str = String::from_utf8(spin_bytes).expect("couldn't convert bytes to string"); + let matched = regex.is_match(&str); + + if !matched { + panic!("Result did not match regex. spin str: {}", str); + } +} + +#[given(regex = r#"^I fund the current application's address with (\d+) microalgos\.$"#)] +async fn i_fund_the_current_applications_address(w: &mut World, micro_algos: u64) { + let algod = w.algod.as_ref().expect("no algod"); + let app_id = w.app_id.expect("no app id"); + let accounts = w.accounts.as_ref().expect("no accounts"); + let kmd = w.kmd.as_ref().expect("no kmd"); + let kmd_handle = w.handle.as_ref().expect("no kmd handle"); + let kmd_pw = w.password.as_ref().expect("no kmd pw"); + + let first_account = accounts[0]; + + let app_address = to_app_address(app_id); + + let tx_params = algod + .suggested_transaction_params() + .await + .expect("couldn't get params"); + + let tx = TxnBuilder::with( + &tx_params, + Pay::new(first_account, app_address, MicroAlgos(micro_algos)).build(), + ) + .build() + .unwrap(); + + let signed_tx = kmd + .sign_transaction(kmd_handle, kmd_pw, &tx) + .await + .expect("couldn't sign tx"); + + let res = algod + .broadcast_raw_transaction(&signed_tx.signed_transaction) + .await + .expect("couldn't send tx"); + + let _ = wait_for_pending_transaction(algod, &res.tx_id); +} + +#[given(regex = r#"^I reset the array of application IDs to remember\.$"#)] +async fn i_reset_the_array_of_application_ids_to_remember(w: &mut World) { + w.app_ids = vec![]; +} diff --git a/tests/step_defs/integration/applications.rs b/tests/step_defs/integration/applications.rs index 5eafe6d7..8e447802 100644 --- a/tests/step_defs/integration/applications.rs +++ b/tests/step_defs/integration/applications.rs @@ -1,142 +1,15 @@ -use crate::step_defs::util::{ - account_from_kmd_response, parse_app_args, split_addresses, split_uint64, - wait_for_pending_transaction, -}; -use algonaut::{algod::v2::Algod, kmd::v1::Kmd}; -use algonaut_core::{Address, CompiledTeal, MicroAlgos}; +use crate::step_defs::integration::world::World; +use crate::step_defs::util::{parse_app_args, read_teal, split_addresses, split_uint64}; use algonaut_model::algod::v2::{Application, ApplicationLocalState}; -use algonaut_transaction::account::Account; use algonaut_transaction::builder::{ CallApplication, ClearApplication, CloseApplication, DeleteApplication, OptInApplication, UpdateApplication, }; -use algonaut_transaction::transaction::StateSchema; -use algonaut_transaction::{CreateApplication, Pay, Transaction, TxnBuilder}; -use async_trait::async_trait; -use cucumber::{given, then, WorldInit}; +use algonaut_transaction::transaction::{ApplicationCallOnComplete, StateSchema}; +use algonaut_transaction::{CreateApplication, TxnBuilder}; +use cucumber::{given, then, when}; use data_encoding::BASE64; -use std::convert::Infallible; use std::error::Error; -use std::fs; - -#[derive(Default, Debug, WorldInit)] -pub struct World { - algod: Option, - - kmd: Option, - handle: Option, - password: Option, - accounts: Option>, - - transient_account: Option, - - tx: Option, - tx_id: Option, - - app_id: Option, -} - -#[async_trait(?Send)] -impl cucumber::World for World { - type Error = Infallible; - - async fn new() -> Result { - Ok(Self::default()) - } -} - -#[given(expr = "an algod client")] -async fn an_algod_client(_: &mut World) { - // Do nothing - we don't support v1 - // The reference (Go) SDK doesn't use it in the definitions -} - -#[given(expr = "a kmd client")] -async fn a_kmd_client(w: &mut World) { - let kmd = Kmd::new( - "http://localhost:60001", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ) - .unwrap(); - w.kmd = Some(kmd) -} - -#[given(expr = "wallet information")] -async fn wallet_information(w: &mut World) -> Result<(), Box> { - let kmd = w.kmd.as_ref().unwrap(); - - let list_response = kmd.list_wallets().await?; - let wallet_id = match list_response - .wallets - .into_iter() - .find(|wallet| wallet.name == "unencrypted-default-wallet") - { - Some(wallet) => wallet.id, - None => return Err("Wallet not found".into()), - }; - let password = ""; - let init_response = kmd.init_wallet_handle(&wallet_id, "").await?; - - let keys = kmd - .list_keys(init_response.wallet_handle_token.as_ref()) - .await?; - - w.password = Some(password.to_owned()); - w.handle = Some(init_response.wallet_handle_token); - w.accounts = Some( - keys.addresses - .into_iter() - .map(|s| s.parse().unwrap()) - .collect(), - ); - - Ok(()) -} - -#[given(regex = r#"^an algod v2 client connected to "([^"]*)" port (\d+) with token "([^"]*)"$"#)] -async fn an_algod_v2_client_connected_to(w: &mut World, host: String, port: String, token: String) { - let algod = Algod::new(&format!("http://{}:{}", host, port), &token).unwrap(); - w.algod = Some(algod) -} - -#[given(regex = r#"^I create a new transient account and fund it with (\d+) microalgos\.$"#)] -async fn i_create_a_new_transient_account_and_fund_it_with_microalgos( - w: &mut World, - micro_algos: u64, -) -> Result<(), Box> { - let kmd = w.kmd.as_ref().unwrap(); - let algod = w.algod.as_ref().unwrap(); - let accounts = w.accounts.as_ref().unwrap(); - let password = w.password.as_ref().unwrap(); - let handle = w.handle.as_ref().unwrap(); - - let sender_address = accounts[1]; - - let sender_key = kmd.export_key(handle, password, &sender_address).await?; - - let sender_account = account_from_kmd_response(&sender_key)?; - - let params = algod.suggested_transaction_params().await?; - let tx = TxnBuilder::with( - ¶ms, - Pay::new( - accounts[1], - sender_account.address(), - MicroAlgos(micro_algos), - ) - .build(), - ) - .build()?; - - let s_tx = sender_account.sign_transaction(tx)?; - - let send_response = algod.broadcast_signed_transaction(&s_tx).await?; - let _ = wait_for_pending_transaction(&algod, &send_response.tx_id); - - w.transient_account = Some(sender_account); - - Ok(()) -} #[given( regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+)$"# @@ -144,6 +17,9 @@ async fn i_create_a_new_transient_account_and_fund_it_with_microalgos( #[then( regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+)$"# )] +#[when( + regex = r#"^I build an application transaction with the transient account, the current application, suggested params, operation "([^"]*)", approval-program "([^"]*)", clear-program "([^"]*)", global-bytes (\d+), global-ints (\d+), local-bytes (\d+), local-ints (\d+), app-args "([^"]*)", foreign-apps "([^"]*)", foreign-assets "([^"]*)", app-accounts "([^"]*)", extra-pages (\d+)$"# +)] async fn i_build_an_application_transaction( w: &mut World, operation: String, @@ -157,7 +33,7 @@ async fn i_build_an_application_transaction( foreign_apps: String, foreign_assets: String, app_accounts: String, - extra_pages: u64, + extra_pages: u32, ) -> Result<(), Box> { let algod = w.algod.as_ref().unwrap(); let transient_account = w.transient_account.as_ref().unwrap(); @@ -173,8 +49,8 @@ async fn i_build_an_application_transaction( let tx_type = match operation.as_str() { "create" => { - let approval_program = load_teal(&approval_program_file)?; - let clear_program = load_teal(&clear_program_file)?; + let approval_program = read_teal(algod, &approval_program_file).await; + let clear_program = read_teal(algod, &clear_program_file).await; let global_schema = StateSchema { number_ints: global_ints, @@ -188,8 +64,8 @@ async fn i_build_an_application_transaction( CreateApplication::new( transient_account.address(), - CompiledTeal(approval_program), - CompiledTeal(clear_program), + approval_program, + clear_program, global_schema, local_schema, ) @@ -203,14 +79,14 @@ async fn i_build_an_application_transaction( "update" => { let app_id = w.app_id.unwrap(); - let approval_program = load_teal(&approval_program_file)?; - let clear_program = load_teal(&clear_program_file)?; + let approval_program = read_teal(algod, &approval_program_file).await; + let clear_program = read_teal(algod, &clear_program_file).await; UpdateApplication::new( transient_account.address(), app_id, - CompiledTeal(approval_program), - CompiledTeal(clear_program), + approval_program, + clear_program, ) .foreign_assets(foreign_assets) .foreign_apps(foreign_apps) @@ -220,12 +96,16 @@ async fn i_build_an_application_transaction( } "call" => { let app_id = w.app_id.unwrap(); - CallApplication::new(transient_account.address(), app_id) - .foreign_assets(foreign_assets) - .foreign_apps(foreign_apps) - .accounts(accounts) - .app_arguments(args) - .build() + CallApplication::new( + transient_account.address(), + app_id, + ApplicationCallOnComplete::NoOp, + ) + .foreign_assets(foreign_assets) + .foreign_apps(foreign_apps) + .accounts(accounts) + .app_arguments(args) + .build() } "optin" => { let app_id = w.app_id.unwrap(); @@ -273,50 +153,19 @@ async fn i_build_an_application_transaction( Ok(()) } -#[given( - regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# -)] -#[then( - regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# -)] -async fn i_sign_and_submit_the_transaction_saving_the_tx_id_if_there_is_an_error_it_is( - w: &mut World, - err: String, -) { - let algod = w.algod.as_ref().unwrap(); - let transient_account = w.transient_account.as_ref().unwrap(); - let tx = w.tx.as_ref().unwrap(); - - let s_tx = transient_account.sign_transaction(tx.clone()).unwrap(); - - match algod.broadcast_signed_transaction(&s_tx).await { - Ok(response) => { - w.tx_id = Some(response.tx_id); - } - Err(e) => { - assert!(e.to_string().contains(&err)); - } - } -} - -#[given(expr = "I wait for the transaction to be confirmed.")] -#[then(expr = "I wait for the transaction to be confirmed.")] -async fn i_wait_for_the_transaction_to_be_confirmed(w: &mut World) { - let algod = w.algod.as_ref().unwrap(); - let tx_id = w.tx_id.as_ref().unwrap(); - - wait_for_pending_transaction(&algod, &tx_id).await.unwrap(); -} - #[given(expr = "I remember the new application ID.")] +#[when(expr = "I remember the new application ID.")] async fn i_remember_the_new_application_id(w: &mut World) { let algod = w.algod.as_ref().unwrap(); let tx_id = w.tx_id.as_ref().unwrap(); + let app_ids: &mut Vec = w.app_ids.as_mut(); let p_tx = algod.pending_transaction_with_id(tx_id).await.unwrap(); assert!(p_tx.application_index.is_some()); + let app_id = p_tx.application_index.unwrap(); - w.app_id = p_tx.application_index; + w.app_id = Some(app_id); + app_ids.push(app_id); } #[then( @@ -430,6 +279,12 @@ async fn the_transient_account_should_have( Ok(()) } -fn load_teal(file_name: &str) -> Result, Box> { - Ok(fs::read(format!("tests/features/resources/{}", file_name))?) -} +// fn load_teal(file_name: &str) -> Result, Box> { +// Ok(fs::read(format!("tests/features/resources/{}", file_name))?) +// } + +// async fn load_and_compile_teal(algod: &Algod, file_name: &str) -> Result { +// let source = load_teal(file_name)?; + +// Ok(fs::read(format!("tests/features/resources/{}", file_name))?) +// } diff --git a/tests/step_defs/integration/general.rs b/tests/step_defs/integration/general.rs new file mode 100644 index 00000000..073de3f6 --- /dev/null +++ b/tests/step_defs/integration/general.rs @@ -0,0 +1,161 @@ +use std::error::Error; + +use crate::step_defs::{ + integration::world::World, + util::{account_from_kmd_response, wait_for_pending_transaction}, +}; +use algonaut::{algod::v2::Algod, kmd::v1::Kmd}; +use algonaut_core::{MicroAlgos, Round}; +use algonaut_transaction::{Pay, TxnBuilder}; +use cucumber::{given, then, when}; + +#[given(regex = "an algod v2 client")] +async fn an_algod_v2_client(w: &mut World) -> Result<(), Box> { + let algod = Algod::new( + "http://localhost:60000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .unwrap(); + + algod.status_after_round(Round(1)).await?; + w.algod = Some(algod); + + Ok(()) +} + +#[given(regex = r#"^an algod v2 client connected to "([^"]*)" port (\d+) with token "([^"]*)"$"#)] +async fn an_algod_v2_client_connected_to(w: &mut World, host: String, port: String, token: String) { + let algod = Algod::new(&format!("http://{}:{}", host, port), &token).unwrap(); + w.algod = Some(algod) +} + +#[given(expr = "a kmd client")] +async fn a_kmd_client(w: &mut World) { + let kmd = Kmd::new( + "http://localhost:60001", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .unwrap(); + w.kmd = Some(kmd) +} + +#[given(expr = "wallet information")] +async fn wallet_information(w: &mut World) -> Result<(), Box> { + let kmd = w.kmd.as_ref().unwrap(); + + let list_response = kmd.list_wallets().await?; + let wallet_id = match list_response + .wallets + .into_iter() + .find(|wallet| wallet.name == "unencrypted-default-wallet") + { + Some(wallet) => wallet.id, + None => return Err("Wallet not found".into()), + }; + let password = ""; + let init_response = kmd.init_wallet_handle(&wallet_id, "").await?; + + let keys = kmd + .list_keys(init_response.wallet_handle_token.as_ref()) + .await?; + + w.password = Some(password.to_owned()); + w.handle = Some(init_response.wallet_handle_token); + w.accounts = Some( + keys.addresses + .into_iter() + .map(|s| s.parse().unwrap()) + .collect(), + ); + + Ok(()) +} + +#[given(regex = "suggested transaction parameters from the algod v2 client")] +async fn suggested_params(w: &mut World) -> Result<(), Box> { + let algod = w.algod.as_ref().unwrap(); + + w.tx_params = Some(algod.suggested_transaction_params().await?); + + Ok(()) +} + +#[given(regex = r#"^I create a new transient account and fund it with (\d+) microalgos\.$"#)] +async fn i_create_a_new_transient_account_and_fund_it_with_microalgos( + w: &mut World, + micro_algos: u64, +) -> Result<(), Box> { + let kmd = w.kmd.as_ref().unwrap(); + let algod = w.algod.as_ref().unwrap(); + let accounts = w.accounts.as_ref().unwrap(); + let password = w.password.as_ref().unwrap(); + let handle = w.handle.as_ref().unwrap(); + + let sender_address = accounts[1]; + + let sender_key = kmd.export_key(handle, password, &sender_address).await?; + + let sender_account = account_from_kmd_response(&sender_key)?; + + let params = algod.suggested_transaction_params().await?; + let tx = TxnBuilder::with( + ¶ms, + Pay::new( + accounts[1], + sender_account.address(), + MicroAlgos(micro_algos), + ) + .build(), + ) + .build()?; + + let s_tx = sender_account.sign_transaction(tx)?; + + let send_response = algod.broadcast_signed_transaction(&s_tx).await?; + let _ = wait_for_pending_transaction(&algod, &send_response.tx_id); + + w.transient_account = Some(sender_account); + + Ok(()) +} + +#[given( + regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# +)] +#[then( + regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# +)] +#[when( + regex = r#"I sign and submit the transaction, saving the txid\. If there is an error it is "([^"]*)"\.$"# +)] +async fn i_sign_and_submit_the_transaction_saving_the_tx_id_if_there_is_an_error_it_is( + w: &mut World, + err: String, +) { + let algod = w.algod.as_ref().unwrap(); + let transient_account = w.transient_account.as_ref().unwrap(); + let tx = w.tx.as_ref().unwrap(); + + let s_tx = transient_account.sign_transaction(tx.clone()).unwrap(); + + match algod.broadcast_signed_transaction(&s_tx).await { + Ok(response) => { + w.tx_id = Some(response.tx_id); + } + Err(e) => { + assert!(e.to_string().contains(&err)); + } + } +} + +#[given(expr = "I wait for the transaction to be confirmed.")] +#[then(expr = "I wait for the transaction to be confirmed.")] +#[when(expr = "I wait for the transaction to be confirmed.")] +async fn i_wait_for_the_transaction_to_be_confirmed(w: &mut World) { + let algod = w.algod.as_ref().expect("algod not set"); + let tx_id = w.tx_id.as_ref().expect("tx id not set"); + + wait_for_pending_transaction(&algod, &tx_id) + .await + .expect("couldn't get pending tx"); +} diff --git a/tests/step_defs/integration/mod.rs b/tests/step_defs/integration/mod.rs index c0e90580..8403db21 100644 --- a/tests/step_defs/integration/mod.rs +++ b/tests/step_defs/integration/mod.rs @@ -1 +1,4 @@ +pub mod abi; pub mod applications; +pub mod general; +pub mod world; diff --git a/tests/step_defs/integration/world.rs b/tests/step_defs/integration/world.rs new file mode 100644 index 00000000..a7cfaea8 --- /dev/null +++ b/tests/step_defs/integration/world.rs @@ -0,0 +1,55 @@ +use algonaut::{ + algod::v2::Algod, + atomic_transaction_composer::AtomicTransactionComposer, + atomic_transaction_composer::{ + transaction_signer::TransactionSigner, AbiArgValue, ExecuteResult, TransactionWithSigner, + }, + kmd::v1::Kmd, +}; +use algonaut_abi::{abi_interactions::AbiMethod, abi_type::AbiType}; +use algonaut_core::{Address, SuggestedTransactionParams}; +use algonaut_transaction::{account::Account, SignedTransaction, Transaction}; +use async_trait::async_trait; +use cucumber::WorldInit; +use std::convert::Infallible; + +#[derive(Default, Debug, WorldInit)] +pub struct World { + pub algod: Option, + + pub kmd: Option, + pub handle: Option, + pub password: Option, + pub accounts: Option>, + + pub transient_account: Option, + + pub tx: Option, + pub tx_id: Option, + + pub app_id: Option, + pub app_ids: Vec, + + pub tx_params: Option, + + pub note: Option>, + + pub tx_signer: Option, + pub tx_with_signer: Option, + pub tx_composer: Option, + pub tx_composer_methods: Option>, + pub signed_txs: Option>, + pub abi_method: Option, + pub abi_method_arg_types: Option>, + pub abi_method_arg_values: Option>, + pub tx_composer_res: Option, +} + +#[async_trait(?Send)] +impl cucumber::World for World { + type Error = Infallible; + + async fn new() -> Result { + Ok(Self::default()) + } +} diff --git a/tests/step_defs/util.rs b/tests/step_defs/util.rs index 5e53ee26..4204fd08 100644 --- a/tests/step_defs/util.rs +++ b/tests/step_defs/util.rs @@ -1,12 +1,13 @@ use std::{ convert::TryInto, error::Error, + fs, num::ParseIntError, time::{Duration, Instant}, }; use algonaut::{algod::v2::Algod, error::ServiceError}; -use algonaut_core::Address; +use algonaut_core::{Address, CompiledTeal}; use algonaut_model::{algod::v2::PendingTransaction, kmd::v1::ExportKeyResponse}; use algonaut_transaction::account::Account; @@ -73,3 +74,13 @@ pub fn parse_app_args(args_str: String) -> Result>, Box> pub fn account_from_kmd_response(key_res: &ExportKeyResponse) -> Result> { Ok(Account::from_seed(key_res.private_key[0..32].try_into()?)) } + +pub async fn read_teal(algod: &Algod, file_name: &str) -> CompiledTeal { + let file_bytes = fs::read(&format!("tests/features/resources/{file_name}")).unwrap(); + + if file_name.ends_with(".teal") { + algod.compile_teal(&file_bytes).await.unwrap() + } else { + CompiledTeal(file_bytes) + } +}