use algonaut::abi::abi_type::AbiValue;
use algonaut::atomic::{AbiMethodReturnValue, AtomicGroupBuilder};
use algonaut::core::{Address, MicroAlgos};
use algonaut::transaction::account::Account;
use algonaut::transaction::{Pay, Signer};
use algonaut::{Algod, Kmd};
use std::env;
use std::sync::Arc;
algonaut::contract!("tests/fixtures/vault.arc56.json");
algonaut::contract!("tests/fixtures/arc56_test.arc56.json");
algonaut::contract!("tests/fixtures/bytecode_bare.arc56.json");
fn env_or(key: &str, default: &str) -> String {
env::var(key).unwrap_or_else(|_| default.to_owned())
}
fn algod() -> Algod {
Algod::new(
&env_or("ALGOD_URL", "http://localhost:4001"),
&env_or("ALGOD_TOKEN", &"a".repeat(64)),
)
.expect("algod client")
}
fn kmd() -> Kmd {
Kmd::new(
&env_or("KMD_URL", "http://localhost:4002"),
&env_or("KMD_TOKEN", &"a".repeat(64)),
)
.expect("kmd client")
}
async fn funded_account(algod: &Algod, kmd: &Kmd) -> Account {
let account = Account::generate();
let wallet_id = kmd
.list_wallets()
.await
.expect("list wallets")
.wallets
.into_iter()
.find(|w| w.name == "unencrypted-default-wallet")
.expect("default wallet present (run `make sandbox`)")
.id;
let handle = kmd
.init_wallet_handle(&wallet_id, "")
.await
.expect("init wallet handle")
.wallet_handle_token;
let dispenser: Address = kmd
.list_keys(&handle)
.await
.expect("list keys")
.addresses
.first()
.expect("a funded genesis account")
.parse()
.expect("genesis address parses");
let params = algod.suggested_params().await.expect("suggested params");
let pay = Pay::new(dispenser, account.address(), MicroAlgos(10_000_000))
.build(¶ms)
.expect("build funding payment");
let signed = kmd
.sign(&handle, "", &pay)
.await
.expect("kmd signs funding payment")
.signed_transaction;
algod
.submit_raw(&signed)
.await
.expect("submit funding payment")
.confirm()
.await
.expect("funding payment confirms");
account
}
async fn deploy_vault(algod: &Algod, kmd: &Kmd) -> (Vault, Address) {
let account = funded_account(algod, kmd).await;
let sender = account.address();
let signer: Arc<dyn Signer> = Arc::new(account);
let params = algod.suggested_params().await.expect("suggested params");
let vault = Vault::deploy(algod, sender, signer, ¶ms)
.await
.expect("deploy Vault");
(vault, sender)
}
async fn deploy_arc56_test(algod: &Algod, kmd: &Kmd) -> (ARC56Test, Address) {
let account = funded_account(algod, kmd).await;
let sender = account.address();
let signer: Arc<dyn Signer> = Arc::new(account);
let params = algod.suggested_params().await.expect("suggested params");
let client = ARC56Test::deploy(algod, sender, signer, ¶ms, 1)
.await
.expect("deploy ARC56Test");
(client, sender)
}
async fn deploy_bytecode_bare(algod: &Algod, kmd: &Kmd) -> (ByteCodeBare, Address) {
let account = funded_account(algod, kmd).await;
let sender = account.address();
let signer: Arc<dyn Signer> = Arc::new(account);
let params = algod.suggested_params().await.expect("suggested params");
let client = ByteCodeBare::deploy(algod, sender, signer, ¶ms)
.await
.expect("deploy ByteCodeBare");
(client, sender)
}
#[tokio::test]
async fn deploy_creates_app() {
let (vault, _) = deploy_vault(&algod(), &kmd()).await;
assert!(vault.app_id().0 > 0, "deploy should create an application");
}
#[tokio::test]
async fn deploy_from_bytecode_creates_app() {
let (client, _) = deploy_bytecode_bare(&algod(), &kmd()).await;
assert!(
client.app_id().0 > 0,
"byteCode-only deploy should create an application"
);
}
#[tokio::test]
async fn full_walkthrough() {
let algod = algod();
let (vault, sender) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
let store = vault
.store(Pair {
first: 2,
second: 3,
})
.build(¶ms);
let scale = vault
.scale(
Pair {
first: 4,
second: 5,
},
10,
)
.build(¶ms);
let wrapped = vault
.store_wrapped(Wrapper {
inner: Pair {
first: 6,
second: 7,
},
label: "demo".to_owned(),
})
.build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(store)
.add_method_call(scale)
.add_method_call(wrapped)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
assert!(executed.confirmed_round.is_some(), "group should confirm");
match &executed.method_results[1].return_value {
Ok(AbiMethodReturnValue::Some(value)) => assert_eq!(value, &AbiValue::from(90u64)),
other => panic!("unexpected scale return: {other:?}"),
}
let logs = executed.method_results[0]
.transaction_info
.logs
.as_ref()
.expect("store should produce a log");
let raw: Vec<Vec<u8>> = logs.iter().map(|log| log.0.clone()).collect();
assert_eq!(
Vault::decode_events(&raw),
vec![VaultEvent::Counted(AbiValue::Array(vec![AbiValue::from(
5u64
)]))],
);
let simulated = vault.get_pair().simulate(&algod, ¶ms).await.unwrap();
match &simulated.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(value)) => assert_eq!(
value,
&AbiValue::Array(vec![AbiValue::from(13u64), AbiValue::from(13u64)])
),
other => panic!("unexpected get_pair return: {other:?}"),
}
assert_eq!(
vault.global_total(&algod).await.unwrap(),
Some(AbiValue::from(13u64)),
);
assert_eq!(
vault.global_owner(&algod).await.unwrap(),
Some(AbiValue::Address(sender)),
);
}
#[tokio::test]
async fn opt_in_via_enroll_succeeds() {
let algod = algod();
let (vault, sender) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
assert_eq!(
vault.local_seen(&algod, &sender).await.unwrap(),
None,
"no local state before opt-in",
);
let opt_in = vault.enroll().opt_in().build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(opt_in)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
assert!(executed.confirmed_round.is_some(), "opt-in should confirm");
assert_eq!(
vault.local_seen(&algod, &sender).await.unwrap(),
Some(AbiValue::from(1u64)),
"opt-in records local seen = 1",
);
}
#[tokio::test]
async fn incr_uses_literal_default_on_chain() {
let algod = algod();
let (vault, _) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
let incr = vault.incr(None).build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(incr)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
match &executed.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(value)) => assert_eq!(value, &AbiValue::from(1u64)),
other => panic!("unexpected incr return: {other:?}"),
}
let incr_over = vault.incr(Some(10)).build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(incr_over)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
match &executed.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(value)) => assert_eq!(value, &AbiValue::from(11u64)),
other => panic!("unexpected incr override return: {other:?}"),
}
}
#[tokio::test]
async fn get_pair_simulate_decoded_returns_typed_struct() {
let algod = algod();
let (vault, _) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
let store = vault
.store(Pair {
first: 2,
second: 3,
})
.build(¶ms);
AtomicGroupBuilder::new()
.add_method_call(store)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
let pair: Pair = vault
.get_pair()
.simulate_decoded(&algod, ¶ms)
.await
.unwrap();
assert_eq!(pair.first, 5);
assert_eq!(pair.second, 5);
let scaled: u64 = vault
.scale(
Pair {
first: 4,
second: 5,
},
10,
)
.simulate_decoded(&algod, ¶ms)
.await
.unwrap();
assert_eq!(scaled, 90);
}
#[tokio::test]
async fn arc56_test_deploys_via_abi_create() {
let (client, _) = deploy_arc56_test(&algod(), &kmd()).await;
assert!(
client.app_id().0 > 0,
"ABI-method create with a substituted template variable should create the app"
);
}
#[tokio::test]
async fn non_creator_can_call_and_owner_stays_the_creator() {
let algod = algod();
let kmd = kmd();
let (vault, creator) = deploy_vault(&algod, &kmd).await;
let caller = funded_account(&algod, &kmd).await;
let caller_addr = caller.address();
assert_ne!(
caller_addr, creator,
"the caller must differ from the creator"
);
let caller_signer: Arc<dyn Signer> = Arc::new(caller);
let as_caller = Vault::new(vault.app_id(), caller_addr, caller_signer);
let params = algod.suggested_params().await.unwrap();
let store = as_caller
.store(Pair {
first: 8,
second: 9,
})
.build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(store)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
assert!(
executed.confirmed_round.is_some(),
"a non-creator's call should confirm"
);
assert_eq!(
vault.global_total(&algod).await.unwrap(),
Some(AbiValue::from(17u64)),
"store(8,9) sets total = 8 + 9",
);
assert_eq!(
vault.global_owner(&algod).await.unwrap(),
Some(AbiValue::Address(creator)),
"owner stays the creator even though a different account called",
);
}
#[tokio::test]
async fn account_reference_round_trips() {
use algonaut::atomic::AbiMethodReturnValue;
let algod = algod();
let (vault, _) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
let target = Account::generate();
let executed = AtomicGroupBuilder::new()
.add_method_call(vault.who(target.address()).build(¶ms))
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
match &executed.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(value)) => {
assert_eq!(value, &AbiValue::Address(target.address()))
}
other => panic!("unexpected who return: {other:?}"),
}
}
#[tokio::test]
async fn array_arguments_sum_on_chain() {
use algonaut::atomic::AbiMethodReturnValue;
let algod = algod();
let (vault, _) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
let dynamic = vault.sum(vec![10u64, 20, 30]).build(¶ms);
let stat = vault.sum3([1u64, 2, 3]).build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(dynamic)
.add_method_call(stat)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
match &executed.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(v)) => assert_eq!(v, &AbiValue::from(60u64)),
other => panic!("unexpected sum return: {other:?}"),
}
match &executed.method_results[1].return_value {
Ok(AbiMethodReturnValue::Some(v)) => assert_eq!(v, &AbiValue::from(6u64)),
other => panic!("unexpected sum3 return: {other:?}"),
}
}
#[tokio::test]
async fn tuple_and_ufixed_arguments_round_trip_on_chain() {
use algonaut::abi::macro_support::Ufixed;
use algonaut::atomic::AbiMethodReturnValue;
let algod = algod();
let (vault, _) = deploy_vault(&algod, &kmd()).await;
let params = algod.suggested_params().await.unwrap();
let pair = vault.add_pair((4u64, 5u64)).build(¶ms);
let price = vault.price_raw(Ufixed::new(150u64)).build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(pair)
.add_method_call(price)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
match &executed.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(v)) => assert_eq!(v, &AbiValue::from(9u64)),
other => panic!("unexpected add_pair return: {other:?}"),
}
match &executed.method_results[1].return_value {
Ok(AbiMethodReturnValue::Some(v)) => assert_eq!(v, &AbiValue::from(150u64)),
other => panic!("unexpected price_raw return: {other:?}"),
}
}
#[tokio::test]
async fn box_put_get_round_trips() {
use algonaut::atomic::AbiMethodReturnValue;
use algonaut::core::MicroAlgos;
use algonaut::transaction::Pay;
let algod = algod();
let kmd = kmd();
let account = funded_account(&algod, &kmd).await;
let sender = account.address();
let signer: Arc<dyn Signer> = Arc::new(account);
let params = algod.suggested_params().await.unwrap();
let vault = Vault::deploy(&algod, sender, Arc::clone(&signer), ¶ms)
.await
.expect("deploy Vault");
let fund = Pay::new(sender, vault.app_id().address(), MicroAlgos(200_000))
.build(¶ms)
.expect("build app funding");
AtomicGroupBuilder::new()
.add_transaction(algonaut::atomic::TransactionWithSigner::new(
fund,
Arc::clone(&signer),
))
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
let put = vault
.box_put("k".to_owned(), 42)
.box_ref(b"k".to_vec())
.build(¶ms);
AtomicGroupBuilder::new()
.add_method_call(put)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
let get = vault
.box_get("k".to_owned())
.box_ref(b"k".to_vec())
.build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(get)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
match &executed.method_results[0].return_value {
Ok(AbiMethodReturnValue::Some(v)) => assert_eq!(v, &AbiValue::from(42u64)),
other => panic!("unexpected box_get return: {other:?}"),
}
assert_eq!(
vault.box_boxes(&algod, "k").await.unwrap(),
Some(AbiValue::from(42u64)),
"box-map accessor reads the box `k` written by box_put",
);
assert_eq!(
vault.box_boxes(&algod, "missing").await.unwrap(),
None,
"an unwritten box reads as None",
);
}
#[tokio::test]
async fn payment_transaction_argument_round_trips() {
use algonaut::atomic::TransactionWithSigner;
use algonaut::core::MicroAlgos;
use algonaut::transaction::Pay;
let algod = algod();
let kmd = kmd();
let account = funded_account(&algod, &kmd).await;
let sender = account.address();
let signer: Arc<dyn Signer> = Arc::new(account);
let params = algod.suggested_params().await.unwrap();
let vault = Vault::deploy(&algod, sender, Arc::clone(&signer), ¶ms)
.await
.expect("deploy Vault");
let pay = Pay::new(sender, vault.app_id().address(), MicroAlgos(200_000))
.build(¶ms)
.expect("build pay");
let call = vault
.deposit(
TransactionWithSigner::new(pay, Arc::clone(&signer)),
200_000u64,
)
.build(¶ms);
let executed = AtomicGroupBuilder::new()
.add_method_call(call)
.build()
.unwrap()
.sign()
.await
.unwrap()
.execute(&algod)
.await
.unwrap();
assert!(
executed.confirmed_round.is_some(),
"the grouped payment + method call should confirm"
);
}