diff --git a/Cargo.toml b/Cargo.toml index b2f9f8cc..59f37ea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,5 +87,6 @@ speculoos = "0.11.0" log = "0.4.22" # Interchain -ibc-relayer-types = { version = "0.29.2" } +ibc-relayer = { version = "0.29.3" } +ibc-relayer-types = { version = "0.29.3" } ibc-chain-registry = { version = "0.29.2" } diff --git a/packages/interchain/hermes-relayer/Cargo.toml b/packages/interchain/hermes-relayer/Cargo.toml new file mode 100644 index 00000000..106f0d5f --- /dev/null +++ b/packages/interchain/hermes-relayer/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "hermes-relayer" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +anyhow.workspace = true +cosmwasm-std.workspace = true + +cw-orch-interchain-core = { workspace = true } +cw-orch-interchain-daemon = { workspace = true } +cw-orch-networks = { workspace = true } +cw-orch-core = { workspace = true } +cw-orch-traits = { workspace = true } +cw-orch-daemon = { workspace = true } + + +hdpath = "0.6.3" +ibc-relayer = { workspace = true } +ibc-relayer-cli = "1.9.0" +ibc-relayer-types = { workspace = true } +tokio.workspace = true +log.workspace = true +cosmrs.workspace = true +futures = "0.3.30" +tonic.workspace = true +futures-util = "0.3.30" +async-recursion = "1.1.1" +serde_json.workspace = true +prost-types = { workspace = true } + +[dev-dependencies] +cw-orch = { workspace = true, features = ["daemon"] } +cw-orch-interchain = { workspace = true, features = ["daemon"] } +dotenv = "0.15.0" +ibc-proto = "0.47.0" +pretty_env_logger = "0.5.0" diff --git a/packages/interchain/hermes-relayer/examples/pion-xion.rs b/packages/interchain/hermes-relayer/examples/pion-xion.rs new file mode 100644 index 00000000..702195ee --- /dev/null +++ b/packages/interchain/hermes-relayer/examples/pion-xion.rs @@ -0,0 +1,89 @@ +use cw_orch::prelude::*; +use cw_orch::{ + daemon::networks::{PION_1, XION_TESTNET_1}, + tokio::runtime::Runtime, +}; +use cw_orch_interchain::prelude::*; +use cw_orch_traits::Stargate; +use hermes_relayer::core::HermesRelayer; +use ibc_relayer_types::core::ics24_host::identifier::PortId; +use ibc_relayer_types::tx_msg::Msg; +use ibc_relayer_types::{ + applications::transfer::msgs::transfer::MsgTransfer, + core::ics04_channel::timeout::TimeoutHeight, timestamp::Timestamp, +}; + +pub fn main() -> cw_orch::anyhow::Result<()> { + dotenv::dotenv()?; + pretty_env_logger::init(); + let rt = Runtime::new()?; + + let relayer = HermesRelayer::new( + rt.handle(), + vec![ + ( + PION_1, + None, + true, + "https://rpc-falcron.pion-1.ntrn.tech/".to_string(), + ), + ( + XION_TESTNET_1, + None, + false, + "https://xion-testnet-rpc.polkachu.com".to_string(), + ), + ], + vec![( + ( + XION_TESTNET_1.chain_id.to_string(), + PION_1.chain_id.to_string(), + ), + "connection-63".to_string(), + )] + .into_iter() + .collect(), + )?; + + let channel = relayer.create_channel( + "xion-testnet-1", + "pion-1", + &PortId::transfer(), + &PortId::transfer(), + "ics20-1", + None, + )?; + + let xion = relayer.get_chain("xion-testnet-1")?; + let pion = relayer.get_chain("pion-1")?; + + let msg = MsgTransfer { + source_port: PortId::transfer(), + source_channel: channel + .interchain_channel + .get_chain("xion-testnet-1")? + .channel + .unwrap(), + token: ibc_proto::cosmos::base::v1beta1::Coin { + denom: "uxion".to_string(), + amount: "1987".to_string(), + }, + + sender: xion.sender_addr().to_string().parse().unwrap(), + receiver: pion.sender_addr().to_string().parse().unwrap(), + timeout_height: TimeoutHeight::Never, + timeout_timestamp: Timestamp::from_nanoseconds(1_800_000_000_000_000_000)?, + memo: None, + }; + let response = xion.commit_any::( + vec![prost_types::Any { + type_url: msg.type_url(), + value: msg.to_any().value, + }], + None, + )?; + + relayer.await_and_check_packets("xion-testnet-1", response)?; + + Ok(()) +} diff --git a/packages/interchain/hermes-relayer/src/channel.rs b/packages/interchain/hermes-relayer/src/channel.rs new file mode 100644 index 00000000..7a44f10c --- /dev/null +++ b/packages/interchain/hermes-relayer/src/channel.rs @@ -0,0 +1,105 @@ +use crate::core::HermesRelayer; +use cosmwasm_std::IbcOrder; +use cw_orch_interchain_core::env::ChainId; +use cw_orch_interchain_daemon::ChannelCreator; +use cw_orch_interchain_daemon::InterchainDaemonError; +use ibc_relayer::chain::requests::{IncludeProof, QueryClientStateRequest, QueryConnectionRequest}; +use ibc_relayer::chain::{handle::ChainHandle, requests::QueryHeight}; +use ibc_relayer::channel::Channel; +use ibc_relayer::connection::Connection; +use ibc_relayer::foreign_client::ForeignClient; +use ibc_relayer_cli::cli_utils::spawn_chain_runtime; +use ibc_relayer_types::core::ics03_connection::connection::IdentifiedConnectionEnd; +use ibc_relayer_types::core::ics04_channel::channel::Ordering; +use ibc_relayer_types::core::ics24_host::identifier::{self}; + +impl ChannelCreator for HermesRelayer { + fn create_ibc_channel( + &self, + src_chain: ChainId, + dst_chain: ChainId, + src_port: &ibc_relayer_types::core::ics24_host::identifier::PortId, + dst_port: &ibc_relayer_types::core::ics24_host::identifier::PortId, + version: &str, + order: Option, + ) -> Result { + let src_connection = self + .connection_ids + .get(&(src_chain.to_string(), dst_chain.to_string())) + .unwrap(); + + let config = self.duplex_config(src_chain, dst_chain); + + // Validate & spawn runtime for side a. + let chain_a = + spawn_chain_runtime(&config, &identifier::ChainId::from_string(src_chain)).unwrap(); + + self.add_key(&chain_a); + // Query the connection end. + let (conn_end, _) = chain_a + .query_connection( + QueryConnectionRequest { + connection_id: src_connection.parse().unwrap(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .unwrap(); + + // Query the client state, obtain the identifier of chain b. + let chain_b = chain_a + .query_client_state( + QueryClientStateRequest { + client_id: conn_end.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map(|(cs, _)| cs.chain_id()) + .unwrap(); + + // Spawn the runtime for side b. + let chain_b = spawn_chain_runtime(&config, &chain_b).unwrap(); + self.add_key(&chain_b); + + // Create the foreign client handles. + let client_a = + ForeignClient::find(chain_b.clone(), chain_a.clone(), conn_end.client_id()).unwrap(); + + let client_b = + ForeignClient::find(chain_a, chain_b, conn_end.counterparty().client_id()).unwrap(); + + let identified_end = + IdentifiedConnectionEnd::new(src_connection.parse().unwrap(), conn_end); + + let connection = Connection::find(client_a, client_b, &identified_end).unwrap(); + + Channel::new( + connection, + cosmwasm_to_hermes_order(order), + src_port.to_string().parse().unwrap(), + dst_port.to_string().parse().unwrap(), + Some(version.to_string().into()), + ) + .unwrap(); + + Ok(src_connection.to_string()) + } + + fn interchain_env(&self) -> cw_orch_interchain_daemon::DaemonInterchain { + unimplemented!(" + The Hermes Relayer is a channel creator as well as an Interchain env. + You don't need to use this function, you can simply await packets directly on this structure" + ) + } +} + +fn cosmwasm_to_hermes_order(order: Option) -> Ordering { + match order { + Some(order) => match order { + IbcOrder::Unordered => Ordering::Unordered, + IbcOrder::Ordered => Ordering::Ordered, + }, + None => Ordering::Unordered, + } +} diff --git a/packages/interchain/hermes-relayer/src/config.rs b/packages/interchain/hermes-relayer/src/config.rs new file mode 100644 index 00000000..6a4da85c --- /dev/null +++ b/packages/interchain/hermes-relayer/src/config.rs @@ -0,0 +1,66 @@ +use std::time::Duration; + +use cw_orch_core::environment::ChainInfoOwned; +use ibc_relayer::chain::cosmos::config::CosmosSdkConfig; +use ibc_relayer::config::gas_multiplier::GasMultiplier; +use ibc_relayer::config::types::{MaxMsgNum, MaxTxSize, Memo}; +use ibc_relayer::config::{AddressType, ChainConfig, EventSourceMode, GasPrice, RefreshRate}; +use ibc_relayer::keyring::Store; +use ibc_relayer_types::core::ics02_client::trust_threshold::TrustThreshold; +use ibc_relayer_types::core::ics24_host::identifier::{self}; + +pub const KEY_NAME: &str = "relayer"; + +pub fn chain_config( + chain: &str, + rpc_url: &str, + chain_data: &ChainInfoOwned, + is_consumer_chain: bool, +) -> ChainConfig { + ChainConfig::CosmosSdk(CosmosSdkConfig { + id: identifier::ChainId::from_string(chain), + + rpc_addr: rpc_url.parse().unwrap(), + grpc_addr: chain_data.grpc_urls[0].parse().unwrap(), + event_source: EventSourceMode::Pull { + interval: Duration::from_secs(4), + max_retries: 4, + }, + rpc_timeout: Duration::from_secs(10), + trusted_node: false, + account_prefix: chain_data.network_info.pub_address_prefix.to_string(), + key_name: KEY_NAME.to_string(), + key_store_type: Store::Memory, + key_store_folder: None, + store_prefix: "ibc".to_string(), + default_gas: Some(100000), + max_gas: Some(2000000), + genesis_restart: None, + gas_adjustment: None, + gas_multiplier: Some(GasMultiplier::new(1.3).unwrap()), + fee_granter: None, + max_msg_num: MaxMsgNum::new(30).unwrap(), + max_tx_size: MaxTxSize::new(180000).unwrap(), + max_grpc_decoding_size: 33554432u64.into(), + clock_drift: Duration::from_secs(5), + max_block_time: Duration::from_secs(30), + trusting_period: None, + ccv_consumer_chain: is_consumer_chain, + memo_prefix: Memo::new("").unwrap(), + sequential_batch_tx: false, + proof_specs: None, + trust_threshold: TrustThreshold::new(1, 3).unwrap(), + gas_price: GasPrice::new(chain_data.gas_price, chain_data.gas_denom.to_string()), + packet_filter: Default::default(), + address_type: AddressType::Cosmos, + extension_options: Default::default(), + query_packets_chunk_size: 10, + client_refresh_rate: RefreshRate::new(5, 1), + memo_overwrite: Default::default(), + dynamic_gas_price: Default::default(), + compat_mode: Default::default(), + clear_interval: Default::default(), + excluded_sequences: Default::default(), + allow_ccq: true, + }) +} diff --git a/packages/interchain/hermes-relayer/src/core.rs b/packages/interchain/hermes-relayer/src/core.rs new file mode 100644 index 00000000..aa3d7d0d --- /dev/null +++ b/packages/interchain/hermes-relayer/src/core.rs @@ -0,0 +1,149 @@ +use std::collections::HashMap; + +use cw_orch_core::environment::ChainInfoOwned; +use cw_orch_core::environment::ChainState; +use cw_orch_daemon::Daemon; +use cw_orch_interchain_core::env::ChainId; +use cw_orch_interchain_daemon::{IcDaemonResult, Mnemonic}; +use ibc_relayer::chain::handle::ChainHandle; +use tokio::runtime::Handle; + +use ibc_relayer::config::{Config, RestConfig, TelemetryConfig}; + +use crate::config::chain_config; +use crate::config::KEY_NAME; +use crate::keys::restore_key; + +#[derive(Clone)] +pub struct HermesRelayer { + /// Daemon objects representing all the chains available inside the starship environment + pub daemons: HashMap, + /// Runtime handle for awaiting async functions + pub rt_handle: Handle, + + pub connection_ids: HashMap<(String, String), String>, +} + +impl HermesRelayer { + /// Builds a new `InterchainEnv` instance. + /// For use with starship, we advise to use `Starship::interchain_env` instead + pub fn new( + runtime: &Handle, + chains: Vec<(T, Option, bool, String)>, + connections: HashMap<(String, String), String>, + ) -> IcDaemonResult + where + T: Into, + { + let mut env = Self::raw(runtime); + + // We create daemons for each chains + for (chain_data, mnemonic, is_consumer_chain, rpc) in chains { + let daemon = env.build_daemon(runtime, chain_data.into(), mnemonic)?; + env.daemons.insert( + daemon.state().chain_data.chain_id.to_string(), + (daemon, is_consumer_chain, rpc), + ); + } + env.connection_ids = connections; + + Ok(env) + } + + /// This creates an interchain environment from existing daemon instances + /// The `channel_creator` argument will be responsible for creation interchain channel + /// If using starship, prefer using Starship::interchain_env for environment creation + pub fn from_daemons(rt: &Handle, daemons: Vec<(Daemon, bool, String)>) -> Self { + let mut env = Self::raw(rt); + for (daemon, is_consumer_chain, rpc) in daemons { + env.daemons.insert( + daemon.state().chain_data.chain_id.to_string(), + (daemon, is_consumer_chain, rpc), + ); + } + env + } + + fn raw(rt: &Handle) -> Self { + Self { + daemons: HashMap::new(), + rt_handle: rt.clone(), + connection_ids: Default::default(), + } + } + + /// Build a daemon from chain data and mnemonic and add it to the current configuration + fn build_daemon( + &mut self, + runtime: &Handle, + chain_data: ChainInfoOwned, + mnemonic: Option, + ) -> IcDaemonResult { + let mut daemon_builder = Daemon::builder(chain_data.clone()); + let mut daemon_builder = daemon_builder.handle(runtime); + + daemon_builder = if let Some(mn) = mnemonic { + daemon_builder.mnemonic(mn.to_string()) + } else { + daemon_builder + }; + + // State is shared between daemons, so if a daemon already exists, we use its state + daemon_builder = if let Some((daemon, _, _)) = self.daemons.values().next() { + daemon_builder.state(daemon.state()) + } else { + daemon_builder + }; + + let daemon = daemon_builder.build().unwrap(); + + Ok(daemon) + } + + pub fn duplex_config(&self, src_chain: ChainId, dst_chain: ChainId) -> Config { + let (src_daemon, src_is_consumer_chain, src_rpc_url) = self.daemons.get(src_chain).unwrap(); + let src_chain_data = &src_daemon.state().chain_data; + + let (dst_daemon, dst_is_consumer_chain, dst_rpc_url) = self.daemons.get(dst_chain).unwrap(); + let dst_chain_data = &dst_daemon.state().chain_data; + + Config { + global: ibc_relayer::config::GlobalConfig { + log_level: ibc_relayer::config::LogLevel::Info, + }, + mode: ibc_relayer::config::ModeConfig::default(), + rest: RestConfig::default(), + telemetry: TelemetryConfig::default(), + chains: vec![ + chain_config( + src_chain, + src_rpc_url, + src_chain_data, + *src_is_consumer_chain, + ), + chain_config( + dst_chain, + dst_rpc_url, + dst_chain_data, + *dst_is_consumer_chain, + ), + ], + tracing_server: Default::default(), + } + } + + pub fn add_key(&self, chain: &impl ChainHandle) { + let chain_id = chain.config().unwrap().id().to_string(); + + let (daemon, _, _) = self.daemons.get(&chain_id).unwrap(); + + let chain_data = &daemon.state().chain_data; + let hd_path = daemon.sender().options().hd_index; + let key = restore_key(self.mnemonic().clone(), hd_path.unwrap_or(0), chain_data).unwrap(); + chain.add_key(KEY_NAME.to_string(), key).unwrap(); + } + + fn mnemonic(&self) -> String { + std::env::var("TEST_MNEMONIC").unwrap() + } +} diff --git a/packages/interchain/hermes-relayer/src/interchain_env.rs b/packages/interchain/hermes-relayer/src/interchain_env.rs new file mode 100644 index 00000000..8512710c --- /dev/null +++ b/packages/interchain/hermes-relayer/src/interchain_env.rs @@ -0,0 +1,411 @@ +use cosmwasm_std::IbcOrder; +use cw_orch_core::environment::IndexResponse; +use cw_orch_daemon::queriers::{Ibc, Node}; +use cw_orch_daemon::{CosmTxResponse, Daemon, DaemonError}; +use cw_orch_interchain_core::channel::{IbcPort, InterchainChannel}; +use cw_orch_interchain_core::env::{ChainId, ChannelCreation}; +use cw_orch_interchain_core::results::{ + ChannelCreationTransactionsResult, InternalChannelCreationResult, +}; +use cw_orch_interchain_core::{IbcPacketOutcome, InterchainEnv, SinglePacketFlow, TxId}; +use cw_orch_interchain_daemon::packet_inspector::find_ibc_packets_sent_in_tx; +use cw_orch_interchain_daemon::packet_inspector::PacketInspector; + +use crate::core::HermesRelayer; +use cw_orch_interchain_daemon::InterchainDaemonError; +use ibc_relayer_types::core::ics04_channel::packet::Sequence; +use ibc_relayer_types::core::ics24_host::identifier::{ChannelId, PortId}; +use tokio::time::sleep; +use tonic::transport::Channel; + +use cw_orch_interchain_core::NestedPacketsFlow; +use cw_orch_interchain_daemon::ChannelCreator; +use futures::future::try_join4; +use std::str::FromStr; +use std::time::Duration; + +impl InterchainEnv for HermesRelayer { + type ChannelCreationResult = (); + + type Error = InterchainDaemonError; + + /// Get the daemon for a network-id in the interchain. + fn get_chain(&self, chain_id: impl ToString) -> Result { + self.daemons + .get(&chain_id.to_string()) + .map(|(d, _, _)| d) + .ok_or(InterchainDaemonError::DaemonNotFound(chain_id.to_string())) + .cloned() + } + + // In a daemon environmment, you don't create a channel between 2 chains, instead you just do it with external tools and returns here when the channel is ready + fn _internal_create_channel( + &self, + src_chain: ChainId, + dst_chain: ChainId, + src_port: &PortId, + dst_port: &PortId, + version: &str, + order: Option, + ) -> Result, Self::Error> { + let connection_id = + self.create_ibc_channel(src_chain, dst_chain, src_port, dst_port, version, order)?; + + Ok(InternalChannelCreationResult { + result: (), + src_connection_id: connection_id, + }) + } + + // This function creates a channel and returns the 4 transactions hashes for channel creation + fn get_channel_creation_txs( + &self, + src_chain: ChainId, + ibc_channel: &mut InterchainChannel, + _channel_creation_result: (), + ) -> Result, Self::Error> { + let (src_port, dst_port) = ibc_channel.get_mut_ordered_ports_from(src_chain)?; + + // We start by getting the connection-id of the counterparty chain + let connection_end = self.rt_handle.block_on( + Ibc::new_async(src_port.chain.clone()) + ._connection_end(src_port.connection_id.clone().unwrap()), + )?; + + dst_port.connection_id = Some(connection_end.unwrap().counterparty.unwrap().connection_id); + + // Then we make sure the channel is indeed created between the two chains + let channel_creation = self + .rt_handle + .block_on(self.find_channel_creation_tx(src_chain, ibc_channel))?; + + let src_channel_id = channel_creation + .ack + .event_attr_value("channel_open_ack", "channel_id")?; + let dst_channel_id = channel_creation + .confirm + .event_attr_value("channel_open_confirm", "channel_id")?; + + log::info!("Successfully created a channel between {} and {} on '{}:{}' and channels {}:'{}'(txhash : {}) and {}:'{}' (txhash : {})", + ibc_channel.port_a.port.clone(), + ibc_channel.port_b.port.clone(), + ibc_channel.port_a.connection_id.clone().unwrap(), + ibc_channel.port_b.connection_id.clone().unwrap(), + ibc_channel.port_a.chain_id.clone(), + src_channel_id, + channel_creation.ack.txhash, + ibc_channel.port_b.chain_id.clone(), + dst_channel_id, + channel_creation.confirm.txhash, + ); + + Ok(ChannelCreationTransactionsResult { + src_channel_id: ChannelId::from_str(&src_channel_id)?, + dst_channel_id: ChannelId::from_str(&dst_channel_id)?, + channel_creation_txs: channel_creation, + }) + } + + // This function follows every IBC packet sent out in a tx result + fn await_packets( + &self, + chain_id: ChainId, + tx_response: CosmTxResponse, + ) -> Result, Self::Error> { + log::info!( + target: chain_id, + "Investigating sent packet events on tx {}", + tx_response.txhash + ); + + // 1. Getting IBC related events for the current tx + finding all IBC packets sent out in the transaction + let daemon_1 = self.get_chain(chain_id)?; + let grpc_channel1 = daemon_1.channel(); + + let sent_packets = daemon_1.rt_handle.block_on(find_ibc_packets_sent_in_tx( + chain_id.to_string(), + grpc_channel1.clone(), + tx_response.clone(), + ))?; + + // 2. We follow the packet history for each packet found inside the transaction + let ibc_packet_results = sent_packets + .iter() + .map(|packet| { + self.await_single_packet( + chain_id, + packet.src_port.clone(), + packet.src_channel.clone(), + &packet.dst_chain_id, + packet.sequence, + ) + }) + .collect::, _>>()?; + + let send_tx_id = TxId::new(chain_id.to_string(), tx_response); + + // We follow all results from outgoing packets in the resulting transactions + let full_results = ibc_packet_results + .into_iter() + .map(|ibc_result| { + let txs_to_analyze = match ibc_result.outcome.clone() { + IbcPacketOutcome::Timeout { timeout_tx } => vec![timeout_tx], + IbcPacketOutcome::Success { + receive_tx, ack_tx, .. + } => vec![receive_tx, ack_tx], + }; + + let txs_results = txs_to_analyze + .iter() + .map(|tx| { + let chain_id = tx.chain_id.clone(); + let response = tx.response.clone(); + self.await_packets(&chain_id, response) + }) + .collect::, _>>()?; + + let analyzed_outcome = match ibc_result.outcome { + IbcPacketOutcome::Timeout { .. } => IbcPacketOutcome::Timeout { + timeout_tx: txs_results[0].clone(), + }, + IbcPacketOutcome::Success { ack, .. } => IbcPacketOutcome::Success { + ack: ack.clone(), + receive_tx: txs_results[0].clone(), + ack_tx: txs_results[1].clone(), + }, + }; + + Ok::<_, InterchainDaemonError>(analyzed_outcome.clone()) + }) + .collect::>()?; + + let tx_identification = NestedPacketsFlow { + tx_id: send_tx_id.clone(), + packets: full_results, + }; + + Ok(tx_identification) + } + + // This function follow the execution of an IBC packet across the chain + fn await_single_packet( + &self, + src_chain: ChainId, + src_port: PortId, + src_channel: ChannelId, + dst_chain: ChainId, + sequence: Sequence, + ) -> Result, Self::Error> { + // We crate an interchain env object that is safe to send between threads + let interchain_env = self.rt_handle.block_on(PacketInspector::new( + self.daemons.values().map(|(d, _, _)| d).collect(), + ))?; + + // We try to relay the packets using the HERMES relayer + self.force_packet_relay( + src_chain, + src_port.clone(), + src_channel.clone(), + dst_chain, + sequence, + ); + + // We follow the trail + let ibc_trail = self.rt_handle.block_on(interchain_env.follow_packet( + src_chain, + src_port, + src_channel, + dst_chain, + sequence, + ))?; + + Ok(ibc_trail) + } + + fn chains<'a>(&'a self) -> impl Iterator + where + Daemon: 'a, + { + self.daemons.values().map(|(daemon, _, _)| daemon) + } +} + +impl HermesRelayer { + /// This function follows every IBC packet sent out in a tx result + /// This allows only providing the transaction hash when you don't have access to the whole response object + pub fn wait_ibc_from_txhash( + &self, + chain_id: ChainId, + packet_send_tx_hash: String, + ) -> Result, InterchainDaemonError> { + let grpc_channel1 = self.get_chain(chain_id)?.channel(); + + let tx = self.rt_handle.block_on( + Node::new_async(grpc_channel1.clone())._find_tx(packet_send_tx_hash.clone()), + )?; + + let ibc_trail = self.await_packets(chain_id, tx)?; + + Ok(ibc_trail) + } + + async fn find_channel_creation_tx<'a>( + &self, + src_chain: ChainId<'a>, + ibc_channel: &InterchainChannel, + ) -> Result, InterchainDaemonError> { + for _ in 0..5 { + match self.get_last_channel_creation(src_chain, ibc_channel).await { + Ok(tx) => { + if tx.init.is_some() + && tx.r#try.is_some() + && tx.ack.is_some() + && tx.confirm.is_some() + { + let creation = ChannelCreation { + init: tx.init.unwrap(), + r#try: tx.r#try.unwrap(), + ack: tx.ack.unwrap(), + confirm: tx.confirm.unwrap(), + }; + return Ok(creation); + } + log::debug!("No new TX by events found"); + log::debug!("Waiting 20s"); + sleep(Duration::from_secs(20)).await; + } + Err(e) => { + log::debug!("{:?}", e); + break; + } + } + } + + Err(InterchainDaemonError::ChannelCreationEventsNotFound { + src_chain: src_chain.to_string(), + channel: ibc_channel.clone(), + }) + } + + /// Queries the last transactions that is related to creating a channel from chain from to the counterparty chain defined in the structure + async fn get_last_channel_creation<'a>( + &self, + src_chain: ChainId<'a>, + ibc_channel: &InterchainChannel, + ) -> Result>, InterchainDaemonError> { + let (channel_init, channel_try, channel_ack, channel_confirm) = try_join4( + self.get_channel_creation_init(src_chain, ibc_channel), + self.get_channel_creation_try(src_chain, ibc_channel), + self.get_channel_creation_ack(src_chain, ibc_channel), + self.get_channel_creation_confirm(src_chain, ibc_channel), + ) + .await?; + + Ok(ChannelCreation::new( + channel_init, + channel_try, + channel_ack, + channel_confirm, + )) + } + + async fn get_channel_creation_init<'a>( + &self, + from: ChainId<'a>, + ibc_channel: &'a InterchainChannel, + ) -> Result, InterchainDaemonError> { + let (src_port, dst_port) = ibc_channel.get_ordered_ports_from(from)?; + + let channel_creation_events_init_events = vec![ + format!("channel_open_init.port_id='{}'", src_port.port), + format!("channel_open_init.counterparty_port_id='{}'", dst_port.port), + format!( + "channel_open_init.connection_id='{}'", + src_port.connection_id.clone().unwrap() + ), + ]; + + Ok(find_one_tx_by_events(src_port, channel_creation_events_init_events).await?) + } + + async fn get_channel_creation_try<'a>( + &self, + from: ChainId<'a>, + ibc_channel: &'a InterchainChannel, + ) -> Result, InterchainDaemonError> { + let (src_port, dst_port) = ibc_channel.get_ordered_ports_from(from)?; + + let channel_creation_try_events = vec![ + format!("channel_open_try.port_id='{}'", dst_port.port), + format!("channel_open_try.counterparty_port_id='{}'", src_port.port), + format!( + "channel_open_try.connection_id='{}'", + dst_port.connection_id.clone().unwrap() + ), + ]; + + log::debug!( + "Try {} {:?}", + dst_port.chain_id, + channel_creation_try_events + ); + + Ok(find_one_tx_by_events(dst_port, channel_creation_try_events).await?) + } + + async fn get_channel_creation_ack<'a>( + &self, + from: ChainId<'a>, + ibc_channel: &'a InterchainChannel, + ) -> Result, InterchainDaemonError> { + let (src_port, dst_port) = ibc_channel.get_ordered_ports_from(from)?; + + let channel_creation_ack_events = vec![ + format!("channel_open_ack.port_id='{}'", src_port.port), + format!("channel_open_ack.counterparty_port_id='{}'", dst_port.port), + format!( + "channel_open_ack.connection_id='{}'", + src_port.connection_id.clone().unwrap() + ), + ]; + + Ok(find_one_tx_by_events(src_port, channel_creation_ack_events).await?) + } + + async fn get_channel_creation_confirm<'a>( + &self, + from: ChainId<'a>, + ibc_channel: &'a InterchainChannel, + ) -> Result, InterchainDaemonError> { + let (src_port, dst_port) = ibc_channel.get_ordered_ports_from(from)?; + + let channel_creation_confirm_events = vec![ + format!("channel_open_confirm.port_id='{}'", dst_port.port), + format!( + "channel_open_confirm.counterparty_port_id='{}'", + src_port.port + ), + format!( + "channel_open_confirm.connection_id='{}'", + dst_port.connection_id.clone().unwrap() + ), + ]; + + Ok(find_one_tx_by_events(dst_port, channel_creation_confirm_events).await?) + } +} + +async fn find_one_tx_by_events( + port: IbcPort, + events: Vec, +) -> Result, DaemonError> { + let optional_tx = Node::new_async(port.chain.clone()) + ._find_tx_by_events( + events, + None, + Some(cosmrs::proto::cosmos::tx::v1beta1::OrderBy::Desc), + ) + .await?; + + Ok(optional_tx.first().cloned()) +} diff --git a/packages/interchain/hermes-relayer/src/keys.rs b/packages/interchain/hermes-relayer/src/keys.rs new file mode 100644 index 00000000..5e1e88cb --- /dev/null +++ b/packages/interchain/hermes-relayer/src/keys.rs @@ -0,0 +1,29 @@ +use cw_orch_core::environment::ChainInfoOwned; +use hdpath::{Purpose, StandardHDPath}; +use ibc_relayer::{ + config::AddressType, + keyring::{AnySigningKeyPair, Secp256k1KeyPair, SigningKeyPair}, +}; + +pub fn restore_key( + mnemonic: String, + hdpath_index: u32, + chain_data: &ChainInfoOwned, +) -> anyhow::Result { + let hdpath = StandardHDPath::new( + Purpose::Pubkey, + chain_data.network_info.coin_type, + 0, + 0, + hdpath_index, + ); + + let key_pair = Secp256k1KeyPair::from_mnemonic( + &mnemonic, + &hdpath, + &AddressType::Cosmos, + &chain_data.network_info.pub_address_prefix, + )?; + + Ok(key_pair.into()) +} diff --git a/packages/interchain/hermes-relayer/src/lib.rs b/packages/interchain/hermes-relayer/src/lib.rs new file mode 100644 index 00000000..5339baa3 --- /dev/null +++ b/packages/interchain/hermes-relayer/src/lib.rs @@ -0,0 +1,6 @@ +pub mod channel; +pub mod config; +pub mod core; +pub mod interchain_env; +pub mod keys; +pub mod packet; diff --git a/packages/interchain/hermes-relayer/src/packet.rs b/packages/interchain/hermes-relayer/src/packet.rs new file mode 100644 index 00000000..423f3e64 --- /dev/null +++ b/packages/interchain/hermes-relayer/src/packet.rs @@ -0,0 +1,131 @@ +use crate::core::HermesRelayer; +use cw_orch_core::environment::QuerierGetter; +use cw_orch_daemon::queriers::Ibc; +use cw_orch_interchain_core::env::ChainId; +use ibc_relayer::link::{Link, LinkParameters}; +use ibc_relayer_cli::cli_utils::ChainHandlePair; +use ibc_relayer_types::core::{ics04_channel, ics24_host::identifier}; +use ibc_relayer_types::core::{ + ics04_channel::packet::Sequence, + ics24_host::identifier::{ChannelId, PortId}, +}; + +impl HermesRelayer { + pub fn force_packet_relay( + &self, + src_chain: ChainId, + src_port: PortId, + src_channel: ChannelId, + dst_chain: ChainId, + sequence: Sequence, + ) { + self.receive_packet( + src_chain, + src_port.clone(), + src_channel.clone(), + dst_chain, + sequence, + ); + self.ack_packet( + src_chain, + src_port.clone(), + src_channel.clone(), + dst_chain, + sequence, + ); + } + + pub fn receive_packet( + &self, + src_chain: ChainId, + src_port: PortId, + src_channel: ChannelId, + dst_chain: ChainId, + sequence: Sequence, + ) { + let config = self.duplex_config(src_chain, dst_chain); + let chains = ChainHandlePair::spawn( + &config, + &identifier::ChainId::from_string(src_chain), + &identifier::ChainId::from_string(dst_chain), + ) + .unwrap(); + + let opts = LinkParameters { + src_port_id: src_port.to_string().parse().unwrap(), + src_channel_id: src_channel.to_string().parse().unwrap(), + max_memo_size: config.mode.packets.ics20_max_memo_size, + max_receiver_size: config.mode.packets.ics20_max_receiver_size, + + // Packets are only excluded when clearing + exclude_src_sequences: vec![], + }; + + self.add_key(&chains.src); + self.add_key(&chains.dst); + + let link = Link::new_from_opts(chains.src, chains.dst, opts, false, false).unwrap(); + + let sequence: u64 = sequence.into(); + let sequence = ics04_channel::packet::Sequence::from(sequence); + + link.relay_recv_packet_and_timeout_messages_with_packet_data_query_height( + vec![sequence..=sequence], + None, + ) + .unwrap(); + } + + pub fn ack_packet( + &self, + src_chain: ChainId, + src_port: PortId, + src_channel: ChannelId, + dst_chain: ChainId, + sequence: Sequence, + ) { + let config = self.duplex_config(src_chain, dst_chain); + + let chains = ChainHandlePair::spawn( + &config, + &identifier::ChainId::from_string(dst_chain), + &identifier::ChainId::from_string(src_chain), + ) + .unwrap(); + + let (d, _, _) = self.daemons.get(src_chain).unwrap(); + + let ibc: Ibc = d.querier(); + + let counterparty = d + .rt_handle + .block_on(ibc._channel(src_port.to_string(), src_channel.to_string())) + .unwrap() + .counterparty + .unwrap(); + + let opts = LinkParameters { + src_port_id: counterparty.port_id.to_string().parse().unwrap(), + src_channel_id: counterparty.channel_id.to_string().parse().unwrap(), + max_memo_size: config.mode.packets.ics20_max_memo_size, + max_receiver_size: config.mode.packets.ics20_max_receiver_size, + + // Packets are only excluded when clearing + exclude_src_sequences: vec![], + }; + + self.add_key(&chains.src); + self.add_key(&chains.dst); + + let link = Link::new_from_opts(chains.src, chains.dst, opts, false, false).unwrap(); + + let sequence: u64 = sequence.into(); + let sequence = ics04_channel::packet::Sequence::from(sequence); + + link.relay_ack_packet_messages_with_packet_data_query_height( + vec![sequence..=sequence], + None, + ) + .unwrap(); + } +} diff --git a/packages/interchain/interchain-daemon/src/interchain_env.rs b/packages/interchain/interchain-daemon/src/interchain_env.rs index f7ed82fc..592685d9 100644 --- a/packages/interchain/interchain-daemon/src/interchain_env.rs +++ b/packages/interchain/interchain-daemon/src/interchain_env.rs @@ -40,6 +40,9 @@ pub struct DaemonInterchain { rt_handle: Handle, } +/// Mnemonic type to clarify code +pub type Mnemonic = String; + impl DaemonInterchain { /// Builds a new [`DaemonInterchain`] instance. /// For use with starship, we advise to use [`cw_orch_starship::Starship::interchain_env`] instead diff --git a/packages/interchain/interchain-daemon/src/lib.rs b/packages/interchain/interchain-daemon/src/lib.rs index 95585f82..9b83c080 100644 --- a/packages/interchain/interchain-daemon/src/lib.rs +++ b/packages/interchain/interchain-daemon/src/lib.rs @@ -19,3 +19,4 @@ pub type IcDaemonResult = Result; pub use channel_creator::{ChannelCreationValidator, ChannelCreator}; pub use interchain_env::DaemonInterchain; +pub use interchain_env::Mnemonic; diff --git a/packages/interchain/interchain-daemon/src/packet_inspector.rs b/packages/interchain/interchain-daemon/src/packet_inspector.rs index c72d4f03..75383091 100644 --- a/packages/interchain/interchain-daemon/src/packet_inspector.rs +++ b/packages/interchain/interchain-daemon/src/packet_inspector.rs @@ -30,7 +30,7 @@ use std::collections::HashMap; /// This struct is safe to be sent between threads /// In contrary to InterchainStructure that holds Daemon in its definition which is not sendable #[derive(Default, Clone)] -pub(crate) struct PacketInspector { +pub struct PacketInspector { registered_chains: HashMap, } @@ -410,7 +410,7 @@ impl PacketInspector { } // From is the channel from which the send packet has been sent - pub async fn get_packet_send_tx<'a>( + async fn get_packet_send_tx<'a>( &self, from: ChainId<'a>, ibc_channel: &'a InterchainChannel, @@ -437,7 +437,7 @@ impl PacketInspector { } // on is the chain on which the packet will be received - pub async fn get_packet_receive_tx<'a>( + async fn get_packet_receive_tx<'a>( &self, from: ChainId<'a>, ibc_channel: &'a InterchainChannel, @@ -464,7 +464,7 @@ impl PacketInspector { } // on is the chain on which the packet will be received - pub async fn get_packet_timeout_tx<'a>( + async fn get_packet_timeout_tx<'a>( &self, from: ChainId<'a>, ibc_channel: &'a InterchainChannel, @@ -491,7 +491,7 @@ impl PacketInspector { } // From is the channel from which the original send packet has been sent - pub async fn get_packet_ack_receive_tx<'a>( + async fn get_packet_ack_receive_tx<'a>( &self, from: ChainId<'a>, ibc_channel: &'a InterchainChannel, @@ -525,7 +525,7 @@ fn get_events(events: &[TxResultBlockEvent], attr_name: &str) -> Vec { .collect() } -#[allow(missing_docs)] +/// Find all the ibc packets that were sent during a transaction from the transaction events pub async fn find_ibc_packets_sent_in_tx( chain: NetworkId, grpc_channel: Channel,