From a949a1e1bae168c4e1b4e5b2201f6a624b127fbd Mon Sep 17 00:00:00 2001 From: Tomasz Pastusiak Date: Thu, 17 Oct 2024 17:01:18 +0200 Subject: [PATCH] feat(iota-indexer): Add tests for CoinApi --- .../data/dummy_modules_publish/Move.toml | 9 + .../sources/trusted_coin.move | 40 ++ .../iota-indexer/tests/rpc-tests/coin_api.rs | 534 ++++++++++++++++++ crates/iota-indexer/tests/rpc-tests/main.rs | 3 + 4 files changed, 586 insertions(+) create mode 100644 crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml create mode 100644 crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move create mode 100644 crates/iota-indexer/tests/rpc-tests/coin_api.rs diff --git a/crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml b/crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml new file mode 100644 index 00000000000..e518fc169a9 --- /dev/null +++ b/crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "Examples" +version = "0.0.1" + +[dependencies] +Iota = { local = "../../../../iota-framework/packages/iota-framework" } + +[addresses] +examples = "0x0" diff --git a/crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move b/crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move new file mode 100644 index 00000000000..1a53fb598a5 --- /dev/null +++ b/crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move @@ -0,0 +1,40 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Example coin with a trusted owner responsible for minting/burning (e.g., a stablecoin) +module examples::trusted_coin { + use std::option; + use iota::coin::{Self, TreasuryCap}; + use iota::transfer; + use iota::tx_context::{Self, TxContext}; + + /// Name of the coin + struct TRUSTED_COIN has drop {} + + /// Register the trusted currency to acquire its `TreasuryCap`. Because + /// this is a module initializer, it ensures the currency only gets + /// registered once. + fun init(witness: TRUSTED_COIN, ctx: &mut TxContext) { + // Get a treasury cap for the coin and give it to the transaction + // sender + let (treasury_cap, metadata) = coin::create_currency(witness, 2, b"TRUSTED", b"Trusted Coin", b"Trusted Coin for test", option::none(), ctx); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)) + } + + public entry fun mint(treasury_cap: &mut TreasuryCap, amount: u64, ctx: &mut TxContext) { + let coin = coin::mint(treasury_cap, amount, ctx); + transfer::public_transfer(coin, tx_context::sender(ctx)); + } + + public entry fun transfer(treasury_cap: TreasuryCap, recipient: address) { + transfer::public_transfer(treasury_cap, recipient); + } + + #[test_only] + /// Wrapper of module initializer for testing + public fun test_init(ctx: &mut TxContext) { + init(TRUSTED_COIN {}, ctx) + } +} diff --git a/crates/iota-indexer/tests/rpc-tests/coin_api.rs b/crates/iota-indexer/tests/rpc-tests/coin_api.rs new file mode 100644 index 00000000000..b00ecbdf36b --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/coin_api.rs @@ -0,0 +1,534 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{path::PathBuf, str::FromStr, sync::OnceLock}; + +use iota_indexer::store::PgIndexerStore; +use iota_json::{call_args, type_args}; +use iota_json_rpc_api::{CoinReadApiClient, TransactionBuilderClient, WriteApiClient}; +use iota_json_rpc_types::{ + Balance, CoinPage, IotaCoinMetadata, IotaExecutionStatus, IotaObjectRef, + IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponse, + IotaTransactionBlockResponseOptions, ObjectChange, TransactionBlockBytes, +}; +use iota_move_build::BuildConfig; +use iota_types::{ + IOTA_FRAMEWORK_ADDRESS, + balance::Supply, + base_types::{IotaAddress, ObjectID}, + coin::{COIN_MODULE_NAME, TreasuryCap}, + crypto::{AccountKeyPair, get_key_pair}, + parse_iota_struct_tag, + quorum_driver_types::ExecuteTransactionRequestType, + utils::to_sender_signed_transaction, +}; +use jsonrpsee::http_client::HttpClient; +use test_cluster::TestCluster; +use tokio::sync::Mutex; + +use crate::common::{ApiTestSetup, indexer_wait_for_object}; + +static COMMON_TESTING_ADDR_AND_CUSTOM_COIN_NAME: OnceLock>> = + OnceLock::new(); + +async fn once_prepare_addr_with_iota_and_custom_coins( + cluster: &TestCluster, + store: &PgIndexerStore, +) -> (IotaAddress, String) { + let addr_and_coin_mutex = + COMMON_TESTING_ADDR_AND_CUSTOM_COIN_NAME.get_or_init(|| Mutex::new(None)); + let mut addr_and_coin = addr_and_coin_mutex.lock().await; + + if addr_and_coin.is_none() { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + + for _ in 0..5 { + cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + } + + let (coin_name, coin_object_ref) = create_and_mint_coin(cluster, address, keypair, 100_000) + .await + .unwrap(); + + indexer_wait_for_object(store, coin_object_ref.object_id, coin_object_ref.version).await; + + *addr_and_coin = Some((address, coin_name)); + } + + addr_and_coin.clone().unwrap() +} + +#[test] +fn get_coins_basic_scenario() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_coins_fullnode_indexer(cluster, client, owner, None, None, None).await; + + assert!(result_indexer.data.len() > 0); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coins_with_cursor() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + let all_coins = cluster + .rpc_client() + .get_coins(owner, None, None, None) + .await + .unwrap(); + let cursor = all_coins.data[3].coin_object_id; // get some coin from the middle + + let (result_fullnode, result_indexer) = + call_get_coins_fullnode_indexer(cluster, client, owner, None, Some(cursor), None).await; + + assert!(result_indexer.data.len() > 0); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coins_with_limit() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_coins_fullnode_indexer(cluster, client, owner, None, None, Some(2)).await; + + assert!(result_indexer.data.len() > 0); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coins_custom_coin() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, coin_name) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_coins_fullnode_indexer(cluster, client, owner, Some(coin_name), None, None) + .await; + + assert_eq!(result_indexer.data.len(), 1); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_all_coins_basic_scenario() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_all_coins_fullnode_indexer(cluster, client, owner, None, None).await; + + assert!(result_indexer.data.len() > 0); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[ignore = "This test is flaky, sometimes the newly created coin is missing +from the indexer response, but only when getting coins by cursor, without +cursor the object is present in both responses"] +#[test] +fn get_all_coins_with_cursor() { + let ApiTestSetup { + runtime, + client, + cluster, + store, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let all_coins = cluster + .rpc_client() + .get_coins(owner, None, None, None) + .await + .unwrap(); + let cursor = all_coins.data[3].coin_object_id; // get some coin from the middle + + let (result_fullnode_all, result_indexer_all) = + call_get_all_coins_fullnode_indexer(cluster, client, owner, None, None).await; + + let (result_fullnode, result_indexer) = + call_get_all_coins_fullnode_indexer(cluster, client, owner, Some(cursor), None).await; + + println!("Fullnode all: {:#?}", result_fullnode_all); + println!("Indexer all: {:#?}", result_indexer_all); + println!("Fullnode: {:#?}", result_fullnode); + println!("Indexer: {:#?}", result_indexer); + println!("Cursor: {:#?}", cursor); + + assert!(result_indexer.data.len() > 0); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_all_coins_with_limit() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_all_coins_fullnode_indexer(cluster, client, owner, None, Some(2)).await; + + assert!(result_indexer.data.len() > 0); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_balance_iota_coin() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_balance_fullnode_indexer(cluster, client, owner, None).await; + + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_balance_custom_coin() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, coin_name) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_balance_fullnode_indexer(cluster, client, owner, Some(coin_name.to_string())) + .await; + + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_all_balances() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (mut result_fullnode, mut result_indexer) = + call_get_all_balances_fullnode_indexer(cluster, client, owner).await; + + result_fullnode.sort_by_key(|balance: &Balance| balance.coin_type.clone()); + result_indexer.sort_by_key(|balance: &Balance| balance.coin_type.clone()); + + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coin_metadata() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (_, coin_name) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_coin_metadata_fullnode_indexer(cluster, client, coin_name.to_string()).await; + + assert!(result_indexer.is_some()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_total_supply() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (_, coin_name) = once_prepare_addr_with_iota_and_custom_coins(cluster, store).await; + + let (result_fullnode, result_indexer) = + call_get_total_supply_fullnode_indexer(cluster, client, coin_name.to_string()).await; + + assert_eq!(result_fullnode, result_indexer); + }); +} + +async fn call_get_coins_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, + coin_type: Option, + cursor: Option, + limit: Option, +) -> (CoinPage, CoinPage) { + let result_fullnode = cluster + .rpc_client() + .get_coins(owner, coin_type.clone(), cursor, limit) + .await + .unwrap(); + let result_indexer = client + .get_coins(owner, coin_type, cursor, limit) + .await + .unwrap(); + (result_fullnode, result_indexer) +} + +async fn call_get_all_coins_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, + cursor: Option, + limit: Option, +) -> (CoinPage, CoinPage) { + let result_fullnode = cluster + .rpc_client() + .get_all_coins(owner, cursor, limit) + .await + .unwrap(); + let result_indexer = client.get_all_coins(owner, cursor, limit).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn call_get_balance_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, + coin_type: Option, +) -> (Balance, Balance) { + let result_fullnode = cluster + .rpc_client() + .get_balance(owner, coin_type.clone()) + .await + .unwrap(); + let result_indexer = client.get_balance(owner, coin_type).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn call_get_all_balances_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, +) -> (Vec, Vec) { + let result_fullnode = cluster.rpc_client().get_all_balances(owner).await.unwrap(); + let result_indexer = client.get_all_balances(owner).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn call_get_coin_metadata_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + coin_type: String, +) -> (Option, Option) { + let result_fullnode = cluster + .rpc_client() + .get_coin_metadata(coin_type.clone()) + .await + .unwrap(); + let result_indexer = client.get_coin_metadata(coin_type).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn call_get_total_supply_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + coin_type: String, +) -> (Supply, Supply) { + let result_fullnode = cluster + .rpc_client() + .get_total_supply(coin_type.clone()) + .await + .unwrap(); + let result_indexer = client.get_total_supply(coin_type).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn create_and_mint_coin( + cluster: &TestCluster, + address: IotaAddress, + account_keypair: AccountKeyPair, + amount: u64, +) -> Result<(String, IotaObjectRef), anyhow::Error> { + let http_client = cluster.rpc_client(); + let coins = http_client + .get_coins(address, None, None, Some(1)) + .await + .unwrap() + .data; + let gas = &coins[0]; + + // Publish test coin package + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.extend(["tests", "data", "dummy_modules_publish"]); + let compiled_package = BuildConfig::default().build(path).unwrap(); + let with_unpublished_deps = false; + let compiled_modules_bytes = compiled_package.get_package_base64(with_unpublished_deps); + let dependencies = compiled_package.get_dependency_original_package_ids(); + + let transaction_bytes: TransactionBlockBytes = http_client + .publish( + address, + compiled_modules_bytes, + dependencies, + Some(gas.coin_object_id), + 100_000_000.into(), + ) + .await + .unwrap(); + + let signed_transaction = + to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &account_keypair); + let (tx_bytes, signatures) = signed_transaction.to_tx_bytes_and_signatures(); + + let tx_response: IotaTransactionBlockResponse = http_client + .execute_transaction_block( + tx_bytes, + signatures, + Some( + IotaTransactionBlockResponseOptions::new() + .with_object_changes() + .with_events(), + ), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + let object_changes = tx_response.object_changes.as_ref().unwrap(); + let package_id = object_changes + .iter() + .find_map(|change| match change { + ObjectChange::Published { package_id, .. } => Some(package_id), + _ => None, + }) + .unwrap(); + + let coin_name = format!("{package_id}::trusted_coin::TRUSTED_COIN"); + let result: Supply = http_client + .get_total_supply(coin_name.clone()) + .await + .unwrap(); + + assert_eq!(0, result.value); + + let object_changes = tx_response.object_changes.as_ref().unwrap(); + let treasury_cap = object_changes + .iter() + .filter_map(|change| match change { + ObjectChange::Created { + object_id, + object_type, + .. + } => Some((object_id, object_type)), + _ => None, + }) + .find_map(|(object_id, object_type)| { + let coin_type = parse_iota_struct_tag(&coin_name).unwrap(); + (&TreasuryCap::type_(coin_type) == object_type).then_some(object_id) + }) + .unwrap(); + + let transaction_bytes: TransactionBlockBytes = http_client + .move_call( + address, + IOTA_FRAMEWORK_ADDRESS.into(), + COIN_MODULE_NAME.to_string(), + "mint_and_transfer".into(), + type_args![coin_name.clone()].unwrap(), + call_args![treasury_cap, amount, address].unwrap(), + Some(gas.coin_object_id), + 10_000_000.into(), + None, + ) + .await + .unwrap(); + + let signed_transaction = + to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &account_keypair); + let (tx_bytes, signatures) = signed_transaction.to_tx_bytes_and_signatures(); + + let tx_response = http_client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new().with_effects()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + let IotaTransactionBlockResponse { effects, .. } = tx_response; + + assert_eq!( + IotaExecutionStatus::Success, + *effects.as_ref().unwrap().status() + ); + + let created_coin_obj_ref = effects.unwrap().created()[0].reference.clone(); + + Ok((coin_name, created_coin_obj_ref)) +} diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 27a2c9f3e62..734a43296f1 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -14,5 +14,8 @@ mod indexer_api; #[cfg(feature = "shared_test_runtime")] mod read_api; +#[cfg(feature = "shared_test_runtime")] +mod coin_api; + #[cfg(feature = "shared_test_runtime")] mod write_api;