use std::sync::Arc;
use algonaut_abi::{
MethodInvocation, abi_error::AbiError, abi_interactions::AbiMethod, abi_type::AbiValue,
};
use algonaut_core::{Address, AppId, CompiledTeal, MicroAlgos, Round};
use algonaut_crypto::HashDigest;
use algonaut_model::algod::SuggestedParams;
use algonaut_transaction::{
Signer,
transaction::{ApplicationCallOnComplete, BoxReference, StateSchema},
};
use num_bigint::BigUint;
use num_traits::ToPrimitive;
use crate::Error;
use super::TransactionWithSigner;
#[derive(Debug, Clone)]
pub enum AbiArgValue {
TxWithSigner(Box<TransactionWithSigner>),
AbiValue(AbiValue),
}
impl From<AbiValue> for AbiArgValue {
fn from(v: AbiValue) -> Self {
Self::AbiValue(v)
}
}
impl From<TransactionWithSigner> for AbiArgValue {
fn from(t: TransactionWithSigner) -> Self {
Self::TxWithSigner(Box::new(t))
}
}
macro_rules! abi_arg_value_from {
($($t:ty),* $(,)?) => {$(
impl From<$t> for AbiArgValue {
fn from(v: $t) -> Self {
Self::AbiValue(AbiValue::from(v))
}
}
)*};
}
abi_arg_value_from!(u64, u128, bool, Address, &str, String, Vec<u8>);
impl TryFrom<&AbiArgValue> for Address {
type Error = Error;
fn try_from(value: &AbiArgValue) -> Result<Self, Self::Error> {
match value {
AbiArgValue::AbiValue(AbiValue::Address(address)) => Ok(*address),
other => Err(Error::InvalidAbiArgument {
expected: "address",
actual: format!("{other:?}"),
}),
}
}
}
impl TryFrom<&AbiArgValue> for BigUint {
type Error = Error;
fn try_from(value: &AbiArgValue) -> Result<Self, Self::Error> {
match value {
AbiArgValue::AbiValue(AbiValue::Int(int)) => Ok(int.clone()),
other => Err(Error::InvalidAbiArgument {
expected: "uint",
actual: format!("{other:?}"),
}),
}
}
}
impl TryFrom<&AbiArgValue> for u64 {
type Error = Error;
fn try_from(value: &AbiArgValue) -> Result<Self, Self::Error> {
let int = BigUint::try_from(value)?;
int.to_u64().ok_or_else(|| {
Error::from(AbiError::ValueOutOfRange {
abi_type: "uint64".to_owned(),
reason: format!("value {int} exceeds u64 capacity"),
})
})
}
}
#[derive(Clone, Debug)]
pub struct Invocation {
method: AbiMethod,
args: Vec<AbiArgValue>,
}
impl Invocation {
pub fn new(method: AbiMethod, args: impl IntoIterator<Item = impl Into<AbiArgValue>>) -> Self {
Invocation {
method,
args: args.into_iter().map(Into::into).collect(),
}
}
}
impl From<MethodInvocation> for Invocation {
fn from(invocation: MethodInvocation) -> Self {
let (method, values) = invocation.into_parts();
Invocation {
method,
args: values.into_iter().map(AbiArgValue::AbiValue).collect(),
}
}
}
#[derive(Clone, Debug)]
pub struct MethodCall {
pub(super) app_id: AppId,
pub(super) method: AbiMethod,
pub(super) method_args: Vec<AbiArgValue>,
pub(super) fee: MicroAlgos,
pub(super) sender: Address,
pub(super) first_valid: Round,
pub(super) last_valid: Round,
pub(super) genesis_hash: HashDigest,
pub(super) genesis_id: String,
pub(super) on_complete: ApplicationCallOnComplete,
pub(super) approval_program: Option<CompiledTeal>,
pub(super) clear_program: Option<CompiledTeal>,
pub(super) global_schema: Option<StateSchema>,
pub(super) local_schema: Option<StateSchema>,
pub(super) extra_pages: u32,
pub(super) note: Vec<u8>,
pub(super) lease: Option<HashDigest>,
pub(super) rekey_to: Option<Address>,
pub(super) signer: Arc<dyn Signer>,
pub(super) boxes: Vec<BoxReference>,
}
impl MethodCall {
pub fn builder(app_id: AppId, sender: Address, signer: Arc<dyn Signer>) -> MethodCallBuilder {
MethodCallBuilder {
app_id,
method: None,
sender,
signer,
method_args: Vec::new(),
fee: None,
on_complete: ApplicationCallOnComplete::NoOp,
approval_program: None,
clear_program: None,
global_schema: None,
local_schema: None,
extra_pages: 0,
note: Vec::new(),
lease: None,
rekey_to: None,
boxes: Vec::new(),
}
}
}
pub struct MethodCallBuilder {
app_id: AppId,
method: Option<AbiMethod>,
sender: Address,
signer: Arc<dyn Signer>,
method_args: Vec<AbiArgValue>,
fee: Option<MicroAlgos>,
on_complete: ApplicationCallOnComplete,
approval_program: Option<CompiledTeal>,
clear_program: Option<CompiledTeal>,
global_schema: Option<StateSchema>,
local_schema: Option<StateSchema>,
extra_pages: u32,
note: Vec<u8>,
lease: Option<HashDigest>,
rekey_to: Option<Address>,
boxes: Vec<BoxReference>,
}
impl MethodCallBuilder {
pub fn invoke(mut self, invocation: impl Into<Invocation>) -> Self {
let Invocation { method, args } = invocation.into();
self.method = Some(method);
self.method_args = args;
self
}
pub fn fee(mut self, fee: MicroAlgos) -> Self {
self.fee = Some(fee);
self
}
pub fn on_complete(mut self, on_complete: ApplicationCallOnComplete) -> Self {
self.on_complete = on_complete;
self
}
pub fn approval_program(mut self, approval: CompiledTeal) -> Self {
self.approval_program = Some(approval);
self
}
pub fn clear_program(mut self, clear: CompiledTeal) -> Self {
self.clear_program = Some(clear);
self
}
pub fn global_schema(mut self, schema: StateSchema) -> Self {
self.global_schema = Some(schema);
self
}
pub fn local_schema(mut self, schema: StateSchema) -> Self {
self.local_schema = Some(schema);
self
}
pub fn extra_pages(mut self, pages: u32) -> Self {
self.extra_pages = pages;
self
}
pub fn note(mut self, note: Vec<u8>) -> Self {
self.note = note;
self
}
pub fn lease(mut self, lease: HashDigest) -> Self {
self.lease = Some(lease);
self
}
pub fn rekey_to(mut self, rekey_to: Address) -> Self {
self.rekey_to = Some(rekey_to);
self
}
pub fn boxes(mut self, boxes: Vec<BoxReference>) -> Self {
self.boxes = boxes;
self
}
pub fn build(self, params: &SuggestedParams) -> MethodCall {
let fee = self.fee.unwrap_or(params.min_fee);
let method = self
.method
.expect("MethodCall: call `.invoke(...)` before `.build(...)`");
MethodCall {
app_id: self.app_id,
method,
method_args: self.method_args,
fee,
sender: self.sender,
first_valid: params.last_round,
last_valid: Round(params.last_round.0 + 1000),
genesis_hash: params.genesis_hash,
genesis_id: params.genesis_id.clone(),
on_complete: self.on_complete,
approval_program: self.approval_program,
clear_program: self.clear_program,
global_schema: self.global_schema,
local_schema: self.local_schema,
extra_pages: self.extra_pages,
note: self.note,
lease: self.lease,
rekey_to: self.rekey_to,
signer: self.signer,
boxes: self.boxes,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use algonaut_abi::abi_call;
#[test]
fn abi_call_macro_produces_checked_invocation() {
let invocation: Invocation = abi_call!("add(uint64,uint64)uint64", 2u64, 3u64).into();
assert_eq!(invocation.method.name, "add");
assert_eq!(invocation.method.args.len(), 2);
assert_eq!(invocation.args.len(), 2);
let values: Vec<&AbiValue> = invocation
.args
.iter()
.map(|a| match a {
AbiArgValue::AbiValue(v) => v,
AbiArgValue::TxWithSigner(_) => panic!("expected plain ABI values"),
})
.collect();
assert_eq!(values[0], &AbiValue::from(2u64));
assert_eq!(values[1], &AbiValue::from(3u64));
}
#[test]
fn abi_call_macro_allows_widening() {
let invocation: Invocation = abi_call!("add(uint64,uint64)uint64", 2u32, 3u8).into();
assert_eq!(invocation.args.len(), 2);
}
#[test]
fn abi_call_macro_checks_non_integer_value_types() {
let invocation: Invocation =
abi_call!("f(bool,string,byte[])void", true, "hi", vec![1u8, 2u8]).into();
assert_eq!(invocation.method.name, "f");
assert_eq!(invocation.args.len(), 3);
}
}