diff --git a/Cargo.toml b/Cargo.toml index f1bf31c..9878dee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,15 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] -localic-std = { git = "https://github.com/strangelove-ventures/interchaintest", rev = "f326371" } -cosmwasm-std = "1.5.4" -serde = { version = "1.0.203", features = ["derive"] } -serde_json = "1.0.117" -thiserror = "1.0" +localic-std = { git = "https://github.com/strangelove-ventures/interchaintest", branch = "main" } +cosmwasm-std = "1.5.5" +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.120" +thiserror = "1.0.61" derive_builder = "0.20.0" -log = "0.4.21" +log = "0.4.22" astroport = "5.1.0" reqwest = { version = "0.11.20", features = ["rustls-tls"] } +sha2 = "0.10.8" [dev-dependencies] -env_logger = "0.11.3" \ No newline at end of file +env_logger = "0.11.3" diff --git a/README.md b/README.md index 6161628..dc20130 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ For a full list of builder calls available under the `TestContextBuilder`, see [ * `.with_api_url(api_url: impl Into)` * `.with_chain(chain: ConfigChain)` * The `TestContext` is not configured to use any chains by default. Calling this builder method adds a `ConfigChain`, which grants the `TestContext` access to that chain's related helper functions. These helper functions will error without access to their requisite chains. -* `.with_transfer_channel(chain_a: impl Into, chain_b: impl Into)` - * Registers a transfer channel ID upon building the `TestContext` between chain A and chain B. Assumes that chain A and chain B are chains registered with `.with_chain` +* `.with_transfer_channels(chain_a: impl Into, chain_b: impl Into)` + * Registers transfer channels IDs upon building the `TestContext` between chain A and chain B. Assumes that chain A and chain B are chains registered with `.with_chain` * `.with_unwrap_raw_logs(unwrap_logs: bool)` * Enables or disables log unwrapping - an assertion upon every `tx_*` helper function's execution that ensures no errors are present in logs returned by the transaction diff --git a/examples/neutron_osmosis.rs b/examples/neutron_osmosis.rs index 81855cf..5170c69 100644 --- a/examples/neutron_osmosis.rs +++ b/examples/neutron_osmosis.rs @@ -1,4 +1,4 @@ -use localic_utils::{error::Error as LocalIcUtilsError, ConfigChainBuilder, TestContextBuilder}; +use localic_utils::{ConfigChainBuilder, TestContextBuilder}; use std::error::Error; const ACC_0_ADDR: &str = "osmo1hj5fveer5cjtn4wd6wstzugjfdxzl0xpwhpz63"; @@ -15,8 +15,7 @@ fn main() -> Result<(), Box> { .with_artifacts_dir("contracts") .with_chain(ConfigChainBuilder::default_neutron().build()?) .with_chain(ConfigChainBuilder::default_osmosis().build()?) - .with_transfer_channel("osmosis", "neutron") - .with_transfer_channel("neutron", "osmosis") + .with_transfer_channels("osmosis", "neutron") .build()?; ctx.build_tx_create_tokenfactory_token() @@ -44,12 +43,8 @@ fn main() -> Result<(), Box> { .with_amount(1000000) .send()?; - let ibc_bruhtoken = ctx.get_ibc_denom(&bruhtoken, "neutron", "osmosis").ok_or( - LocalIcUtilsError::MissingContextVariable(format!("ibc_denom::{}", &bruhtoken)), - )?; - let ibc_neutron = ctx.get_ibc_denom("untrn", "neutron", "osmosis").ok_or( - LocalIcUtilsError::MissingContextVariable(format!("ibc_denom::{}", "untrn")), - )?; + let ibc_bruhtoken = ctx.get_ibc_denom(&bruhtoken, "neutron", "osmosis"); + let ibc_neutron = ctx.get_ibc_denom("untrn", "neutron", "osmosis"); // Create an osmosis pool ctx.build_tx_create_osmo_pool() diff --git a/src/lib.rs b/src/lib.rs index 45e6af7..31a6f58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,15 @@ pub const TRANSFER_PORT: &str = "transfer"; /// File extension for WASM files pub const WASM_EXTENSION: &str = "wasm"; +// Gaia chain info +pub const GAIA_CHAIN_ID: &str = "localcosmos-1"; +pub const GAIA_CHAIN_NAME: &str = "gaia"; +pub const GAIA_CHAIN_PREFIX: &str = "cosmos"; +pub const GAIA_CHAIN_DENOM: &str = "uatom"; +pub const GAIA_CHAIN_ADMIN_ADDR: &str = "cosmos1hj5fveer5cjtn4wd6wstzugjfdxzl0xpxvjjvr"; + /// Neutron chain info +pub const NEUTRON_CHAIN_ID: &str = "localneutron-1"; pub const NEUTRON_CHAIN_NAME: &str = "neutron"; pub const NEUTRON_CHAIN_PREFIX: &str = "neutron"; pub const NEUTRON_CHAIN_DENOM: &str = "untrn"; @@ -50,10 +58,10 @@ pub const PRICE_ORACLE_NAME: &str = "price_oracle"; pub const LOCAL_IC_API_URL: &str = "http://localhost:42069/"; /// Builder defautls +pub const ADMIN_KEY: &str = "admin"; pub const DEFAULT_KEY: &str = "acc0"; pub const DEFAULT_TRANSFER_PORT: &str = "transfer"; pub const DEFAULT_AUCTION_LABEL: &str = "auction"; -pub const DEFAULT_NEUTRON_CHAIN_ID: &str = "localneutron-1"; pub const TX_HASH_QUERY_RETRIES: u16 = 5; pub const TX_HASH_QUERY_PAUSE_SEC: u64 = 2; diff --git a/src/types/config.rs b/src/types/config.rs index 2e14e1f..92a204f 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -1,8 +1,13 @@ +use crate::{ + GAIA_CHAIN_ADMIN_ADDR, GAIA_CHAIN_DENOM, GAIA_CHAIN_ID, GAIA_CHAIN_NAME, GAIA_CHAIN_PREFIX, + NEUTRON_CHAIN_ID, +}; + use super::super::{ - DEFAULT_NEUTRON_CHAIN_ID, NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_DENOM, NEUTRON_CHAIN_NAME, - NEUTRON_CHAIN_PREFIX, OSMOSIS_CHAIN_ADMIN_ADDR, OSMOSIS_CHAIN_DENOM, OSMOSIS_CHAIN_ID, - OSMOSIS_CHAIN_NAME, OSMOSIS_CHAIN_PREFIX, STRIDE_CHAIN_ADMIN_ADDR, STRIDE_CHAIN_DENOM, - STRIDE_CHAIN_ID, STRIDE_CHAIN_NAME, STRIDE_CHAIN_PREFIX, + NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_DENOM, NEUTRON_CHAIN_NAME, NEUTRON_CHAIN_PREFIX, + OSMOSIS_CHAIN_ADMIN_ADDR, OSMOSIS_CHAIN_DENOM, OSMOSIS_CHAIN_ID, OSMOSIS_CHAIN_NAME, + OSMOSIS_CHAIN_PREFIX, STRIDE_CHAIN_ADMIN_ADDR, STRIDE_CHAIN_DENOM, STRIDE_CHAIN_ID, + STRIDE_CHAIN_NAME, STRIDE_CHAIN_PREFIX, }; use derive_builder::Builder; use serde::Deserialize; @@ -13,9 +18,9 @@ pub struct ChainsVec { pub chains: Vec, } -impl Into> for ChainsVec { - fn into(self) -> Vec { - self.chains +impl From for Vec { + fn from(val: ChainsVec) -> Vec { + val.chains } } @@ -31,11 +36,22 @@ pub struct ConfigChain { } impl ConfigChainBuilder { + pub fn default_gaia() -> Self { + Self { + denom: Some(String::from(GAIA_CHAIN_DENOM)), + debugging: Some(true), + chain_id: Some(String::from(GAIA_CHAIN_ID)), + chain_name: Some(String::from(GAIA_CHAIN_NAME)), + chain_prefix: Some(String::from(GAIA_CHAIN_PREFIX)), + admin_addr: Some(String::from(GAIA_CHAIN_ADMIN_ADDR)), + } + } + pub fn default_neutron() -> Self { Self { denom: Some(String::from(NEUTRON_CHAIN_DENOM)), debugging: Some(true), - chain_id: Some(String::from(DEFAULT_NEUTRON_CHAIN_ID)), + chain_id: Some(String::from(NEUTRON_CHAIN_ID)), chain_name: Some(String::from(NEUTRON_CHAIN_NAME)), chain_prefix: Some(String::from(NEUTRON_CHAIN_PREFIX)), admin_addr: Some(String::from(NEUTRON_CHAIN_ADMIN_ADDR)), diff --git a/src/types/ibc.rs b/src/types/ibc.rs index dca0c25..c8bc0c7 100644 --- a/src/types/ibc.rs +++ b/src/types/ibc.rs @@ -1,12 +1,6 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize)] -pub struct Trace { - pub channel_id: String, - pub port_id: String, - pub base_denom: String, - pub dest_denom: String, -} +use crate::TRANSFER_PORT; +use serde::Deserialize; +use sha2::{Digest, Sha256}; #[derive(Deserialize)] pub struct Channel { @@ -24,3 +18,78 @@ pub struct Counterparty { pub channel_id: String, pub port_id: String, } + +#[derive(Debug)] +pub struct DenomTrace { + pub path: String, + pub base_denom: String, +} + +impl DenomTrace { + pub fn ibc_denom(&self) -> String { + if !self.path.is_empty() { + return format!("ibc/{}", self.hash()); + } + self.base_denom.clone() + } + + fn hash(&self) -> String { + let trace = format!("{}/{}", self.path, self.base_denom); + let mut hasher = Sha256::new(); + hasher.update(trace.as_bytes()); + format!("{:x}", hasher.finalize()).to_uppercase() + } +} + +pub fn get_prefixed_denom(port_id: String, channel_id: String, native_denom: String) -> String { + format!("{}/{}/{}", port_id, channel_id, native_denom) +} + +pub fn get_multihop_ibc_denom(native_denom: &str, channel_trace: Vec<&str>) -> String { + let mut port_channel_trace = vec![]; + + for channel in channel_trace { + port_channel_trace.push(TRANSFER_PORT); + port_channel_trace.push(channel); + } + + let prefixed_denom = format!("{}/{}", port_channel_trace.join("/"), native_denom); + + let src_denom_trace = parse_denom_trace(prefixed_denom); + src_denom_trace.ibc_denom() +} + +pub fn parse_denom_trace(raw_denom: String) -> DenomTrace { + let denom_split = raw_denom.split('/').collect::>(); + + if denom_split[0] == raw_denom { + return DenomTrace { + path: "".to_string(), + base_denom: raw_denom.to_string(), + }; + } + + let (path, base_denom) = extract_path_and_base_from_full_denom(denom_split); + + DenomTrace { path, base_denom } +} + +pub fn extract_path_and_base_from_full_denom(full_denom_items: Vec<&str>) -> (String, String) { + let mut path: Vec<&str> = Vec::new(); + let mut base_denom: Vec<&str> = Vec::new(); + + let length = full_denom_items.len(); + let mut i = 0; + while i < length { + if i < length - 1 && length > 2 { + path.push(full_denom_items[i]); + path.push(full_denom_items[i + 1]); + } else { + base_denom = full_denom_items[i..].to_vec(); + break; + } + i += 2; + } + + (path.join("/"), base_denom.join("/")) +} diff --git a/src/utils/fixtures.rs b/src/utils/fixtures.rs index ce10294..92aa718 100644 --- a/src/utils/fixtures.rs +++ b/src/utils/fixtures.rs @@ -1,8 +1,13 @@ +use crate::{ + types::ibc::{get_prefixed_denom, parse_denom_trace}, + TRANSFER_PORT, +}; + use super::{ super::{ - error::Error, types::ibc::Trace, AUCTION_CONTRACT_NAME, FACTORY_NAME, NEUTRON_CHAIN_NAME, - OSMOSIS_CHAIN_NAME, PAIR_NAME, PRICE_ORACLE_NAME, STABLE_PAIR_NAME, - TX_HASH_QUERY_PAUSE_SEC, TX_HASH_QUERY_RETRIES, + error::Error, AUCTION_CONTRACT_NAME, FACTORY_NAME, NEUTRON_CHAIN_NAME, OSMOSIS_CHAIN_NAME, + PAIR_NAME, PRICE_ORACLE_NAME, STABLE_PAIR_NAME, TX_HASH_QUERY_PAUSE_SEC, + TX_HASH_QUERY_RETRIES, }, test_context::TestContext, }; @@ -37,7 +42,7 @@ impl TestContext { let raw_log = logs.as_ref().and_then(|raw_log| raw_log.as_str()).unwrap(); - if &raw_log == &"" { + if raw_log.is_empty() { return Ok(()); } @@ -89,7 +94,7 @@ impl TestContext { Ok(CosmWasm::new_from_existing( &neutron.rb, Some(contract_info.artifact_path.clone()), - Some(contract_info.code_id.clone()), + Some(contract_info.code_id), Some(contract_info.address.clone()), )) } @@ -102,7 +107,7 @@ impl TestContext { let contract_addr = neutron .contract_addrs .get(PRICE_ORACLE_NAME) - .and_then(|addrs| addrs.get(0)) + .and_then(|addrs| addrs.first()) .cloned() .ok_or(Error::MissingContextVariable(String::from( "contract_addrs::price_oracle", @@ -165,7 +170,7 @@ impl TestContext { let artifacts_path = self.artifacts_dir.as_str(); Ok(contract_addrs - .into_iter() + .iter() .map(|addr| { CosmWasm::new_from_existing( &neutron.rb, @@ -187,7 +192,7 @@ impl TestContext { ) -> Result { let factories = self.get_astroport_factory()?; let factory = factories - .get(0) + .first() .ok_or(Error::MissingContextVariable(String::from(FACTORY_NAME)))?; let pair_info = factory.query_value(&serde_json::json!( @@ -284,71 +289,38 @@ impl TestContext { } /// Gets the IBC denom for a base denom given a src and dest chain. - pub fn get_ibc_denom( - &mut self, - base_denom: impl AsRef, - src_chain: impl Into, - dest_chain: impl Into, - ) -> Option { - let src_chain_string = src_chain.into(); - let dest_chain_string = dest_chain.into(); - let base_denom_str = base_denom.as_ref(); - + pub fn get_ibc_denom(&mut self, base_denom: &str, src_chain: &str, dest_chain: &str) -> String { if let Some(denom) = self .ibc_denoms - .get(&(base_denom_str.into(), dest_chain_string.clone())) + .get(&(base_denom.to_string(), dest_chain.to_string())) { - return Some(denom.clone()); + return denom.clone(); } - let dest_chain = self.get_chain(&dest_chain_string); - - let channel = self - .transfer_channel_ids - .get(&(dest_chain_string.clone(), src_chain_string.clone()))?; - let trace = format!("transfer/{}/{}", channel, base_denom_str); - - let resp = dest_chain - .rb - .q(&format!("q ibc-transfer denom-hash {trace}"), true); + let channel_id = self + .get_transfer_channels() + .src(dest_chain) + .dest(src_chain) + .get(); - let ibc_denom = format!( - "ibc/{}", - serde_json::from_str::(&resp.get("text")?.as_str()?) - .ok()? - .get("hash")? - .as_str()? + let prefixed_denom = get_prefixed_denom( + TRANSFER_PORT.to_string(), + channel_id.to_string(), + base_denom.to_string(), ); + let src_denom_trace = parse_denom_trace(prefixed_denom); + let ibc_denom = src_denom_trace.ibc_denom(); + self.ibc_denoms.insert( - (base_denom_str.into(), dest_chain_string.clone()), + (base_denom.to_string(), dest_chain.to_string()), ibc_denom.clone(), ); - self.ibc_denoms - .insert((ibc_denom.clone(), src_chain_string), base_denom_str.into()); - - Some(ibc_denom) - } + self.ibc_denoms.insert( + (ibc_denom.clone(), src_chain.to_string()), + base_denom.to_string(), + ); - /// Gets the IBC channel and port for a given denom. - pub fn get_ibc_trace( - &mut self, - base_denom: impl Into + AsRef, - src_chain: impl Into + AsRef, - dest_chain: impl Into + AsRef, - ) -> Option { - let dest_denom = - self.get_ibc_denom(base_denom.as_ref(), src_chain.as_ref(), dest_chain.as_ref())?; - - let channel = self - .transfer_channel_ids - .get(&(src_chain.into(), dest_chain.into()))?; - - Some(Trace { - port_id: "transfer".to_owned(), - channel_id: channel.to_owned(), - base_denom: base_denom.into(), - dest_denom, - }) + ibc_denom } } diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 9d5d162..a932cd5 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -3,7 +3,13 @@ use super::{ test_context::TestContext, }; use localic_std::modules::cosmwasm::CosmWasm; -use std::{ffi::OsStr, fs}; +use log::{error, info}; +use std::{ + collections::HashMap, + ffi::OsStr, + fs::{self, File}, + io::{Read, Write}, +}; /// A tx uploading contract artifacts. pub struct UploadContractsTxBuilder<'a> { @@ -25,6 +31,22 @@ impl<'a> UploadContractsTxBuilder<'a> { .ok_or(Error::MissingBuilderParam(String::from("key")))?, ) } + + /// Sends the transaction using a path, chain and local cache path + pub fn send_with_local_cache( + &mut self, + path: &str, + chain_name: &str, + local_cache_path: &str, + ) -> Result<(), Error> { + self.test_ctx.tx_upload_contracts_with_local_cache( + self.key + .ok_or(Error::MissingBuilderParam(String::from("key")))?, + path, + chain_name, + local_cache_path, + ) + } } impl TestContext { @@ -62,4 +84,80 @@ impl TestContext { Ok(()) }) } + + fn tx_upload_contracts_with_local_cache( + &mut self, + key: &str, + path: &str, + chain_name: &str, + local_cache_path: &str, + ) -> Result<(), Error> { + if fs::metadata(path).is_ok_and(|m| m.is_dir()) { + info!("Path {} exists, deploying contracts...", path); + } else { + error!( + "Path {} does not exist, you might have to build and optimize contracts", + path + ); + return Err(Error::Misc(String::from("Path does not exist"))); + }; + + let artifacts = fs::read_dir(path).unwrap(); + + let mut dir_entries = vec![]; + for dir in artifacts.into_iter() { + dir_entries.push(dir.unwrap()); + } + + // Use a local cache to avoid storing the same contract multiple times, useful for local testing + let mut content = String::new(); + let cache: HashMap = match File::open(local_cache_path) { + Ok(mut file) => { + if let Err(err) = file.read_to_string(&mut content) { + error!("Failed to read cache file: {}", err); + HashMap::new() + } else { + serde_json::from_str(&content).unwrap_or_default() + } + } + Err(_) => { + // If the file does not exist, we'll create it later + HashMap::new() + } + }; + + let local_chain = self.get_mut_chain(chain_name); + // Add all cache entries to the local chain + for (id, code_id) in cache { + local_chain.contract_codes.insert(id, code_id); + } + + for entry in dir_entries { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some(WASM_EXTENSION) { + let abs_path = path.canonicalize().unwrap(); + let mut cw = CosmWasm::new(&local_chain.rb); + let id = abs_path.file_stem().unwrap().to_str().unwrap(); + + // To avoid storing multiple times during the same execution + if local_chain.contract_codes.contains_key(id) { + info!( + "Contract {} already deployed on chain {}, skipping...", + id, chain_name + ); + continue; + } + + let code_id = cw.store(key, abs_path.as_path()).unwrap(); + + local_chain.contract_codes.insert(id.to_string(), code_id); + } + } + + let contract_codes = serde_json::to_string(&local_chain.contract_codes).unwrap(); + let mut file = File::create(local_cache_path).unwrap(); + file.write_all(contract_codes.as_bytes()).unwrap(); + + Ok(()) + } } diff --git a/src/utils/setup/astroport.rs b/src/utils/setup/astroport.rs index 1de8b89..bf99007 100644 --- a/src/utils/setup/astroport.rs +++ b/src/utils/setup/astroport.rs @@ -308,7 +308,7 @@ impl TestContext { let native_registry_addr = neutron .contract_addrs .get(TOKEN_REGISTRY_NAME) - .and_then(|maybe_addr| maybe_addr.get(0)) + .and_then(|maybe_addr| maybe_addr.first()) .ok_or(Error::MissingContextVariable(String::from( "contract_ddrs::astroport_native_coin_registry", )))?; @@ -385,7 +385,7 @@ impl TestContext { // Factory contract instance let contracts = self.get_astroport_factory()?; let contract_a = contracts - .get(0) + .first() .ok_or(Error::MissingContextVariable(String::from(FACTORY_NAME)))?; // Create the pair @@ -412,7 +412,7 @@ impl TestContext { "transaction did not produce a tx hash", )))?; - let _ = self.guard_tx_errors(NEUTRON_CHAIN_NAME, tx_hash.as_str())?; + self.guard_tx_errors(NEUTRON_CHAIN_NAME, tx_hash.as_str())?; Ok(()) } @@ -432,6 +432,7 @@ impl TestContext { } /// Provides liquidity for a specific astroport pool. + #[allow(clippy::too_many_arguments)] fn tx_fund_pool( &mut self, key: &str, @@ -478,7 +479,7 @@ impl TestContext { .tx_hash .ok_or(Error::TxMissingLogs)?; - let _ = self.guard_tx_errors(NEUTRON_CHAIN_NAME, tx.as_str())?; + self.guard_tx_errors(NEUTRON_CHAIN_NAME, tx.as_str())?; Ok(()) } diff --git a/src/utils/setup/ibc.rs b/src/utils/setup/ibc.rs index d887543..83a5dce 100644 --- a/src/utils/setup/ibc.rs +++ b/src/utils/setup/ibc.rs @@ -108,7 +108,7 @@ impl TestContext { let receipt = chain.rb.tx(&format!("tx ibc-transfer transfer {port} {channel} {recipient} {amount}{denom} --fees=100000{fee_denom} --from={key}"), true)?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( src_chain_name, receipt .get("txhash") diff --git a/src/utils/setup/mod.rs b/src/utils/setup/mod.rs index 4f7ba16..189d39a 100644 --- a/src/utils/setup/mod.rs +++ b/src/utils/setup/mod.rs @@ -1,5 +1,6 @@ pub mod astroport; pub mod ibc; pub mod osmosis; +pub mod stride; pub mod tokens; pub mod valence; diff --git a/src/utils/setup/osmosis.rs b/src/utils/setup/osmosis.rs index 0bd49d7..9871297 100644 --- a/src/utils/setup/osmosis.rs +++ b/src/utils/setup/osmosis.rs @@ -155,9 +155,9 @@ impl TestContext { .open(OSMOSIS_POOLFILE_PATH)?; f.write_all(poolfile_str.as_bytes())?; - let _ = osmosis + osmosis .rb - .upload_file(&Path::new(OSMOSIS_POOLFILE_PATH), true)? + .upload_file(Path::new(OSMOSIS_POOLFILE_PATH), true)? .send()? .text()?; @@ -171,7 +171,7 @@ impl TestContext { true, )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( OSMOSIS_CHAIN_NAME, receipt .get("txhash") @@ -209,7 +209,7 @@ impl TestContext { true, )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( OSMOSIS_CHAIN_NAME, receipt .get("txhash") diff --git a/src/utils/setup/stride.rs b/src/utils/setup/stride.rs new file mode 100644 index 0000000..bd3eaf3 --- /dev/null +++ b/src/utils/setup/stride.rs @@ -0,0 +1,139 @@ +use localic_std::{errors::LocalError, transactions::ChainRequestBuilder}; +use log::info; +use serde_json::Value; + +use crate::{ + error::Error, utils::test_context::TestContext, ADMIN_KEY, DEFAULT_KEY, STRIDE_CHAIN_NAME, +}; + +/// A tx liquid staking. +pub struct LiquidStakeTxBuilder<'a> { + key: &'a str, + denom: Option<&'a str>, + amount: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> LiquidStakeTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_denom(&mut self, denom: &'a str) -> &mut Self { + self.denom = Some(denom); + + self + } + + pub fn with_amount(&mut self, amount: u128) -> &mut Self { + self.amount = Some(amount); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_liquid_stake( + self.key, + self.denom + .ok_or(Error::MissingBuilderParam(String::from("denom")))?, + self.amount + .ok_or(Error::MissingBuilderParam(String::from("amount")))?, + ) + } +} + +impl TestContext { + pub fn build_tx_liquid_stake(&mut self) -> LiquidStakeTxBuilder { + LiquidStakeTxBuilder { + key: DEFAULT_KEY, + denom: None, + amount: None, + test_ctx: self, + } + } + + pub fn set_up_stride_host_zone(&mut self, dest_chain: &str) { + let native_denom = self.get_native_denom().src(dest_chain).get().clone(); + + let host_denom_on_stride = self.get_ibc_denom(&native_denom, STRIDE_CHAIN_NAME, dest_chain); + + let stride = self.get_chain(STRIDE_CHAIN_NAME); + let stride_rb = &stride.rb; + + let stride_to_host_channel_id = self + .get_transfer_channels() + .src(STRIDE_CHAIN_NAME) + .dest(dest_chain) + .get(); + + let dest_chain_id = &self.get_chain(dest_chain).rb.chain_id; + + if self.query_host_zone(stride_rb, dest_chain_id) { + info!("Host zone registered."); + } else { + info!("Host zone not registered."); + self.register_stride_host_zone( + stride_rb, + &self + .get_connections() + .src(STRIDE_CHAIN_NAME) + .dest(dest_chain) + .get(), + &self.get_native_denom().src(dest_chain).get(), + &self.get_chain_prefix().src(dest_chain).get(), + &host_denom_on_stride, + &stride_to_host_channel_id, + ADMIN_KEY, + ) + .unwrap(); + } + } + + fn query_host_zone(&self, rb: &ChainRequestBuilder, chain_id: &str) -> bool { + let query_cmd = format!("stakeibc show-host-zone {chain_id} --output=json"); + let host_zone_query_response = rb.q(&query_cmd, false); + + host_zone_query_response["host_zone"].is_object() + } + + #[allow(clippy::too_many_arguments)] + fn register_stride_host_zone( + &self, + rb: &ChainRequestBuilder, + connection_id: &str, + host_denom: &str, + bech_32_prefix: &str, + ibc_denom: &str, + channel_id: &str, + from_key: &str, + ) -> Result { + let cmd = format!( + "tx stakeibc register-host-zone {} {} {} {} {} 1 --from={} --gas auto --gas-adjustment 1.3 --output=json", + connection_id, + host_denom, + bech_32_prefix, + ibc_denom, + channel_id, + from_key, + ); + rb.tx(&cmd, true) + } + + fn tx_liquid_stake( + &mut self, + sender_key: &str, + liquid_stake_denom: &str, + liquid_stake_amount: u128, + ) -> Result<(), Error> { + let cmd = format!( + "tx liquid-staking stake {} {} --from={} --gas auto --gas-adjustment 1.3 --output=json", + liquid_stake_denom, liquid_stake_amount, sender_key, + ); + self.get_chain(STRIDE_CHAIN_NAME).rb.tx(&cmd, true)?; + + Ok(()) + } +} diff --git a/src/utils/setup/tokens.rs b/src/utils/setup/tokens.rs index 1ba06c3..2375696 100644 --- a/src/utils/setup/tokens.rs +++ b/src/utils/setup/tokens.rs @@ -128,7 +128,7 @@ impl TestContext { true, )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( chain_name, receipt .get("txhash") @@ -169,7 +169,7 @@ impl TestContext { true, )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( chain_name, receipt .get("txhash") @@ -186,7 +186,7 @@ impl TestContext { true, )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( chain_name, receipt .get("txhash") diff --git a/src/utils/setup/valence.rs b/src/utils/setup/valence.rs index 9534037..81e138c 100644 --- a/src/utils/setup/valence.rs +++ b/src/utils/setup/valence.rs @@ -455,7 +455,7 @@ impl TestContext { /// Creates an auction manager on Neutron, updating the autions manager /// code id and address in the TestContext. - fn tx_create_price_oracle<'a>( + fn tx_create_price_oracle( &mut self, sender_key: &str, seconds_allow_manual_change: u64, @@ -496,7 +496,7 @@ impl TestContext { } /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. - pub fn build_tx_create_auction<'a>(&mut self) -> CreateAuctionTxBuilder { + pub fn build_tx_create_auction(&mut self) -> CreateAuctionTxBuilder { CreateAuctionTxBuilder { key: DEFAULT_KEY, offer_asset: Default::default(), @@ -520,7 +520,8 @@ impl TestContext { } /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. - fn tx_create_auction<'a, TDenomA: AsRef, TDenomB: AsRef>( + #[allow(clippy::too_many_arguments)] + fn tx_create_auction, TDenomB: AsRef>( &mut self, sender_key: &str, pair: (TDenomA, TDenomB), @@ -561,7 +562,7 @@ impl TestContext { receipt ); - let _ = self.guard_tx_errors( + self.guard_tx_errors( NEUTRON_CHAIN_NAME, receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), )?; @@ -570,7 +571,7 @@ impl TestContext { } /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. - pub fn build_tx_migrate_auction<'a>(&mut self) -> MigrateAuctionTxBuilder { + pub fn build_tx_migrate_auction(&mut self) -> MigrateAuctionTxBuilder { MigrateAuctionTxBuilder { key: DEFAULT_KEY, offer_asset: Default::default(), @@ -580,7 +581,7 @@ impl TestContext { } /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. - fn tx_migrate_auction<'a, TDenomA: AsRef, TDenomB: AsRef>( + fn tx_migrate_auction, TDenomB: AsRef>( &mut self, sender_key: &str, pair: (TDenomA, TDenomB), @@ -606,7 +607,7 @@ impl TestContext { }}) .to_string() .as_str(), - format!("--gas 2000000").as_str(), + "--gas 2000000", )?; log::debug!( @@ -616,7 +617,7 @@ impl TestContext { receipt ); - let _ = self.guard_tx_errors( + self.guard_tx_errors( NEUTRON_CHAIN_NAME, receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), )?; @@ -639,7 +640,7 @@ impl TestContext { let oracle = neutron .contract_addrs .get(PRICE_ORACLE_NAME) - .and_then(|addrs| addrs.get(0)) + .and_then(|addrs| addrs.first()) .ok_or(Error::MissingContextVariable(String::from( "contract_addrs::price_oracle", )))?; @@ -655,10 +656,10 @@ impl TestContext { }}) .to_string() .as_str(), - format!("--gas 2000000").as_str(), + "--gas 2000000", )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( NEUTRON_CHAIN_NAME, receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), )?; @@ -698,10 +699,10 @@ impl TestContext { }) .to_string() .as_str(), - format!("--gas 2000000").as_str(), + "--gas 2000000", )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( NEUTRON_CHAIN_NAME, receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), )?; @@ -743,7 +744,7 @@ impl TestContext { format!("--amount {amt_offer_asset}{denom_a} --gas 1000000").as_str(), )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( NEUTRON_CHAIN_NAME, receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), )?; @@ -808,7 +809,7 @@ impl TestContext { "--gas 1000000", )?; - let _ = self.guard_tx_errors( + self.guard_tx_errors( NEUTRON_CHAIN_NAME, receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), )?; diff --git a/src/utils/test_context.rs b/src/utils/test_context.rs index d2e29a3..171661a 100644 --- a/src/utils/test_context.rs +++ b/src/utils/test_context.rs @@ -3,7 +3,7 @@ use super::super::{ types::{config::ConfigChain, contract::DeployedContractInfo, ibc::Channel as QueryChannel}, LOCAL_IC_API_URL, TRANSFER_PORT, }; -use cosmwasm_std::{StdError, StdResult}; + use localic_std::{ modules::cosmwasm::CosmWasm, relayer::Channel, relayer::Relayer, transactions::ChainRequestBuilder, @@ -21,6 +21,7 @@ pub struct TestContextBuilder { artifacts_dir: Option, unwrap_raw_logs: bool, transfer_channels: Vec<(String, String)>, + ccv_channels: Vec<(String, String)>, } impl Default for TestContextBuilder { @@ -35,6 +36,7 @@ impl Default for TestContextBuilder { artifacts_dir: Default::default(), unwrap_raw_logs: Default::default(), transfer_channels: Default::default(), + ccv_channels: Default::default(), } } } @@ -84,15 +86,31 @@ impl TestContextBuilder { self } - /// Inserts a channel for transfer between the specified chains. - pub fn with_transfer_channel( + /// Inserts a transfer channel between the specified chains in both directions. + pub fn with_transfer_channels( &mut self, - chain_a: impl Into, - chain_b: impl Into, + chain_a: impl Into + std::marker::Copy, + chain_b: impl Into + std::marker::Copy, ) -> &mut Self { self.transfer_channels .push((chain_a.into(), chain_b.into())); + self.transfer_channels + .push((chain_b.into(), chain_a.into())); + + self + } + + // Inserts a ccv channel b etween the specified chains in both directions. + pub fn with_ccv_channels( + &mut self, + chain_a: impl Into + std::marker::Copy, + chain_b: impl Into + std::marker::Copy, + ) -> &mut Self { + self.ccv_channels.push((chain_a.into(), chain_b.into())); + + self.ccv_channels.push((chain_b.into(), chain_a.into())); + self } @@ -191,6 +209,7 @@ impl TestContextBuilder { artifacts_dir, unwrap_raw_logs, transfer_channels, + ccv_channels, } = self; // Upload contract artifacts @@ -226,9 +245,9 @@ impl TestContextBuilder { .ok_or(Error::MissingBuilderParam(String::from("api_url")))?, ) }) - .fold(Ok(HashMap::new()), |acc, x| { + .try_fold(HashMap::new(), |acc, x| { let x = x?; - let mut acc = acc?; + let mut acc = acc; acc.insert(x.chain_name.clone(), x); @@ -237,6 +256,7 @@ impl TestContextBuilder { let chains = chains_res?; let mut transfer_channel_ids = transfer_channel_ids.clone(); + let mut connection_ids = connection_ids.clone(); for (chain_a, chain_b) in transfer_channels { let chain_a_chain = chains @@ -252,8 +272,42 @@ impl TestContextBuilder { &chain_b_chain.rb.chain_id, )?; - transfer_channel_ids.insert((chain_a.clone(), chain_b.clone()), conns.0.channel_id); - transfer_channel_ids.insert((chain_b.clone(), chain_a.clone()), conns.1.channel_id); + transfer_channel_ids + .insert((chain_a.clone(), chain_b.clone()), conns.channel_id.clone()); + connection_ids.insert((chain_a.clone(), chain_b.clone()), conns.connection_id); + + let conns = find_pairwise_transfer_channel_ids( + &chain_b_chain.rb, + &chain_b_chain.rb.chain_id, + &chain_a_chain.rb.chain_id, + )?; + + transfer_channel_ids + .insert((chain_b.clone(), chain_a.clone()), conns.channel_id.clone()); + connection_ids.insert((chain_b.clone(), chain_a.clone()), conns.connection_id); + } + + let mut ccv_channel_ids = ccv_channel_ids.clone(); + + for (chain_a, chain_b) in ccv_channels { + let chain_a_chain = chains + .get(chain_a) + .ok_or(Error::MissingBuilderParam(String::from("chain")))?; + let chain_b_chain = chains + .get(chain_b) + .ok_or(Error::MissingBuilderParam(String::from("chain")))?; + + let conns = + find_pairwise_ccv_channel_ids(&chain_a_chain.channels, &chain_b_chain.channels)?; + + ccv_channel_ids.insert( + (chain_a.clone(), chain_b.clone()), + conns.0.channel_id.clone(), + ); + ccv_channel_ids.insert( + (chain_b.clone(), chain_a.clone()), + conns.1.channel_id.clone(), + ); } Ok(TestContext { @@ -373,6 +427,10 @@ impl TestContext { TestContextQuery::new(self, QueryType::NativeDenom) } + pub fn get_chain_prefix(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::ChainPrefix) + } + pub fn get_request_builder(&self) -> TestContextQuery { TestContextQuery::new(self, QueryType::RequestBuilder) } @@ -393,6 +451,7 @@ pub enum QueryType { IBCDenom, AdminAddr, NativeDenom, + ChainPrefix, RequestBuilder, } @@ -438,6 +497,7 @@ impl<'a> TestContextQuery<'a> { QueryType::IBCDenom => self.get_ibc_denom(), QueryType::AdminAddr => self.get_admin_addr(), QueryType::NativeDenom => self.get_native_denom(), + QueryType::ChainPrefix => self.get_chain_prefix(), _ => None, }; query_response.unwrap() @@ -552,6 +612,17 @@ impl<'a> TestContextQuery<'a> { } } + fn get_chain_prefix(self) -> Option { + if let Some(ref src) = self.src_chain { + self.context + .chains + .get(src) + .map(|chain| chain.chain_prefix.clone()) + } else { + None + } + } + fn get_rb(self) -> Option<&'a ChainRequestBuilder> { if let Some(ref src) = self.src_chain { self.context.chains.get(src).map(|chain| &chain.rb) @@ -565,7 +636,7 @@ pub fn find_pairwise_transfer_channel_ids( rb: &ChainRequestBuilder, src_chain_id: &str, dest_chain_id: &str, -) -> Result<(PairwiseChannelResult, PairwiseChannelResult), Error> { +) -> Result { let relayer = Relayer::new(rb); let cmd = format!("rly q channels {src_chain_id} {dest_chain_id}"); let result = relayer.execute(cmd.as_str(), true).unwrap(); @@ -573,36 +644,29 @@ pub fn find_pairwise_transfer_channel_ids( let channels = json_string .split('\n') .filter(|s| !s.is_empty()) - .map(|s| serde_json::from_str(s)); - - for maybe_channel in channels { - let channel: QueryChannel = maybe_channel?; + .map(serde_json::from_str) + .collect::, _>>()?; + for channel in channels { if channel.port_id == TRANSFER_PORT { let party_channel = PairwiseChannelResult { index: 0, - channel_id: channel.channel_id.to_owned(), - connection_id: channel.connection_hops[0].to_owned(), - }; - let counterparty_channel = PairwiseChannelResult { - index: 0, - channel_id: channel.counterparty.channel_id.to_owned(), - connection_id: channel.connection_hops[0].to_owned(), + channel_id: channel.channel_id.to_string(), + connection_id: channel.connection_hops[0].to_string(), }; - - return Ok((party_channel, counterparty_channel)); + return Ok(party_channel); } } - Err(Error::MissingContextVariable(String::from(format!( + Err(Error::MissingContextVariable(format!( "channel_ids::{src_chain_id}-{dest_chain_id}" - )))) + ))) } pub fn find_pairwise_ccv_channel_ids( provider_channels: &[Channel], consumer_channels: &[Channel], -) -> StdResult<(PairwiseChannelResult, PairwiseChannelResult)> { +) -> Result<(PairwiseChannelResult, PairwiseChannelResult), Error> { for (a_i, a_chan) in provider_channels.iter().enumerate() { for (b_i, b_chan) in consumer_channels.iter().enumerate() { if a_chan.channel_id == b_chan.counterparty.channel_id @@ -626,8 +690,8 @@ pub fn find_pairwise_ccv_channel_ids( } } } - Err(StdError::generic_err( - "failed to match pairwise ccv channels", + Err(Error::MissingContextVariable( + "Failed to match ccv channels".to_string(), )) }