diff --git a/.dockerignore b/.dockerignore index e3f1919d5c3d8..9cd118c7665ab 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,6 +22,7 @@ !aptos-move/aptos-release-builder/data/release.yaml !aptos-move/aptos-release-builder/data/proposals/* !aptos-move/framework/ +!aptos-move/move-examples/hello_blockchain/ !crates/aptos/src/move_tool/*.bpl !crates/aptos-faucet/doc/ !api/doc/ diff --git a/Cargo.lock b/Cargo.lock index 41110d2023de8..2636c5e223589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,6 +411,31 @@ dependencies = [ "warp-reverse-proxy", ] +[[package]] +name = "aptos-api-tester" +version = "0.1.0" +dependencies = [ + "anyhow", + "aptos-api-types", + "aptos-cached-packages", + "aptos-framework", + "aptos-logger", + "aptos-network", + "aptos-push-metrics", + "aptos-rest-client", + "aptos-sdk", + "aptos-types", + "futures", + "move-core-types", + "once_cell", + "prometheus", + "rand 0.7.3", + "serde 1.0.149", + "serde_json", + "tokio", + "url", +] + [[package]] name = "aptos-api-types" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 13c1254ecd220..a98e249e53ad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "consensus/consensus-types", "consensus/safety-rules", "crates/aptos", + "crates/aptos-api-tester", "crates/aptos-bitvec", "crates/aptos-build-info", "crates/aptos-compression", diff --git a/crates/aptos-api-tester/Cargo.toml b/crates/aptos-api-tester/Cargo.toml new file mode 100644 index 0000000000000..33177e2f18daa --- /dev/null +++ b/crates/aptos-api-tester/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "aptos-api-tester" +description = "Aptos developer API tester" +version = "0.1.0" + +# Workspace inherited keys +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +aptos-api-types = { workspace = true } +aptos-cached-packages = { workspace = true } +aptos-framework = { workspace = true } +aptos-logger = { workspace = true } +aptos-network = { workspace = true } +aptos-push-metrics = { workspace = true } +aptos-rest-client = { workspace = true } +aptos-sdk = { workspace = true } +aptos-types = { workspace = true } +futures = { workspace = true } +move-core-types = { workspace = true } +once_cell = { workspace = true } +prometheus = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +url = { workspace = true } diff --git a/crates/aptos-api-tester/src/consts.rs b/crates/aptos-api-tester/src/consts.rs new file mode 100644 index 0000000000000..76fd1a022d169 --- /dev/null +++ b/crates/aptos-api-tester/src/consts.rs @@ -0,0 +1,68 @@ +// Copyright © Aptos Foundation + +use crate::utils::NetworkName; +use once_cell::sync::Lazy; +use std::{env, time::Duration}; +use url::Url; + +// Node and faucet constants + +// TODO: consider making this a CLI argument +pub static NETWORK_NAME: Lazy = Lazy::new(|| { + env::var("NETWORK_NAME") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(NetworkName::Devnet) +}); + +pub static DEVNET_NODE_URL: Lazy = + Lazy::new(|| Url::parse("https://fullnode.devnet.aptoslabs.com").unwrap()); + +pub static DEVNET_FAUCET_URL: Lazy = + Lazy::new(|| Url::parse("https://faucet.devnet.aptoslabs.com").unwrap()); + +pub static TESTNET_NODE_URL: Lazy = + Lazy::new(|| Url::parse("https://fullnode.testnet.aptoslabs.com").unwrap()); + +pub static TESTNET_FAUCET_URL: Lazy = + Lazy::new(|| Url::parse("https://faucet.testnet.aptoslabs.com").unwrap()); + +pub const FUND_AMOUNT: u64 = 100_000_000; + +// Persistency check constants + +// How long a persistent check runs for. +pub static PERSISTENCY_TIMEOUT: Lazy = Lazy::new(|| { + env::var("PERSISTENCY_TIMEOUT") + .ok() + .and_then(|s| s.parse().ok()) + .map(Duration::from_secs) + .unwrap_or(Duration::from_secs(30)) +}); + +// Wait time between tries during a persistent check. +pub static SLEEP_PER_CYCLE: Lazy = Lazy::new(|| { + env::var("SLEEP_PER_CYCLE") + .ok() + .and_then(|s| s.parse().ok()) + .map(Duration::from_millis) + .unwrap_or(Duration::from_millis(100)) +}); + +// Runtime constants + +// The number of threads to use for running tests. +pub static NUM_THREADS: Lazy = Lazy::new(|| { + env::var("NUM_THREADS") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(4) +}); + +// The size of the stack for each thread. +pub static STACK_SIZE: Lazy = Lazy::new(|| { + env::var("STACK_SIZE") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(4 * 1024 * 1024) +}); diff --git a/crates/aptos-api-tester/src/counters.rs b/crates/aptos-api-tester/src/counters.rs new file mode 100644 index 0000000000000..1f6305dd644b6 --- /dev/null +++ b/crates/aptos-api-tester/src/counters.rs @@ -0,0 +1,75 @@ +// Copyright © Aptos Foundation + +use once_cell::sync::Lazy; +use prometheus::{register_histogram_vec, Histogram, HistogramVec}; + +pub static API_TEST_SUCCESS: Lazy = Lazy::new(|| { + register_histogram_vec!( + "api_test_success", + "Number of user flows which succesfully passed", + &["test_name", "network_name", "run_id"], + ) + .unwrap() +}); + +pub fn test_success(test_name: &str, network_name: &str, run_id: &str) -> Histogram { + API_TEST_SUCCESS.with_label_values(&[test_name, network_name, run_id]) +} + +pub static API_TEST_FAIL: Lazy = Lazy::new(|| { + register_histogram_vec!( + "api_test_fail", + "Number of user flows which failed checks", + &["test_name", "network_name", "run_id"], + ) + .unwrap() +}); + +pub fn test_fail(test_name: &str, network_name: &str, run_id: &str) -> Histogram { + API_TEST_FAIL.with_label_values(&[test_name, network_name, run_id]) +} + +pub static API_TEST_ERROR: Lazy = Lazy::new(|| { + register_histogram_vec!("api_test_error", "Number of user flows which crashed", &[ + "test_name", + "network_name", + "run_id" + ],) + .unwrap() +}); + +pub fn test_error(test_name: &str, network_name: &str, run_id: &str) -> Histogram { + API_TEST_ERROR.with_label_values(&[test_name, network_name, run_id]) +} + +pub static API_TEST_LATENCY: Lazy = Lazy::new(|| { + register_histogram_vec!( + "api_test_latency", + "Time it takes to complete a user flow", + &["test_name", "network_name", "run_id", "result"], + ) + .unwrap() +}); + +pub fn test_latency(test_name: &str, network_name: &str, run_id: &str, result: &str) -> Histogram { + API_TEST_LATENCY.with_label_values(&[test_name, network_name, run_id, result]) +} + +pub static API_TEST_STEP_LATENCY: Lazy = Lazy::new(|| { + register_histogram_vec!( + "api_test_step_latency", + "Time it takes to complete a user flow step", + &["test_name", "step_name", "network_name", "run_id", "result"], + ) + .unwrap() +}); + +pub fn test_step_latency( + test_name: &str, + step_name: &str, + network_name: &str, + run_id: &str, + result: &str, +) -> Histogram { + API_TEST_STEP_LATENCY.with_label_values(&[test_name, step_name, network_name, run_id, result]) +} diff --git a/crates/aptos-api-tester/src/macros.rs b/crates/aptos-api-tester/src/macros.rs new file mode 100644 index 0000000000000..f40d2fd093b58 --- /dev/null +++ b/crates/aptos-api-tester/src/macros.rs @@ -0,0 +1,18 @@ +// Copyright © Aptos Foundation + +#[macro_export] +macro_rules! time_fn { + ($func:expr, $($arg:expr), *) => {{ + // start timer + let start = tokio::time::Instant::now(); + + // call the flow + let result = $func($($arg),+).await; + + // end timer + let time = (tokio::time::Instant::now() - start).as_micros() as f64; + + // return + (result, time) + }}; +} diff --git a/crates/aptos-api-tester/src/main.rs b/crates/aptos-api-tester/src/main.rs new file mode 100644 index 0000000000000..947bb07e7ef3f --- /dev/null +++ b/crates/aptos-api-tester/src/main.rs @@ -0,0 +1,97 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +#![forbid(unsafe_code)] + +mod consts; +mod counters; +mod persistent_check; +mod strings; +mod tests; +mod tokenv1_client; +mod utils; +#[macro_use] +mod macros; + +use crate::utils::{NetworkName, TestName}; +use anyhow::Result; +use aptos_logger::{info, Level, Logger}; +use aptos_push_metrics::MetricsPusher; +use consts::{NETWORK_NAME, NUM_THREADS, STACK_SIZE}; +use futures::future::join_all; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::runtime::{Builder, Runtime}; + +async fn test_flows(runtime: &Runtime, network_name: NetworkName) -> Result<()> { + let run_id = SystemTime::now() + .duration_since(UNIX_EPOCH)? + .as_secs() + .to_string(); + info!( + "----- STARTING TESTS FOR {} WITH RUN ID {} -----", + network_name.to_string(), + run_id + ); + + // Flow 1: New account + let test_time = run_id.clone(); + let handle_newaccount = runtime.spawn(async move { + TestName::NewAccount.run(network_name, &test_time).await; + }); + + // Flow 2: Coin transfer + let test_time = run_id.clone(); + let handle_cointransfer = runtime.spawn(async move { + TestName::CoinTransfer.run(network_name, &test_time).await; + }); + + // Flow 3: NFT transfer + let test_time = run_id.clone(); + let handle_nfttransfer = runtime.spawn(async move { + TestName::TokenV1Transfer + .run(network_name, &test_time) + .await; + }); + + // Flow 4: Publishing module + let test_time = run_id.clone(); + let handle_publishmodule = runtime.spawn(async move { + TestName::PublishModule.run(network_name, &test_time).await; + }); + + // Flow 5: View function + let test_time = run_id.clone(); + let handle_viewfunction = runtime.spawn(async move { + TestName::ViewFunction.run(network_name, &test_time).await; + }); + + join_all(vec![ + handle_newaccount, + handle_cointransfer, + handle_nfttransfer, + handle_publishmodule, + handle_viewfunction, + ]) + .await; + Ok(()) +} + +fn main() -> Result<()> { + // create runtime + let runtime = Builder::new_multi_thread() + .worker_threads(*NUM_THREADS) + .enable_all() + .thread_stack_size(*STACK_SIZE) + .build()?; + + // log metrics + Logger::builder().level(Level::Info).build(); + let _mp = MetricsPusher::start_for_local_run("api-tester"); + + // run tests + runtime.block_on(async { + let _ = test_flows(&runtime, *NETWORK_NAME).await; + }); + + Ok(()) +} diff --git a/crates/aptos-api-tester/src/persistent_check.rs b/crates/aptos-api-tester/src/persistent_check.rs new file mode 100644 index 0000000000000..5a000c125ceee --- /dev/null +++ b/crates/aptos-api-tester/src/persistent_check.rs @@ -0,0 +1,226 @@ +// Copyright © Aptos Foundation + +// Persistent checking is a mechanism to increase tolerancy to eventual consistency issues. In our +// earlier tests we have observed that parallel runs of the flows returned higher failure rates +// than serial runs, and these extra failures displayed the following pattern: 1) the flow submits +// a transaction to the API (such as account creation), 2) the flow reads the state from the API, +// and gets a result that does not include the transaction. We attribute this to the second call +// ending up on a different node which is not yet up to sync. Therefore, for state checks, we +// repeat the whole check for a period of time until it is successful, and throw a failure only if +// it fails to succeed. Note that every time a check fails we will still get a failure log. + +// TODO: The need for having a different persistent check wrapper for each function signature is +// due to a lack of overloading in Rust. Consider using macros to reduce code duplication. + +use crate::{ + consts::{PERSISTENCY_TIMEOUT, SLEEP_PER_CYCLE}, + strings::ERROR_COULD_NOT_CHECK, + tokenv1_client::TokenClient, + utils::TestFailure, +}; +use anyhow::anyhow; +use aptos_api_types::HexEncodedBytes; +use aptos_rest_client::Client; +use aptos_sdk::types::LocalAccount; +use aptos_types::account_address::AccountAddress; +use futures::Future; +use tokio::time::{sleep, Instant}; + +pub async fn account<'a, 'b, F, Fut>( + step: &str, + f: F, + client: &'a Client, + account: &'b LocalAccount, +) -> Result<(), TestFailure> +where + F: Fn(&'a Client, &'b LocalAccount) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(client, account).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +pub async fn address<'a, F, Fut>( + step: &str, + f: F, + client: &'a Client, + address: AccountAddress, +) -> Result<(), TestFailure> +where + F: Fn(&'a Client, AccountAddress) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(client, address).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +pub async fn address_address<'a, F, Fut>( + step: &str, + f: F, + client: &'a Client, + address: AccountAddress, + address2: AccountAddress, +) -> Result<(), TestFailure> +where + F: Fn(&'a Client, AccountAddress, AccountAddress) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(client, address, address2).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +pub async fn address_bytes<'a, 'b, F, Fut>( + step: &str, + f: F, + client: &'a Client, + address: AccountAddress, + bytes: &'b HexEncodedBytes, +) -> Result<(), TestFailure> +where + F: Fn(&'a Client, AccountAddress, &'b HexEncodedBytes) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(client, address, bytes).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +pub async fn address_version<'a, F, Fut>( + step: &str, + f: F, + client: &'a Client, + address: AccountAddress, + version: u64, +) -> Result<(), TestFailure> +where + F: Fn(&'a Client, AccountAddress, u64) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(client, address, version).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +pub async fn token_address<'a, F, Fut>( + step: &str, + f: F, + token_client: &'a TokenClient<'a>, + address: AccountAddress, +) -> Result<(), TestFailure> +where + F: Fn(&'a TokenClient<'a>, AccountAddress) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(token_client, address).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +pub async fn token_address_address<'a, F, Fut>( + step: &str, + f: F, + token_client: &'a TokenClient<'a>, + address: AccountAddress, + address2: AccountAddress, +) -> Result<(), TestFailure> +where + F: Fn(&'a TokenClient<'a>, AccountAddress, AccountAddress) -> Fut, + Fut: Future>, +{ + // set a default error in case checks never start + let mut result: Result<(), TestFailure> = Err(could_not_check(step)); + let timer = Instant::now(); + + // try to get a good result + while Instant::now().duration_since(timer) < *PERSISTENCY_TIMEOUT { + result = f(token_client, address, address2).await; + if result.is_ok() { + break; + } + sleep(*SLEEP_PER_CYCLE).await; + } + + // return last failure if no good result occurs + result +} + +// Utils + +fn could_not_check(step: &str) -> TestFailure { + anyhow!(format!("{} in step: {}", ERROR_COULD_NOT_CHECK, step)).into() +} diff --git a/crates/aptos-api-tester/src/strings.rs b/crates/aptos-api-tester/src/strings.rs new file mode 100644 index 0000000000000..99dbb40c312a9 --- /dev/null +++ b/crates/aptos-api-tester/src/strings.rs @@ -0,0 +1,59 @@ +// Copyright © Aptos Foundation + +// Fail messages + +pub const FAIL_WRONG_ACCOUNT_DATA: &str = "wrong account data"; +pub const FAIL_WRONG_BALANCE: &str = "wrong balance"; +pub const FAIL_WRONG_BALANCE_AT_VERSION: &str = "wrong balance at version"; +pub const FAIL_WRONG_COLLECTION_DATA: &str = "wrong collection data"; +pub const FAIL_WRONG_MESSAGE: &str = "wrong message"; +pub const FAIL_WRONG_MODULE: &str = "wrong module"; +pub const FAIL_WRONG_TOKEN_BALANCE: &str = "wrong token balance"; +pub const FAIL_WRONG_TOKEN_DATA: &str = "wrong token data"; + +// Error messages + +pub const ERROR_BAD_BALANCE_STRING: &str = "bad balance string"; +pub const ERROR_COULD_NOT_BUILD_PACKAGE: &str = "failed to build package"; +pub const ERROR_COULD_NOT_CHECK: &str = "persistency check never started"; +pub const ERROR_COULD_NOT_CREATE_ACCOUNT: &str = "failed to create account"; +pub const ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION: &str = + "failed to create and submit transaction"; +pub const ERROR_COULD_NOT_FINISH_TRANSACTION: &str = "failed to finish transaction"; +pub const ERROR_COULD_NOT_FUND_ACCOUNT: &str = "failed to fund account"; +pub const ERROR_COULD_NOT_SERIALIZE: &str = "failed to serialize"; +pub const ERROR_COULD_NOT_VIEW: &str = "view function failed"; +pub const ERROR_NO_ACCOUNT_DATA: &str = "can't find account data"; +pub const ERROR_NO_BALANCE: &str = "can't find account balance"; +pub const ERROR_NO_BALANCE_STRING: &str = "the API did not return a balance string"; +pub const ERROR_NO_BYTECODE: &str = "can't find bytecode"; +pub const ERROR_NO_COLLECTION_DATA: &str = "can't find collection data"; +pub const ERROR_NO_MESSAGE: &str = "can't find message"; +pub const ERROR_NO_METADATA: &str = "can't find metadata"; +pub const ERROR_NO_MODULE: &str = "can't find module"; +pub const ERROR_NO_TOKEN_BALANCE: &str = "can't find token balance"; +pub const ERROR_NO_TOKEN_DATA: &str = "can't find token data"; +pub const ERROR_NO_VERSION: &str = "can't find transaction version"; + +// Step names + +pub const SETUP: &str = "setup"; +pub const CHECK_ACCOUNT_DATA: &str = "check_account_data"; +pub const FUND: &str = "fund"; +pub const CHECK_ACCOUNT_BALANCE: &str = "check_account_balance"; +pub const TRANSFER_COINS: &str = "transfer_coins"; +pub const CHECK_ACCOUNT_BALANCE_AT_VERSION: &str = "check_account_balance_at_version"; +pub const CREATE_COLLECTION: &str = "create_collection"; +pub const CHECK_COLLECTION_METADATA: &str = "check_collection_metadata"; +pub const CREATE_TOKEN: &str = "create_token"; +pub const CHECK_TOKEN_METADATA: &str = "check_token_metadata"; +pub const CHECK_SENDER_BALANCE: &str = "check_sender_balance"; +pub const OFFER_TOKEN: &str = "offer_token"; +pub const CLAIM_TOKEN: &str = "claim_token"; +pub const CHECK_RECEIVER_BALANCE: &str = "check_receiver_balance"; +pub const BUILD_MODULE: &str = "build_module"; +pub const PUBLISH_MODULE: &str = "publish_module"; +pub const CHECK_MODULE_DATA: &str = "check_module_data"; +pub const SET_MESSAGE: &str = "set_message"; +pub const CHECK_MESSAGE: &str = "check_message"; +pub const CHECK_VIEW_ACCOUNT_BALANCE: &str = "check_view_account_balance"; diff --git a/crates/aptos-api-tester/src/tests/coin_transfer.rs b/crates/aptos-api-tester/src/tests/coin_transfer.rs new file mode 100644 index 0000000000000..3496a59ff662f --- /dev/null +++ b/crates/aptos-api-tester/src/tests/coin_transfer.rs @@ -0,0 +1,265 @@ +// Copyright © Aptos Foundation + +use crate::{ + consts::FUND_AMOUNT, + persistent_check, + strings::{ + CHECK_ACCOUNT_BALANCE, CHECK_ACCOUNT_BALANCE_AT_VERSION, CHECK_ACCOUNT_DATA, + ERROR_COULD_NOT_CREATE_ACCOUNT, ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, + ERROR_COULD_NOT_FINISH_TRANSACTION, ERROR_COULD_NOT_FUND_ACCOUNT, ERROR_NO_BALANCE, + ERROR_NO_VERSION, FAIL_WRONG_BALANCE, FAIL_WRONG_BALANCE_AT_VERSION, SETUP, TRANSFER_COINS, + }, + time_fn, + utils::{ + check_balance, create_account, create_and_fund_account, emit_step_metrics, NetworkName, + TestFailure, TestName, + }, +}; +use anyhow::{anyhow, Result}; +use aptos_api_types::U64; +use aptos_logger::error; +use aptos_rest_client::Client; +use aptos_sdk::{coin_client::CoinClient, types::LocalAccount}; +use aptos_types::account_address::AccountAddress; + +const TRANSFER_AMOUNT: u64 = 1_000; + +/// Tests coin transfer. Checks that: +/// - receiver balance reflects transferred amount +/// - receiver balance shows correct amount at the previous version +pub async fn test(network_name: NetworkName, run_id: &str) -> Result<(), TestFailure> { + // setup + let (client, mut account, receiver) = emit_step_metrics( + time_fn!(setup, network_name), + TestName::CoinTransfer, + SETUP, + network_name, + run_id, + )?; + let coin_client = CoinClient::new(&client); + + // persistently check that API returns correct account data (auth key and sequence number) + emit_step_metrics( + time_fn!( + persistent_check::address_address, + CHECK_ACCOUNT_DATA, + check_account_data, + &client, + account.address(), + receiver + ), + TestName::CoinTransfer, + CHECK_ACCOUNT_DATA, + network_name, + run_id, + )?; + + // transfer coins to the receiver + let version = emit_step_metrics( + time_fn!( + transfer_coins, + &client, + &coin_client, + &mut account, + receiver + ), + TestName::CoinTransfer, + TRANSFER_COINS, + network_name, + run_id, + )?; + + // persistently check that receiver balance is correct + emit_step_metrics( + time_fn!( + persistent_check::address, + CHECK_ACCOUNT_BALANCE, + check_account_balance, + &client, + receiver + ), + TestName::CoinTransfer, + CHECK_ACCOUNT_BALANCE, + network_name, + run_id, + )?; + + // persistently check that account balance is correct at previoud version + emit_step_metrics( + time_fn!( + persistent_check::address_version, + CHECK_ACCOUNT_BALANCE_AT_VERSION, + check_account_balance_at_version, + &client, + receiver, + version + ), + TestName::CoinTransfer, + CHECK_ACCOUNT_BALANCE_AT_VERSION, + network_name, + run_id, + )?; + + Ok(()) +} + +// Steps + +async fn setup( + network_name: NetworkName, +) -> Result<(Client, LocalAccount, AccountAddress), TestFailure> { + // spin up clients + let client = network_name.get_client(); + let faucet_client = network_name.get_faucet_client(); + + // create account + let account = match create_and_fund_account(&faucet_client, TestName::CoinTransfer).await { + Ok(account) => account, + Err(e) => { + error!( + "test: coin_transfer part: setup ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FUND_ACCOUNT, e + ); + return Err(e.into()); + }, + }; + + // create receiver + let receiver = match create_account(&faucet_client, TestName::CoinTransfer).await { + Ok(account) => account.address(), + Err(e) => { + error!( + "test: coin_transfer part: setup ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_ACCOUNT, e + ); + return Err(e.into()); + }, + }; + + Ok((client, account, receiver)) +} + +async fn check_account_data( + client: &Client, + account: AccountAddress, + receiver: AccountAddress, +) -> Result<(), TestFailure> { + check_balance(TestName::CoinTransfer, client, account, U64(FUND_AMOUNT)).await?; + check_balance(TestName::CoinTransfer, client, receiver, U64(0)).await?; + + Ok(()) +} + +async fn transfer_coins( + client: &Client, + coin_client: &CoinClient<'_>, + account: &mut LocalAccount, + receiver: AccountAddress, +) -> Result { + // create transaction + let pending_txn = match coin_client + .transfer(account, receiver, TRANSFER_AMOUNT, None) + .await + { + Ok(pending_txn) => pending_txn, + Err(e) => { + error!( + "test: coin_transfer part: transfer_coins ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait and get version + let response = match client.wait_for_transaction(&pending_txn).await { + Ok(response) => response, + Err(e) => { + error!( + "test: coin_transfer part: transfer_coins ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + let version = match response.inner().version() { + Some(version) => version, + None => { + error!( + "test: coin_transfer part: transfer_coins ERROR: {}", + ERROR_NO_VERSION + ); + return Err(anyhow!(ERROR_NO_VERSION).into()); + }, + }; + + // return version + Ok(version) +} + +async fn check_account_balance( + client: &Client, + address: AccountAddress, +) -> Result<(), TestFailure> { + // expected + let expected = U64(TRANSFER_AMOUNT); + + // actual + let actual = match client.get_account_balance(address).await { + Ok(response) => response.into_inner().coin.value, + Err(e) => { + error!( + "test: coin_transfer part: check_account_balance ERROR: {}, with error {:?}", + ERROR_NO_BALANCE, e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: coin_transfer part: check_account_balance FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_BALANCE, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_BALANCE)); + } + + Ok(()) +} + +async fn check_account_balance_at_version( + client: &Client, + address: AccountAddress, + transaction_version: u64, +) -> Result<(), TestFailure> { + // expected + let expected = U64(0); + + // actual + let actual = match client + .get_account_balance_at_version(address, transaction_version - 1) + .await + { + Ok(response) => response.into_inner().coin.value, + Err(e) => { + error!( + "test: coin_transfer part: check_account_balance_at_version ERROR: {}, with error {:?}", + ERROR_NO_BALANCE, e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: coin_transfer part: check_account_balance_at_version FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_BALANCE_AT_VERSION, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_BALANCE_AT_VERSION)); + } + + Ok(()) +} diff --git a/crates/aptos-api-tester/src/tests/mod.rs b/crates/aptos-api-tester/src/tests/mod.rs new file mode 100644 index 0000000000000..73e66cdf0ef58 --- /dev/null +++ b/crates/aptos-api-tester/src/tests/mod.rs @@ -0,0 +1,7 @@ +// Copyright © Aptos Foundation + +pub mod coin_transfer; +pub mod new_account; +pub mod publish_module; +pub mod tokenv1_transfer; +pub mod view_function; diff --git a/crates/aptos-api-tester/src/tests/new_account.rs b/crates/aptos-api-tester/src/tests/new_account.rs new file mode 100644 index 0000000000000..feff0153c74c1 --- /dev/null +++ b/crates/aptos-api-tester/src/tests/new_account.rs @@ -0,0 +1,147 @@ +// Copyright © Aptos Foundation + +use crate::{ + consts::FUND_AMOUNT, + persistent_check, + strings::{ + CHECK_ACCOUNT_BALANCE, CHECK_ACCOUNT_DATA, ERROR_COULD_NOT_CREATE_ACCOUNT, + ERROR_COULD_NOT_FUND_ACCOUNT, ERROR_NO_ACCOUNT_DATA, FAIL_WRONG_ACCOUNT_DATA, FUND, SETUP, + }, + time_fn, + utils::{check_balance, create_account, emit_step_metrics, NetworkName, TestFailure, TestName}, +}; +use aptos_api_types::U64; +use aptos_logger::error; +use aptos_rest_client::{Account, Client, FaucetClient}; +use aptos_sdk::types::LocalAccount; +use aptos_types::account_address::AccountAddress; + +/// Tests new account creation. Checks that: +/// - account data exists +/// - account balance reflects funded amount +pub async fn test(network_name: NetworkName, run_id: &str) -> Result<(), TestFailure> { + // setup + let (client, faucet_client, account) = emit_step_metrics( + time_fn!(setup, network_name), + TestName::NewAccount, + SETUP, + network_name, + run_id, + )?; + + // persistently check that API returns correct account data (auth key and sequence number) + emit_step_metrics( + time_fn!( + persistent_check::account, + CHECK_ACCOUNT_DATA, + check_account_data, + &client, + &account + ), + TestName::NewAccount, + CHECK_ACCOUNT_DATA, + network_name, + run_id, + )?; + + // fund account + emit_step_metrics( + time_fn!(fund, &faucet_client, account.address()), + TestName::NewAccount, + FUND, + network_name, + run_id, + )?; + + // persistently check that account balance is correct + emit_step_metrics( + time_fn!( + persistent_check::address, + CHECK_ACCOUNT_BALANCE, + check_account_balance, + &client, + account.address() + ), + TestName::NewAccount, + CHECK_ACCOUNT_BALANCE, + network_name, + run_id, + )?; + + Ok(()) +} + +// Steps + +async fn setup( + network_name: NetworkName, +) -> Result<(Client, FaucetClient, LocalAccount), TestFailure> { + // spin up clients + let client = network_name.get_client(); + let faucet_client = network_name.get_faucet_client(); + + // create account + let account = match create_account(&faucet_client, TestName::NewAccount).await { + Ok(account) => account, + Err(e) => { + error!( + "test: new_account part: setup ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_ACCOUNT, e + ); + return Err(e.into()); + }, + }; + + Ok((client, faucet_client, account)) +} + +async fn fund(faucet_client: &FaucetClient, address: AccountAddress) -> Result<(), TestFailure> { + // fund account + if let Err(e) = faucet_client.fund(address, FUND_AMOUNT).await { + error!( + "test: new_account part: fund ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FUND_ACCOUNT, e + ); + return Err(e.into()); + } + + Ok(()) +} + +async fn check_account_data(client: &Client, account: &LocalAccount) -> Result<(), TestFailure> { + // expected + let expected = Account { + authentication_key: account.authentication_key(), + sequence_number: account.sequence_number(), + }; + + // actual + let actual = match client.get_account(account.address()).await { + Ok(response) => response.into_inner(), + Err(e) => { + error!( + "test: new_account part: check_account_data ERROR: {}, with error {:?}", + ERROR_NO_ACCOUNT_DATA, e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: new_account part: check_account_data FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_ACCOUNT_DATA, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_ACCOUNT_DATA)); + } + + Ok(()) +} + +async fn check_account_balance( + client: &Client, + address: AccountAddress, +) -> Result<(), TestFailure> { + check_balance(TestName::NewAccount, client, address, U64(FUND_AMOUNT)).await +} diff --git a/crates/aptos-api-tester/src/tests/publish_module.rs b/crates/aptos-api-tester/src/tests/publish_module.rs new file mode 100644 index 0000000000000..620395a29bbfc --- /dev/null +++ b/crates/aptos-api-tester/src/tests/publish_module.rs @@ -0,0 +1,385 @@ +// Copyright © Aptos Foundation + +use crate::{ + consts::FUND_AMOUNT, + persistent_check, + strings::{ + BUILD_MODULE, CHECK_ACCOUNT_DATA, CHECK_MESSAGE, CHECK_MODULE_DATA, + ERROR_COULD_NOT_BUILD_PACKAGE, ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, + ERROR_COULD_NOT_FINISH_TRANSACTION, ERROR_COULD_NOT_FUND_ACCOUNT, + ERROR_COULD_NOT_SERIALIZE, ERROR_NO_BYTECODE, ERROR_NO_MESSAGE, ERROR_NO_METADATA, + ERROR_NO_MODULE, FAIL_WRONG_MESSAGE, FAIL_WRONG_MODULE, PUBLISH_MODULE, SETUP, SET_MESSAGE, + }, + time_fn, + tokenv1_client::{build_and_submit_transaction, TransactionOptions}, + utils::{ + check_balance, create_and_fund_account, emit_step_metrics, NetworkName, TestFailure, + TestName, + }, +}; +use anyhow::{anyhow, Result}; +use aptos_api_types::{HexEncodedBytes, U64}; +use aptos_cached_packages::aptos_stdlib::EntryFunctionCall; +use aptos_framework::{BuildOptions, BuiltPackage}; +use aptos_logger::error; +use aptos_rest_client::Client; +use aptos_sdk::{bcs, types::LocalAccount}; +use aptos_types::{ + account_address::AccountAddress, + transaction::{EntryFunction, TransactionPayload}, +}; +use move_core_types::{ident_str, language_storage::ModuleId}; +use std::{collections::BTreeMap, path::PathBuf}; + +static MODULE_NAME: &str = "message"; +static TEST_MESSAGE: &str = "test message"; + +/// Tests module publishing and interaction. Checks that: +/// - can publish module +/// - module data exists +/// - can interact with module +/// - interaction is reflected correctly +pub async fn test(network_name: NetworkName, run_id: &str) -> Result<(), TestFailure> { + // setup + let (client, mut account) = emit_step_metrics( + time_fn!(setup, network_name), + TestName::PublishModule, + SETUP, + network_name, + run_id, + )?; + + // persistently check that API returns correct account data (auth key and sequence number) + emit_step_metrics( + time_fn!( + persistent_check::address, + CHECK_ACCOUNT_DATA, + check_account_data, + &client, + account.address() + ), + TestName::PublishModule, + CHECK_ACCOUNT_DATA, + network_name, + run_id, + )?; + + // build module + let package = emit_step_metrics( + time_fn!(build_module, account.address()), + TestName::PublishModule, + BUILD_MODULE, + network_name, + run_id, + )?; + + // publish module + let blob = emit_step_metrics( + time_fn!(publish_module, &client, &mut account, package), + TestName::PublishModule, + PUBLISH_MODULE, + network_name, + run_id, + )?; + + // persistently check that API returns correct module package data + emit_step_metrics( + time_fn!( + persistent_check::address_bytes, + CHECK_MODULE_DATA, + check_module_data, + &client, + account.address(), + &blob + ), + TestName::PublishModule, + CHECK_MODULE_DATA, + network_name, + run_id, + )?; + + // set message + emit_step_metrics( + time_fn!(set_message, &client, &mut account), + TestName::PublishModule, + SET_MESSAGE, + network_name, + run_id, + )?; + + // persistently check that the message is correct + emit_step_metrics( + time_fn!( + persistent_check::address, + CHECK_MESSAGE, + check_message, + &client, + account.address() + ), + TestName::PublishModule, + CHECK_MESSAGE, + network_name, + run_id, + )?; + + Ok(()) +} + +// Steps + +async fn setup(network_name: NetworkName) -> Result<(Client, LocalAccount), TestFailure> { + // spin up clients + let client = network_name.get_client(); + let faucet_client = network_name.get_faucet_client(); + + // create account + let account = match create_and_fund_account(&faucet_client, TestName::PublishModule).await { + Ok(account) => account, + Err(e) => { + error!( + "test: publish_module part: setup ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FUND_ACCOUNT, e + ); + return Err(e.into()); + }, + }; + + Ok((client, account)) +} + +async fn check_account_data(client: &Client, account: AccountAddress) -> Result<(), TestFailure> { + check_balance(TestName::PublishModule, client, account, U64(FUND_AMOUNT)).await?; + + Ok(()) +} + +async fn build_module(address: AccountAddress) -> Result { + // get file to compile + let move_dir = PathBuf::from("./aptos-move/move-examples/hello_blockchain"); + + // insert address + let mut named_addresses: BTreeMap = BTreeMap::new(); + named_addresses.insert("hello_blockchain".to_string(), address); + + // build options + let options = BuildOptions { + named_addresses, + ..BuildOptions::default() + }; + + // build module + let package = match BuiltPackage::build(move_dir, options) { + Ok(package) => package, + Err(e) => { + error!( + "test: publish_module part: publish_module ERROR: {}, with error {:?}", + ERROR_COULD_NOT_BUILD_PACKAGE, e + ); + return Err(e.into()); + }, + }; + + Ok(package) +} + +async fn publish_module( + client: &Client, + account: &mut LocalAccount, + package: BuiltPackage, +) -> Result { + // get bytecode + let blobs = package.extract_code(); + + // get metadata + let metadata = match package.extract_metadata() { + Ok(data) => data, + Err(e) => { + error!( + "test: publish_module part: publish_module ERROR: {}, with error {:?}", + ERROR_NO_METADATA, e + ); + return Err(e.into()); + }, + }; + + // serialize metadata + let metadata_serialized = match bcs::to_bytes(&metadata) { + Ok(data) => data, + Err(e) => { + error!( + "test: publish_module part: publish_module ERROR: {}, with error {:?}", + ERROR_COULD_NOT_SERIALIZE, e + ); + return Err(anyhow!(e).into()); + }, + }; + + // create payload + let payload: aptos_types::transaction::TransactionPayload = + EntryFunctionCall::CodePublishPackageTxn { + metadata_serialized, + code: blobs.clone(), + } + .encode(); + + // create transaction + let pending_txn = + match build_and_submit_transaction(client, account, payload, TransactionOptions::default()) + .await + { + Ok(txn) => txn, + Err(e) => { + error!( + "test: publish_module part: publish_module ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait for transaction to finish + if let Err(e) = client.wait_for_transaction(&pending_txn).await { + error!( + "test: publish_module part: publish_module ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }; + + // get blob for later comparison + let blob = match blobs.get(0) { + Some(bytecode) => HexEncodedBytes::from(bytecode.clone()), + None => { + error!( + "test: publish_module part: publish_module ERROR: {}", + ERROR_NO_BYTECODE + ); + return Err(anyhow!(ERROR_NO_BYTECODE).into()); + }, + }; + + Ok(blob) +} + +async fn check_module_data( + client: &Client, + address: AccountAddress, + expected: &HexEncodedBytes, +) -> Result<(), TestFailure> { + // actual + let response = match client.get_account_module(address, MODULE_NAME).await { + Ok(response) => response, + Err(e) => { + error!( + "test: publish_module part: check_module_data ERROR: {}, with error {:?}", + ERROR_NO_MODULE, e + ); + return Err(e.into()); + }, + }; + let actual = &response.inner().bytecode; + + // compare + if expected != actual { + error!( + "test: publish_module part: check_module_data FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_MODULE, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_MODULE)); + } + + Ok(()) +} + +async fn set_message(client: &Client, account: &mut LocalAccount) -> Result<(), TestFailure> { + // set up message + let message = match bcs::to_bytes(TEST_MESSAGE) { + Ok(data) => data, + Err(e) => { + error!( + "test: publish_module part: set_message ERROR: {}, with error {:?}", + ERROR_COULD_NOT_SERIALIZE, e + ); + return Err(anyhow!(e).into()); + }, + }; + + // create payload + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new(account.address(), ident_str!(MODULE_NAME).to_owned()), + ident_str!("set_message").to_owned(), + vec![], + vec![message], + )); + + // create transaction + let pending_txn = + match build_and_submit_transaction(client, account, payload, TransactionOptions::default()) + .await + { + Ok(txn) => txn, + Err(e) => { + error!( + "test: publish_module part: set_message ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait for transaction to finish + if let Err(e) = client.wait_for_transaction(&pending_txn).await { + error!( + "test: publish_module part: set_message ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }; + + Ok(()) +} + +async fn check_message(client: &Client, address: AccountAddress) -> Result<(), TestFailure> { + // expected + let expected = TEST_MESSAGE.to_string(); + + // actual + let actual = match get_message(client, address).await { + Some(message) => message, + None => { + error!( + "test: publish_module part: check_message ERROR: {}", + ERROR_NO_MESSAGE + ); + return Err(anyhow!(ERROR_NO_MESSAGE).into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: publish_module part: check_message FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_MESSAGE, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_MESSAGE)); + } + + Ok(()) +} + +// Utils + +async fn get_message(client: &Client, address: AccountAddress) -> Option { + let resource = match client + .get_account_resource( + address, + format!("{}::message::MessageHolder", address.to_hex_literal()).as_str(), + ) + .await + { + Ok(response) => response.into_inner()?, + Err(_) => return None, + }; + + Some(resource.data.get("message")?.as_str()?.to_owned()) +} diff --git a/crates/aptos-api-tester/src/tests/tokenv1_transfer.rs b/crates/aptos-api-tester/src/tests/tokenv1_transfer.rs new file mode 100644 index 0000000000000..a95272e59e6b4 --- /dev/null +++ b/crates/aptos-api-tester/src/tests/tokenv1_transfer.rs @@ -0,0 +1,576 @@ +// Copyright © Aptos Foundation + +use crate::{ + consts::FUND_AMOUNT, + persistent_check, + strings::{ + CHECK_ACCOUNT_DATA, CHECK_COLLECTION_METADATA, CHECK_RECEIVER_BALANCE, + CHECK_SENDER_BALANCE, CHECK_TOKEN_METADATA, CLAIM_TOKEN, CREATE_COLLECTION, CREATE_TOKEN, + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, ERROR_COULD_NOT_FINISH_TRANSACTION, + ERROR_COULD_NOT_FUND_ACCOUNT, ERROR_NO_COLLECTION_DATA, ERROR_NO_TOKEN_BALANCE, + ERROR_NO_TOKEN_DATA, FAIL_WRONG_COLLECTION_DATA, FAIL_WRONG_TOKEN_BALANCE, + FAIL_WRONG_TOKEN_DATA, OFFER_TOKEN, SETUP, + }, + time_fn, + tokenv1_client::{ + CollectionData, CollectionMutabilityConfig, RoyaltyOptions, TokenClient, TokenData, + TokenMutabilityConfig, + }, + utils::{ + check_balance, create_and_fund_account, emit_step_metrics, NetworkName, TestFailure, + TestName, + }, +}; +use aptos_api_types::U64; +use aptos_logger::error; +use aptos_rest_client::Client; +use aptos_sdk::types::LocalAccount; +use aptos_types::account_address::AccountAddress; + +const COLLECTION_NAME: &str = "test collection"; +const TOKEN_NAME: &str = "test token"; +const TOKEN_SUPPLY: u64 = 10; +const OFFER_AMOUNT: u64 = 2; + +/// Tests nft transfer. Checks that: +/// - collection data exists +/// - token data exists +/// - token balance reflects transferred amount +pub async fn test(network_name: NetworkName, run_id: &str) -> Result<(), TestFailure> { + // setup + let (client, mut account, mut receiver) = emit_step_metrics( + time_fn!(setup, network_name), + TestName::TokenV1Transfer, + SETUP, + network_name, + run_id, + )?; + let token_client = TokenClient::new(&client); + + // persistently check that API returns correct account data (auth key and sequence number) + emit_step_metrics( + time_fn!( + persistent_check::address_address, + CHECK_ACCOUNT_DATA, + check_account_data, + &client, + account.address(), + receiver.address() + ), + TestName::TokenV1Transfer, + CHECK_ACCOUNT_DATA, + network_name, + run_id, + )?; + + // create collection + emit_step_metrics( + time_fn!(create_collection, &client, &token_client, &mut account), + TestName::TokenV1Transfer, + CREATE_COLLECTION, + network_name, + run_id, + )?; + + // persistently check that API returns correct collection metadata + emit_step_metrics( + time_fn!( + persistent_check::token_address, + CHECK_COLLECTION_METADATA, + check_collection_metadata, + &token_client, + account.address() + ), + TestName::TokenV1Transfer, + CHECK_COLLECTION_METADATA, + network_name, + run_id, + )?; + + // create token + emit_step_metrics( + time_fn!(create_token, &client, &token_client, &mut account), + TestName::TokenV1Transfer, + CREATE_TOKEN, + network_name, + run_id, + )?; + + // persistently check that API returns correct token metadata + emit_step_metrics( + time_fn!( + persistent_check::token_address, + CHECK_TOKEN_METADATA, + check_token_metadata, + &token_client, + account.address() + ), + TestName::TokenV1Transfer, + CHECK_TOKEN_METADATA, + network_name, + run_id, + )?; + + // offer token + emit_step_metrics( + time_fn!( + offer_token, + &client, + &token_client, + &mut account, + receiver.address() + ), + TestName::TokenV1Transfer, + OFFER_TOKEN, + network_name, + run_id, + )?; + + // persistently check that sender token balance is correct + emit_step_metrics( + time_fn!( + persistent_check::token_address, + CHECK_SENDER_BALANCE, + check_sender_balance, + &token_client, + account.address() + ), + TestName::TokenV1Transfer, + CHECK_SENDER_BALANCE, + network_name, + run_id, + )?; + + // claim token + emit_step_metrics( + time_fn!( + claim_token, + &client, + &token_client, + &mut receiver, + account.address() + ), + TestName::TokenV1Transfer, + CLAIM_TOKEN, + network_name, + run_id, + )?; + + // persistently check that receiver token balance is correct + emit_step_metrics( + time_fn!( + persistent_check::token_address_address, + CHECK_RECEIVER_BALANCE, + check_receiver_balance, + &token_client, + receiver.address(), + account.address() + ), + TestName::TokenV1Transfer, + CHECK_RECEIVER_BALANCE, + network_name, + run_id, + )?; + + Ok(()) +} + +// Steps + +async fn setup( + network_name: NetworkName, +) -> Result<(Client, LocalAccount, LocalAccount), TestFailure> { + // spin up clients + let client = network_name.get_client(); + let faucet_client = network_name.get_faucet_client(); + + // create account + let account = match create_and_fund_account(&faucet_client, TestName::TokenV1Transfer).await { + Ok(account) => account, + Err(e) => { + error!( + "test: nft_transfer part: setup ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FUND_ACCOUNT, e + ); + return Err(e.into()); + }, + }; + + // create receiver + let receiver = match create_and_fund_account(&faucet_client, TestName::TokenV1Transfer).await { + Ok(receiver) => receiver, + Err(e) => { + error!( + "test: nft_transfer part: setup ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FUND_ACCOUNT, e + ); + return Err(e.into()); + }, + }; + + Ok((client, account, receiver)) +} + +async fn check_account_data( + client: &Client, + account: AccountAddress, + receiver: AccountAddress, +) -> Result<(), TestFailure> { + check_balance(TestName::TokenV1Transfer, client, account, U64(FUND_AMOUNT)).await?; + check_balance( + TestName::TokenV1Transfer, + client, + receiver, + U64(FUND_AMOUNT), + ) + .await?; + + Ok(()) +} + +async fn create_collection( + client: &Client, + token_client: &TokenClient<'_>, + account: &mut LocalAccount, +) -> Result<(), TestFailure> { + // set up collection data + let collection_data = collection_data(); + + // create transaction + let pending_txn = match token_client + .create_collection( + account, + &collection_data.name, + &collection_data.description, + &collection_data.uri, + collection_data.maximum.into(), + None, + ) + .await + { + Ok(txn) => txn, + Err(e) => { + error!( + "test: nft_transfer part: create_collection ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait for transaction to finish + if let Err(e) = client.wait_for_transaction(&pending_txn).await { + error!( + "test: nft_transfer part: create_collection ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }; + + Ok(()) +} + +async fn check_collection_metadata( + token_client: &TokenClient<'_>, + address: AccountAddress, +) -> Result<(), TestFailure> { + // set up collection data + let collection_data = collection_data(); + + // expected + let expected = collection_data.clone(); + + // actual + let actual = match token_client + .get_collection_data(address, &collection_data.name) + .await + { + Ok(data) => data, + Err(e) => { + error!( + "test: nft_transfer part: check_collection_metadata ERROR: {}, with error {:?}", + ERROR_NO_COLLECTION_DATA, e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: nft_transfer part: check_collection_metadata FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_COLLECTION_DATA, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_COLLECTION_DATA)); + } + + Ok(()) +} + +async fn create_token( + client: &Client, + token_client: &TokenClient<'_>, + account: &mut LocalAccount, +) -> Result<(), TestFailure> { + // set up token data + let token_data = token_data(account.address()); + + // create transaction + let pending_txn = match token_client + .create_token( + account, + COLLECTION_NAME, + &token_data.name, + &token_data.description, + token_data.supply.into(), + &token_data.uri, + token_data.maximum.into(), + None, + None, + ) + .await + { + Ok(txn) => txn, + Err(e) => { + error!( + "test: nft_transfer part: create_token ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait for transaction to finish + if let Err(e) = client.wait_for_transaction(&pending_txn).await { + error!( + "test: nft_transfer part: create_token ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }; + + Ok(()) +} + +async fn check_token_metadata( + token_client: &TokenClient<'_>, + address: AccountAddress, +) -> Result<(), TestFailure> { + // set up token data + let token_data = token_data(address); + + // expected + let expected = token_data; + + // actual + let actual = match token_client + .get_token_data(address, COLLECTION_NAME, TOKEN_NAME) + .await + { + Ok(data) => data, + Err(e) => { + error!( + "test: nft_transfer part: check_token_metadata ERROR: {}, with error {:?}", + ERROR_NO_TOKEN_DATA, e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: nft_transfer part: check_token_metadata FAIL: {}, expected {:?}, got {:?}", + FAIL_WRONG_TOKEN_DATA, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_TOKEN_DATA)); + } + + Ok(()) +} + +async fn offer_token( + client: &Client, + token_client: &TokenClient<'_>, + account: &mut LocalAccount, + receiver: AccountAddress, +) -> Result<(), TestFailure> { + // create transaction + let pending_txn = match token_client + .offer_token( + account, + receiver, + account.address(), + COLLECTION_NAME, + TOKEN_NAME, + OFFER_AMOUNT, + None, + None, + ) + .await + { + Ok(txn) => txn, + Err(e) => { + error!( + "test: nft_transfer part: offer_token ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait for transaction to finish + if let Err(e) = client.wait_for_transaction(&pending_txn).await { + error!( + "test: nft_transfer part: offer_token ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }; + + Ok(()) +} + +async fn check_sender_balance( + token_client: &TokenClient<'_>, + address: AccountAddress, +) -> Result<(), TestFailure> { + check_token_balance( + token_client, + address, + address, + U64(TOKEN_SUPPLY - OFFER_AMOUNT), + "check_sender_balance", + ) + .await +} + +async fn claim_token( + client: &Client, + token_client: &TokenClient<'_>, + receiver: &mut LocalAccount, + sender: AccountAddress, +) -> Result<(), TestFailure> { + // create transaction + let pending_txn = match token_client + .claim_token( + receiver, + sender, + sender, + COLLECTION_NAME, + TOKEN_NAME, + None, + None, + ) + .await + { + Ok(txn) => txn, + Err(e) => { + error!( + "test: nft_transfer part: claim_token ERROR: {}, with error {:?}", + ERROR_COULD_NOT_CREATE_AND_SUBMIT_TRANSACTION, e + ); + return Err(e.into()); + }, + }; + + // wait for transaction to finish + if let Err(e) = client.wait_for_transaction(&pending_txn).await { + error!( + "test: nft_transfer part: claim_token ERROR: {}, with error {:?}", + ERROR_COULD_NOT_FINISH_TRANSACTION, e + ); + return Err(e.into()); + }; + + Ok(()) +} + +async fn check_receiver_balance( + token_client: &TokenClient<'_>, + address: AccountAddress, + creator: AccountAddress, +) -> Result<(), TestFailure> { + check_token_balance( + token_client, + address, + creator, + U64(OFFER_AMOUNT), + "check_receiver_balance", + ) + .await +} + +// Utils + +fn collection_data() -> CollectionData { + CollectionData { + name: COLLECTION_NAME.to_string(), + description: "collection description".to_string(), + uri: "collection uri".to_string(), + maximum: U64(1000), + mutability_config: CollectionMutabilityConfig { + description: false, + maximum: false, + uri: false, + }, + } +} + +fn token_data(address: AccountAddress) -> TokenData { + TokenData { + name: TOKEN_NAME.to_string(), + description: "token description".to_string(), + uri: "token uri".to_string(), + maximum: U64(1000), + mutability_config: TokenMutabilityConfig { + description: false, + maximum: false, + properties: false, + royalty: false, + uri: false, + }, + supply: U64(TOKEN_SUPPLY), + royalty: RoyaltyOptions { + // change this when you use! + payee_address: address, + royalty_points_denominator: U64(0), + royalty_points_numerator: U64(0), + }, + largest_property_version: U64(0), + } +} + +async fn check_token_balance( + token_client: &TokenClient<'_>, + address: AccountAddress, + creator: AccountAddress, + expected: U64, + part: &str, +) -> Result<(), TestFailure> { + // actual + let actual = match token_client + .get_token(address, creator, COLLECTION_NAME, TOKEN_NAME) + .await + { + Ok(data) => data.amount, + Err(e) => { + error!( + "test: nft_transfer part: {} ERROR: {}, with error {:?}", + part, ERROR_NO_TOKEN_BALANCE, e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: nft_transfer part: {} FAIL: {}, expected {:?}, got {:?}", + part, FAIL_WRONG_TOKEN_BALANCE, expected, actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_TOKEN_BALANCE)); + } + + Ok(()) +} diff --git a/crates/aptos-api-tester/src/tests/view_function.rs b/crates/aptos-api-tester/src/tests/view_function.rs new file mode 100644 index 0000000000000..345daf3ef1338 --- /dev/null +++ b/crates/aptos-api-tester/src/tests/view_function.rs @@ -0,0 +1,177 @@ +// Copyright © Aptos Foundation + +use crate::{ + consts::FUND_AMOUNT, + persistent_check, + strings::{ + CHECK_ACCOUNT_DATA, CHECK_VIEW_ACCOUNT_BALANCE, ERROR_BAD_BALANCE_STRING, + ERROR_COULD_NOT_FUND_ACCOUNT, ERROR_COULD_NOT_VIEW, ERROR_NO_BALANCE_STRING, + FAIL_WRONG_BALANCE, SETUP, + }, + time_fn, + utils::{ + check_balance, create_and_fund_account, emit_step_metrics, NetworkName, TestFailure, + TestName, + }, +}; +use anyhow::anyhow; +use aptos_api_types::{ViewRequest, U64}; +use aptos_logger::error; +use aptos_rest_client::Client; +use aptos_sdk::types::LocalAccount; +use aptos_types::account_address::AccountAddress; + +/// Tests view function use. Checks that: +/// - view function returns correct value +pub async fn test(network_name: NetworkName, run_id: &str) -> Result<(), TestFailure> { + // setup + let (client, account) = emit_step_metrics( + time_fn!(setup, network_name), + TestName::ViewFunction, + SETUP, + network_name, + run_id, + )?; + + // check account data persistently + emit_step_metrics( + time_fn!( + persistent_check::address, + CHECK_ACCOUNT_DATA, + check_account_data, + &client, + account.address() + ), + TestName::ViewFunction, + CHECK_ACCOUNT_DATA, + network_name, + run_id, + )?; + + // check account balance from view function persistently + emit_step_metrics( + time_fn!( + persistent_check::address, + CHECK_VIEW_ACCOUNT_BALANCE, + check_view_account_balance, + &client, + account.address() + ), + TestName::ViewFunction, + CHECK_VIEW_ACCOUNT_BALANCE, + network_name, + run_id, + )?; + + Ok(()) +} + +// Steps + +async fn setup(network_name: NetworkName) -> Result<(Client, LocalAccount), TestFailure> { + // spin up clients + let client = network_name.get_client(); + let faucet_client = network_name.get_faucet_client(); + + // create account + let account = match create_and_fund_account(&faucet_client, TestName::ViewFunction).await { + Ok(account) => account, + Err(e) => { + error!( + "test: {} part: {} ERROR: {}, with error {:?}", + TestName::ViewFunction.to_string(), + SETUP, + ERROR_COULD_NOT_FUND_ACCOUNT, + e + ); + return Err(e.into()); + }, + }; + + Ok((client, account)) +} + +async fn check_account_data(client: &Client, account: AccountAddress) -> Result<(), TestFailure> { + check_balance(TestName::ViewFunction, client, account, U64(FUND_AMOUNT)).await?; + + Ok(()) +} + +async fn check_view_account_balance( + client: &Client, + address: AccountAddress, +) -> Result<(), TestFailure> { + // expected + let expected = U64(FUND_AMOUNT); + + // actual + + // get client response + let response = match client + .view( + &ViewRequest { + function: "0x1::coin::balance".parse()?, + type_arguments: vec!["0x1::aptos_coin::AptosCoin".parse()?], + arguments: vec![serde_json::Value::String(address.to_hex_literal())], + }, + None, + ) + .await + { + Ok(response) => response, + Err(e) => { + error!( + "test: {} part: {} ERROR: {}, with error {:?}", + TestName::ViewFunction.to_string(), + CHECK_VIEW_ACCOUNT_BALANCE, + ERROR_COULD_NOT_VIEW, + e + ); + return Err(e.into()); + }, + }; + + // get the string value from the serde_json value + let value = match response.inner()[0].as_str() { + Some(value) => value, + None => { + error!( + "test: {} part: {} ERROR: {}, with error {:?}", + TestName::ViewFunction.to_string(), + CHECK_VIEW_ACCOUNT_BALANCE, + ERROR_NO_BALANCE_STRING, + response.inner() + ); + return Err(anyhow!(ERROR_NO_BALANCE_STRING).into()); + }, + }; + + // parse the string into a U64 + let actual = match value.parse::() { + Ok(value) => U64(value), + Err(e) => { + error!( + "test: {} part: {} ERROR: {}, with error {:?}", + TestName::ViewFunction.to_string(), + CHECK_VIEW_ACCOUNT_BALANCE, + ERROR_BAD_BALANCE_STRING, + e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: {} part: {} FAIL: {}, expected {:?}, got {:?}", + TestName::ViewFunction.to_string(), + CHECK_VIEW_ACCOUNT_BALANCE, + FAIL_WRONG_BALANCE, + expected, + actual + ); + } + + Ok(()) +} diff --git a/crates/aptos-api-tester/src/tokenv1_client.rs b/crates/aptos-api-tester/src/tokenv1_client.rs new file mode 100644 index 0000000000000..7ef4f3c25b03b --- /dev/null +++ b/crates/aptos-api-tester/src/tokenv1_client.rs @@ -0,0 +1,460 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// TODO: this should be part of the SDK + +use anyhow::{anyhow, Context, Result}; +use aptos_api_types::U64; +use aptos_cached_packages::aptos_token_sdk_builder::EntryFunctionCall; +use aptos_sdk::{ + rest_client::{Client as ApiClient, PendingTransaction}, + transaction_builder::TransactionFactory, + types::LocalAccount, +}; +use aptos_types::{ + account_address::AccountAddress, chain_id::ChainId, transaction::TransactionPayload, +}; +use serde::{Deserialize, Serialize}; + +/// Gets chain ID for use in submitting transactions. +async fn get_chain_id(client: &ApiClient) -> Result { + let id = client + .get_index() + .await + .context("Failed to get chain ID")? + .inner() + .chain_id; + + Ok(ChainId::new(id)) +} + +/// Helper function to take care of a transaction after creating the payload. +pub async fn build_and_submit_transaction( + client: &ApiClient, + account: &mut LocalAccount, + payload: TransactionPayload, + options: TransactionOptions, +) -> Result { + // create factory + let factory = TransactionFactory::new(get_chain_id(client).await?) + .with_gas_unit_price(options.gas_unit_price) + .with_max_gas_amount(options.max_gas_amount) + .with_transaction_expiration_time(options.timeout_secs); + + // create transaction + let builder = factory + .payload(payload) + .sender(account.address()) + .sequence_number(account.sequence_number()); + + // sign transaction + let signed_txn = account.sign_with_transaction_builder(builder); + + // submit and return + Ok(client + .submit(&signed_txn) + .await + .context("Failed to submit transaction")? + .into_inner()) +} + +#[derive(Clone, Debug)] +pub struct TokenClient<'a> { + api_client: &'a ApiClient, +} + +impl<'a> TokenClient<'a> { + pub fn new(api_client: &'a ApiClient) -> Self { + Self { api_client } + } + + /// Helper function to get the handle address of collection_data for 0x3::token::Collections + /// resources. + async fn get_collection_data_handle(&self, address: AccountAddress) -> Option { + if let Ok(response) = self + .api_client + .get_account_resource(address, "0x3::token::Collections") + .await + { + Some( + response + .into_inner()? + .data + .get("collection_data")? + .get("handle")? + .as_str()? + .to_owned(), + ) + } else { + None + } + } + + /// Helper function to get the handle address of token_data for 0x3::token::Collections + /// resources. + async fn get_token_data_handle(&self, address: AccountAddress) -> Option { + if let Ok(response) = self + .api_client + .get_account_resource(address, "0x3::token::Collections") + .await + { + Some( + response + .into_inner()? + .data + .get("token_data")? + .get("handle")? + .as_str()? + .to_owned(), + ) + } else { + None + } + } + + /// Helper function to get the handle address of tokens for 0x3::token::TokenStore resources. + async fn get_tokens_handle(&self, address: AccountAddress) -> Option { + if let Ok(response) = self + .api_client + .get_account_resource(address, "0x3::token::TokenStore") + .await + { + Some( + response + .into_inner()? + .data + .get("tokens")? + .get("handle")? + .as_str()? + .to_owned(), + ) + } else { + None + } + } + + /// Creates a collection with the given fields. + pub async fn create_collection( + &self, + account: &mut LocalAccount, + name: &str, + description: &str, + uri: &str, + max_amount: u64, + options: Option, + ) -> Result { + // create payload + let payload = EntryFunctionCall::TokenCreateCollectionScript { + name: name.to_owned().into_bytes(), + description: description.to_owned().into_bytes(), + uri: uri.to_owned().into_bytes(), + maximum: max_amount, + mutate_setting: vec![false, false, false], + } + .encode(); + + // create and submit transaction + build_and_submit_transaction( + self.api_client, + account, + payload, + options.unwrap_or_default(), + ) + .await + } + + /// Creates a token with the given fields. Does not support property keys. + pub async fn create_token( + &self, + account: &mut LocalAccount, + collection_name: &str, + name: &str, + description: &str, + supply: u64, + uri: &str, + max_amount: u64, + royalty_options: Option, + options: Option, + ) -> Result { + // set default royalty options + let royalty_options = match royalty_options { + Some(opt) => opt, + None => RoyaltyOptions { + payee_address: account.address(), + royalty_points_denominator: U64(0), + royalty_points_numerator: U64(0), + }, + }; + + // create payload + let payload = EntryFunctionCall::TokenCreateTokenScript { + collection: collection_name.to_owned().into_bytes(), + name: name.to_owned().into_bytes(), + description: description.to_owned().into_bytes(), + balance: supply, + maximum: max_amount, + uri: uri.to_owned().into_bytes(), + royalty_payee_address: royalty_options.payee_address, + royalty_points_denominator: royalty_options.royalty_points_denominator.0, + royalty_points_numerator: royalty_options.royalty_points_numerator.0, + mutate_setting: vec![false, false, false, false, false], + // todo: add property support + property_keys: vec![], + property_values: vec![], + property_types: vec![], + } + .encode(); + + // create and submit transaction + build_and_submit_transaction( + self.api_client, + account, + payload, + options.unwrap_or_default(), + ) + .await + } + + /// Retrieves collection metadata from the API. + pub async fn get_collection_data( + &self, + creator: AccountAddress, + collection_name: &str, + ) -> Result { + // get handle for collection_data + let handle = match self.get_collection_data_handle(creator).await { + Some(s) => AccountAddress::from_hex_literal(&s)?, + None => return Err(anyhow!("Couldn't retrieve handle for collections data")), + }; + + // get table item with the handle + let value = self + .api_client + .get_table_item( + handle, + "0x1::string::String", + "0x3::token::CollectionData", + collection_name, + ) + .await? + .into_inner(); + + Ok(serde_json::from_value(value)?) + } + + /// Retrieves token metadata from the API. + pub async fn get_token_data( + &self, + creator: AccountAddress, + collection_name: &str, + token_name: &str, + ) -> Result { + // get handle for token_data + let handle = match self.get_token_data_handle(creator).await { + Some(s) => AccountAddress::from_hex_literal(&s)?, + None => return Err(anyhow!("Couldn't retrieve handle for token data")), + }; + + // construct key for table lookup + let token_data_id = TokenDataId { + creator: creator.to_hex_literal(), + collection: collection_name.to_string(), + name: token_name.to_string(), + }; + + // get table item with the handle + let value = self + .api_client + .get_table_item( + handle, + "0x3::token::TokenDataId", + "0x3::token::TokenData", + token_data_id, + ) + .await? + .into_inner(); + + Ok(serde_json::from_value(value)?) + } + + /// Retrieves the information for a given token. + pub async fn get_token( + &self, + account: AccountAddress, + creator: AccountAddress, + collection_name: &str, + token_name: &str, + ) -> Result { + // get handle for tokens + let handle = match self.get_tokens_handle(account).await { + Some(s) => AccountAddress::from_hex_literal(&s)?, + None => return Err(anyhow!("Couldn't retrieve handle for tokens")), + }; + + // construct key for table lookup + let token_id = TokenId { + token_data_id: TokenDataId { + creator: creator.to_hex_literal(), + collection: collection_name.to_string(), + name: token_name.to_string(), + }, + property_version: U64(0), + }; + + // get table item with the handle + let value = self + .api_client + .get_table_item(handle, "0x3::token::TokenId", "0x3::token::Token", token_id) + .await? + .into_inner(); + + Ok(serde_json::from_value(value)?) + } + + /// Transfers specified amount of tokens from account to receiver. + pub async fn offer_token( + &self, + account: &mut LocalAccount, + receiver: AccountAddress, + creator: AccountAddress, + collection_name: &str, + name: &str, + amount: u64, + property_version: Option, + options: Option, + ) -> Result { + // create payload + let payload = EntryFunctionCall::TokenTransfersOfferScript { + receiver, + creator, + collection: collection_name.to_owned().into_bytes(), + name: name.to_owned().into_bytes(), + property_version: property_version.unwrap_or(0), + amount, + } + .encode(); + + // create and submit transaction + build_and_submit_transaction( + self.api_client, + account, + payload, + options.unwrap_or_default(), + ) + .await + } + + pub async fn claim_token( + &self, + account: &mut LocalAccount, + sender: AccountAddress, + creator: AccountAddress, + collection_name: &str, + name: &str, + property_version: Option, + options: Option, + ) -> Result { + // create payload + let payload = EntryFunctionCall::TokenTransfersClaimScript { + sender, + creator, + collection: collection_name.to_owned().into_bytes(), + name: name.to_owned().into_bytes(), + property_version: property_version.unwrap_or(0), + } + .encode(); + + // create and submit transaction + build_and_submit_transaction( + self.api_client, + account, + payload, + options.unwrap_or_default(), + ) + .await + } +} + +pub struct TransactionOptions { + pub max_gas_amount: u64, + + pub gas_unit_price: u64, + + /// This is the number of seconds from now you're willing to wait for the + /// transaction to be committed. + pub timeout_secs: u64, +} + +impl Default for TransactionOptions { + fn default() -> Self { + Self { + max_gas_amount: 5_000, + gas_unit_price: 100, + timeout_secs: 10, + } + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct CollectionData { + pub name: String, + pub description: String, + pub uri: String, + pub maximum: U64, + pub mutability_config: CollectionMutabilityConfig, +} + +#[derive(Clone, Deserialize, Debug, PartialEq)] +pub struct CollectionMutabilityConfig { + pub description: bool, + pub maximum: bool, + pub uri: bool, +} + +#[derive(Debug, PartialEq, Deserialize)] +pub struct TokenData { + pub name: String, + pub description: String, + pub uri: String, + pub maximum: U64, + pub supply: U64, + pub royalty: RoyaltyOptions, + pub mutability_config: TokenMutabilityConfig, + pub largest_property_version: U64, +} + +#[derive(Debug, PartialEq, Deserialize)] +pub struct RoyaltyOptions { + pub payee_address: AccountAddress, + pub royalty_points_denominator: U64, + pub royalty_points_numerator: U64, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct TokenMutabilityConfig { + pub description: bool, + pub maximum: bool, + pub properties: bool, + pub royalty: bool, + pub uri: bool, +} + +#[derive(Debug, Deserialize)] +pub struct Token { + // id: TokenId, + pub amount: U64, + // todo: add property support +} + +#[derive(Debug, Deserialize, Serialize)] +struct TokenId { + token_data_id: TokenDataId, + property_version: U64, +} + +#[derive(Debug, Deserialize, Serialize)] +struct TokenDataId { + creator: String, + collection: String, + name: String, +} diff --git a/crates/aptos-api-tester/src/utils.rs b/crates/aptos-api-tester/src/utils.rs new file mode 100644 index 0000000000000..1ed118b8a558d --- /dev/null +++ b/crates/aptos-api-tester/src/utils.rs @@ -0,0 +1,310 @@ +// Copyright © Aptos Foundation + +use crate::{ + consts::{ + DEVNET_FAUCET_URL, DEVNET_NODE_URL, FUND_AMOUNT, TESTNET_FAUCET_URL, TESTNET_NODE_URL, + }, + counters::{test_error, test_fail, test_latency, test_step_latency, test_success}, + strings::{ERROR_NO_BALANCE, FAIL_WRONG_BALANCE}, + tests::{coin_transfer, new_account, publish_module, tokenv1_transfer, view_function}, + time_fn, +}; +use anyhow::{anyhow, Error, Result}; +use aptos_api_types::U64; +use aptos_logger::{error, info}; +use aptos_rest_client::{error::RestError, Client, FaucetClient}; +use aptos_sdk::types::LocalAccount; +use aptos_types::account_address::AccountAddress; +use std::{env, num::ParseIntError, str::FromStr}; + +// Test failure + +#[derive(Debug)] +pub enum TestFailure { + // Variant for failed checks, e.g. wrong balance + Fail(&'static str), + // Variant for test failures, e.g. client returns an error + Error(anyhow::Error), +} + +impl From for TestFailure { + fn from(e: anyhow::Error) -> TestFailure { + TestFailure::Error(e) + } +} + +impl From for TestFailure { + fn from(e: RestError) -> TestFailure { + TestFailure::Error(e.into()) + } +} + +impl From for TestFailure { + fn from(e: ParseIntError) -> TestFailure { + TestFailure::Error(e.into()) + } +} + +// Test name + +#[derive(Clone, Copy)] +pub enum TestName { + NewAccount, + CoinTransfer, + TokenV1Transfer, + PublishModule, + ViewFunction, +} + +impl TestName { + pub async fn run(&self, network_name: NetworkName, run_id: &str) { + let output = match &self { + TestName::NewAccount => time_fn!(new_account::test, network_name, run_id), + TestName::CoinTransfer => time_fn!(coin_transfer::test, network_name, run_id), + TestName::TokenV1Transfer => time_fn!(tokenv1_transfer::test, network_name, run_id), + TestName::PublishModule => time_fn!(publish_module::test, network_name, run_id), + TestName::ViewFunction => time_fn!(view_function::test, network_name, run_id), + }; + + emit_test_metrics(output, *self, network_name, run_id); + } +} + +impl ToString for TestName { + fn to_string(&self) -> String { + match &self { + TestName::NewAccount => "new_account".to_string(), + TestName::CoinTransfer => "coin_transfer".to_string(), + TestName::TokenV1Transfer => "tokenv1_transfer".to_string(), + TestName::PublishModule => "publish_module".to_string(), + TestName::ViewFunction => "view_function".to_string(), + } + } +} + +// Network name + +#[derive(Clone, Copy)] +pub enum NetworkName { + Testnet, + Devnet, +} + +impl ToString for NetworkName { + fn to_string(&self) -> String { + match &self { + NetworkName::Testnet => "testnet".to_string(), + NetworkName::Devnet => "devnet".to_string(), + } + } +} + +impl FromStr for NetworkName { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "testnet" => Ok(NetworkName::Testnet), + "devnet" => Ok(NetworkName::Devnet), + _ => Err(anyhow!("invalid network name")), + } + } +} + +impl NetworkName { + /// Create a REST client. + pub fn get_client(&self) -> Client { + match self { + NetworkName::Testnet => Client::new(TESTNET_NODE_URL.clone()), + NetworkName::Devnet => Client::new(DEVNET_NODE_URL.clone()), + } + } + + /// Create a faucet client. + pub fn get_faucet_client(&self) -> FaucetClient { + match self { + NetworkName::Testnet => { + let faucet_client = + FaucetClient::new(TESTNET_FAUCET_URL.clone(), TESTNET_NODE_URL.clone()); + match env::var("TESTNET_FAUCET_CLIENT_TOKEN") { + Ok(token) => faucet_client.with_auth_token(token), + Err(_) => faucet_client, + } + }, + NetworkName::Devnet => { + FaucetClient::new(DEVNET_FAUCET_URL.clone(), DEVNET_NODE_URL.clone()) + }, + } + } +} + +// Setup helpers + +/// Create an account with zero balance. +pub async fn create_account( + faucet_client: &FaucetClient, + test_name: TestName, +) -> Result { + let account = LocalAccount::generate(&mut rand::rngs::OsRng); + faucet_client.create_account(account.address()).await?; + + info!( + "CREATED ACCOUNT {} for test: {}", + account.address(), + test_name.to_string() + ); + Ok(account) +} + +/// Create an account with 100_000_000 balance. +pub async fn create_and_fund_account( + faucet_client: &FaucetClient, + test_name: TestName, +) -> Result { + let account = LocalAccount::generate(&mut rand::rngs::OsRng); + faucet_client.fund(account.address(), FUND_AMOUNT).await?; + + info!( + "CREATED ACCOUNT {} for test: {}", + account.address(), + test_name.to_string() + ); + Ok(account) +} + +/// Check account balance. +pub async fn check_balance( + test_name: TestName, + client: &Client, + address: AccountAddress, + expected: U64, +) -> Result<(), TestFailure> { + // actual + let actual = match client.get_account_balance(address).await { + Ok(response) => response.into_inner().coin.value, + Err(e) => { + error!( + "test: {} part: check_account_data ERROR: {}, with error {:?}", + &test_name.to_string(), + ERROR_NO_BALANCE, + e + ); + return Err(e.into()); + }, + }; + + // compare + if expected != actual { + error!( + "test: {} part: check_account_data FAIL: {}, expected {:?}, got {:?}", + &test_name.to_string(), + FAIL_WRONG_BALANCE, + expected, + actual + ); + return Err(TestFailure::Fail(FAIL_WRONG_BALANCE)); + } + + Ok(()) +} + +// Metrics helpers + +/// Emit metrics based on test result. +pub fn emit_test_metrics( + output: (Result<(), TestFailure>, f64), + test_name: TestName, + network_name: NetworkName, + run_id: &str, +) { + // deconstruct + let (result, time) = output; + + // emit success rate and get result word + let result_label = match result { + Ok(_) => { + test_success(&test_name.to_string(), &network_name.to_string(), run_id).observe(1_f64); + test_fail(&test_name.to_string(), &network_name.to_string(), run_id).observe(0_f64); + test_error(&test_name.to_string(), &network_name.to_string(), run_id).observe(0_f64); + + "success" + }, + Err(e) => match e { + TestFailure::Fail(_) => { + test_success(&test_name.to_string(), &network_name.to_string(), run_id) + .observe(0_f64); + test_fail(&test_name.to_string(), &network_name.to_string(), run_id).observe(1_f64); + test_error(&test_name.to_string(), &network_name.to_string(), run_id) + .observe(0_f64); + + "fail" + }, + TestFailure::Error(_) => { + test_success(&test_name.to_string(), &network_name.to_string(), run_id) + .observe(0_f64); + test_fail(&test_name.to_string(), &network_name.to_string(), run_id).observe(0_f64); + test_error(&test_name.to_string(), &network_name.to_string(), run_id) + .observe(1_f64); + + "error" + }, + }, + }; + + // log result + info!( + "----- TEST FINISHED test: {} result: {} time: {} -----", + test_name.to_string(), + result_label, + time, + ); + + // emit latency + test_latency( + &test_name.to_string(), + &network_name.to_string(), + run_id, + result_label, + ) + .observe(time); +} + +/// Emit metrics based on result. +pub fn emit_step_metrics( + output: (Result, f64), + test_name: TestName, + step_name: &str, + network_name: NetworkName, + run_id: &str, +) -> Result { + // deconstruct and get result word + let (result, time) = output; + let result_label = match &result { + Ok(_) => "success", + Err(e) => match e { + TestFailure::Fail(_) => "fail", + TestFailure::Error(_) => "error", + }, + }; + + // log result + info!( + "STEP FINISHED test: {} step: {} result: {} time: {}", + test_name.to_string(), + step_name, + result_label, + time, + ); + + // emit latency + test_step_latency( + &test_name.to_string(), + step_name, + &network_name.to_string(), + run_id, + result_label, + ) + .observe(time); + + result +} diff --git a/crates/aptos-rest-client/src/faucet.rs b/crates/aptos-rest-client/src/faucet.rs index fba4b04ebc599..d3920627bb570 100644 --- a/crates/aptos-rest-client/src/faucet.rs +++ b/crates/aptos-rest-client/src/faucet.rs @@ -5,13 +5,14 @@ use crate::{error::FaucetClientError, Client, Result}; use aptos_types::transaction::SignedTransaction; use move_core_types::account_address::AccountAddress; -use reqwest::{Client as ReqwestClient, Url}; +use reqwest::{Client as ReqwestClient, Response, Url}; use std::time::Duration; pub struct FaucetClient { faucet_url: Url, inner: ReqwestClient, rest_client: Client, + token: Option, } impl FaucetClient { @@ -23,6 +24,7 @@ impl FaucetClient { .build() .unwrap(), rest_client: Client::new(rest_url), + token: None, } } @@ -39,9 +41,16 @@ impl FaucetClient { // versioned API however, so we just set it to `/`. .version_path_base("/".to_string()) .unwrap(), + token: None, } } + // Set auth token. + pub fn with_auth_token(mut self, token: String) -> Self { + self.token = Some(token); + self + } + /// Create an account with zero balance. pub async fn create_account(&self, address: AccountAddress) -> Result<()> { let mut url = self.faucet_url.clone(); @@ -49,13 +58,7 @@ impl FaucetClient { let query = format!("auth_key={}&amount=0&return_txns=true", address); url.set_query(Some(&query)); - let response = self - .inner - .post(url) - .header("content-length", 0) - .send() - .await - .map_err(FaucetClientError::request)?; + let response = self.build_and_submit_request(url).await?; let status_code = response.status(); let body = response.text().await.map_err(FaucetClientError::decode)?; if !status_code.is_success() { @@ -83,13 +86,7 @@ impl FaucetClient { // Faucet returns the transaction that creates the account and needs to be waited on before // returning. - let response = self - .inner - .post(url) - .header("content-length", 0) - .send() - .await - .map_err(FaucetClientError::request)?; + let response = self.build_and_submit_request(url).await?; let status_code = response.status(); let body = response.text().await.map_err(FaucetClientError::decode)?; if !status_code.is_success() { @@ -115,4 +112,17 @@ impl FaucetClient { Ok(()) } + + // Helper to carry out requests. + async fn build_and_submit_request(&self, url: Url) -> Result { + // build request + let mut request = self.inner.post(url).header("content-length", 0); + if let Some(token) = &self.token { + request = request.header("Authorization", format!("Bearer {}", token)); + } + + // carry out and return response + let response = request.send().await.map_err(FaucetClientError::request)?; + Ok(response) + } } diff --git a/crates/aptos-rest-client/src/lib.rs b/crates/aptos-rest-client/src/lib.rs index 1a6f653636f47..996ce2a5b9354 100644 --- a/crates/aptos-rest-client/src/lib.rs +++ b/crates/aptos-rest-client/src/lib.rs @@ -566,6 +566,7 @@ impl Client { F: Fn(HashValue) -> Fut, Fut: Future>>, { + // TODO: make this configurable const DEFAULT_DELAY: Duration = Duration::from_millis(500); let mut reached_mempool = false; let start = std::time::Instant::now(); diff --git a/crates/aptos-rest-client/src/types.rs b/crates/aptos-rest-client/src/types.rs index 9b825ddd81221..fe12ac2ff9001 100644 --- a/crates/aptos-rest-client/src/types.rs +++ b/crates/aptos-rest-client/src/types.rs @@ -40,7 +40,7 @@ where parse_struct_tag(&s).map_err(D::Error::custom) } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq)] pub struct Account { #[serde(deserialize_with = "deserialize_from_prefixed_hex_string")] pub authentication_key: AuthenticationKey, diff --git a/docker/builder/build-tools.sh b/docker/builder/build-tools.sh index 908380b46b4b3..afca44fbbae11 100755 --- a/docker/builder/build-tools.sh +++ b/docker/builder/build-tools.sh @@ -27,6 +27,7 @@ cargo build --locked --profile=$PROFILE \ -p aptos-indexer-grpc-data-service \ -p aptos-indexer-grpc-post-processor \ -p aptos-nft-metadata-crawler-parser \ + -p aptos-api-tester \ "$@" # After building, copy the binaries we need to `dist` since the `target` directory is used as docker cache mount and only available during the RUN step @@ -46,6 +47,7 @@ BINS=( aptos-indexer-grpc-data-service aptos-indexer-grpc-post-processor aptos-nft-metadata-crawler-parser + aptos-api-tester ) mkdir dist diff --git a/docker/builder/tools.Dockerfile b/docker/builder/tools.Dockerfile index 55b467e01e386..ff268ea05d3c7 100644 --- a/docker/builder/tools.Dockerfile +++ b/docker/builder/tools.Dockerfile @@ -31,6 +31,13 @@ COPY --link --from=tools-builder /aptos/dist/aptos /usr/local/bin/aptos COPY --link --from=tools-builder /aptos/dist/aptos-openapi-spec-generator /usr/local/bin/aptos-openapi-spec-generator COPY --link --from=tools-builder /aptos/dist/aptos-fn-check-client /usr/local/bin/aptos-fn-check-client COPY --link --from=tools-builder /aptos/dist/aptos-transaction-emitter /usr/local/bin/aptos-transaction-emitter +COPY --link --from=tools-builder /aptos/dist/aptos-api-tester /usr/local/bin/aptos-api-tester + +# Copy the example module to publish for api-tester +COPY --link --from=tools-builder /aptos/aptos-move/framework/aptos-framework /aptos-move/framework/aptos-framework +COPY --link --from=tools-builder /aptos/aptos-move/framework/aptos-stdlib /aptos-move/framework/aptos-stdlib +COPY --link --from=tools-builder /aptos/aptos-move/framework/move-stdlib /aptos-move/framework/move-stdlib +COPY --link --from=tools-builder /aptos/aptos-move/move-examples/hello_blockchain /aptos-move/move-examples/hello_blockchain ### Get Aptos Move releases for genesis ceremony RUN mkdir -p /aptos-framework/move