From 8ac6a2c955153c53050fc20182f69ea54f8902f6 Mon Sep 17 00:00:00 2001 From: maancham Date: Wed, 5 Feb 2025 12:18:58 -0500 Subject: [PATCH 01/17] feat: add ItsChains query message and utility --- .../interchain-token-service/src/contract.rs | 5 +++ .../src/contract/query.rs | 6 +++ contracts/interchain-token-service/src/msg.rs | 15 +++++++ .../interchain-token-service/src/state.rs | 44 ++++++++++++++++++- 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index e37c24d89..2a3285ff6 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -49,6 +49,8 @@ pub enum Error { QueryTokenConfig, #[error("failed to query the status of contract")] QueryContractStatus, + #[error("failed to query chain configs")] + QueryAllChainConfigs, } #[cfg_attr(not(feature = "library"), entry_point)] @@ -137,6 +139,9 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result QueryMsg::AllItsContracts => { query::all_its_contracts(deps).change_context(Error::QueryAllItsContracts) } + QueryMsg::ItsChains { filter } => { + query::its_chains(deps, filter).change_context(Error::QueryAllChainConfigs) + } QueryMsg::TokenInstance { chain, token_id } => { query::token_instance(deps, chain, token_id).change_context(Error::QueryTokenInstance) } diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 59694d74a..ad789ea2d 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -34,6 +34,12 @@ pub fn all_its_contracts(deps: Deps) -> Result { to_json_binary(&contract_addresses).change_context(Error::JsonSerialization) } +pub fn its_chains(deps: Deps, filter: Option) -> Result { + let chain_configs = + state::load_filtered_chain_configs(deps.storage, filter).change_context(Error::State)?; + to_json_binary(&chain_configs).change_context(Error::JsonSerialization) +} + pub fn token_instance(deps: Deps, chain: ChainNameRaw, token_id: TokenId) -> Result { let token_instance = state::may_load_token_instance(deps.storage, chain, token_id) .change_context(Error::State)?; diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 43f70f76d..3d8b09c82 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -53,6 +53,17 @@ pub enum ExecuteMsg { EnableExecution, } +#[cw_serde] +pub enum ChainStatusFilter { + Frozen, + Active, +} + +#[cw_serde] +pub struct ChainConfigFilter { + pub status: Option, +} + #[cw_serde] pub struct ChainConfig { pub chain: ChainNameRaw, @@ -85,6 +96,10 @@ pub enum QueryMsg { #[returns(HashMap)] AllItsContracts, + /// Query all chain configs with optional frozen filter + #[returns(Vec)] + ItsChains { filter: Option }, + /// Query a token instance on a specific chain #[returns(Option)] TokenInstance { diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index 94f27e105..65b4ab006 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, OverflowError, StdError, Storage, Uint256}; +use cosmwasm_std::{Addr, Order, OverflowError, StdError, Storage, Uint256}; use cw_storage_plus::{Item, Map}; use error_stack::{report, Result, ResultExt}; use router_api::{Address, ChainNameRaw}; @@ -170,6 +170,48 @@ pub fn load_chain_config( .ok_or_else(|| report!(Error::ChainNotFound(chain.to_owned()))) } +pub fn load_filtered_chain_configs( + storage: &dyn Storage, + filter: Option, +) -> Result, Error> { + let configs = CHAIN_CONFIGS + .range(storage, None, None, Order::Ascending) + .map(|res| { + res.change_context(Error::Storage) + .map(|(chain, config)| msg::ChainConfigResponse { + chain, + its_edge_contract: config.its_address, + truncation: msg::TruncationConfig { + max_uint: config.truncation.max_uint, + max_decimals_when_truncating: config + .truncation + .max_decimals_when_truncating, + }, + frozen: config.frozen, + }) + }) + .collect::, _>>()?; + + Ok(match filter { + Some(filter) if filter.status.is_some() => configs + .into_iter() + .filter(|config| matches_filter(config, filter.status.as_ref())) + .collect(), + _ => configs, + }) +} + +fn matches_filter( + config: &msg::ChainConfigResponse, + status: Option<&msg::ChainStatusFilter>, +) -> bool { + match status { + Some(msg::ChainStatusFilter::Frozen) => config.frozen, + Some(msg::ChainStatusFilter::Active) => !config.frozen, + None => true, + } +} + pub fn save_chain_config( storage: &mut dyn Storage, chain: &ChainNameRaw, From ab84a1113b687f086ae84c8bca3f31aac8c8e1c2 Mon Sep 17 00:00:00 2001 From: maancham Date: Wed, 5 Feb 2025 15:09:10 -0500 Subject: [PATCH 02/17] chore: add four unit tests --- .../interchain-token-service/tests/query.rs | 117 +++++++++++++++++- .../tests/utils/execute.rs | 9 ++ .../tests/utils/query.rs | 32 ++++- 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 1c96204f8..922ce1f03 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use assert_ok::assert_ok; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::Uint256; -use interchain_token_service::msg::{ChainConfigResponse, TruncationConfig}; +use interchain_token_service::msg::{ + ChainConfigFilter, ChainConfigResponse, ChainStatusFilter, TruncationConfig, +}; use interchain_token_service::TokenId; use router_api::{Address, ChainNameRaw}; @@ -127,3 +129,116 @@ fn query_contract_enable_disable_lifecycle() { let enabled = utils::query_is_contract_enabled(deps.as_ref()).unwrap(); assert!(!enabled); } + +#[test] +fn query_chains_config() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let eth_chain: ChainNameRaw = "Ethereum".parse().unwrap(); + let eth_address: Address = "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(); + + utils::register_chain( + deps.as_mut(), + eth_chain.clone(), + eth_address.clone(), + Uint256::MAX.try_into().unwrap(), + u8::MAX, + ) + .unwrap(); + + let poly_chain: ChainNameRaw = "Polygon".parse().unwrap(); + let poly_address: Address = "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(); + + utils::register_chain( + deps.as_mut(), + poly_chain.clone(), + poly_address.clone(), + Uint256::MAX.try_into().unwrap(), + u8::MAX, + ) + .unwrap(); + + // no filtering + let all_chain_configs = assert_ok!(utils::query_its_chains(deps.as_ref(), None)); + let expected_chain_configs = vec![ + utils::create_expected_chain_config( + eth_chain.clone(), + eth_address.clone(), + Uint256::MAX.try_into().unwrap(), + u8::MAX, + false, + ), + utils::create_expected_chain_config( + poly_chain.clone(), + poly_address.clone(), + Uint256::MAX.try_into().unwrap(), + u8::MAX, + false, + ), + ]; + + for (a, e) in all_chain_configs.iter().zip(expected_chain_configs.iter()) { + assert_eq!(a, e); + } + + // filter active chains, should be the same as all chains + let active_chain_configs = assert_ok!(utils::query_its_chains( + deps.as_ref(), + Some(ChainConfigFilter { + status: Some(ChainStatusFilter::Active), + }) + )); + for (a, e) in active_chain_configs + .iter() + .zip(expected_chain_configs.iter()) + { + assert_eq!(a, e); + } + + // filter frozen chains, should be empty + let frozen_chain_configs = assert_ok!(utils::query_its_chains( + deps.as_ref(), + Some(ChainConfigFilter { + status: Some(ChainStatusFilter::Frozen), + }) + )); + assert_eq!(frozen_chain_configs, vec![]); + + // freeze a chain and query again + utils::freeze_chain(deps.as_mut(), eth_chain.clone()).unwrap(); + let frozen_chain_configs = assert_ok!(utils::query_its_chains( + deps.as_ref(), + Some(ChainConfigFilter { + status: Some(ChainStatusFilter::Frozen), + }) + )); + let expected_frozen_chain_configs = vec![utils::create_expected_chain_config( + eth_chain, + eth_address, + Uint256::MAX.try_into().unwrap(), + u8::MAX, + true, + )]; + assert_eq!(frozen_chain_configs, expected_frozen_chain_configs); + + // filter for active chains after freeze + let active_chain_configs = assert_ok!(utils::query_its_chains( + deps.as_ref(), + Some(ChainConfigFilter { + status: Some(ChainStatusFilter::Active), + }) + )); + let expected_active_chain_configs = vec![utils::create_expected_chain_config( + poly_chain, + poly_address, + Uint256::MAX.try_into().unwrap(), + u8::MAX, + false, + )]; + assert_eq!(active_chain_configs, expected_active_chain_configs); +} diff --git a/contracts/interchain-token-service/tests/utils/execute.rs b/contracts/interchain-token-service/tests/utils/execute.rs index 6d2cd05b1..1803de2ce 100644 --- a/contracts/interchain-token-service/tests/utils/execute.rs +++ b/contracts/interchain-token-service/tests/utils/execute.rs @@ -131,6 +131,15 @@ pub fn update_chain( ) } +pub fn freeze_chain(deps: DepsMut, chain: ChainNameRaw) -> Result { + contract::execute( + deps, + mock_env(), + message_info(&MockApi::default().addr_make(params::GOVERNANCE), &[]), + ExecuteMsg::FreezeChain { chain }, + ) +} + pub fn disable_contract_execution(deps: DepsMut) -> Result { contract::execute( deps, diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index c4cff5040..061448486 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; -use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::{error::ContractError, nonempty}; use cosmwasm_std::testing::mock_env; use cosmwasm_std::{from_json, Deps}; use interchain_token_service::contract::query; -use interchain_token_service::msg::{ChainConfigResponse, QueryMsg}; +use interchain_token_service::msg::{ + ChainConfigFilter, ChainConfigResponse, QueryMsg, TruncationConfig, +}; use interchain_token_service::{TokenConfig, TokenId, TokenInstance}; use router_api::{Address, ChainNameRaw}; @@ -48,3 +50,29 @@ pub fn query_is_contract_enabled(deps: Deps) -> Result { let bin = query(deps, mock_env(), QueryMsg::IsEnabled {})?; Ok(from_json(bin)?) } + +pub fn query_its_chains( + deps: Deps, + filter: Option, +) -> Result, ContractError> { + let bin = query(deps, mock_env(), QueryMsg::ItsChains { filter })?; + Ok(from_json(bin)?) +} + +pub fn create_expected_chain_config( + chain: ChainNameRaw, + address: Address, + max_uint: nonempty::Uint256, + max_decimals: u8, + frozen: bool, +) -> ChainConfigResponse { + ChainConfigResponse { + chain, + its_edge_contract: address, + truncation: TruncationConfig { + max_uint, + max_decimals_when_truncating: max_decimals, + }, + frozen, + } +} From 4d4ac380abb03b20c6c9b677d1d1ac087a916eb5 Mon Sep 17 00:00:00 2001 From: maancham Date: Wed, 5 Feb 2025 15:14:43 -0500 Subject: [PATCH 03/17] fix: run nightly cargo fmt --- contracts/interchain-token-service/tests/utils/query.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 061448486..378737b1a 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; -use axelar_wasm_std::{error::ContractError, nonempty}; +use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::nonempty; use cosmwasm_std::testing::mock_env; use cosmwasm_std::{from_json, Deps}; use interchain_token_service::contract::query; From 9140c07f5cbde776368502f3870b4347541c4872 Mon Sep 17 00:00:00 2001 From: maancham Date: Wed, 5 Feb 2025 16:17:27 -0500 Subject: [PATCH 04/17] fix(lint): run with clippy 1.78 and remove vec! --- contracts/interchain-token-service/tests/query.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 922ce1f03..b8016fef4 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -165,7 +165,7 @@ fn query_chains_config() { // no filtering let all_chain_configs = assert_ok!(utils::query_its_chains(deps.as_ref(), None)); - let expected_chain_configs = vec![ + let expected_chain_configs = [ utils::create_expected_chain_config( eth_chain.clone(), eth_address.clone(), @@ -217,7 +217,7 @@ fn query_chains_config() { status: Some(ChainStatusFilter::Frozen), }) )); - let expected_frozen_chain_configs = vec![utils::create_expected_chain_config( + let expected_frozen_chain_configs = [utils::create_expected_chain_config( eth_chain, eth_address, Uint256::MAX.try_into().unwrap(), @@ -233,7 +233,7 @@ fn query_chains_config() { status: Some(ChainStatusFilter::Active), }) )); - let expected_active_chain_configs = vec![utils::create_expected_chain_config( + let expected_active_chain_configs = [utils::create_expected_chain_config( poly_chain, poly_address, Uint256::MAX.try_into().unwrap(), From 9985b7883f4f2f1c7dfeee844220b1290f318712 Mon Sep 17 00:00:00 2001 From: maancham Date: Thu, 6 Feb 2025 11:00:36 -0500 Subject: [PATCH 05/17] refactor: address PR comments --- .../src/contract/query.rs | 24 +++++++++++++-- contracts/interchain-token-service/src/msg.rs | 6 ++-- .../interchain-token-service/src/state.rs | 25 ++-------------- .../interchain-token-service/tests/query.rs | 30 +++++++------------ .../tests/utils/query.rs | 12 +++++--- 5 files changed, 45 insertions(+), 52 deletions(-) diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index ad789ea2d..4c2a1c74c 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -34,12 +34,30 @@ pub fn all_its_contracts(deps: Deps) -> Result { to_json_binary(&contract_addresses).change_context(Error::JsonSerialization) } -pub fn its_chains(deps: Deps, filter: Option) -> Result { - let chain_configs = - state::load_filtered_chain_configs(deps.storage, filter).change_context(Error::State)?; +pub fn its_chains(deps: Deps, filter: Option) -> Result { + let state_configs = state::load_chain_configs(deps.storage).change_context(Error::State)?; + + let chain_configs = match filter { + Some(filter) if filter.frozen_status.is_some() => state_configs + .into_iter() + .filter(|config| matches_filter(config, filter.frozen_status.as_ref())) + .collect(), + _ => state_configs, + }; to_json_binary(&chain_configs).change_context(Error::JsonSerialization) } +fn matches_filter( + config: &msg::ChainConfigResponse, + status: Option<&msg::ChainStatusFilter>, +) -> bool { + match status { + Some(msg::ChainStatusFilter::Frozen) => config.frozen, + Some(msg::ChainStatusFilter::Active) => !config.frozen, + None => true, + } +} + pub fn token_instance(deps: Deps, chain: ChainNameRaw, token_id: TokenId) -> Result { let token_instance = state::may_load_token_instance(deps.storage, chain, token_id) .change_context(Error::State)?; diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 3d8b09c82..dd6592415 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -60,8 +60,8 @@ pub enum ChainStatusFilter { } #[cw_serde] -pub struct ChainConfigFilter { - pub status: Option, +pub struct ChainFilter { + pub frozen_status: Option, } #[cw_serde] @@ -98,7 +98,7 @@ pub enum QueryMsg { /// Query all chain configs with optional frozen filter #[returns(Vec)] - ItsChains { filter: Option }, + ItsChains { filter: Option }, /// Query a token instance on a specific chain #[returns(Option)] diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index 65b4ab006..a5ffb251c 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -170,10 +170,7 @@ pub fn load_chain_config( .ok_or_else(|| report!(Error::ChainNotFound(chain.to_owned()))) } -pub fn load_filtered_chain_configs( - storage: &dyn Storage, - filter: Option, -) -> Result, Error> { +pub fn load_chain_configs(storage: &dyn Storage) -> Result, Error> { let configs = CHAIN_CONFIGS .range(storage, None, None, Order::Ascending) .map(|res| { @@ -191,25 +188,7 @@ pub fn load_filtered_chain_configs( }) }) .collect::, _>>()?; - - Ok(match filter { - Some(filter) if filter.status.is_some() => configs - .into_iter() - .filter(|config| matches_filter(config, filter.status.as_ref())) - .collect(), - _ => configs, - }) -} - -fn matches_filter( - config: &msg::ChainConfigResponse, - status: Option<&msg::ChainStatusFilter>, -) -> bool { - match status { - Some(msg::ChainStatusFilter::Frozen) => config.frozen, - Some(msg::ChainStatusFilter::Active) => !config.frozen, - None => true, - } + Ok(configs) } pub fn save_chain_config( diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index b8016fef4..808fff530 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -4,7 +4,7 @@ use assert_ok::assert_ok; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::Uint256; use interchain_token_service::msg::{ - ChainConfigFilter, ChainConfigResponse, ChainStatusFilter, TruncationConfig, + ChainConfigResponse, ChainFilter, ChainStatusFilter, TruncationConfig, }; use interchain_token_service::TokenId; use router_api::{Address, ChainNameRaw}; @@ -181,30 +181,22 @@ fn query_chains_config() { false, ), ]; - - for (a, e) in all_chain_configs.iter().zip(expected_chain_configs.iter()) { - assert_eq!(a, e); - } + utils::field_by_field_check(all_chain_configs, expected_chain_configs.to_vec()); // filter active chains, should be the same as all chains let active_chain_configs = assert_ok!(utils::query_its_chains( deps.as_ref(), - Some(ChainConfigFilter { - status: Some(ChainStatusFilter::Active), + Some(ChainFilter { + frozen_status: Some(ChainStatusFilter::Active), }) )); - for (a, e) in active_chain_configs - .iter() - .zip(expected_chain_configs.iter()) - { - assert_eq!(a, e); - } + utils::field_by_field_check(active_chain_configs, expected_chain_configs.to_vec()); // filter frozen chains, should be empty let frozen_chain_configs = assert_ok!(utils::query_its_chains( deps.as_ref(), - Some(ChainConfigFilter { - status: Some(ChainStatusFilter::Frozen), + Some(ChainFilter { + frozen_status: Some(ChainStatusFilter::Frozen), }) )); assert_eq!(frozen_chain_configs, vec![]); @@ -213,8 +205,8 @@ fn query_chains_config() { utils::freeze_chain(deps.as_mut(), eth_chain.clone()).unwrap(); let frozen_chain_configs = assert_ok!(utils::query_its_chains( deps.as_ref(), - Some(ChainConfigFilter { - status: Some(ChainStatusFilter::Frozen), + Some(ChainFilter { + frozen_status: Some(ChainStatusFilter::Frozen), }) )); let expected_frozen_chain_configs = [utils::create_expected_chain_config( @@ -229,8 +221,8 @@ fn query_chains_config() { // filter for active chains after freeze let active_chain_configs = assert_ok!(utils::query_its_chains( deps.as_ref(), - Some(ChainConfigFilter { - status: Some(ChainStatusFilter::Active), + Some(ChainFilter { + frozen_status: Some(ChainStatusFilter::Active), }) )); let expected_active_chain_configs = [utils::create_expected_chain_config( diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 378737b1a..36386f5f9 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -5,9 +5,7 @@ use axelar_wasm_std::nonempty; use cosmwasm_std::testing::mock_env; use cosmwasm_std::{from_json, Deps}; use interchain_token_service::contract::query; -use interchain_token_service::msg::{ - ChainConfigFilter, ChainConfigResponse, QueryMsg, TruncationConfig, -}; +use interchain_token_service::msg::{ChainConfigResponse, ChainFilter, QueryMsg, TruncationConfig}; use interchain_token_service::{TokenConfig, TokenId, TokenInstance}; use router_api::{Address, ChainNameRaw}; @@ -54,7 +52,7 @@ pub fn query_is_contract_enabled(deps: Deps) -> Result { pub fn query_its_chains( deps: Deps, - filter: Option, + filter: Option, ) -> Result, ContractError> { let bin = query(deps, mock_env(), QueryMsg::ItsChains { filter })?; Ok(from_json(bin)?) @@ -77,3 +75,9 @@ pub fn create_expected_chain_config( frozen, } } + +pub fn field_by_field_check(actual: Vec, expected: Vec) { + for (a, e) in actual.iter().zip(expected.iter()) { + assert_eq!(a, e); + } +} From 783c65a289f5968cbe04451f9907aad3b3420daa Mon Sep 17 00:00:00 2001 From: maancham Date: Thu, 6 Feb 2025 12:55:24 -0500 Subject: [PATCH 06/17] feat: add test config setup to its unit tests --- .../interchain-token-service/tests/query.rs | 236 +++++++++--------- .../tests/utils/query.rs | 31 ++- 2 files changed, 138 insertions(+), 129 deletions(-) diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 808fff530..776376d4c 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use assert_ok::assert_ok; -use cosmwasm_std::testing::mock_dependencies; -use cosmwasm_std::Uint256; +use cosmwasm_std::testing::{mock_dependencies, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{Empty, OwnedDeps, Uint256}; use interchain_token_service::msg::{ ChainConfigResponse, ChainFilter, ChainStatusFilter, TruncationConfig, }; @@ -11,41 +11,84 @@ use router_api::{Address, ChainNameRaw}; mod utils; -#[test] -fn query_chain_config() { - let mut deps = mock_dependencies(); - utils::instantiate_contract(deps.as_mut()).unwrap(); +struct ChainConfigTest { + deps: OwnedDeps, + eth: utils::ChainData, + polygon: utils::ChainData, +} - let chain: ChainNameRaw = "Ethereum".parse().unwrap(); - let address: Address = "0x1234567890123456789012345678901234567890" - .parse() +impl ChainConfigTest { + fn setup() -> Self { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let eth = utils::ChainData { + chain: "Ethereum".parse().unwrap(), + address: "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(), + max_uint: Uint256::MAX.try_into().unwrap(), + max_decimals: u8::MAX, + }; + + let polygon = utils::ChainData { + chain: "Polygon".parse().unwrap(), + address: "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(), + max_uint: Uint256::MAX.try_into().unwrap(), + max_decimals: u8::MAX, + }; + + let mut test_config = Self { deps, eth, polygon }; + test_config.register_test_chains(); + test_config + } + + fn register_test_chains(&mut self) { + utils::register_chain( + self.deps.as_mut(), + self.eth.chain.clone(), + self.eth.address.clone(), + self.eth.max_uint, + self.eth.max_decimals, + ) .unwrap(); - utils::register_chain( - deps.as_mut(), - chain.clone(), - address.clone(), - Uint256::MAX.try_into().unwrap(), - u8::MAX, - ) - .unwrap(); + utils::register_chain( + self.deps.as_mut(), + self.polygon.chain.clone(), + self.polygon.address.clone(), + self.polygon.max_uint, + self.polygon.max_decimals, + ) + .unwrap(); + } +} - let original_chain_config = ChainConfigResponse { - chain: chain.clone(), - its_edge_contract: address.clone(), +#[test] +fn query_chain_config() { + let mut test_config = ChainConfigTest::setup(); + + let eth_expected_config_response = ChainConfigResponse { + chain: test_config.eth.chain.clone(), + its_edge_contract: test_config.eth.address.clone(), truncation: TruncationConfig { - max_uint: Uint256::MAX.try_into().unwrap(), - max_decimals_when_truncating: u8::MAX, + max_uint: test_config.eth.max_uint, + max_decimals_when_truncating: test_config.eth.max_decimals, }, frozen: false, }; - let chain_config = assert_ok!(utils::query_its_chain(deps.as_ref(), chain.clone())); - assert_eq!(chain_config.unwrap(), original_chain_config); + let eth_chain_config = assert_ok!(utils::query_its_chain( + test_config.deps.as_ref(), + test_config.eth.chain.clone() + )); + assert_eq!(eth_chain_config.unwrap(), eth_expected_config_response); // case sensitive query let chain_config = assert_ok!(utils::query_its_chain( - deps.as_ref(), + test_config.deps.as_ref(), "ethereum".parse().unwrap() )); assert_eq!(chain_config, None); @@ -54,16 +97,22 @@ fn query_chain_config() { .parse() .unwrap(); assert_ok!(utils::update_chain( - deps.as_mut(), - chain.clone(), + test_config.deps.as_mut(), + test_config.eth.chain.clone(), new_address.clone() )); - let chain_config = assert_ok!(utils::query_its_chain(deps.as_ref(), chain.clone())); + let chain_config = assert_ok!(utils::query_its_chain( + test_config.deps.as_ref(), + test_config.eth.chain.clone() + )); assert_eq!(chain_config.unwrap().its_edge_contract, new_address); let non_existent_chain: ChainNameRaw = "non-existent-chain".parse().unwrap(); - let chain_config = assert_ok!(utils::query_its_chain(deps.as_ref(), non_existent_chain)); + let chain_config = assert_ok!(utils::query_its_chain( + test_config.deps.as_ref(), + non_existent_chain + )); assert_eq!(chain_config, None); } @@ -132,105 +181,60 @@ fn query_contract_enable_disable_lifecycle() { #[test] fn query_chains_config() { - let mut deps = mock_dependencies(); - utils::instantiate_contract(deps.as_mut()).unwrap(); + let mut test_config = ChainConfigTest::setup(); - let eth_chain: ChainNameRaw = "Ethereum".parse().unwrap(); - let eth_address: Address = "0x1234567890123456789012345678901234567890" - .parse() - .unwrap(); + // Test all chains + let all_chains = utils::query_its_chains(test_config.deps.as_ref(), None).unwrap(); + let expected = vec![ + utils::create_config_response(&test_config.eth, false), + utils::create_config_response(&test_config.polygon, false), + ]; + utils::assert_configs_equal(&all_chains, &expected); - utils::register_chain( - deps.as_mut(), - eth_chain.clone(), - eth_address.clone(), - Uint256::MAX.try_into().unwrap(), - u8::MAX, + // Test active chains + let active_chains = utils::query_its_chains( + test_config.deps.as_ref(), + Some(ChainFilter { + frozen_status: Some(ChainStatusFilter::Active), + }), ) .unwrap(); + utils::assert_configs_equal(&active_chains, &expected); - let poly_chain: ChainNameRaw = "Polygon".parse().unwrap(); - let poly_address: Address = "0x1234567890123456789012345678901234567890" - .parse() - .unwrap(); - - utils::register_chain( - deps.as_mut(), - poly_chain.clone(), - poly_address.clone(), - Uint256::MAX.try_into().unwrap(), - u8::MAX, + // Test frozen chains (empty) + let frozen_chains = utils::query_its_chains( + test_config.deps.as_ref(), + Some(ChainFilter { + frozen_status: Some(ChainStatusFilter::Frozen), + }), ) .unwrap(); + assert!(frozen_chains.is_empty()); - // no filtering - let all_chain_configs = assert_ok!(utils::query_its_chains(deps.as_ref(), None)); - let expected_chain_configs = [ - utils::create_expected_chain_config( - eth_chain.clone(), - eth_address.clone(), - Uint256::MAX.try_into().unwrap(), - u8::MAX, - false, - ), - utils::create_expected_chain_config( - poly_chain.clone(), - poly_address.clone(), - Uint256::MAX.try_into().unwrap(), - u8::MAX, - false, - ), - ]; - utils::field_by_field_check(all_chain_configs, expected_chain_configs.to_vec()); - - // filter active chains, should be the same as all chains - let active_chain_configs = assert_ok!(utils::query_its_chains( - deps.as_ref(), - Some(ChainFilter { - frozen_status: Some(ChainStatusFilter::Active), - }) - )); - utils::field_by_field_check(active_chain_configs, expected_chain_configs.to_vec()); + // Test after freezing eth chain + utils::freeze_chain(test_config.deps.as_mut(), test_config.eth.chain.clone()).unwrap(); - // filter frozen chains, should be empty - let frozen_chain_configs = assert_ok!(utils::query_its_chains( - deps.as_ref(), + let frozen_chains = utils::query_its_chains( + test_config.deps.as_ref(), Some(ChainFilter { frozen_status: Some(ChainStatusFilter::Frozen), - }) - )); - assert_eq!(frozen_chain_configs, vec![]); + }), + ) + .unwrap(); + utils::assert_configs_equal( + &frozen_chains, + &vec![utils::create_config_response(&test_config.eth, true)], + ); - // freeze a chain and query again - utils::freeze_chain(deps.as_mut(), eth_chain.clone()).unwrap(); - let frozen_chain_configs = assert_ok!(utils::query_its_chains( - deps.as_ref(), - Some(ChainFilter { - frozen_status: Some(ChainStatusFilter::Frozen), - }) - )); - let expected_frozen_chain_configs = [utils::create_expected_chain_config( - eth_chain, - eth_address, - Uint256::MAX.try_into().unwrap(), - u8::MAX, - true, - )]; - assert_eq!(frozen_chain_configs, expected_frozen_chain_configs); - - // filter for active chains after freeze - let active_chain_configs = assert_ok!(utils::query_its_chains( - deps.as_ref(), + let active_chains = utils::query_its_chains( + test_config.deps.as_ref(), Some(ChainFilter { frozen_status: Some(ChainStatusFilter::Active), - }) - )); - let expected_active_chain_configs = [utils::create_expected_chain_config( - poly_chain, - poly_address, - Uint256::MAX.try_into().unwrap(), - u8::MAX, - false, - )]; - assert_eq!(active_chain_configs, expected_active_chain_configs); + }), + ) + .unwrap(); + utils::assert_configs_equal( + &active_chains, + &vec![utils::create_config_response(&test_config.polygon, false)], + ); } diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 36386f5f9..237d5ed84 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -58,26 +58,31 @@ pub fn query_its_chains( Ok(from_json(bin)?) } -pub fn create_expected_chain_config( - chain: ChainNameRaw, - address: Address, - max_uint: nonempty::Uint256, - max_decimals: u8, - frozen: bool, -) -> ChainConfigResponse { +pub struct ChainData { + pub chain: ChainNameRaw, + pub address: Address, + pub max_uint: nonempty::Uint256, + pub max_decimals: u8, +} + +pub fn create_config_response(chain_data: &ChainData, frozen: bool) -> ChainConfigResponse { ChainConfigResponse { - chain, - its_edge_contract: address, + chain: chain_data.chain.clone(), + its_edge_contract: chain_data.address.clone(), truncation: TruncationConfig { - max_uint, - max_decimals_when_truncating: max_decimals, + max_uint: chain_data.max_uint, + max_decimals_when_truncating: chain_data.max_decimals, }, frozen, } } -pub fn field_by_field_check(actual: Vec, expected: Vec) { +pub fn assert_configs_equal( + actual: &Vec, + expected: &Vec, +) { + assert_eq!(actual.len(), expected.len(), "Different number of configs"); for (a, e) in actual.iter().zip(expected.iter()) { - assert_eq!(a, e); + assert_eq!(a, e, "Config mismatch for chain {}", e.chain); } } From fae8fc9f4067572a298c918edc166d1b059a38ac Mon Sep 17 00:00:00 2001 From: maancham Date: Thu, 6 Feb 2025 13:34:26 -0500 Subject: [PATCH 07/17] fix: lint cargo clippy --- contracts/interchain-token-service/tests/query.rs | 4 ++-- contracts/interchain-token-service/tests/utils/query.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 776376d4c..604dc4942 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -223,7 +223,7 @@ fn query_chains_config() { .unwrap(); utils::assert_configs_equal( &frozen_chains, - &vec![utils::create_config_response(&test_config.eth, true)], + &[utils::create_config_response(&test_config.eth, true)], ); let active_chains = utils::query_its_chains( @@ -235,6 +235,6 @@ fn query_chains_config() { .unwrap(); utils::assert_configs_equal( &active_chains, - &vec![utils::create_config_response(&test_config.polygon, false)], + &[utils::create_config_response(&test_config.polygon, false)], ); } diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 237d5ed84..9253afb1b 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -78,8 +78,8 @@ pub fn create_config_response(chain_data: &ChainData, frozen: bool) -> ChainConf } pub fn assert_configs_equal( - actual: &Vec, - expected: &Vec, + actual: &[ChainConfigResponse], + expected: &[ChainConfigResponse], ) { assert_eq!(actual.len(), expected.len(), "Different number of configs"); for (a, e) in actual.iter().zip(expected.iter()) { From ed452c9de9c48d3faf944e0a61aec8bfdb836763 Mon Sep 17 00:00:00 2001 From: maancham Date: Thu, 6 Feb 2025 13:36:34 -0500 Subject: [PATCH 08/17] fix: lint issues with fmt --- contracts/interchain-token-service/tests/utils/query.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 9253afb1b..c6191cfc0 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -77,10 +77,7 @@ pub fn create_config_response(chain_data: &ChainData, frozen: bool) -> ChainConf } } -pub fn assert_configs_equal( - actual: &[ChainConfigResponse], - expected: &[ChainConfigResponse], -) { +pub fn assert_configs_equal(actual: &[ChainConfigResponse], expected: &[ChainConfigResponse]) { assert_eq!(actual.len(), expected.len(), "Different number of configs"); for (a, e) in actual.iter().zip(expected.iter()) { assert_eq!(a, e, "Config mismatch for chain {}", e.chain); From 838f3d0ddccf1d14effcf25e8bf2e02b0cac5dce Mon Sep 17 00:00:00 2001 From: maancham Date: Fri, 7 Feb 2025 10:59:26 -0500 Subject: [PATCH 09/17] refactor: minor changes for optimization --- .../src/contract/query.rs | 40 ++++++++++--------- contracts/interchain-token-service/src/msg.rs | 18 +++++++++ .../interchain-token-service/src/state.rs | 23 ++--------- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 4c2a1c74c..559208f8f 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -1,6 +1,7 @@ use axelar_wasm_std::{killswitch, IntoContractError}; -use cosmwasm_std::{to_json_binary, Binary, Deps}; +use cosmwasm_std::{to_json_binary, Binary, Deps, Order}; use error_stack::{Result, ResultExt}; +use itertools::Itertools; use router_api::ChainNameRaw; use crate::{msg, state, TokenId}; @@ -35,27 +36,30 @@ pub fn all_its_contracts(deps: Deps) -> Result { } pub fn its_chains(deps: Deps, filter: Option) -> Result { - let state_configs = state::load_chain_configs(deps.storage).change_context(Error::State)?; + let state_chain_configs = state::load_chain_configs(); - let chain_configs = match filter { - Some(filter) if filter.frozen_status.is_some() => state_configs + let chain_config_responses: Vec = state_chain_configs + .range(deps.storage, None, None, Order::Ascending) + .map(|r| r.change_context(Error::State)) + .map_ok(|(chain, config)| msg::ChainConfigResponse { + chain, + its_edge_contract: config.its_address, + truncation: msg::TruncationConfig { + max_uint: config.truncation.max_uint, + max_decimals_when_truncating: config.truncation.max_decimals_when_truncating, + }, + frozen: config.frozen, + }) + .try_collect()?; + + let filtered_chain_configs = match &filter { + Some(filter) => chain_config_responses .into_iter() - .filter(|config| matches_filter(config, filter.frozen_status.as_ref())) + .filter(|config| filter.matches(config)) .collect(), - _ => state_configs, + None => chain_config_responses, }; - to_json_binary(&chain_configs).change_context(Error::JsonSerialization) -} - -fn matches_filter( - config: &msg::ChainConfigResponse, - status: Option<&msg::ChainStatusFilter>, -) -> bool { - match status { - Some(msg::ChainStatusFilter::Frozen) => config.frozen, - Some(msg::ChainStatusFilter::Active) => !config.frozen, - None => true, - } + to_json_binary(&filtered_chain_configs).change_context(Error::JsonSerialization) } pub fn token_instance(deps: Deps, chain: ChainNameRaw, token_id: TokenId) -> Result { diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index dd6592415..351ccfadf 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -64,6 +64,24 @@ pub struct ChainFilter { pub frozen_status: Option, } +impl ChainStatusFilter { + pub fn matches(&self, config: &ChainConfigResponse) -> bool { + match self { + ChainStatusFilter::Frozen => config.frozen, + ChainStatusFilter::Active => !config.frozen, + } + } +} + +impl ChainFilter { + pub fn matches(&self, config: &ChainConfigResponse) -> bool { + match &self.frozen_status { + Some(frozen_status) => frozen_status.matches(config), + None => true, + } + } +} + #[cw_serde] pub struct ChainConfig { pub chain: ChainNameRaw, diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index a5ffb251c..295a38d1f 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Order, OverflowError, StdError, Storage, Uint256}; +use cosmwasm_std::{Addr, OverflowError, StdError, Storage, Uint256}; use cw_storage_plus::{Item, Map}; use error_stack::{report, Result, ResultExt}; use router_api::{Address, ChainNameRaw}; @@ -170,25 +170,8 @@ pub fn load_chain_config( .ok_or_else(|| report!(Error::ChainNotFound(chain.to_owned()))) } -pub fn load_chain_configs(storage: &dyn Storage) -> Result, Error> { - let configs = CHAIN_CONFIGS - .range(storage, None, None, Order::Ascending) - .map(|res| { - res.change_context(Error::Storage) - .map(|(chain, config)| msg::ChainConfigResponse { - chain, - its_edge_contract: config.its_address, - truncation: msg::TruncationConfig { - max_uint: config.truncation.max_uint, - max_decimals_when_truncating: config - .truncation - .max_decimals_when_truncating, - }, - frozen: config.frozen, - }) - }) - .collect::, _>>()?; - Ok(configs) +pub fn load_chain_configs() -> Map<&'static ChainNameRaw, ChainConfig> { + CHAIN_CONFIGS } pub fn save_chain_config( From 731104b637ebd3b683145d01e4dff205d2fbccbc Mon Sep 17 00:00:00 2001 From: maancham Date: Mon, 10 Feb 2025 18:08:47 -0500 Subject: [PATCH 10/17] feat: add pagination to query message --- .../interchain-token-service/src/contract.rs | 9 ++++--- .../src/contract/query.rs | 27 ++++++++++++------- contracts/interchain-token-service/src/msg.rs | 9 ++++++- .../interchain-token-service/tests/query.rs | 10 ++++++- .../tests/utils/query.rs | 12 ++++++++- 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 2a3285ff6..36bb29502 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -139,9 +139,12 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result QueryMsg::AllItsContracts => { query::all_its_contracts(deps).change_context(Error::QueryAllItsContracts) } - QueryMsg::ItsChains { filter } => { - query::its_chains(deps, filter).change_context(Error::QueryAllChainConfigs) - } + QueryMsg::ItsChains { + filter, + start_after, + limit, + } => query::its_chains(deps, filter, start_after, limit) + .change_context(Error::QueryAllChainConfigs), QueryMsg::TokenInstance { chain, token_id } => { query::token_instance(deps, chain, token_id).change_context(Error::QueryTokenInstance) } diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 559208f8f..2ebf80393 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -1,11 +1,15 @@ use axelar_wasm_std::{killswitch, IntoContractError}; use cosmwasm_std::{to_json_binary, Binary, Deps, Order}; +use cw_storage_plus::Bound; use error_stack::{Result, ResultExt}; use itertools::Itertools; use router_api::ChainNameRaw; use crate::{msg, state, TokenId}; +// Pagination limit +const DEFAULT_LIMIT: u32 = u32::MAX; + #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { #[error("failed to serialize data to JSON")] @@ -35,11 +39,19 @@ pub fn all_its_contracts(deps: Deps) -> Result { to_json_binary(&contract_addresses).change_context(Error::JsonSerialization) } -pub fn its_chains(deps: Deps, filter: Option) -> Result { +pub fn its_chains( + deps: Deps, + filter: Option, + start_after: Option, + limit: Option, +) -> Result { let state_chain_configs = state::load_chain_configs(); - let chain_config_responses: Vec = state_chain_configs - .range(deps.storage, None, None, Order::Ascending) + let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; + let start = start_after.as_ref().map(Bound::exclusive); + + let filtered_chain_configs: Vec<_> = state_chain_configs + .range(deps.storage, start, None, Order::Ascending) .map(|r| r.change_context(Error::State)) .map_ok(|(chain, config)| msg::ChainConfigResponse { chain, @@ -50,15 +62,10 @@ pub fn its_chains(deps: Deps, filter: Option) -> Result chain_config_responses - .into_iter() - .filter(|config| filter.matches(config)) - .collect(), - None => chain_config_responses, - }; to_json_binary(&filtered_chain_configs).change_context(Error::JsonSerialization) } diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 351ccfadf..60df6bf7b 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -115,8 +115,15 @@ pub enum QueryMsg { AllItsContracts, /// Query all chain configs with optional frozen filter + // The list is paginated by: + // - start_after: the chain name to start after, which the next page of results should start. + // - limit: limit the number of chains returned, default is u32::MAX. #[returns(Vec)] - ItsChains { filter: Option }, + ItsChains { + filter: Option, + start_after: Option, + limit: Option, + }, /// Query a token instance on a specific chain #[returns(Option)] diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 604dc4942..2993f5ed3 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -184,7 +184,7 @@ fn query_chains_config() { let mut test_config = ChainConfigTest::setup(); // Test all chains - let all_chains = utils::query_its_chains(test_config.deps.as_ref(), None).unwrap(); + let all_chains = utils::query_its_chains(test_config.deps.as_ref(), None, None, None).unwrap(); let expected = vec![ utils::create_config_response(&test_config.eth, false), utils::create_config_response(&test_config.polygon, false), @@ -197,6 +197,8 @@ fn query_chains_config() { Some(ChainFilter { frozen_status: Some(ChainStatusFilter::Active), }), + None, + None, ) .unwrap(); utils::assert_configs_equal(&active_chains, &expected); @@ -207,6 +209,8 @@ fn query_chains_config() { Some(ChainFilter { frozen_status: Some(ChainStatusFilter::Frozen), }), + None, + None, ) .unwrap(); assert!(frozen_chains.is_empty()); @@ -219,6 +223,8 @@ fn query_chains_config() { Some(ChainFilter { frozen_status: Some(ChainStatusFilter::Frozen), }), + None, + None, ) .unwrap(); utils::assert_configs_equal( @@ -231,6 +237,8 @@ fn query_chains_config() { Some(ChainFilter { frozen_status: Some(ChainStatusFilter::Active), }), + None, + None, ) .unwrap(); utils::assert_configs_equal( diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index c6191cfc0..43fde495e 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -53,8 +53,18 @@ pub fn query_is_contract_enabled(deps: Deps) -> Result { pub fn query_its_chains( deps: Deps, filter: Option, + start_after: Option, + limit: Option, ) -> Result, ContractError> { - let bin = query(deps, mock_env(), QueryMsg::ItsChains { filter })?; + let bin = query( + deps, + mock_env(), + QueryMsg::ItsChains { + filter, + start_after, + limit, + }, + )?; Ok(from_json(bin)?) } From 53f80151d2032c88cd810f9bebecf26339262400 Mon Sep 17 00:00:00 2001 From: maancham Date: Tue, 11 Feb 2025 10:13:30 -0500 Subject: [PATCH 11/17] merge branch main into feat/its-chains --- contracts/interchain-token-service/tests/query.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 2993f5ed3..8bf28ade4 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -99,7 +99,9 @@ fn query_chain_config() { assert_ok!(utils::update_chain( test_config.deps.as_mut(), test_config.eth.chain.clone(), - new_address.clone() + new_address.clone(), + Uint256::MAX.try_into().unwrap(), + 18, )); let chain_config = assert_ok!(utils::query_its_chain( From 5dc721d49c8b2cc4d56a1225efa9b5ea3510920f Mon Sep 17 00:00:00 2001 From: maancham Date: Tue, 11 Feb 2025 10:26:53 -0500 Subject: [PATCH 12/17] refactor: bring back status instead of frozen_status --- contracts/interchain-token-service/src/msg.rs | 4 ++-- contracts/interchain-token-service/tests/query.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index f92b7ebcf..df5b10ff7 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -59,7 +59,7 @@ pub enum ChainStatusFilter { #[cw_serde] pub struct ChainFilter { - pub frozen_status: Option, + pub status: Option, } impl ChainStatusFilter { @@ -73,7 +73,7 @@ impl ChainStatusFilter { impl ChainFilter { pub fn matches(&self, config: &ChainConfigResponse) -> bool { - match &self.frozen_status { + match &self.status { Some(frozen_status) => frozen_status.matches(config), None => true, } diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 8bf28ade4..03720550a 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -197,7 +197,7 @@ fn query_chains_config() { let active_chains = utils::query_its_chains( test_config.deps.as_ref(), Some(ChainFilter { - frozen_status: Some(ChainStatusFilter::Active), + status: Some(ChainStatusFilter::Active), }), None, None, @@ -209,7 +209,7 @@ fn query_chains_config() { let frozen_chains = utils::query_its_chains( test_config.deps.as_ref(), Some(ChainFilter { - frozen_status: Some(ChainStatusFilter::Frozen), + status: Some(ChainStatusFilter::Frozen), }), None, None, @@ -223,7 +223,7 @@ fn query_chains_config() { let frozen_chains = utils::query_its_chains( test_config.deps.as_ref(), Some(ChainFilter { - frozen_status: Some(ChainStatusFilter::Frozen), + status: Some(ChainStatusFilter::Frozen), }), None, None, @@ -237,7 +237,7 @@ fn query_chains_config() { let active_chains = utils::query_its_chains( test_config.deps.as_ref(), Some(ChainFilter { - frozen_status: Some(ChainStatusFilter::Active), + status: Some(ChainStatusFilter::Active), }), None, None, From a74e2e4e9a96a84108f4dd9e0e653933682273f6 Mon Sep 17 00:00:00 2001 From: maancham Date: Tue, 11 Feb 2025 16:09:17 -0500 Subject: [PATCH 13/17] refactor: return iterator from state to query --- .../src/contract/query.rs | 45 +++++++++---------- contracts/interchain-token-service/src/msg.rs | 17 ------- .../interchain-token-service/src/state.rs | 22 +++++++-- 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 2ebf80393..48d153b3b 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -1,15 +1,11 @@ use axelar_wasm_std::{killswitch, IntoContractError}; -use cosmwasm_std::{to_json_binary, Binary, Deps, Order}; -use cw_storage_plus::Bound; +use cosmwasm_std::{to_json_binary, Binary, Deps}; use error_stack::{Result, ResultExt}; use itertools::Itertools; use router_api::ChainNameRaw; use crate::{msg, state, TokenId}; -// Pagination limit -const DEFAULT_LIMIT: u32 = u32::MAX; - #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { #[error("failed to serialize data to JSON")] @@ -45,26 +41,29 @@ pub fn its_chains( start_after: Option, limit: Option, ) -> Result { - let state_chain_configs = state::load_chain_configs(); - - let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; - let start = start_after.as_ref().map(Bound::exclusive); - - let filtered_chain_configs: Vec<_> = state_chain_configs - .range(deps.storage, start, None, Order::Ascending) - .map(|r| r.change_context(Error::State)) - .map_ok(|(chain, config)| msg::ChainConfigResponse { - chain, - its_edge_contract: config.its_address, - truncation: msg::TruncationConfig { - max_uint: config.truncation.max_uint, - max_decimals_when_truncating: config.truncation.max_decimals_when_truncating, + let filter_fn = |config: &state::ChainConfig| { + filter.as_ref().map_or(true, |f| match &f.status { + Some(status) => match status { + msg::ChainStatusFilter::Frozen => config.frozen, + msg::ChainStatusFilter::Active => !config.frozen, }, - frozen: config.frozen, + None => true, }) - .filter_ok(|config| !filter.clone().is_some_and(|f| !f.matches(config))) - .take(limit) - .try_collect()?; + }; + + let filtered_chain_configs: Vec<_> = + state::load_chain_configs(deps.storage, filter_fn, start_after, limit) + .map(|r| r.change_context(Error::State)) + .map_ok(|(chain, config)| msg::ChainConfigResponse { + chain, + its_edge_contract: config.its_address, + truncation: msg::TruncationConfig { + max_uint: config.truncation.max_uint, + max_decimals_when_truncating: config.truncation.max_decimals_when_truncating, + }, + frozen: config.frozen, + }) + .try_collect()?; to_json_binary(&filtered_chain_configs).change_context(Error::JsonSerialization) } diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index df5b10ff7..1fe30249c 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -62,23 +62,6 @@ pub struct ChainFilter { pub status: Option, } -impl ChainStatusFilter { - pub fn matches(&self, config: &ChainConfigResponse) -> bool { - match self { - ChainStatusFilter::Frozen => config.frozen, - ChainStatusFilter::Active => !config.frozen, - } - } -} - -impl ChainFilter { - pub fn matches(&self, config: &ChainConfigResponse) -> bool { - match &self.status { - Some(frozen_status) => frozen_status.matches(config), - None => true, - } - } -} #[cw_serde] pub struct ChainConfig { diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index e2e229b70..6c0ce0e82 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -2,13 +2,17 @@ use std::collections::HashMap; use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, OverflowError, StdError, Storage, Uint256}; -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::{Addr, Order, OverflowError, StdError, Storage, Uint256}; +use cw_storage_plus::{Bound, Item, Map}; use error_stack::{report, Result, ResultExt}; +use itertools::Itertools; use router_api::{Address, ChainNameRaw}; use crate::{msg, RegisterTokenMetadata, TokenId}; +// Pagination limit +const DEFAULT_LIMIT: u32 = u32::MAX; + #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { #[error("ITS contract got into an invalid state, its config is missing")] @@ -170,8 +174,20 @@ pub fn load_chain_config( .ok_or_else(|| report!(Error::ChainNotFound(chain.to_owned()))) } -pub fn load_chain_configs() -> Map<&'static ChainNameRaw, ChainConfig> { +pub fn load_chain_configs<'a>( + storage: &'a dyn Storage, + filter: impl Fn(&ChainConfig) -> bool + 'a, + start_after: Option, + limit: Option, +) -> impl Iterator> + 'a { + let start = start_after.as_ref().map(Bound::exclusive); + let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; + CHAIN_CONFIGS + .range(storage, start, None, Order::Ascending) + .map(|r| r.change_context(Error::Storage)) + .filter_ok(move |(_, config)| filter(config)) + .take(limit) } pub fn save_chain_config( From 9cd7651aa781fa017cf09df7c0e91a563efc9167 Mon Sep 17 00:00:00 2001 From: maancham Date: Tue, 11 Feb 2025 16:12:19 -0500 Subject: [PATCH 14/17] fix: remove extra line from fmt error --- contracts/interchain-token-service/src/msg.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 1fe30249c..287dfc51e 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -62,7 +62,6 @@ pub struct ChainFilter { pub status: Option, } - #[cw_serde] pub struct ChainConfig { pub chain: ChainNameRaw, From 3e2d5768cc66fc6dc414a29c5daaf946935c8fbf Mon Sep 17 00:00:00 2001 From: maancham Date: Wed, 12 Feb 2025 09:43:59 -0500 Subject: [PATCH 15/17] refactor: add convert chain filter query utility --- .../src/contract/query.rs | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 48d153b3b..012ade376 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -35,35 +35,50 @@ pub fn all_its_contracts(deps: Deps) -> Result { to_json_binary(&contract_addresses).change_context(Error::JsonSerialization) } +fn convert_chain_filter_option( + message_chain_filter: Option, +) -> impl Fn(&state::ChainConfig) -> bool + 'static { + move |config| match &message_chain_filter { + None => true, + Some(filter) => convert_chain_filter(filter)(config), + } +} + +fn convert_chain_filter( + message_chain_filter: &msg::ChainFilter, +) -> impl Fn(&state::ChainConfig) -> bool + '_ { + move |config| match &message_chain_filter.status { + Some(status) => match status { + msg::ChainStatusFilter::Frozen => config.frozen, + msg::ChainStatusFilter::Active => !config.frozen, + }, + None => true, + } +} + pub fn its_chains( deps: Deps, filter: Option, start_after: Option, limit: Option, ) -> Result { - let filter_fn = |config: &state::ChainConfig| { - filter.as_ref().map_or(true, |f| match &f.status { - Some(status) => match status { - msg::ChainStatusFilter::Frozen => config.frozen, - msg::ChainStatusFilter::Active => !config.frozen, - }, - None => true, - }) - }; - - let filtered_chain_configs: Vec<_> = - state::load_chain_configs(deps.storage, filter_fn, start_after, limit) - .map(|r| r.change_context(Error::State)) - .map_ok(|(chain, config)| msg::ChainConfigResponse { - chain, - its_edge_contract: config.its_address, - truncation: msg::TruncationConfig { - max_uint: config.truncation.max_uint, - max_decimals_when_truncating: config.truncation.max_decimals_when_truncating, - }, - frozen: config.frozen, - }) - .try_collect()?; + let filtered_chain_configs: Vec<_> = state::load_chain_configs( + deps.storage, + convert_chain_filter_option(filter), + start_after, + limit, + ) + .map(|r| r.change_context(Error::State)) + .map_ok(|(chain, config)| msg::ChainConfigResponse { + chain, + its_edge_contract: config.its_address, + truncation: msg::TruncationConfig { + max_uint: config.truncation.max_uint, + max_decimals_when_truncating: config.truncation.max_decimals_when_truncating, + }, + frozen: config.frozen, + }) + .try_collect()?; to_json_binary(&filtered_chain_configs).change_context(Error::JsonSerialization) } From 252d0364581c3dceb21a39c1e9222ce5dc2c9248 Mon Sep 17 00:00:00 2001 From: maancham Date: Wed, 12 Feb 2025 15:21:59 -0500 Subject: [PATCH 16/17] feat: add test for query chains pagination --- .../interchain-token-service/tests/query.rs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 03720550a..146850c77 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -248,3 +248,80 @@ fn query_chains_config() { &[utils::create_config_response(&test_config.polygon, false)], ); } + +#[test] +fn query_chains_pagination() { + let mut test_config = ChainConfigTest::setup(); + + let chains = vec![ + ("Chain1", "0x1234567890123456789012345678901234567890"), + ("Chain2", "0x1234567890123456789012345678901234567891"), + ("Chain3", "0x1234567890123456789012345678901234567892"), + ("Chain4", "0x1234567890123456789012345678901234567893"), + ("Chain5", "0x1234567890123456789012345678901234567894"), + ]; + + for (chain_name, address) in chains { + utils::register_chain( + test_config.deps.as_mut(), + chain_name.parse().unwrap(), + address.parse().unwrap(), + test_config.eth.max_uint, + test_config.eth.max_decimals, + ) + .unwrap(); + } + + let first_page = + utils::query_its_chains(test_config.deps.as_ref(), None, None, Some(2)).unwrap(); + + assert_eq!(first_page.len(), 2); + let last_chain_name = first_page.last().unwrap().chain.clone(); + + let second_page = utils::query_its_chains( + test_config.deps.as_ref(), + None, + Some(last_chain_name), + Some(2), + ) + .unwrap(); + + assert_eq!(second_page.len(), 2); + assert_ne!( + first_page.last().unwrap().chain, + second_page.first().unwrap().chain + ); + + utils::freeze_chain(test_config.deps.as_mut(), "Chain1".parse().unwrap()).unwrap(); + utils::freeze_chain(test_config.deps.as_mut(), "Chain3".parse().unwrap()).unwrap(); + utils::freeze_chain(test_config.deps.as_mut(), "Chain5".parse().unwrap()).unwrap(); + + let frozen_first_page = utils::query_its_chains( + test_config.deps.as_ref(), + Some(ChainFilter { + status: Some(ChainStatusFilter::Frozen), + }), + None, + Some(2), + ) + .unwrap(); + + assert_eq!(frozen_first_page.len(), 2); + let last_frozen_chain = frozen_first_page.last().unwrap().chain.clone(); + + let frozen_second_page = utils::query_its_chains( + test_config.deps.as_ref(), + Some(ChainFilter { + status: Some(ChainStatusFilter::Frozen), + }), + Some(last_frozen_chain), + Some(2), + ) + .unwrap(); + + assert_ne!( + frozen_first_page.last().unwrap().chain, + frozen_second_page.first().unwrap().chain + ); + assert_eq!(frozen_second_page.len(), 1); +} From 01cc6ff2d3eb026c632dae16c21419d7b622c58e Mon Sep 17 00:00:00 2001 From: maancham Date: Thu, 13 Feb 2025 10:34:31 -0500 Subject: [PATCH 17/17] chore: add default pagination limit to msg --- .../src/contract/query.rs | 2 +- contracts/interchain-token-service/src/msg.rs | 10 ++++++- .../interchain-token-service/src/state.rs | 8 ++---- .../interchain-token-service/tests/query.rs | 26 +++++++------------ .../tests/utils/query.rs | 2 +- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 012ade376..1b3a65219 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -60,7 +60,7 @@ pub fn its_chains( deps: Deps, filter: Option, start_after: Option, - limit: Option, + limit: u32, ) -> Result { let filtered_chain_configs: Vec<_> = state::load_chain_configs( deps.storage, diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 287dfc51e..656d166f1 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -9,6 +9,13 @@ use router_api::{Address, ChainNameRaw}; use crate::state::{TokenConfig, TokenInstance}; use crate::TokenId; +// Pagination limit +const DEFAULT_LIMIT: u32 = 30; + +const fn default_pagination_limit() -> u32 { + DEFAULT_LIMIT +} + #[cw_serde] pub struct InstantiateMsg { pub governance_address: String, @@ -102,7 +109,8 @@ pub enum QueryMsg { ItsChains { filter: Option, start_after: Option, - limit: Option, + #[serde(default = "default_pagination_limit")] + limit: u32, }, /// Query a token instance on a specific chain diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index 6c0ce0e82..ee8134e4a 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -10,9 +10,6 @@ use router_api::{Address, ChainNameRaw}; use crate::{msg, RegisterTokenMetadata, TokenId}; -// Pagination limit -const DEFAULT_LIMIT: u32 = u32::MAX; - #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { #[error("ITS contract got into an invalid state, its config is missing")] @@ -178,16 +175,15 @@ pub fn load_chain_configs<'a>( storage: &'a dyn Storage, filter: impl Fn(&ChainConfig) -> bool + 'a, start_after: Option, - limit: Option, + limit: u32, ) -> impl Iterator> + 'a { let start = start_after.as_ref().map(Bound::exclusive); - let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; CHAIN_CONFIGS .range(storage, start, None, Order::Ascending) .map(|r| r.change_context(Error::Storage)) .filter_ok(move |(_, config)| filter(config)) - .take(limit) + .take(limit as usize) } pub fn save_chain_config( diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 146850c77..778695520 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -186,7 +186,7 @@ fn query_chains_config() { let mut test_config = ChainConfigTest::setup(); // Test all chains - let all_chains = utils::query_its_chains(test_config.deps.as_ref(), None, None, None).unwrap(); + let all_chains = utils::query_its_chains(test_config.deps.as_ref(), None, None, 2).unwrap(); let expected = vec![ utils::create_config_response(&test_config.eth, false), utils::create_config_response(&test_config.polygon, false), @@ -200,7 +200,7 @@ fn query_chains_config() { status: Some(ChainStatusFilter::Active), }), None, - None, + 2, ) .unwrap(); utils::assert_configs_equal(&active_chains, &expected); @@ -212,7 +212,7 @@ fn query_chains_config() { status: Some(ChainStatusFilter::Frozen), }), None, - None, + 1, ) .unwrap(); assert!(frozen_chains.is_empty()); @@ -226,7 +226,7 @@ fn query_chains_config() { status: Some(ChainStatusFilter::Frozen), }), None, - None, + 1, ) .unwrap(); utils::assert_configs_equal( @@ -240,7 +240,7 @@ fn query_chains_config() { status: Some(ChainStatusFilter::Active), }), None, - None, + 1, ) .unwrap(); utils::assert_configs_equal( @@ -272,19 +272,13 @@ fn query_chains_pagination() { .unwrap(); } - let first_page = - utils::query_its_chains(test_config.deps.as_ref(), None, None, Some(2)).unwrap(); + let first_page = utils::query_its_chains(test_config.deps.as_ref(), None, None, 2).unwrap(); assert_eq!(first_page.len(), 2); let last_chain_name = first_page.last().unwrap().chain.clone(); - let second_page = utils::query_its_chains( - test_config.deps.as_ref(), - None, - Some(last_chain_name), - Some(2), - ) - .unwrap(); + let second_page = + utils::query_its_chains(test_config.deps.as_ref(), None, Some(last_chain_name), 2).unwrap(); assert_eq!(second_page.len(), 2); assert_ne!( @@ -302,7 +296,7 @@ fn query_chains_pagination() { status: Some(ChainStatusFilter::Frozen), }), None, - Some(2), + 2, ) .unwrap(); @@ -315,7 +309,7 @@ fn query_chains_pagination() { status: Some(ChainStatusFilter::Frozen), }), Some(last_frozen_chain), - Some(2), + 2, ) .unwrap(); diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 43fde495e..aa283e258 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -54,7 +54,7 @@ pub fn query_its_chains( deps: Deps, filter: Option, start_after: Option, - limit: Option, + limit: u32, ) -> Result, ContractError> { let bin = query( deps,