diff --git a/Cargo.lock b/Cargo.lock index 67923636..04392ab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,6 +415,21 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "cw-controllers" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c1804013d21060b994dea28a080f9eab78a3bcb6b617f05e7634b0600bf7b1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "cw-multi-test" version = "2.3.3" @@ -426,6 +441,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "cw-utils", + "cw20-ics20", "hex", "hex-literal", "itertools 0.14.0", @@ -461,6 +477,53 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cw2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "semver", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw20" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42212b6bf29bbdda693743697c621894723f35d3db0d5df930be22903d0e27c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw20-ics20" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a9e377dbbd1ffb3b6a8a2dbf9128609a6458a3292f88f99e0b6840a7e9762e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw20", + "schemars", + "semver", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "der" version = "0.7.9" diff --git a/Cargo.toml b/Cargo.toml index b281b1a6..e0fdbe5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,12 @@ cosmwasm-schema = "2.2.2" cosmwasm-std = "2.2.2" cw-storage-plus = "2.0.0" cw-utils = "2.0.0" +cw20-ics20 = "2.0.0" itertools = "0.14.0" prost = "0.13.5" schemars = "0.8.22" serde = "1.0.219" +serde_json = "1.0.140" sha2 = "0.10.8" thiserror = "2.0.12" @@ -45,4 +47,3 @@ thiserror = "2.0.12" base64 = "0.22.1" hex = "0.4.3" hex-literal = "0.4.1" -serde_json = "1.0.140" diff --git a/src/app.rs b/src/app.rs index d6bd45fb..5b29c791 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,14 +6,17 @@ use crate::featured::staking::{ Distribution, DistributionKeeper, StakeKeeper, Staking, StakingSudo, }; use crate::gov::Gov; -use crate::ibc::Ibc; +use crate::ibc::{ + types::IbcResponse, types::MockIbcQuery, IbcModuleMsg, IbcPacketRelayingMsg as IbcSudo, +}; +use crate::ibc::{Ibc, IbcSimpleModule}; use crate::module::{FailingModule, Module}; use crate::prefixed_storage::{ prefixed, prefixed_multilevel, prefixed_multilevel_read, prefixed_read, }; use crate::transactions::transactional; use crate::wasm::{ContractData, Wasm, WasmKeeper, WasmSudo}; -use crate::{AppBuilder, GovFailingModule, IbcFailingModule, Stargate, StargateFailing}; +use crate::{AppBuilder, GovFailingModule, Stargate, StargateFailing}; use cosmwasm_std::testing::{MockApi, MockStorage}; use cosmwasm_std::{ from_json, to_json_binary, Addr, Api, Binary, BlockInfo, ContractResult, CosmosMsg, CustomMsg, @@ -41,7 +44,7 @@ pub type BasicApp = App< WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >; @@ -58,7 +61,7 @@ pub struct App< Wasm = WasmKeeper, Staking = StakeKeeper, Distr = DistributionKeeper, - Ibc = IbcFailingModule, + Ibc = IbcSimpleModule, Gov = GovFailingModule, Stargate = StargateFailing, > { @@ -94,7 +97,7 @@ impl BasicApp { WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >, @@ -119,7 +122,7 @@ where WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >, @@ -484,6 +487,20 @@ where }) } + /// Queries the IBC module + pub fn ibc_query(&self, query: MockIbcQuery) -> AnyResult { + let Self { + block, + router, + api, + storage, + } = self; + + let querier = router.querier(api, storage, block); + + router.ibc.query(api, storage, &querier, block, query) + } + /// Runs arbitrary SudoMsg. /// This will create a cache before the execution, so no state changes are persisted if this /// returns an error, but all are persisted on success. @@ -566,6 +583,8 @@ pub enum SudoMsg { Staking(StakingSudo), /// Wasm privileged actions. Wasm(WasmSudo), + /// Ibc actions, used namely to create channels and relay packets + Ibc(IbcSudo), } impl From for SudoMsg { @@ -585,6 +604,20 @@ impl From for SudoMsg { SudoMsg::Staking(staking) } } + +/// We use it to allow calling into modules from the ibc module. This is used for receiving packets +pub struct IbcRouterMsg { + pub module: IbcModule, + pub msg: IbcModuleMsg, +} + +#[cosmwasm_schema::cw_serde] +pub enum IbcModule { + Wasm(Addr), // The wasm module needs to contain the wasm contract address (usually decoded from the port) + Bank, + Staking, +} + /// A trait representing the Cosmos based chain's router. /// /// This trait is designed for routing messages within the Cosmos ecosystem. @@ -623,6 +656,15 @@ pub trait CosmosRouter { block: &BlockInfo, msg: SudoMsg, ) -> AnyResult; + + /// Evaluates all ibc related actions + fn ibc( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + block: &BlockInfo, + msg: IbcRouterMsg, + ) -> AnyResult; } impl CosmosRouter @@ -695,7 +737,7 @@ where #[cfg(feature = "staking")] QueryRequest::Staking(req) => self.staking.query(api, storage, &querier, block, req), #[cfg(feature = "stargate")] - QueryRequest::Ibc(req) => self.ibc.query(api, storage, &querier, block, req), + QueryRequest::Ibc(req) => self.ibc.query(api, storage, &querier, block, req.into()), #[allow(deprecated)] #[cfg(feature = "stargate")] QueryRequest::Stargate { path, data } => self @@ -719,9 +761,99 @@ where SudoMsg::Bank(msg) => self.bank.sudo(api, storage, self, block, msg), #[cfg(feature = "staking")] SudoMsg::Staking(msg) => self.staking.sudo(api, storage, self, block, msg), + SudoMsg::Ibc(msg) => self.ibc.sudo(api, storage, self, block, msg), _ => unimplemented!(), } } + + fn ibc( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + block: &BlockInfo, + msg: IbcRouterMsg, + ) -> AnyResult { + match msg.module { + IbcModule::Bank => match msg.msg { + IbcModuleMsg::ChannelOpen(m) => self + .bank + .ibc_channel_open(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::ChannelConnect(m) => self + .bank + .ibc_channel_connect(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::ChannelClose(m) => self + .bank + .ibc_channel_close(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketReceive(m) => self + .bank + .ibc_packet_receive(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketAcknowledgement(m) => self + .bank + .ibc_packet_acknowledge(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketTimeout(m) => self + .bank + .ibc_packet_timeout(api, storage, self, block, m) + .map(Into::into), + }, + IbcModule::Staking => match msg.msg { + IbcModuleMsg::ChannelOpen(m) => self + .staking + .ibc_channel_open(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::ChannelConnect(m) => self + .staking + .ibc_channel_connect(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::ChannelClose(m) => self + .staking + .ibc_channel_close(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketReceive(m) => self + .staking + .ibc_packet_receive(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketAcknowledgement(m) => self + .staking + .ibc_packet_acknowledge(api, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketTimeout(m) => self + .staking + .ibc_packet_timeout(api, storage, self, block, m) + .map(Into::into), + }, + IbcModule::Wasm(contract_addr) => match msg.msg { + IbcModuleMsg::ChannelOpen(m) => self + .wasm + .ibc_channel_open(api, contract_addr, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::ChannelConnect(m) => self + .wasm + .ibc_channel_connect(api, contract_addr, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::ChannelClose(m) => self + .wasm + .ibc_channel_close(api, contract_addr, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketReceive(m) => self + .wasm + .ibc_packet_receive(api, contract_addr, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketAcknowledgement(m) => self + .wasm + .ibc_packet_acknowledge(api, contract_addr, storage, self, block, m) + .map(Into::into), + IbcModuleMsg::PacketTimeout(m) => self + .wasm + .ibc_packet_timeout(api, contract_addr, storage, self, block, m) + .map(Into::into), + }, + } + } } pub struct MockRouter(PhantomData<(ExecC, QueryC)>); @@ -779,6 +911,16 @@ where ) -> AnyResult { panic!("Cannot sudo MockRouters"); } + + fn ibc( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _block: &BlockInfo, + _msg: IbcRouterMsg, + ) -> AnyResult { + panic!("Cannot ibc MockRouters"); + } } pub struct RouterQuerier<'a, ExecC, QueryC> { diff --git a/src/app_builder.rs b/src/app_builder.rs index bc56ece9..e854e110 100644 --- a/src/app_builder.rs +++ b/src/app_builder.rs @@ -1,9 +1,10 @@ //! AppBuilder helps you set up your test blockchain environment step by step [App]. use crate::featured::staking::{Distribution, DistributionKeeper, StakeKeeper, Staking}; +use crate::ibc::IbcSimpleModule; use crate::{ - App, Bank, BankKeeper, FailingModule, Gov, GovFailingModule, Ibc, IbcFailingModule, Module, - Router, Stargate, StargateFailing, Wasm, WasmKeeper, + App, Bank, BankKeeper, FailingModule, Gov, GovFailingModule, Ibc, Module, Router, Stargate, + StargateFailing, Wasm, WasmKeeper, }; use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; use cosmwasm_std::{Api, BlockInfo, CustomMsg, CustomQuery, Empty, Storage}; @@ -36,7 +37,7 @@ pub type BasicAppBuilder = AppBuilder< WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >; @@ -68,7 +69,7 @@ impl Default WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, > @@ -99,7 +100,7 @@ impl WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, > @@ -126,7 +127,7 @@ impl custom: FailingModule::new(), staking: StakeKeeper::new(), distribution: DistributionKeeper::new(), - ibc: IbcFailingModule::new(), + ibc: IbcSimpleModule, gov: GovFailingModule::new(), stargate: StargateFailing, } @@ -142,7 +143,7 @@ impl WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, > @@ -162,7 +163,7 @@ where custom: FailingModule::new(), staking: StakeKeeper::new(), distribution: DistributionKeeper::new(), - ibc: IbcFailingModule::new(), + ibc: IbcSimpleModule, gov: GovFailingModule::new(), stargate: StargateFailing, } diff --git a/src/bank.rs b/src/bank.rs index 8727ac17..bbc791ee 100644 --- a/src/bank.rs +++ b/src/bank.rs @@ -1,6 +1,7 @@ use crate::app::CosmosRouter; use crate::error::{bail, AnyResult}; use crate::executor::AppResponse; +use crate::ibc::types::{AppIbcBasicResponse, AppIbcReceiveResponse}; use crate::module::Module; use crate::prefixed_storage::{prefixed, prefixed_read}; use cosmwasm_std::{ @@ -16,6 +17,9 @@ use cw_utils::NativeBalance; use itertools::Itertools; use schemars::JsonSchema; +use cosmwasm_std::{coins, from_json, IbcPacketAckMsg, IbcPacketReceiveMsg}; +use cw20_ics20::ibc::Ics20Packet; + /// Collection of bank balances. const BALANCES: Map<&Addr, NativeBalance> = Map::new("balances"); @@ -24,6 +28,8 @@ const DENOM_METADATA: Map = Map::new("metadata"); /// Default storage namespace for bank module. const NAMESPACE_BANK: &[u8] = b"bank"; +/// Default address for the locked IBC funds. +pub const IBC_LOCK_MODULE_ADDRESS: &str = "ibc_bank_lock_module"; /// A message representing privileged actions in bank module. #[derive(Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -286,6 +292,126 @@ impl Module for BankKeeper { } } } + + fn ibc_packet_receive( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + request: IbcPacketReceiveMsg, + ) -> AnyResult { + // When receiving a packet, one simply needs to unpack the amount and send that to the the receiver + let packet: Ics20Packet = from_json(&request.packet.data)?; + + let mut bank_storage = prefixed(storage, NAMESPACE_BANK); + + // If the denom is exactly a denom that was sent through this channel, we can mint it directly without denom changes + // This can be verified by checking the ibc_module mock balance + let balances = + self.get_balance(&bank_storage, &Addr::unchecked(IBC_LOCK_MODULE_ADDRESS))?; + let locked_amount = balances.iter().find(|b| b.denom == packet.denom); + + if let Some(locked_amount) = locked_amount { + assert!( + locked_amount.amount >= packet.amount, + "The ibc locked amount is lower than the packet amount" + ); + // We send tokens from the IBC_LOCK_MODULE + self.send( + &mut bank_storage, + Addr::unchecked(IBC_LOCK_MODULE_ADDRESS), + api.addr_validate(&packet.receiver)?, + coins(packet.amount.u128(), packet.denom), + )?; + } else { + // Else, we receive the denom with prefixes + self.mint( + &mut bank_storage, + api.addr_validate(&packet.receiver)?, + coins( + packet.amount.u128(), + wrap_ibc_denom(request.packet.dest.channel_id, packet.denom), + ), + )?; + } + + // No acknowledgment needed + Ok(AppIbcReceiveResponse::default()) + } + + fn ibc_packet_acknowledge( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketAckMsg, + ) -> AnyResult { + // Acknowledgment can't fail, so no need for ack response parsing + Ok(AppIbcBasicResponse::default()) + } + + fn ibc_packet_timeout( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + request: cosmwasm_std::IbcPacketTimeoutMsg, + ) -> AnyResult { + // On timeout, we unpack the amount and sent that back to the receiverwe give the funds back to the sender of the packet + + // When receiving a packet, one simply needs to unpack the amount and send that to the the receiver + let packet: Ics20Packet = from_json(request.packet.data)?; + + let mut bank_storage = prefixed(storage, NAMESPACE_BANK); + + // We verify the denom is exactly a denom that was sent through this channel + // This can be verified by checking the ibc_module mock balance + let balances = + self.get_balance(&bank_storage, &Addr::unchecked(IBC_LOCK_MODULE_ADDRESS))?; + let locked_amount = balances.iter().find(|b| b.denom == packet.denom); + + if let Some(locked_amount) = locked_amount { + assert!( + locked_amount.amount >= packet.amount, + "The ibc locked amount is lower than the packet amount" + ); + // We send tokens from the IBC_LOCK_MODULE + self.send( + &mut bank_storage, + Addr::unchecked(IBC_LOCK_MODULE_ADDRESS), + api.addr_validate(&packet.sender)?, + coins(packet.amount.u128(), packet.denom), + )?; + } else { + bail!("Funds refund after a timeout, can't timeout a transfer that was not initiated") + } + + Ok(AppIbcBasicResponse::default()) + } +} + +pub fn wrap_ibc_denom(channel_id: String, denom: String) -> String { + format!("ibc/{}/{}", channel_id, denom) +} + +pub fn optional_unwrap_ibc_denom(denom: String, expected_channel_id: String) -> String { + let split: Vec<_> = denom.splitn(3, '/').collect(); + if split.len() != 3 { + return denom; + } + + if split[0] != "ibc" { + return denom; + } + + if split[1] != expected_channel_id { + return denom; + } + + split[2].to_string() } #[cfg(test)] diff --git a/src/contracts.rs b/src/contracts.rs index 1f12360e..0f37fc13 100644 --- a/src/contracts.rs +++ b/src/contracts.rs @@ -1,15 +1,21 @@ -//! # Implementation of the contract trait and contract wrapper +//! # Implementation of the contract trait and the contract wrapper use crate::error::{anyhow, bail, AnyError, AnyResult}; use cosmwasm_std::{ from_json, Binary, Checksum, CosmosMsg, CustomMsg, CustomQuery, Deps, DepsMut, Empty, Env, - MessageInfo, QuerierWrapper, Reply, Response, SubMsg, + IbcDestinationCallbackMsg, IbcSourceCallbackMsg, MessageInfo, QuerierWrapper, Reply, Response, + SubMsg, +}; +use cosmwasm_std::{ + IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcChannelOpenResponse, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, + IbcReceiveResponse, }; use serde::de::DeserializeOwned; use std::fmt::{Debug, Display}; use std::ops::Deref; -/// This trait serves as a primary interface for interacting with contracts. +/// A primary interface for interacting with smart contracts. #[rustfmt::skip] pub trait Contract where @@ -34,6 +40,30 @@ where /// Evaluates contract's `migrate` entry-point. fn migrate(&self, deps: DepsMut, env: Env, msg: Vec) -> AnyResult>; + /// Evaluates the contract's `ibc_channel_open` entry-point. + fn ibc_channel_open(&self, deps: DepsMut, env: Env, msg: IbcChannelOpenMsg) -> AnyResult; + + /// Evaluates the contract's `ibc_channel_connect` entry-point. + fn ibc_channel_connect(&self, deps: DepsMut, env: Env, msg: IbcChannelConnectMsg) -> AnyResult>; + + /// Evaluates the contract's `ibc_channel_close` entry-point. + fn ibc_channel_close(&self, deps: DepsMut, env: Env, msg: IbcChannelCloseMsg) -> AnyResult>; + + /// Evaluates the contract's `ibc_packet_receive` entry-point. + fn ibc_packet_receive(&self, deps: DepsMut, env: Env, msg: IbcPacketReceiveMsg) -> AnyResult>; + + /// Evaluates the contract's `ibc_packet_ack` entry-point. + fn ibc_packet_ack(&self, deps: DepsMut, env: Env, msg: IbcPacketAckMsg) -> AnyResult>; + + /// Evaluates the contract's `ibc_packet_timeout` entry-point. + fn ibc_packet_timeout(&self, deps: DepsMut, env: Env, msg: IbcPacketTimeoutMsg) -> AnyResult>; + + /// Evaluates the contract's `ibc_source_callback` entry-point. + fn ibc_source_callback(&self, deps: DepsMut, env: Env, msg: IbcSourceCallbackMsg) -> AnyResult>; + + /// Evaluates the contract's `ibc_destination_callback` entry-point. + fn ibc_destination_callback(&self, deps: DepsMut, env: Env, msg: IbcDestinationCallbackMsg) -> AnyResult>; + /// Returns the provided checksum of the contract's Wasm blob. fn checksum(&self) -> Option { None @@ -44,17 +74,19 @@ where mod closures { use super::*; - // function types + // Function types: pub type ContractFn = fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result, E>; pub type PermissionedFn = fn(deps: DepsMut, env: Env, msg: T) -> Result, E>; pub type ReplyFn = fn(deps: DepsMut, env: Env, msg: Reply) -> Result, E>; pub type QueryFn = fn(deps: Deps, env: Env, msg: T) -> Result; + pub type IbcFn = fn(deps: DepsMut, env: Env, msg: T) -> Result; - // closure types + // Closure types: pub type ContractClosure = Box, Env, MessageInfo, T) -> Result, E>>; pub type PermissionedClosure = Box, Env, T) -> Result, E>>; pub type ReplyClosure = Box, Env, Reply) -> Result, E>>; pub type QueryClosure = Box, Env, T) -> Result>; + pub type IbcClosure = Box,Env, T) -> Result>; } use closures::*; @@ -75,43 +107,77 @@ use closures::*; /// - **E4** type of error returned from [sudo] entry-point. /// - **E5** type of error returned from [reply] entry-point. /// - **E6** type of error returned from [migrate] entry-point. +/// - **E7** type of error returned from [ibc_channel_open] entry-point. +/// - **E8** type of error returned from [ibc_channel_connect] entry-point. +/// - **E9** type of error returned from [ibc_channel_close] entry-point. +/// - **E10** type of error returned from [ibc_packet_receive] entry-point. +/// - **E11** type of error returned from [ibc_packet_ack] entry-point. +/// - **E12** type of error returned from [ibc_packet_timeout] entry-point. +/// - **E13** type of error returned from [ibc_source_callback] entry-point. +/// - **E14** type of error returned from [ibc_destination_callback] entry-point. /// - **C** type of custom message returned from all entry-points except [query]. /// - **Q** type of custom query in `Querier` passed as 'Deps' or 'DepsMut' to all entry-points. /// /// The following table summarizes the purpose of all generic types used in [ContractWrapper]. /// ```text -/// ┌─────────────┬────────────────┬─────────────────────┬─────────┬─────────┬───────┬───────┐ -/// │ Contract │ Contract │ │ │ │ │ │ -/// │ entry-point │ wrapper │ Closure type │ Message │ Message │ Error │ Query │ -/// │ │ member │ │ IN │ OUT │ OUT │ │ -/// ╞═════════════╪════════════════╪═════════════════════╪═════════╪═════════╪═══════╪═══════╡ -/// │ (1) │ │ │ │ │ │ │ -/// ╞═════════════╪════════════════╪═════════════════════╪═════════╪═════════╪═══════╪═══════╡ -/// │ execute │ execute_fn │ ContractClosure │ T1 │ C │ E1 │ Q │ -/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ -/// │ instantiate │ instantiate_fn │ ContractClosure │ T2 │ C │ E2 │ Q │ -/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ -/// │ query │ query_fn │ QueryClosure │ T3 │ Binary │ E3 │ Q │ -/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ -/// │ sudo │ sudo_fn │ PermissionedClosure │ T4 │ C │ E4 │ Q │ -/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ -/// │ reply │ reply_fn │ ReplyClosure │ Reply │ C │ E5 │ Q │ -/// ├─────────────┼────────────────┼─────────────────────┼─────────┼─────────┼───────┼───────┤ -/// │ migrate │ migrate_fn │ PermissionedClosure │ T6 │ C │ E6 │ Q │ -/// └─────────────┴────────────────┴─────────────────────┴─────────┴─────────┴───────┴───────┘ +/// ┌──────────────────────────┬─────────────────────────────┬─────────────────────┬───────────────────────────┬────────────────────────┬───────┬───────┐ +/// │ Contract entry-point │ ContractWrapper function │ Closure type │ message_in │ message_out │ │ │ +/// │ │ │ │ │ │ Error │ Query │ +/// │ │ │ │ │ │ OUT │ │ +/// ╞══════════════════════════╪═════════════════════════════╪═════════════════════╪═══════════════════════════╪════════════════════════╪═══════╪═══════╡ +/// │ execute │ execute_fn │ ContractClosure │ T1 │ C │ E1 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ instantiate │ instantiate_fn │ ContractClosure │ T2 │ C │ E2 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ query │ query_fn │ QueryClosure │ T3 │ Binary │ E3 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ sudo │ sudo_fn │ PermissionedClosure │ T4 │ C │ E4 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ reply │ reply_fn │ ReplyClosure │ Reply │ C │ E5 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ migrate │ migrate_fn │ PermissionedClosure │ T6 │ C │ E6 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_channel_open │ ibc_channel_open_fn │ IbcClosure │ IbcChannelOpenMsg │ IbcChannelOpenResponse │ E7 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_channel_connect │ ibc_channel_connect_fn │ IbcClosure │ IbcChannelConnectMsg │ IbcBasicResponse │ E8 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_channel_close │ ibc_channel_close_fn │ IbcClosure │ IbcChannelCloseMsg │ IbcBasicResponse │ E9 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_packet_receive │ ibc_packet_receive_fn │ IbcClosure │ IbcPacketReceiveMsg │ IbcReceiveResponse │ E10 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_packet_ack │ ibc_packet_ack_fn │ IbcClosure │ IbcPacketAckMsg │ IbcBasicResponse │ E11 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_packet_timeout │ ibc_packet_timeout_fn │ IbcClosure │ IbcPacketTimeoutMsg │ IbcBasicResponse │ E12 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_source_callback │ ibc_source_callback_fn │ IbcClosure │ IbcSourceCallbackMsg │ IbcBasicResponse │ E13 │ Q │ +/// ├──────────────────────────┼─────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼───────┼───────┤ +/// │ ibc_destination_callback │ ibc_destination_callback_fn │ IbcClosure │ IbcDestinationCallbackMsg │ IbcBasicResponse │ E14 │ Q │ +/// └──────────────────────────┴─────────────────────────────┴─────────────────────┴───────────────────────────┴────────────────────────┴───────┴───────┘ /// ``` /// The general schema depicting which generic type is used in entry points is shown below. /// Entry point, when called, is provided minimum two arguments: custom query of type **Q** /// (inside `Deps` or `DepsMut`) and input message of type **T1**, **T2**, **T3**, **T4**, -/// **Reply** or **T6**. As a result, entry point returns custom output message of type -/// Response<**C**> or **Binary** and an error of type **E1**, **E2**, **E3**, **E4**, **E5** -/// or **E6**. +/// **Reply**, **T6**, **IbcChannelOpenMsg**, **IbcChannelConnectMsg**, **IbcChannelCloseMsg**, +/// **IbcPacketReceiveMsg**, **IbcPacketAckMsg**, **IbcPacketTimeoutMsg**, **IbcSourceCallbackMsg** +/// or **IbcDestinationCallbackMsg**. As a result, entry point returns custom output message of type +/// Response<**C**>, **Binary**, **IbcChannelOpenResponse**, **IbcReceiveResponse** or **IbcBasicResponse** +/// and an error of type **E1**, **E2**, **E3**, **E4**, **E5**, **E6**, **E7**, **E8**, **E9**, **E10**, +/// **E11**, **E12**, **E13** or **E14**. /// /// ```text /// entry_point(query, .., message_in) -> Result /// ┬ ┬ ┬ ┬ -/// Q >──┘ │ │ └──> E1,E2,E3,E4,E5,E6 -/// T1,T2,T3,T4,Reply,T6 >────┘ └─────────────> C,Binary +/// Q >──┘ │ │ └──> E1,E2,E3,E4,E5,E6,E7, +/// │ │ E8,E9,E10,E11,E12,E13,E14 +/// T1,T2,T3,T4,Reply,T6, >──┘ └──> C,Binary, +/// IbcChannelOpenMsg, IbcChannelOpenResponse, +/// IbcChannelConnectMsg, IbcReceiveResponse, +/// IbcChannelCloseMsg, IbcBasicResponse +/// IbcPacketReceiveMsg, +/// IbcPacketAckMsg, +/// IbcPacketTimeoutMsg, +/// IbcSourceCallbackMsg, +/// IbcDestinationCallbackMsg /// ``` /// Generic type **C** defines a custom message that is specific for the **whole blockchain**. /// Similarly, the generic type **Q** defines a custom query that is also specific @@ -126,6 +192,15 @@ use closures::*; /// [sudo]: Contract::sudo /// [reply]: Contract::reply /// [migrate]: Contract::migrate +/// [ibc_channel_open]: Contract::ibc_channel_open +/// [ibc_channel_connect]: Contract::ibc_channel_connect +/// [ibc_channel_close]: Contract::ibc_channel_close +/// [ibc_packet_receive]: Contract::ibc_packet_receive +/// [ibc_packet_ack]: Contract::ibc_packet_ack +/// [ibc_packet_timeout]: Contract::ibc_packet_timeout +/// [ibc_source_callback]: Contract::ibc_source_callback +/// [ibc_destination_callback]: Contract::ibc_destination_callback +#[rustfmt::skip] pub struct ContractWrapper< T1, T2, @@ -140,20 +215,36 @@ pub struct ContractWrapper< E5 = AnyError, T6 = Empty, E6 = AnyError, + E7 = AnyError, + E8 = AnyError, + E9 = AnyError, + E10 = AnyError, + E11 = AnyError, + E12 = AnyError, + E13 = AnyError, + E14 = AnyError, > where - T1: DeserializeOwned, // Type of message passed to `execute` entry-point. - T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. - T3: DeserializeOwned, // Type of message passed to `query` entry-point. - T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. - T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. - E1: Display + Debug + Send + Sync, // Type of error returned from `execute` entry-point. - E2: Display + Debug + Send + Sync, // Type of error returned from `instantiate` entry-point. - E3: Display + Debug + Send + Sync, // Type of error returned from `query` entry-point. - E4: Display + Debug + Send + Sync, // Type of error returned from `sudo` entry-point. - E5: Display + Debug + Send + Sync, // Type of error returned from `reply` entry-point. - E6: Display + Debug + Send + Sync, // Type of error returned from `migrate` entry-point. - C: CustomMsg, // Type of custom message returned from all entry-points except `query`. - Q: CustomQuery + DeserializeOwned, // Type of custom query in querier passed as deps/deps_mut to all entry-points. + T1: DeserializeOwned, // Type of message passed to `execute` entry-point. + T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. + T3: DeserializeOwned, // Type of message passed to `query` entry-point. + T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. + T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. + E1: Display + Debug + Send + Sync, // Type of error returned from `execute` entry-point. + E2: Display + Debug + Send + Sync, // Type of error returned from `instantiate` entry-point. + E3: Display + Debug + Send + Sync, // Type of error returned from `query` entry-point. + E4: Display + Debug + Send + Sync, // Type of error returned from `sudo` entry-point. + E5: Display + Debug + Send + Sync, // Type of error returned from `reply` entry-point. + E6: Display + Debug + Send + Sync, // Type of error returned from `migrate` entry-point. + E7: Display + Debug + Send + Sync, // Type of error returned from `ibc_channel_open` entry-point. + E8: Display + Debug + Send + Sync, // Type of error returned from `ibc_channel_connect` entry-point. + E9: Display + Debug + Send + Sync, // Type of error returned from `ibc_channel_close` entry-point. + E10: Display + Debug + Send + Sync, // Type of error returned from `ibc_packet_receive` entry-point. + E11: Display + Debug + Send + Sync, // Type of error returned from `ibc_packet_ack` entry-point. + E12: Display + Debug + Send + Sync, // Type of error returned from `ibc_packet_timeout` entry-point. + E13: Display + Debug + Send + Sync, // Type of error returned from `ibc_source_callback_fn` entry-point. + E14: Display + Debug + Send + Sync, // Type of error returned from `ibc_destination_callback_fn` entry-point. + C: CustomMsg, // Type of custom message returned from all entry-points except `query`. + Q: CustomQuery + DeserializeOwned, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { execute_fn: ContractClosure, instantiate_fn: ContractClosure, @@ -161,6 +252,14 @@ pub struct ContractWrapper< sudo_fn: Option>, reply_fn: Option>, migrate_fn: Option>, + ibc_channel_open_fn: Option>, + ibc_channel_connect_fn: Option, E8, Q>>, + ibc_channel_close_fn: Option, E9, Q>>, + ibc_packet_receive_fn: Option, E10, Q>>, + ibc_packet_ack_fn: Option, E11, Q>>, + ibc_packet_timeout_fn: Option, E12, Q>>, + ibc_source_callback_fn: Option, E13, Q>>, + ibc_destination_callback_fn: Option, E14, Q>>, checksum: Option, } @@ -188,6 +287,14 @@ where sudo_fn: None, reply_fn: None, migrate_fn: None, + ibc_channel_open_fn: None, + ibc_channel_connect_fn: None, + ibc_channel_close_fn: None, + ibc_packet_receive_fn: None, + ibc_packet_ack_fn: None, + ibc_packet_timeout_fn: None, + ibc_source_callback_fn: None, + ibc_destination_callback_fn: None, checksum: None, } } @@ -206,27 +313,36 @@ where sudo_fn: None, reply_fn: None, migrate_fn: None, + ibc_channel_open_fn: None, + ibc_channel_connect_fn: None, + ibc_channel_close_fn: None, + ibc_packet_receive_fn: None, + ibc_packet_ack_fn: None, + ibc_packet_timeout_fn: None, + ibc_source_callback_fn: None, + ibc_destination_callback_fn: None, checksum: None, } } } #[allow(clippy::type_complexity)] +#[rustfmt::skip] impl ContractWrapper where - T1: DeserializeOwned, // Type of message passed to `execute` entry-point. - T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. - T3: DeserializeOwned, // Type of message passed to `query` entry-point. - T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. - T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. - E1: Display + Debug + Send + Sync, // Type of error returned from `execute` entry-point. - E2: Display + Debug + Send + Sync, // Type of error returned from `instantiate` entry-point. - E3: Display + Debug + Send + Sync, // Type of error returned from `query` entry-point. - E4: Display + Debug + Send + Sync, // Type of error returned from `sudo` entry-point. - E5: Display + Debug + Send + Sync, // Type of error returned from `reply` entry-point. - E6: Display + Debug + Send + Sync, // Type of error returned from `migrate` entry-point. - C: CustomMsg + 'static, // Type of custom message returned from all entry-points except `query`. + T1: DeserializeOwned, // Type of message passed to `execute` entry-point. + T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. + T3: DeserializeOwned, // Type of message passed to `query` entry-point. + T4: DeserializeOwned, // Type of message passed to `sudo` entry-point. + T6: DeserializeOwned, // Type of message passed to `migrate` entry-point. + E1: Display + Debug + Send + Sync, // Type of error returned from `execute` entry-point. + E2: Display + Debug + Send + Sync, // Type of error returned from `instantiate` entry-point. + E3: Display + Debug + Send + Sync, // Type of error returned from `query` entry-point. + E4: Display + Debug + Send + Sync, // Type of error returned from `sudo` entry-point. + E5: Display + Debug + Send + Sync, // Type of error returned from `reply` entry-point. + E6: Display + Debug + Send + Sync, // Type of error returned from `migrate` entry-point. + C: CustomMsg + 'static, // Type of custom message returned from all entry-points except `query`. Q: CustomQuery + DeserializeOwned + 'static, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { /// Populates [ContractWrapper] with contract's `sudo` entry-point and custom message type. @@ -245,6 +361,14 @@ where sudo_fn: Some(Box::new(sudo_fn)), reply_fn: self.reply_fn, migrate_fn: self.migrate_fn, + ibc_channel_open_fn: self.ibc_channel_open_fn, + ibc_channel_connect_fn: self.ibc_channel_connect_fn, + ibc_channel_close_fn: self.ibc_channel_close_fn, + ibc_packet_receive_fn: self.ibc_packet_receive_fn, + ibc_packet_ack_fn: self.ibc_packet_ack_fn, + ibc_packet_timeout_fn: self.ibc_packet_timeout_fn, + ibc_source_callback_fn: self.ibc_source_callback_fn, + ibc_destination_callback_fn: self.ibc_destination_callback_fn, checksum: None, } } @@ -265,6 +389,14 @@ where sudo_fn: Some(customize_permissioned_fn(sudo_fn)), reply_fn: self.reply_fn, migrate_fn: self.migrate_fn, + ibc_channel_open_fn: self.ibc_channel_open_fn, + ibc_channel_connect_fn: self.ibc_channel_connect_fn, + ibc_channel_close_fn: self.ibc_channel_close_fn, + ibc_packet_receive_fn: self.ibc_packet_receive_fn, + ibc_packet_ack_fn: self.ibc_packet_ack_fn, + ibc_packet_timeout_fn: self.ibc_packet_timeout_fn, + ibc_source_callback_fn: self.ibc_source_callback_fn, + ibc_destination_callback_fn: self.ibc_destination_callback_fn, checksum: None, } } @@ -284,6 +416,14 @@ where sudo_fn: self.sudo_fn, reply_fn: Some(Box::new(reply_fn)), migrate_fn: self.migrate_fn, + ibc_channel_open_fn: self.ibc_channel_open_fn, + ibc_channel_connect_fn: self.ibc_channel_connect_fn, + ibc_channel_close_fn: self.ibc_channel_close_fn, + ibc_packet_receive_fn: self.ibc_packet_receive_fn, + ibc_packet_ack_fn: self.ibc_packet_ack_fn, + ibc_packet_timeout_fn: self.ibc_packet_timeout_fn, + ibc_source_callback_fn: self.ibc_source_callback_fn, + ibc_destination_callback_fn: self.ibc_destination_callback_fn, checksum: None, } } @@ -303,6 +443,14 @@ where sudo_fn: self.sudo_fn, reply_fn: Some(customize_permissioned_fn(reply_fn)), migrate_fn: self.migrate_fn, + ibc_channel_open_fn: self.ibc_channel_open_fn, + ibc_channel_connect_fn: self.ibc_channel_connect_fn, + ibc_channel_close_fn: self.ibc_channel_close_fn, + ibc_packet_receive_fn: self.ibc_packet_receive_fn, + ibc_packet_ack_fn: self.ibc_packet_ack_fn, + ibc_packet_timeout_fn: self.ibc_packet_timeout_fn, + ibc_source_callback_fn: self.ibc_source_callback_fn, + ibc_destination_callback_fn: self.ibc_destination_callback_fn, checksum: None, } } @@ -323,6 +471,14 @@ where sudo_fn: self.sudo_fn, reply_fn: self.reply_fn, migrate_fn: Some(Box::new(migrate_fn)), + ibc_channel_open_fn: self.ibc_channel_open_fn, + ibc_channel_connect_fn: self.ibc_channel_connect_fn, + ibc_channel_close_fn: self.ibc_channel_close_fn, + ibc_packet_receive_fn: self.ibc_packet_receive_fn, + ibc_packet_ack_fn: self.ibc_packet_ack_fn, + ibc_packet_timeout_fn: self.ibc_packet_timeout_fn, + ibc_source_callback_fn: self.ibc_source_callback_fn, + ibc_destination_callback_fn: self.ibc_destination_callback_fn, checksum: None, } } @@ -343,6 +499,14 @@ where sudo_fn: self.sudo_fn, reply_fn: self.reply_fn, migrate_fn: Some(customize_permissioned_fn(migrate_fn)), + ibc_channel_open_fn: self.ibc_channel_open_fn, + ibc_channel_connect_fn: self.ibc_channel_connect_fn, + ibc_channel_close_fn: self.ibc_channel_close_fn, + ibc_packet_receive_fn: self.ibc_packet_receive_fn, + ibc_packet_ack_fn: self.ibc_packet_ack_fn, + ibc_packet_timeout_fn: self.ibc_packet_timeout_fn, + ibc_source_callback_fn: self.ibc_source_callback_fn, + ibc_destination_callback_fn: self.ibc_destination_callback_fn, checksum: None, } } @@ -352,6 +516,69 @@ where self.checksum = Some(checksum); self } + + /// Adding IBC capabilities. + pub fn with_ibc( + self, + channel_open_fn: IbcFn, + channel_connect_fn: IbcFn, E8A, Q>, + channel_close_fn: IbcFn, E9A, Q>, + ibc_packet_receive_fn: IbcFn, E10A, Q>, + ibc_packet_ack_fn: IbcFn, E11A, Q>, + ibc_packet_timeout_fn: IbcFn, E12A, Q>, + ibc_source_callback_fn: IbcClosure, E13A, Q>, + ibc_destination_callback_fn: IbcClosure, E14A, Q>, + ) -> ContractWrapper< + T1, + T2, + T3, + E1, + E2, + E3, + C, + Q, + T4, + E4, + E5, + T6, + E6, + E7A, + E8A, + E9A, + E10A, + E11A, + E12A, + E13A, + E14A, + > + where + E7A: Display + Debug + Send + Sync + 'static, + E8A: Display + Debug + Send + Sync + 'static, + E9A: Display + Debug + Send + Sync + 'static, + E10A: Display + Debug + Send + Sync + 'static, + E11A: Display + Debug + Send + Sync + 'static, + E12A: Display + Debug + Send + Sync + 'static, + E13A: Display + Debug + Send + Sync + 'static, + E14A: Display + Debug + Send + Sync + 'static, + { + ContractWrapper { + execute_fn: self.execute_fn, + instantiate_fn: self.instantiate_fn, + query_fn: self.query_fn, + sudo_fn: self.sudo_fn, + reply_fn: self.reply_fn, + migrate_fn: self.migrate_fn, + ibc_channel_open_fn: Some(Box::new(channel_open_fn)), + ibc_channel_connect_fn: Some(Box::new(channel_connect_fn)), + ibc_channel_close_fn: Some(Box::new(channel_close_fn)), + ibc_packet_receive_fn: Some(Box::new(ibc_packet_receive_fn)), + ibc_packet_ack_fn: Some(Box::new(ibc_packet_ack_fn)), + ibc_packet_timeout_fn: Some(Box::new(ibc_packet_timeout_fn)), + ibc_source_callback_fn: Some(Box::new(ibc_source_callback_fn)), + ibc_destination_callback_fn: Some(Box::new(ibc_destination_callback_fn)), + checksum: None, + } + } } fn customize_contract_fn( @@ -466,8 +693,31 @@ where } } -impl Contract - for ContractWrapper +impl + Contract + for ContractWrapper< + T1, + T2, + T3, + E1, + E2, + E3, + C, + Q, + T4, + E4, + E5, + T6, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + E13, + E14, + > where T1: DeserializeOwned, // Type of message passed to `execute` entry-point. T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. @@ -480,6 +730,14 @@ where E4: Display + Debug + Send + Sync + 'static, // Type of error returned from `sudo` entry-point. E5: Display + Debug + Send + Sync + 'static, // Type of error returned from `reply` entry-point. E6: Display + Debug + Send + Sync + 'static, // Type of error returned from `migrate` entry-point. + E7: Display + Debug + Send + Sync + 'static, // Type of error returned from `channel_open` entry-point. + E8: Display + Debug + Send + Sync + 'static, // Type of error returned from `channel_connect` entry-point. + E9: Display + Debug + Send + Sync + 'static, // Type of error returned from `channel_close` entry-point. + E10: Display + Debug + Send + Sync + 'static, // Type of error returned from `ibc_packet_receive` entry-point. + E11: Display + Debug + Send + Sync + 'static, // Type of error returned from `ibc_packet_ack` entry-point. + E12: Display + Debug + Send + Sync + 'static, // Type of error returned from `ibc_packet_timeout` entry-point. + E13: Display + Debug + Send + Sync + 'static, // Type of error returned from `ibc_source_callback` entry-point. + E14: Display + Debug + Send + Sync + 'static, // Type of error returned from `ibc_destination_callback` entry-point. C: CustomMsg, // Type of custom message returned from all entry-points except `query`. Q: CustomQuery + DeserializeOwned, // Type of custom query in querier passed as deps/deps_mut to all entry-points. { @@ -555,7 +813,141 @@ where } } - /// Returns the provided checksum of the contract's Wasm blob. + /// Calls [ibc_channel_open] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_channel_open]. + /// + /// [ibc_channel_open]: Contract::ibc_channel_open + fn ibc_channel_open( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelOpenMsg, + ) -> AnyResult { + match &self.ibc_channel_open_fn { + Some(channel_open) => channel_open(deps, env, msg).map_err(|err: E7| anyhow!(err)), + None => bail!("ibc_channel_open is not implemented for contract"), + } + } + + /// Calls [ibc_channel_connect] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_channel_connect]. + /// + /// [ibc_channel_connect]: Contract::ibc_channel_connect + fn ibc_channel_connect( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelConnectMsg, + ) -> AnyResult> { + match &self.ibc_channel_connect_fn { + Some(channel_connect) => { + channel_connect(deps, env, msg).map_err(|err: E8| anyhow!(err)) + } + None => bail!("ibc_channel_connect is not implemented for contract"), + } + } + + /// Calls [ibc_channel_close] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_channel_close]. + /// + /// [ibc_channel_close]: Contract::ibc_channel_close + fn ibc_channel_close( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelCloseMsg, + ) -> AnyResult> { + match &self.ibc_channel_close_fn { + Some(channel_close) => channel_close(deps, env, msg).map_err(|err: E9| anyhow!(err)), + None => bail!("ibc_channel_close is not implemented for contract"), + } + } + + /// Calls [ibc_packet_receive] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_packet_receive]. + /// + /// [ibc_packet_receive]: Contract::ibc_packet_receive + fn ibc_packet_receive( + &self, + deps: DepsMut, + env: Env, + msg: IbcPacketReceiveMsg, + ) -> AnyResult> { + match &self.ibc_packet_receive_fn { + Some(packet_receive) => packet_receive(deps, env, msg).map_err(|err: E10| anyhow!(err)), + None => bail!("ibc_packet_receive is not implemented for contract"), + } + } + + /// Calls [ibc_packet_acknowledge] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_packet_acknowledge]. + /// + /// [ibc_packet_acknowledge]: Contract::ibc_packet_ack + fn ibc_packet_ack( + &self, + deps: DepsMut, + env: Env, + msg: IbcPacketAckMsg, + ) -> AnyResult> { + match &self.ibc_packet_ack_fn { + Some(packet_ack) => packet_ack(deps, env, msg).map_err(|err: E11| anyhow!(err)), + None => bail!("ibc_packet_acknowledge is not implemented for contract"), + } + } + + /// Calls [ibc_packet_timeout] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_packet_timeout]. + /// + /// [ibc_packet_timeout]: Contract::ibc_packet_timeout + fn ibc_packet_timeout( + &self, + deps: DepsMut, + env: Env, + msg: IbcPacketTimeoutMsg, + ) -> AnyResult> { + match &self.ibc_packet_timeout_fn { + Some(packet_timeout) => packet_timeout(deps, env, msg).map_err(|err: E12| anyhow!(err)), + None => bail!("ibc_packet_timeout is not implemented for contract"), + } + } + + /// Calls [ibc_source_callback] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_source_callback]. + /// + /// [ibc_source_callback]: Contract::ibc_source_callback + fn ibc_source_callback( + &self, + deps: DepsMut, + env: Env, + msg: IbcSourceCallbackMsg, + ) -> AnyResult> { + match &self.ibc_source_callback_fn { + Some(source_callback) => { + source_callback(deps, env, msg).map_err(|err: E13| anyhow!(err)) + } + None => bail!("ibc_source_callback is not implemented for contract"), + } + } + + /// Calls [ibc_destination_callback] on wrapped [Contract] trait implementor. + /// Returns an error when the contract does not implement [ibc_destination_callback]. + /// + /// [ibc_destination_callback]: Contract::ibc_destination_callback + fn ibc_destination_callback( + &self, + deps: DepsMut, + env: Env, + msg: IbcDestinationCallbackMsg, + ) -> AnyResult> { + match &self.ibc_destination_callback_fn { + Some(destination_callback) => { + destination_callback(deps, env, msg).map_err(|err: E14| anyhow!(err)) + } + None => bail!("ibc_destination_callback is not implemented for contract"), + } + } + + /// Returns the provided checksum of the contract's `wasm` blob. fn checksum(&self) -> Option { self.checksum } diff --git a/src/ibc.rs b/src/ibc.rs index 32236c34..bdbf321e 100644 --- a/src/ibc.rs +++ b/src/ibc.rs @@ -1,15 +1,55 @@ +//! Ibc Module adds IBC support to cw-multi-test +#![allow(missing_docs)] +use cosmwasm_std::{ + IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcMsg, IbcPacketAckMsg, + IbcPacketReceiveMsg, IbcPacketTimeoutMsg, +}; + use crate::{AcceptingModule, FailingModule, Module}; -use cosmwasm_std::{Empty, IbcMsg, IbcQuery}; -/// This trait implements the interface for IBC functionalities. -pub trait Ibc: Module {} +pub mod events; +pub mod relayer; +mod simple_ibc; +mod state; +pub mod types; +pub use self::types::IbcPacketRelayingMsg; +use self::types::MockIbcQuery; +pub use simple_ibc::IbcSimpleModule; -/// Implementation of the always accepting IBC module. -pub type IbcAcceptingModule = AcceptingModule; +/// This is added for modules to implement actions upon ibc actions. +/// This kind of execution flow is copied from the WASM way of doing things and is not 100% completely compatible with the IBC standard +/// Those messages should only be called by the Ibc module. +/// For additional Modules, the packet endpoints should be implemented +/// The Channel endpoints are usually not implemented besides storing the channel ids +#[cosmwasm_schema::cw_serde] +pub enum IbcModuleMsg { + /// Open an IBC Channel (2 first steps) + ChannelOpen(IbcChannelOpenMsg), + /// Connect an IBC Channel (2 last steps) + ChannelConnect(IbcChannelConnectMsg), + /// Close an IBC Channel + ChannelClose(IbcChannelCloseMsg), -impl Ibc for IbcAcceptingModule {} + /// Receive an IBC Packet + PacketReceive(IbcPacketReceiveMsg), + /// Receive an IBC Acknowledgement for a packet + PacketAcknowledgement(IbcPacketAckMsg), + /// Receive an IBC Timeout for a packet + PacketTimeout(IbcPacketTimeoutMsg), +} +///Manages Inter-Blockchain Communication (IBC) functionalities. +///This trait is critical for testing contracts that involve cross-chain interactions, +///reflecting the interconnected nature of the Cosmos ecosystem. +pub trait Ibc: Module {} +/// Ideal for testing contracts that involve IBC, this module is designed to successfully +/// handle cross-chain messages. It's key for ensuring that your contract can smoothly interact +/// with other blockchains in the Cosmos network. +pub type IbcAcceptingModule = AcceptingModule; -/// implementation of the always failing IBC module. -pub type IbcFailingModule = FailingModule; +impl Ibc for IbcAcceptingModule {} +/// Use this to test how your contract deals with problematic IBC scenarios. +/// It's a module that deliberately fails in handling IBC messages, allowing you +/// to check how your contract behaves in less-than-ideal cross-chain communication situations. +pub type IbcFailingModule = FailingModule; impl Ibc for IbcFailingModule {} diff --git a/src/ibc/events.rs b/src/ibc/events.rs new file mode 100644 index 00000000..a08f439c --- /dev/null +++ b/src/ibc/events.rs @@ -0,0 +1,9 @@ +pub const SEND_PACKET_EVENT: &str = "send_packet"; +pub const RECEIVE_PACKET_EVENT: &str = "recv_packet"; +pub const WRITE_ACK_EVENT: &str = "write_acknowledgement"; +pub const ACK_PACKET_EVENT: &str = "acknowledge_packet"; +pub const TIMEOUT_RECEIVE_PACKET_EVENT: &str = "timeout_received_packet"; +pub const TIMEOUT_PACKET_EVENT: &str = "timeout_packet"; + +pub const CHANNEL_CLOSE_INIT_EVENT: &str = "channel_close_init"; +pub const CHANNEL_CLOSE_CONFIRM_EVENT: &str = "channel_close_confirm"; diff --git a/src/ibc/relayer/channel.rs b/src/ibc/relayer/channel.rs new file mode 100644 index 00000000..ff0caa2e --- /dev/null +++ b/src/ibc/relayer/channel.rs @@ -0,0 +1,232 @@ +use crate::featured::staking::{Distribution, Staking}; +use crate::{ + ibc::{ + types::{Connection, MockIbcQuery}, + IbcPacketRelayingMsg, + }, + App, AppResponse, Bank, Gov, Ibc, Module, Wasm, +}; +use anyhow::Result as AnyResult; +use cosmwasm_std::{from_json, Api, CustomMsg, CustomQuery, IbcEndpoint, IbcOrder, Storage}; +use serde::de::DeserializeOwned; + +use super::get_event_attr_value; + +#[derive(Debug)] +pub struct ChannelCreationResult { + pub init: AppResponse, + pub r#try: AppResponse, + pub ack: AppResponse, + pub confirm: AppResponse, + pub src_channel: String, + pub dst_channel: String, +} + +pub fn create_connection< + BankT1, + ApiT1, + StorageT1, + CustomT1, + WasmT1, + StakingT1, + DistrT1, + IbcT1, + GovT1, + BankT2, + ApiT2, + StorageT2, + CustomT2, + WasmT2, + StakingT2, + DistrT2, + IbcT2, + GovT2, +>( + src_app: &mut App, + dst_app: &mut App, +) -> AnyResult<(String, String)> +where + CustomT1::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT1::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT1: Wasm, + BankT1: Bank, + ApiT1: Api, + StorageT1: Storage, + CustomT1: Module, + StakingT1: Staking, + DistrT1: Distribution, + IbcT1: Ibc, + GovT1: Gov, + + CustomT2::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT2::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT2: Wasm, + BankT2: Bank, + ApiT2: Api, + StorageT2: Storage, + CustomT2: Module, + StakingT2: Staking, + DistrT2: Distribution, + IbcT2: Ibc, + GovT2: Gov, +{ + let src_connection_msg = IbcPacketRelayingMsg::CreateConnection { + remote_chain_id: dst_app.block_info().chain_id, + connection_id: None, + counterparty_connection_id: None, + }; + let src_create_response = src_app.sudo(crate::SudoMsg::Ibc(src_connection_msg))?; + let src_connection = + get_event_attr_value(&src_create_response, "connection_open", "connection_id")?; + + let dst_connection_msg = IbcPacketRelayingMsg::CreateConnection { + remote_chain_id: src_app.block_info().chain_id, + connection_id: None, + counterparty_connection_id: Some(src_connection.clone()), + }; + let dst_create_response = dst_app.sudo(crate::SudoMsg::Ibc(dst_connection_msg))?; + let dst_connection = + get_event_attr_value(&dst_create_response, "connection_open", "connection_id")?; + + let src_connection_msg = IbcPacketRelayingMsg::CreateConnection { + remote_chain_id: dst_app.block_info().chain_id, + connection_id: Some(src_connection.clone()), + counterparty_connection_id: Some(dst_connection.clone()), + }; + src_app.sudo(crate::SudoMsg::Ibc(src_connection_msg))?; + + Ok((src_connection, dst_connection)) +} +pub fn create_channel< + BankT1, + ApiT1, + StorageT1, + CustomT1, + WasmT1, + StakingT1, + DistrT1, + IbcT1, + GovT1, + BankT2, + ApiT2, + StorageT2, + CustomT2, + WasmT2, + StakingT2, + DistrT2, + IbcT2, + GovT2, +>( + src_app: &mut App, + dst_app: &mut App, + src_connection_id: String, + src_port: String, + dst_port: String, + version: String, + order: IbcOrder, +) -> AnyResult +where + CustomT1::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT1::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT1: Wasm, + BankT1: Bank, + ApiT1: Api, + StorageT1: Storage, + CustomT1: Module, + StakingT1: Staking, + DistrT1: Distribution, + IbcT1: Ibc, + GovT1: Gov, + + CustomT2::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT2::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT2: Wasm, + BankT2: Bank, + ApiT2: Api, + StorageT2: Storage, + CustomT2: Module, + StakingT2: Staking, + DistrT2: Distribution, + IbcT2: Ibc, + GovT2: Gov, +{ + let ibc_init_msg = IbcPacketRelayingMsg::OpenChannel { + local_connection_id: src_connection_id.clone(), + local_port: src_port.clone(), + version: version.clone(), + order: order.clone(), + counterparty_version: None, + counterparty_endpoint: IbcEndpoint { + port_id: dst_port.clone(), + channel_id: "".to_string(), + }, + }; + + let init_response = src_app.sudo(crate::SudoMsg::Ibc(ibc_init_msg))?; + + println!("@DDD Channel init {:?}", init_response); + + // Get the returned version + let new_version = get_event_attr_value(&init_response, "channel_open_init", "version")?; + // Get the returned channel id + let src_channel = get_event_attr_value(&init_response, "channel_open_init", "channel_id")?; + + let counterparty: Connection = from_json(src_app.ibc_query(MockIbcQuery::ConnectedChain { + connection_id: src_connection_id, + })?)?; + + let ibc_try_msg = IbcPacketRelayingMsg::OpenChannel { + local_connection_id: counterparty.counterparty_connection_id.unwrap(), + local_port: dst_port.clone(), + version, + order, + counterparty_version: Some(new_version), + counterparty_endpoint: IbcEndpoint { + port_id: src_port.clone(), + channel_id: src_channel.clone(), + }, + }; + + let try_response: crate::AppResponse = dst_app.sudo(crate::SudoMsg::Ibc(ibc_try_msg))?; + println!("@DDD Channel try {:?}", try_response); + + // Get the returned version + let new_version = get_event_attr_value(&try_response, "channel_open_try", "version")?; + // Get the returned channel id + let dst_channel = get_event_attr_value(&try_response, "channel_open_try", "channel_id")?; + + let ibc_ack_msg = IbcPacketRelayingMsg::ConnectChannel { + port_id: src_port.clone(), + channel_id: src_channel.clone(), + counterparty_version: Some(new_version.clone()), + counterparty_endpoint: IbcEndpoint { + port_id: dst_port.clone(), + channel_id: dst_channel.clone(), + }, + }; + + let ack_response: crate::AppResponse = src_app.sudo(crate::SudoMsg::Ibc(ibc_ack_msg))?; + println!("@DDD Channel ack {:?}", ack_response); + + let ibc_ack_msg = IbcPacketRelayingMsg::ConnectChannel { + port_id: dst_port, + channel_id: dst_channel.clone(), + counterparty_version: Some(new_version), + counterparty_endpoint: IbcEndpoint { + port_id: src_port, + channel_id: src_channel.clone(), + }, + }; + + let confirm_response: crate::AppResponse = dst_app.sudo(crate::SudoMsg::Ibc(ibc_ack_msg))?; + println!("@DDD Channel confirm {:?}", confirm_response); + + Ok(ChannelCreationResult { + init: init_response, + r#try: try_response, + ack: ack_response, + confirm: confirm_response, + src_channel, + dst_channel, + }) +} diff --git a/src/ibc/relayer/mod.rs b/src/ibc/relayer/mod.rs new file mode 100644 index 00000000..8c9f03ff --- /dev/null +++ b/src/ibc/relayer/mod.rs @@ -0,0 +1,56 @@ +use cosmwasm_std::{StdError, StdResult}; + +use crate::AppResponse; + +mod channel; +mod packet; + +pub use channel::{create_channel, create_connection, ChannelCreationResult}; +pub use packet::{relay_packet, relay_packets_in_tx, RelayPacketResult, RelayingResult}; + +pub fn get_event_attr_value( + response: &AppResponse, + event_type: &str, + attr_key: &str, +) -> StdResult { + for event in &response.events { + if event.ty == event_type { + for attr in &event.attributes { + if attr.key == attr_key { + return Ok(attr.value.clone()); + } + } + } + } + + Err(StdError::generic_err(format!( + "event of type {event_type} does not have a value at key {attr_key}" + ))) +} + +pub fn has_event(response: &AppResponse, event_type: &str) -> bool { + for event in &response.events { + if event.ty == event_type { + return true; + } + } + false +} + +pub fn get_all_event_attr_value( + response: &AppResponse, + event: &str, + attribute: &str, +) -> Vec { + response + .events + .iter() + .filter(|e| e.ty.eq(event)) + .flat_map(|e| { + e.attributes + .iter() + .filter(|a| a.key.eq(attribute)) + .map(|a| a.value.clone()) + }) + .collect() +} diff --git a/src/ibc/relayer/packet.rs b/src/ibc/relayer/packet.rs new file mode 100644 index 00000000..34644c41 --- /dev/null +++ b/src/ibc/relayer/packet.rs @@ -0,0 +1,217 @@ +use crate::featured::staking::{Distribution, Staking}; +use crate::{ + ibc::{ + events::{ + CHANNEL_CLOSE_INIT_EVENT, SEND_PACKET_EVENT, TIMEOUT_RECEIVE_PACKET_EVENT, + WRITE_ACK_EVENT, + }, + types::{IbcPacketData, MockIbcQuery}, + IbcPacketRelayingMsg, + }, + App, AppResponse, Bank, Gov, Ibc, Module, SudoMsg, Wasm, +}; +use anyhow::Result as AnyResult; +use cosmwasm_std::{from_hex, from_json, Api, Binary, CustomMsg, CustomQuery, Storage}; +use serde::de::DeserializeOwned; + +use super::{get_all_event_attr_value, get_event_attr_value, has_event}; + +#[derive(Debug, Clone)] +pub struct RelayPacketResult { + pub receive_tx: AppResponse, + pub result: RelayingResult, +} + +#[derive(Debug, Clone)] +pub enum RelayingResult { + Timeout { + timeout_tx: AppResponse, + close_channel_confirm: Option, + }, + Acknowledgement { + tx: AppResponse, + ack: Binary, + }, +} + +pub fn relay_packets_in_tx< + BankT1, + ApiT1, + StorageT1, + CustomT1, + WasmT1, + StakingT1, + DistrT1, + IbcT1, + GovT1, + BankT2, + ApiT2, + StorageT2, + CustomT2, + WasmT2, + StakingT2, + DistrT2, + IbcT2, + GovT2, +>( + app1: &mut App, + app2: &mut App, + app1_tx_response: AppResponse, +) -> AnyResult> +where + CustomT1::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT1::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT1: Wasm, + BankT1: Bank, + ApiT1: Api, + StorageT1: Storage, + CustomT1: Module, + StakingT1: Staking, + DistrT1: Distribution, + IbcT1: Ibc, + GovT1: Gov, + + CustomT2::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT2::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT2: Wasm, + BankT2: Bank, + ApiT2: Api, + StorageT2: Storage, + CustomT2: Module, + StakingT2: Staking, + DistrT2: Distribution, + IbcT2: Ibc, + GovT2: Gov, +{ + // Find all packets and their data + let packets = get_all_event_attr_value(&app1_tx_response, SEND_PACKET_EVENT, "packet_sequence"); + let channels = + get_all_event_attr_value(&app1_tx_response, SEND_PACKET_EVENT, "packet_src_channel"); + let ports = get_all_event_attr_value(&app1_tx_response, SEND_PACKET_EVENT, "packet_src_port"); + + // For all packets, query the packetdata and relay them + + let mut packet_forwarding = vec![]; + + for i in 0..packets.len() { + let relay_response = relay_packet( + app1, + app2, + ports[i].clone(), + channels[i].clone(), + packets[i].parse()?, + )?; + + packet_forwarding.push(relay_response); + } + + Ok(packet_forwarding) +} + +/// Relays (rcv + ack) any pending packet between 2 chains +pub fn relay_packet< + BankT1, + ApiT1, + StorageT1, + CustomT1, + WasmT1, + StakingT1, + DistrT1, + IbcT1, + GovT1, + BankT2, + ApiT2, + StorageT2, + CustomT2, + WasmT2, + StakingT2, + DistrT2, + IbcT2, + GovT2, +>( + app1: &mut App, + app2: &mut App, + src_port_id: String, + src_channel_id: String, + sequence: u64, +) -> AnyResult +where + CustomT1::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT1::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT1: Wasm, + BankT1: Bank, + ApiT1: Api, + StorageT1: Storage, + CustomT1: Module, + StakingT1: Staking, + DistrT1: Distribution, + IbcT1: Ibc, + GovT1: Gov, + + CustomT2::ExecT: CustomMsg + DeserializeOwned + 'static, + CustomT2::QueryT: CustomQuery + DeserializeOwned + 'static, + WasmT2: Wasm, + BankT2: Bank, + ApiT2: Api, + StorageT2: Storage, + CustomT2: Module, + StakingT2: Staking, + DistrT2: Distribution, + IbcT2: Ibc, + GovT2: Gov, +{ + let packet: IbcPacketData = from_json(app1.ibc_query(MockIbcQuery::SendPacket { + channel_id: src_channel_id.clone(), + port_id: src_port_id.clone(), + sequence, + })?)?; + + // First we start by sending the packet on chain 2 + let receive_response = app2.sudo(SudoMsg::Ibc(IbcPacketRelayingMsg::Receive { + packet: packet.clone(), + }))?; + + // We start by verifying that we have an acknowledgment and not a timeout + if has_event(&receive_response, TIMEOUT_RECEIVE_PACKET_EVENT) { + // If there was a timeout, we timeout the packet on the sending chain + // TODO: We don't handle the chain closure in here for now in case of ordered channels + let timeout_response = app1.sudo(SudoMsg::Ibc(IbcPacketRelayingMsg::Timeout { packet }))?; + + // We close the channel on the sending chain if it's request by the receiving chain + let close_confirm_response = if has_event(&receive_response, CHANNEL_CLOSE_INIT_EVENT) { + Some(app1.sudo(SudoMsg::Ibc(IbcPacketRelayingMsg::CloseChannel { + port_id: src_port_id, + channel_id: src_channel_id, + init: false, + }))?) + } else { + None + }; + + return Ok(RelayPacketResult { + receive_tx: receive_response, + result: RelayingResult::Timeout { + timeout_tx: timeout_response, + close_channel_confirm: close_confirm_response, + }, + }); + } + + // Then we query the packet ack to deliver the response on chain 1 + let hex_ack = get_event_attr_value(&receive_response, WRITE_ACK_EVENT, "packet_ack_hex")?; + + let ack = Binary::from(from_hex(hex_ack)?); + + let ack_response = app1.sudo(SudoMsg::Ibc(IbcPacketRelayingMsg::Acknowledge { + packet, + ack: ack.clone(), + }))?; + + Ok(RelayPacketResult { + receive_tx: receive_response, + result: RelayingResult::Acknowledgement { + tx: ack_response, + ack, + }, + }) +} diff --git a/src/ibc/simple_ibc.rs b/src/ibc/simple_ibc.rs new file mode 100644 index 00000000..555bb645 --- /dev/null +++ b/src/ibc/simple_ibc.rs @@ -0,0 +1,1250 @@ +use crate::{ + app::IbcRouterMsg, + bank::{optional_unwrap_ibc_denom, IBC_LOCK_MODULE_ADDRESS}, + ibc::types::Connection, + prefixed_storage::{prefixed, prefixed_read}, + transactions::transactional, + AppResponse, Ibc, Module, SudoMsg, +}; +use anyhow::Result as AnyResult; +use anyhow::{anyhow, bail}; +use cosmwasm_std::{ + ensure_eq, to_hex, to_json_binary, Addr, BankMsg, Binary, ChannelResponse, Coin, CustomMsg, + Event, IbcAcknowledgement, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, + IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcQuery, IbcTimeout, IbcTimeoutBlock, + ListChannelsResponse, Order, Storage, +}; +use cw20_ics20::ibc::Ics20Packet; + +#[derive(Default)] +pub struct IbcSimpleModule; + +use super::{ + events::{ + ACK_PACKET_EVENT, CHANNEL_CLOSE_CONFIRM_EVENT, CHANNEL_CLOSE_INIT_EVENT, + RECEIVE_PACKET_EVENT, SEND_PACKET_EVENT, TIMEOUT_PACKET_EVENT, + TIMEOUT_RECEIVE_PACKET_EVENT, WRITE_ACK_EVENT, + }, + state::{ + ibc_connections, load_port_info, ACK_PACKET_MAP, CHANNEL_HANDSHAKE_INFO, CHANNEL_INFO, + NAMESPACE_IBC, PORT_INFO, RECEIVE_PACKET_MAP, SEND_PACKET_MAP, TIMEOUT_PACKET_MAP, + }, + types::{ + ChannelHandshakeInfo, ChannelHandshakeState, ChannelInfo, IbcPacketAck, IbcPacketData, + IbcPacketReceived, IbcPacketRelayingMsg, IbcResponse, MockIbcPort, MockIbcQuery, + }, +}; + +pub const RELAYER_ADDR: &str = "relayer"; + +fn packet_from_data_and_channel(packet: &IbcPacketData, channel_info: &ChannelInfo) -> IbcPacket { + IbcPacket::new( + packet.data.clone(), + IbcEndpoint { + port_id: packet.src_port_id.clone(), + channel_id: packet.src_channel_id.clone(), + }, + IbcEndpoint { + port_id: channel_info.info.counterparty_endpoint.port_id.to_string(), + channel_id: packet.dst_channel_id.clone(), + }, + packet.sequence, + packet.timeout.clone(), + ) +} + +impl IbcSimpleModule { + fn create_connection( + &self, + storage: &mut dyn Storage, + remote_chain_id: String, + connection_id: Option, + counterparty_connection_id: Option, + ) -> AnyResult { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // First we get the data (from storage or create it) + let (connection_id, mut data) = if let Some(connection_id) = connection_id { + ( + connection_id.clone(), + ibc_connections().load(&ibc_storage, &connection_id)?, + ) + } else { + let connection_count = ibc_connections() + .range(&ibc_storage, None, None, Order::Ascending) + .count(); + let connection_id = format!("connection-{}", connection_count); + ( + connection_id, + Connection { + counterparty_connection_id: None, + counterparty_chain_id: remote_chain_id.clone(), + }, + ) + }; + + // We make sure we're not doing weird things + ensure_eq!( + remote_chain_id, + data.counterparty_chain_id, + anyhow!( + "Wrong chain id already registered with this connection {}, {}!={}", + connection_id.clone(), + data.counterparty_chain_id, + remote_chain_id + ) + ); + + // We eventually save the counterparty_chain_id + if let Some(counterparty_connection_id) = counterparty_connection_id { + data.counterparty_connection_id = Some(counterparty_connection_id); + } + + // The tx will return the connection id + ibc_connections().save(&mut ibc_storage, &connection_id, &data)?; + + let event = Event::new("connection_open").add_attribute("connection_id", &connection_id); + + Ok(AppResponse { + data: None, + events: vec![event], + }) + } + + #[allow(clippy::too_many_arguments)] + fn open_channel( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + local_connection_id: String, + local_port: String, + version: String, + order: IbcOrder, + + counterparty_endpoint: IbcEndpoint, + counterparty_version: Option, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // We verify the connection_id exists locally + if !ibc_connections().has(&ibc_storage, &local_connection_id) { + bail!( + "connection {local_connection_id} doesn't exist on chain {}", + block.chain_id + ) + }; + + // Here we just verify that the port exists locally. + let port: MockIbcPort = local_port.parse()?; + + // We create a new channel id + let mut port_info = load_port_info(&ibc_storage, local_port.clone())?; + + let channel_id = format!("channel-{}", port_info.next_channel_id); + port_info.next_channel_id += 1; + + PORT_INFO.save(&mut ibc_storage, local_port.clone(), &port_info)?; + + let local_endpoint = IbcEndpoint { + port_id: local_port.clone(), + channel_id: channel_id.clone(), + }; + + let mut handshake_object = ChannelHandshakeInfo { + local_endpoint: local_endpoint.clone(), + remote_endpoint: counterparty_endpoint.clone(), + state: ChannelHandshakeState::Init, + version: version.clone(), + port: port.clone(), + order: order.clone(), + connection_id: local_connection_id.clone(), + }; + + let channel = IbcChannel::new( + local_endpoint.clone(), + counterparty_endpoint.clone(), + order, + version.clone(), + local_connection_id.clone(), + ); + + let (open_request, mut ibc_event) = if let Some(counterparty_version) = counterparty_version + { + handshake_object.state = ChannelHandshakeState::Try; + + let event = Event::new("channel_open_try"); + + ( + IbcChannelOpenMsg::OpenTry { + channel, + counterparty_version, + }, + event, + ) + } else { + let event = Event::new("channel_open_init"); + + (IbcChannelOpenMsg::OpenInit { channel }, event) + }; + + ibc_event = ibc_event + .add_attribute("port_id", local_endpoint.port_id) + .add_attribute("channel_id", local_endpoint.channel_id) + .add_attribute("counterparty_port_id", counterparty_endpoint.port_id) + .add_attribute("counterparty_channel_id", "") + .add_attribute("connection_id", local_connection_id); + + // First we send an ibc message on the wasm module in cache + let res = transactional(storage, |write_cache, _| { + router.ibc( + api, + write_cache, + block, + IbcRouterMsg { + module: port.into(), + msg: super::IbcModuleMsg::ChannelOpen(open_request), + }, + ) + })?; + + // Then, we store the acknowledgement and collect events + match res { + IbcResponse::Open(r) => { + // The channel version may be changed here + let version = r.map(|r| r.version).unwrap_or(version); + handshake_object.version.clone_from(&version); + ibc_event = ibc_event.add_attribute("version", version); + // This is repeated to avoid multiple mutable borrows + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + // We save the channel handshake status + CHANNEL_HANDSHAKE_INFO.save( + &mut ibc_storage, + (local_port, channel_id), + &handshake_object, + )?; + } + _ => panic!("Only an open response was expected when receiving a packet"), + }; + + let events = vec![ibc_event]; + + Ok(AppResponse { data: None, events }) + } + + fn connect_channel( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + port_id: String, + channel_id: String, + + counterparty_endpoint: IbcEndpoint, + counterparty_version: Option, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // We load the channel handshake info (second step) + let mut channel_handshake = + CHANNEL_HANDSHAKE_INFO.load(&ibc_storage, (port_id.clone(), channel_id.clone()))?; + + // We update the remote endpoint + channel_handshake.remote_endpoint = counterparty_endpoint; + + let channel = IbcChannel::new( + channel_handshake.local_endpoint.clone(), + channel_handshake.remote_endpoint.clone(), + channel_handshake.order.clone(), + channel_handshake.version.clone(), + channel_handshake.connection_id.to_string(), + ); + + let (connect_request, mut ibc_event) = + if channel_handshake.state == ChannelHandshakeState::Try { + channel_handshake.state = ChannelHandshakeState::Confirm; + + let event = Event::new("channel_open_confirm"); + + (IbcChannelConnectMsg::OpenConfirm { channel }, event) + } else if channel_handshake.state == ChannelHandshakeState::Init { + // If we were in the init state, now we need to ack the channel creation + + channel_handshake.state = ChannelHandshakeState::Ack; + + let event = Event::new("channel_open_ack"); + + ( + IbcChannelConnectMsg::OpenAck { + channel, + counterparty_version: counterparty_version.clone().unwrap(), // This should be set in case of an ack + }, + event, + ) + } else { + bail!("This is unreachable, configuration error"); + }; + + ibc_event = ibc_event + .add_attribute("port_id", channel_handshake.local_endpoint.port_id.clone()) + .add_attribute( + "channel_id", + channel_handshake.local_endpoint.channel_id.clone(), + ) + .add_attribute( + "counterparty_port_id", + channel_handshake.remote_endpoint.port_id.clone(), + ) + .add_attribute( + "counterparty_channel_id", + channel_handshake.remote_endpoint.channel_id.clone(), + ) + .add_attribute("connection_id", channel_handshake.connection_id.clone()); + + // Remove handshake, add channel + CHANNEL_HANDSHAKE_INFO.remove(&mut ibc_storage, (port_id.clone(), channel_id.clone())); + CHANNEL_INFO.save( + &mut ibc_storage, + (port_id.clone(), channel_id.clone()), + &ChannelInfo { + next_packet_id: 1, + last_packet_relayed: 1, + info: IbcChannel::new( + IbcEndpoint { + port_id, + channel_id, + }, + IbcEndpoint { + port_id: channel_handshake.remote_endpoint.port_id.clone(), + channel_id: channel_handshake.remote_endpoint.channel_id.clone(), + }, + channel_handshake.order, + counterparty_version.unwrap(), + channel_handshake.connection_id, + ), + open: true, + }, + )?; + + // First we send an ibc message on the wasm module in cache + let res = transactional(storage, |write_cache, _| { + router.ibc( + api, + write_cache, + block, + IbcRouterMsg { + module: channel_handshake.port.into(), + msg: super::IbcModuleMsg::ChannelConnect(connect_request), + }, + ) + })?; + + // Then, we store the acknowledgement and collect events + let mut events = match res { + IbcResponse::Basic(r) => r.events, + _ => panic!("Only an Basic response was expected when receiving a packet"), + }; + + events.push(ibc_event); + + Ok(AppResponse { data: None, events }) + } + + /// Closes an already fully established channel + /// This doesn't handle closing half opened channels + fn close_channel( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + port_id: String, + channel_id: String, + init: bool, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // We pass the channel status to closed + let channel_info = CHANNEL_INFO.update( + &mut ibc_storage, + (port_id.clone(), channel_id.clone()), + |channel| match channel { + None => bail!( + "No channel exists with this port and channel id : {}:{}", + port_id, + channel_id + ), + Some(mut channel) => { + channel.open = false; + Ok(channel) + } + }, + )?; + + let (close_request, mut ibc_event) = if init { + ( + IbcChannelCloseMsg::CloseInit { + channel: channel_info.info.clone(), + }, + Event::new(CHANNEL_CLOSE_INIT_EVENT), + ) + } else { + ( + IbcChannelCloseMsg::CloseConfirm { + channel: channel_info.info.clone(), + }, + Event::new(CHANNEL_CLOSE_CONFIRM_EVENT), + ) + }; + + ibc_event = ibc_event + .add_attribute("port_id", port_id.clone()) + .add_attribute("channel_id", channel_id.clone()) + .add_attribute( + "counterparty_port_id", + channel_info.info.counterparty_endpoint.port_id.clone(), + ) + .add_attribute( + "counterparty_channel_id", + channel_info.info.counterparty_endpoint.channel_id.clone(), + ) + .add_attribute("connection_id", channel_info.info.connection_id); + + // Then we send an ibc message on the corresponding module in cache + let res = transactional(storage, |write_cache, _| { + router.ibc( + api, + write_cache, + block, + IbcRouterMsg { + module: port_id.parse::()?.into(), + msg: super::IbcModuleMsg::ChannelClose(close_request), + }, + ) + })?; + + // Then, we store the close events + let mut events = match res { + IbcResponse::Basic(r) => r.events, + _ => panic!("Only an basic response was expected when closing a channel"), + }; + + events.push(ibc_event); + + Ok(AppResponse { data: None, events }) + } + + fn send_packet( + &self, + storage: &mut dyn Storage, + port_id: String, + channel_id: String, + data: Binary, + timeout: IbcTimeout, + ) -> AnyResult { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // On this storage, we need to get the id of the transfer packet + // Get the last packet index + + let mut channel_info = + CHANNEL_INFO.load(&ibc_storage, (port_id.clone(), channel_id.clone()))?; + let packet = IbcPacketData { + ack: None, + src_channel_id: channel_id.clone(), + src_port_id: channel_info.info.endpoint.port_id.to_string(), + dst_channel_id: channel_info.info.counterparty_endpoint.channel_id.clone(), + dst_port_id: channel_info.info.counterparty_endpoint.port_id.clone(), + sequence: channel_info.next_packet_id, + data, + timeout, + }; + // Saving this packet for relaying purposes + SEND_PACKET_MAP.save( + &mut ibc_storage, + ( + port_id.clone(), + channel_id.clone(), + channel_info.next_packet_id, + ), + &packet, + )?; + + // Incrementing the packet sequence + channel_info.next_packet_id += 1; + CHANNEL_INFO.save(&mut ibc_storage, (port_id, channel_id), &channel_info)?; + + // We add custom packet sending events + let timeout_height = packet.timeout.block().unwrap_or(IbcTimeoutBlock { + revision: 0, + height: 0, + }); + let timeout_timestamp = packet.timeout.timestamp().map(|t| t.nanos()).unwrap_or(0); + + let send_event = Event::new(SEND_PACKET_EVENT) + .add_attribute( + "packet_data", + String::from_utf8_lossy(packet.data.as_slice()), + ) + .add_attribute("packet_data_hex", to_hex(packet.data.as_slice())) + .add_attribute( + "packet_timeout_height", + format!("{}-{}", timeout_height.revision, timeout_height.height), + ) + .add_attribute("packet_timeout_timestamp", timeout_timestamp.to_string()) + .add_attribute("packet_sequence", packet.sequence.to_string()) + .add_attribute("packet_src_port", packet.src_port_id.clone()) + .add_attribute("packet_src_channel", packet.src_channel_id.clone()) + .add_attribute("packet_dst_port", packet.dst_port_id.clone()) + .add_attribute("packet_dst_channel", packet.dst_channel_id.clone()) + .add_attribute( + "packet_channel_ordering", + serde_json::to_value(channel_info.info.order)?.to_string(), + ) + .add_attribute("packet_connection", channel_info.info.connection_id); + + let events = vec![send_event]; + Ok(AppResponse { data: None, events }) + } + + fn receive_packet( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + packet: IbcPacketData, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // First we get the channel info to get the port out of it + let channel_info: ChannelInfo = CHANNEL_INFO.load( + &ibc_storage, + (packet.dst_port_id.clone(), packet.dst_channel_id.clone()), + )?; + + // First we verify it's not already in storage. If it is, we error, not possible to receive the same packet twice + if RECEIVE_PACKET_MAP + .load( + &ibc_storage, + ( + packet.dst_port_id.clone(), + packet.dst_channel_id.clone(), + packet.sequence, + ), + ) + .is_ok() + { + bail!("You can't receive the same packet twice on the chain") + } + + // We take a look at the timeout status of the packet + let timeout = packet.timeout.clone(); + let mut has_timeout = false; + if let Some(packet_block) = timeout.block() { + // We verify the block indicated is not passed + if block.height >= packet_block.height { + has_timeout = true; + } + } + if let Some(packet_timestamp) = timeout.timestamp() { + // We verify the timestamp indicated is not passed + if block.time >= packet_timestamp { + has_timeout = true; + } + } + + // We save it into storage (for tracking purposes and making sure we don't broadcast the message twice) + RECEIVE_PACKET_MAP.save( + &mut ibc_storage, + ( + packet.dst_port_id.clone(), + packet.dst_channel_id.clone(), + packet.sequence, + ), + &IbcPacketReceived { + data: packet.clone(), + timeout: has_timeout, + }, + )?; + + // If the packet has timeout on an ordered channel, we need to return an appropriate response AND close the channel + if has_timeout { + let res = if channel_info.info.order == IbcOrder::Ordered { + // We send a close channel response + transactional(storage, |write_cache, _| { + router.sudo( + api, + write_cache, + block, + SudoMsg::Ibc(IbcPacketRelayingMsg::CloseChannel { + port_id: packet.dst_port_id.clone(), + channel_id: packet.dst_channel_id.clone(), + init: true, + }), + ) + })? + } else { + AppResponse { + events: vec![], + data: None, + } + }; + + // We add timeout events + let timeout_height = packet.timeout.block().unwrap_or(IbcTimeoutBlock { + revision: 0, + height: 0, + }); + let timeout_timestamp = packet.timeout.timestamp().map(|t| t.nanos()).unwrap_or(0); + let timeout_event = Event::new(TIMEOUT_RECEIVE_PACKET_EVENT) + .add_attribute( + "packet_data", + String::from_utf8_lossy(packet.data.as_slice()), + ) + .add_attribute("packet_data_hex", to_hex(packet.data.as_slice())) + .add_attribute( + "packet_timeout_height", + format!("{}-{}", timeout_height.revision, timeout_height.height), + ) + .add_attribute("packet_timeout_timestamp", timeout_timestamp.to_string()) + .add_attribute("packet_sequence", packet.sequence.to_string()) + .add_attribute("packet_src_port", packet.src_port_id.clone()) + .add_attribute("packet_src_channel", packet.src_channel_id.clone()) + .add_attribute("packet_dst_port", packet.dst_port_id.clone()) + .add_attribute("packet_dst_channel", packet.dst_channel_id.clone()) + .add_attribute( + "packet_channel_ordering", + serde_json::to_value(channel_info.info.order)?.to_string(), + ) + .add_attribute("packet_connection", channel_info.info.connection_id); + + let mut events = res.events; + events.push(timeout_event); + return Ok(AppResponse { + events, + data: res.data, + }); + } + + let packet_msg = packet_from_data_and_channel(&packet, &channel_info); + + let receive_msg = IbcPacketReceiveMsg::new(packet_msg, Addr::unchecked(RELAYER_ADDR)); + + // First we send an ibc message on the corresponding module + let port: MockIbcPort = channel_info.info.endpoint.port_id.parse()?; + + let res = transactional(storage, |write_cache, _| { + router.ibc( + api, + write_cache, + block, + IbcRouterMsg { + module: port.into(), + msg: super::IbcModuleMsg::PacketReceive(receive_msg), + }, + ) + })?; + + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + let acknowledgement; + // Then, we store the acknowledgement and collect events + let mut events = match res { + IbcResponse::Receive(r) => { + // We save the acknowledgment in the structure + acknowledgement = r.acknowledgement.clone(); + ACK_PACKET_MAP.save( + &mut ibc_storage, + ( + packet.dst_port_id.clone(), + packet.dst_channel_id.clone(), + packet.sequence, + ), + &IbcPacketAck { + ack: r.acknowledgement, + }, + )?; + r.events + } + _ => panic!("Only a receive response was expected when receiving a packet"), + }; + + let timeout_height = packet.timeout.block().unwrap_or(IbcTimeoutBlock { + revision: 0, + height: 0, + }); + let timeout_timestamp = packet.timeout.timestamp().map(|t| t.nanos()).unwrap_or(0); + + let recv_event = Event::new(RECEIVE_PACKET_EVENT) + .add_attribute( + "packet_data", + String::from_utf8_lossy(packet.data.as_slice()), + ) + .add_attribute("packet_data_hex", to_hex(packet.data.as_slice())) + .add_attribute( + "packet_timeout_height", + format!("{}-{}", timeout_height.revision, timeout_height.height), + ) + .add_attribute("packet_timeout_timestamp", timeout_timestamp.to_string()) + .add_attribute("packet_sequence", packet.sequence.to_string()) + .add_attribute("packet_src_port", packet.src_port_id.clone()) + .add_attribute("packet_src_channel", packet.src_channel_id.clone()) + .add_attribute("packet_dst_port", packet.dst_port_id.clone()) + .add_attribute("packet_dst_channel", packet.dst_channel_id.clone()) + .add_attribute( + "packet_channel_ordering", + serde_json::to_value(channel_info.info.order)?.to_string(), + ) + .add_attribute("packet_connection", channel_info.info.connection_id); + + let ack_event = Event::new(WRITE_ACK_EVENT) + .add_attribute( + "packet_data", + serde_json::to_value(&packet.data)?.to_string(), + ) + .add_attribute("packet_data_hex", to_hex(packet.data.as_slice())) + .add_attribute( + "packet_timeout_height", + format!("{}-{}", timeout_height.revision, timeout_height.height), + ) + .add_attribute("packet_timeout_timestamp", timeout_timestamp.to_string()) + .add_attribute("packet_sequence", packet.sequence.to_string()) + .add_attribute("packet_src_port", packet.src_port_id) + .add_attribute("packet_src_channel", packet.src_channel_id) + .add_attribute("packet_dst_port", packet.dst_port_id) + .add_attribute("packet_dst_channel", packet.dst_channel_id) + .add_attribute( + "packet_ack", + acknowledgement + .as_ref() + .map(|a| String::from_utf8_lossy(a.as_slice()).to_string()) + .unwrap_or("".to_string()), + ) + .add_attribute( + "packet_ack_hex", + acknowledgement + .as_ref() + .map(to_hex) + .unwrap_or("".to_string()), + ); + + events.push(recv_event); + events.push(ack_event); + + Ok(AppResponse { data: None, events }) + } + + fn acknowledge_packet( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + packet: IbcPacketData, + ack: Binary, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // First we get the channel info to get the port out of it + let channel_info = CHANNEL_INFO.load( + &ibc_storage, + (packet.src_port_id.clone(), packet.src_channel_id.clone()), + )?; + + // First we verify the packet exists and the acknowledgement is not received yet + let mut packet_data: IbcPacketData = SEND_PACKET_MAP.load( + &ibc_storage, + ( + packet.src_port_id.clone(), + packet.src_channel_id.clone(), + packet.sequence, + ), + )?; + if packet_data.ack.is_some() { + bail!("You can't ack the same packet twice on the chain") + } + + if TIMEOUT_PACKET_MAP.has( + &ibc_storage, + ( + packet.src_port_id.clone(), + packet.src_channel_id.clone(), + packet.sequence, + ), + ) { + bail!("Packet has timed_out, can't acknowledge"); + } + + // We save the ack into storage + packet_data.ack = Some(ack.clone()); + SEND_PACKET_MAP.save( + &mut ibc_storage, + ( + packet.src_port_id.clone(), + packet.src_channel_id.clone(), + packet.sequence, + ), + &packet_data, + )?; + + let acknowledgement = IbcAcknowledgement::new(ack); + let original_packet = packet_from_data_and_channel(&packet_data, &channel_info); + + let ack_message = IbcPacketAckMsg::new( + acknowledgement, + original_packet, + Addr::unchecked(RELAYER_ADDR), + ); + + let port: MockIbcPort = channel_info.info.endpoint.port_id.parse()?; + let res = transactional(storage, |write_cache, _| { + router.ibc( + api, + write_cache, + block, + IbcRouterMsg { + module: port.into(), + msg: super::IbcModuleMsg::PacketAcknowledgement(ack_message), + }, + ) + })?; + + let mut events = match res { + // Only type allowed as an ack response + IbcResponse::Basic(r) => r.events, + _ => panic!("Only a basic response was expected when ack a packet"), + }; + + // We add custom packet ack events + let timeout_height = packet.timeout.block().unwrap_or(IbcTimeoutBlock { + revision: 0, + height: 0, + }); + let timeout_timestamp = packet.timeout.timestamp().map(|t| t.nanos()).unwrap_or(0); + + let ack_event = Event::new(ACK_PACKET_EVENT) + .add_attribute( + "packet_timeout_height", + format!("{}-{}", timeout_height.revision, timeout_height.height), + ) + .add_attribute("packet_timeout_timestamp", timeout_timestamp.to_string()) + .add_attribute("packet_sequence", packet.sequence.to_string()) + .add_attribute("packet_src_port", packet.src_port_id.clone()) + .add_attribute("packet_src_channel", packet.src_channel_id.clone()) + .add_attribute("packet_dst_port", packet.dst_port_id.clone()) + .add_attribute("packet_dst_channel", packet.dst_channel_id) + .add_attribute( + "packet_channel_ordering", + serde_json::to_value(channel_info.info.order)?.to_string(), + ) + .add_attribute("packet_connection", channel_info.info.connection_id); + + events.push(ack_event); + + Ok(AppResponse { data: None, events }) + } + + fn timeout_packet( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + packet: IbcPacketData, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let mut ibc_storage = prefixed(storage, NAMESPACE_IBC); + + // First we get the channel info to get the port out of it + let channel_info = CHANNEL_INFO.load( + &ibc_storage, + (packet.src_port_id.clone(), packet.src_channel_id.clone()), + )?; + + // We verify the timeout is indeed passed on the packet + let packet_data: IbcPacketData = SEND_PACKET_MAP.load( + &ibc_storage, + ( + packet.src_port_id.clone(), + packet.src_channel_id.clone(), + packet.sequence, + ), + )?; + + // If the packet was already ack, no timeout possible + if packet_data.ack.is_some() { + bail!("You can't timeout an acked packet") + } + + if TIMEOUT_PACKET_MAP + .may_load( + &ibc_storage, + ( + packet.src_port_id.clone(), + packet.src_channel_id.clone(), + packet.sequence, + ), + )? + .is_some() + { + bail!("You can't timeout an packet twice") + } + + // We don't check timeout conditions here, because when calling this function, we assume the counterparty chain has received the packet after the timeout + + TIMEOUT_PACKET_MAP.save( + &mut ibc_storage, + ( + packet.src_port_id.clone(), + packet.src_channel_id.clone(), + packet.sequence, + ), + &true, + )?; + + let original_packet = packet_from_data_and_channel(&packet_data, &channel_info); + + let timeout_message = + IbcPacketTimeoutMsg::new(original_packet, Addr::unchecked(RELAYER_ADDR)); + + // First we send an ibc message on the module in cache + let port: MockIbcPort = channel_info.info.endpoint.port_id.parse()?; + let res = transactional(storage, |write_cache, _| { + router.ibc( + api, + write_cache, + block, + IbcRouterMsg { + module: port.into(), + msg: super::IbcModuleMsg::PacketTimeout(timeout_message), + }, + ) + })?; + + // Then we collect events + let mut events = match res { + // Only type allowed as in timeout response + IbcResponse::Basic(r) => r.events, + _ => panic!("Only a basic response was expected when timeout a packet"), + }; + + // We add custom packet timeout events + let timeout_height = packet.timeout.block().unwrap_or(IbcTimeoutBlock { + revision: 0, + height: 0, + }); + let timeout_timestamp = packet.timeout.timestamp().map(|t| t.nanos()).unwrap_or(0); + + let timeout_event = Event::new(TIMEOUT_PACKET_EVENT) + .add_attribute( + "packet_timeout_height", + format!("{}-{}", timeout_height.revision, timeout_height.height), + ) + .add_attribute("packet_timeout_timestamp", timeout_timestamp.to_string()) + .add_attribute("packet_sequence", packet.sequence.to_string()) + .add_attribute("packet_src_port", packet.src_port_id.clone()) + .add_attribute("packet_src_channel", packet.src_channel_id.clone()) + .add_attribute("packet_dst_port", packet.dst_port_id.clone()) + .add_attribute("packet_dst_channel", packet.dst_channel_id) + .add_attribute( + "packet_channel_ordering", + serde_json::to_value(channel_info.info.order)?.to_string(), + ); + + events.push(timeout_event); + + Ok(AppResponse { data: None, events }) + } + + // Applications + fn transfer( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + sender: Addr, + channel_id: String, + to_address: String, + amount: Coin, + timeout: IbcTimeout, + ) -> AnyResult + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + // Transfer is : + // 1. Lock user funds into the port balance. We send from the sender to the lock address + let msg: cosmwasm_std::CosmosMsg = BankMsg::Send { + to_address: IBC_LOCK_MODULE_ADDRESS.to_string(), + amount: vec![amount.clone()], + } + .into(); + router.execute(api, storage, block, sender.clone(), msg)?; + + // We unwrap the denom if the funds were received on this specific channel + let denom = optional_unwrap_ibc_denom(amount.denom, channel_id.clone()); + + // 2. Send an ICS20 Packet to the remote chain + let packet_formed = Ics20Packet { + amount: amount.amount, + denom, + receiver: to_address, + sender: sender.to_string(), + memo: None, + }; + + self.send_packet( + storage, + "transfer".to_string(), + channel_id, + to_json_binary(&packet_formed)?, + timeout, + ) + } +} + +impl Module for IbcSimpleModule { + type ExecT = IbcMsg; + type QueryT = MockIbcQuery; + type SudoT = IbcPacketRelayingMsg; + + fn execute( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + sender: Addr, + msg: Self::ExecT, + ) -> anyhow::Result + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + match msg { + IbcMsg::Transfer { + channel_id, + to_address, + amount, + timeout, + // We could add IBC-hooks capabilities here + memo: _, + } => self.transfer( + api, storage, router, block, sender, channel_id, to_address, amount, timeout, + ), + IbcMsg::SendPacket { + channel_id, + data, + timeout, + } => { + // This should come from a contract. So the port_id is always the same format + // If you want to send a packet form a module use the sudo Send Packet msg + let port_id: String = format!("wasm.{}", sender); + self.send_packet(storage, port_id, channel_id, data, timeout) + } + IbcMsg::CloseChannel { channel_id } => { + let port_id: String = format!("wasm.{}", sender); + // This message correspond to init closing a channel + self.close_channel(api, storage, router, block, port_id, channel_id, true) + } + _ => bail!("Not implemented on the ibc module"), + } + } + + fn query( + &self, + _api: &dyn cosmwasm_std::Api, + storage: &dyn Storage, + _querier: &dyn cosmwasm_std::Querier, + _block: &cosmwasm_std::BlockInfo, + request: Self::QueryT, + ) -> anyhow::Result { + let ibc_storage = prefixed_read(storage, NAMESPACE_IBC); + match request { + MockIbcQuery::CosmWasm(m) => { + match m { + IbcQuery::Channel { + channel_id, + port_id, + } => { + // Port id has to be specified unfortunately here + let port_id = port_id.unwrap(); + // We load the channel of the port + let channel_info = + CHANNEL_INFO.may_load(&ibc_storage, (port_id, channel_id))?; + + Ok(to_json_binary(&ChannelResponse::new( + channel_info.map(|c| c.info), + ))?) + } + #[allow(deprecated)] + IbcQuery::ListChannels { port_id } => { + // Port_id has to be specified here, unfortunately we can't access the contract address + let port_id = port_id.unwrap(); + + let channels = CHANNEL_INFO + .prefix(port_id) + .range(&ibc_storage, None, None, Order::Ascending) + .collect::, _>>()?; + + Ok(to_json_binary(&ListChannelsResponse::new( + channels.iter().map(|c| c.1.info.clone()).collect(), + ))?) + } + _ => bail!("Query not available"), + } + } + MockIbcQuery::SendPacket { + channel_id, + port_id, + sequence, + } => { + let packet_data = + SEND_PACKET_MAP.load(&ibc_storage, (port_id, channel_id, sequence))?; + + Ok(to_json_binary(&packet_data)?) + } + MockIbcQuery::ConnectedChain { connection_id } => { + let chain_id = ibc_connections().load(&ibc_storage, &connection_id)?; + + Ok(to_json_binary(&chain_id)?) + } + MockIbcQuery::ChainConnections { chain_id } => { + let connections = ibc_connections() + .idx + .chain_id + .prefix(chain_id) + .range(&ibc_storage, None, None, Order::Descending) + .collect::, _>>()?; + + Ok(to_json_binary(&connections)?) + } + MockIbcQuery::ChannelInfo { + port_id, + channel_id, + } => { + let channel_info = CHANNEL_INFO.load(&ibc_storage, (port_id, channel_id))?; + + Ok(to_json_binary(&channel_info)?) + } + } + } + + fn sudo( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + msg: Self::SudoT, + ) -> anyhow::Result + where + ExecC: CustomMsg, + QueryC: cosmwasm_std::CustomQuery + serde::de::DeserializeOwned + 'static, + { + let response = match msg { + IbcPacketRelayingMsg::CreateConnection { + connection_id, + remote_chain_id, + counterparty_connection_id, + } => self.create_connection( + storage, + remote_chain_id, + connection_id, + counterparty_connection_id, + ), + + IbcPacketRelayingMsg::OpenChannel { + local_connection_id, + local_port, + version, + order, + counterparty_version, + counterparty_endpoint, + } => self.open_channel( + api, + storage, + router, + block, + local_connection_id, + local_port, + version, + order, + counterparty_endpoint, + counterparty_version, + ), + IbcPacketRelayingMsg::ConnectChannel { + counterparty_version, + counterparty_endpoint, + port_id, + channel_id, + } => self.connect_channel( + api, + storage, + router, + block, + port_id, + channel_id, + counterparty_endpoint, + counterparty_version, + ), + IbcPacketRelayingMsg::CloseChannel { + port_id, + channel_id, + init, + } => self.close_channel(api, storage, router, block, port_id, channel_id, init), + + IbcPacketRelayingMsg::Send { + port_id, + channel_id, + data, + timeout, + } => self.send_packet(storage, port_id, channel_id, data, timeout), + IbcPacketRelayingMsg::Receive { packet } => { + self.receive_packet(api, storage, router, block, packet) + } + IbcPacketRelayingMsg::Acknowledge { packet, ack } => { + self.acknowledge_packet(api, storage, router, block, packet, ack) + } + IbcPacketRelayingMsg::Timeout { packet } => { + self.timeout_packet(api, storage, router, block, packet) + } + }?; + + Ok(response) + } + + //Ibc endpoints are not available on the IBC module. This module is only a fix for receiving IBC messages. The IBC module doesn't and will never have ports opened to other blockchains +} + +impl Ibc for IbcSimpleModule {} + +#[cfg(test)] +mod test {} diff --git a/src/ibc/state.rs b/src/ibc/state.rs new file mode 100644 index 00000000..952de6e8 --- /dev/null +++ b/src/ibc/state.rs @@ -0,0 +1,55 @@ +use cosmwasm_std::Storage; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex}; + +use super::types::*; + +use anyhow::Result as AnyResult; + +pub const NAMESPACE_IBC: &[u8] = b"ibc-namespace"; + +/// This maps a connection id to a remote chain id +pub struct ConnectionIndexes<'a> { + // chain_id, Connection info, connection_id + pub chain_id: MultiIndex<'a, String, Connection, String>, +} + +impl IndexList for ConnectionIndexes<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.chain_id]; + Box::new(v.into_iter()) + } +} + +pub fn ibc_connections<'a>() -> IndexedMap<&'a str, Connection, ConnectionIndexes<'a>> { + let indexes = ConnectionIndexes { + chain_id: MultiIndex::new( + |_, d: &Connection| d.counterparty_chain_id.clone(), + "connections", + "connections_chain_id", + ), + }; + IndexedMap::new("connections", indexes) +} + +pub const PORT_INFO: Map = Map::new("port_info"); + +pub const CHANNEL_HANDSHAKE_INFO: Map<(String, String), ChannelHandshakeInfo> = + Map::new("channel_handshake_info"); +pub const CHANNEL_INFO: Map<(String, String), ChannelInfo> = Map::new("channel_info"); + +// channel id, packet_id ==> Packet data +pub const SEND_PACKET_MAP: Map<(String, String, u64), IbcPacketData> = Map::new("send_packet"); + +// channel id, packet_id ==> Packet data +pub const RECEIVE_PACKET_MAP: Map<(String, String, u64), IbcPacketReceived> = + Map::new("receive_packet"); + +// channel id, packet_id ==> Packet data +pub const ACK_PACKET_MAP: Map<(String, String, u64), IbcPacketAck> = Map::new("ack_packet"); + +// channel id, packet_id ==> Packet data +pub const TIMEOUT_PACKET_MAP: Map<(String, String, u64), bool> = Map::new("timeout_packet"); + +pub fn load_port_info(storage: &dyn Storage, port_id: String) -> AnyResult { + Ok(PORT_INFO.may_load(storage, port_id)?.unwrap_or_default()) +} diff --git a/src/ibc/types.rs b/src/ibc/types.rs new file mode 100644 index 00000000..b2b46ca6 --- /dev/null +++ b/src/ibc/types.rs @@ -0,0 +1,252 @@ +use crate::app::IbcModule; +use anyhow::bail; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + Addr, Binary, Event, IbcChannel, IbcChannelOpenResponse, IbcEndpoint, IbcOrder, IbcQuery, + IbcTimeout, +}; +use std::{fmt::Display, str::FromStr}; + +#[cw_serde] +/// IBC connection +pub struct Connection { + /// Connection id on the counterparty chain + pub counterparty_connection_id: Option, + /// Chain id of the counterparty chain + pub counterparty_chain_id: String, +} + +#[cw_serde] +#[derive(Default)] +/// IBC Port Info +pub struct PortInfo { + /// Channel id of the next opened channel + pub next_channel_id: u64, +} + +#[cw_serde] +pub struct ChannelHandshakeInfo { + pub connection_id: String, + pub port: MockIbcPort, + pub local_endpoint: IbcEndpoint, + pub remote_endpoint: IbcEndpoint, + pub state: ChannelHandshakeState, + pub order: IbcOrder, + pub version: String, +} + +#[cw_serde] +pub enum ChannelHandshakeState { + Init, + Try, + Ack, + Confirm, +} + +#[cw_serde] +pub struct ChannelInfo { + pub next_packet_id: u64, + pub last_packet_relayed: u64, + + pub info: IbcChannel, + + pub open: bool, +} + +#[cw_serde] +pub enum MockIbcPort { + Wasm(String), // A wasm port is simply a wasm contract address + Bank, // The bank port simply talks to the bank module + Staking, // The staking port simply talks to the staking module +} + +impl From for IbcModule { + fn from(port: MockIbcPort) -> IbcModule { + match port { + MockIbcPort::Bank => IbcModule::Bank, + MockIbcPort::Staking => IbcModule::Staking, + MockIbcPort::Wasm(contract) => IbcModule::Wasm(Addr::unchecked(contract)), + } + } +} + +pub const BANK_MODULE_PORT: &str = "transfer"; + +impl Display for MockIbcPort { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MockIbcPort::Wasm(c) => write!(f, "wasm.{}", c), + MockIbcPort::Bank => write!(f, "{BANK_MODULE_PORT}"), + MockIbcPort::Staking => panic!("No ibc port for the staking module"), + } + } +} + +impl FromStr for MockIbcPort { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + // For the bank module + if s.eq(BANK_MODULE_PORT) { + return Ok(MockIbcPort::Bank); + } + + // For the wasm module + let wasm = s.split('.').collect::>(); + if wasm.len() == 2 && wasm[0] == "wasm" { + return Ok(MockIbcPort::Wasm(wasm[1].to_string())); + } + // Error + bail!( + "The ibc port {} can't be linked to an mock ibc implementation", + s + ) + } +} + +#[cw_serde] +pub struct IbcPacketData { + pub ack: Option, + /// This also tells us whether this packet was already sent on the other chain or not + pub src_port_id: String, + pub src_channel_id: String, + pub dst_port_id: String, + pub dst_channel_id: String, + pub sequence: u64, + pub data: Binary, + pub timeout: IbcTimeout, +} + +#[cw_serde] +pub struct IbcPacketReceived { + pub data: IbcPacketData, + /// Indicates whether the packet was received with a timeout + pub timeout: bool, +} + +#[cw_serde] +pub struct IbcPacketAck { + pub ack: Option, +} + +/// This is a custom msg that is used for executing actions on the IBC module +/// We trust all packets that are relayed. Remember, this is a test environment. +#[cw_serde] +pub enum IbcPacketRelayingMsg { + CreateConnection { + remote_chain_id: String, + // And in the case we need to register the counterparty id as well + connection_id: Option, + counterparty_connection_id: Option, + }, + + OpenChannel { + local_connection_id: String, + local_port: String, + version: String, + order: IbcOrder, + + counterparty_version: Option, + counterparty_endpoint: IbcEndpoint, + }, + ConnectChannel { + port_id: String, + channel_id: String, + + counterparty_version: Option, + counterparty_endpoint: IbcEndpoint, + }, + CloseChannel { + port_id: String, + channel_id: String, + init: bool, + }, + Send { + port_id: String, + channel_id: String, + data: Binary, + timeout: IbcTimeout, + }, + Receive { + packet: IbcPacketData, + }, + Acknowledge { + packet: IbcPacketData, + ack: Binary, + }, + Timeout { + packet: IbcPacketData, + }, +} + +/// This type allows to wrap the IBC response to return from the Router. +#[cw_serde] +pub enum IbcResponse { + Open(IbcChannelOpenResponse), + Basic(AppIbcBasicResponse), + Receive(AppIbcReceiveResponse), +} + +#[cw_serde] +#[derive(Default)] +pub struct AppIbcBasicResponse { + pub events: Vec, +} + +#[cw_serde] +#[derive(Default)] +pub struct AppIbcReceiveResponse { + pub events: Vec, + pub acknowledgement: Option, +} + +impl From for IbcResponse { + fn from(c: IbcChannelOpenResponse) -> IbcResponse { + IbcResponse::Open(c) + } +} + +impl From for IbcResponse { + fn from(c: AppIbcBasicResponse) -> IbcResponse { + IbcResponse::Basic(c) + } +} + +impl From for IbcResponse { + fn from(c: AppIbcReceiveResponse) -> IbcResponse { + IbcResponse::Receive(c) + } +} + +/// This extends the cosmwasm-std IBC query type with internal tools needed. +#[cw_serde] +pub enum MockIbcQuery { + CosmWasm(IbcQuery), + /// Only used inside cw-multi-test + /// Queries a packet that was sent on the chain + /// Returns `IbcPacketData` + SendPacket { + channel_id: String, + port_id: String, + sequence: u64, + }, + /// This is used to get the chain_id of the connected chain + ConnectedChain { + connection_id: String, + }, + /// Gets all the connections with a chain + ChainConnections { + chain_id: String, + }, + /// Gets information on a channel + ChannelInfo { + port_id: String, + channel_id: String, + }, +} + +impl From for MockIbcQuery { + fn from(value: IbcQuery) -> Self { + MockIbcQuery::CosmWasm(value) + } +} diff --git a/src/lib.rs b/src/lib.rs index a38f84dc..d4797e26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,7 +173,7 @@ pub mod error; mod executor; mod featured; mod gov; -mod ibc; +pub mod ibc; mod module; mod prefixed_storage; #[cfg(feature = "staking")] diff --git a/src/module.rs b/src/module.rs index cc73a4d6..83b6a902 100644 --- a/src/module.rs +++ b/src/module.rs @@ -6,6 +6,12 @@ use serde::de::DeserializeOwned; use std::fmt::Debug; use std::marker::PhantomData; +use crate::ibc::types::{AppIbcBasicResponse, AppIbcReceiveResponse}; +use cosmwasm_std::{ + IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, + IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, +}; + /// # General module /// /// Provides a generic interface for modules within the test environment. @@ -62,6 +68,78 @@ pub trait Module { where ExecC: CustomMsg + DeserializeOwned + 'static, QueryC: CustomQuery + DeserializeOwned + 'static; + + /// Executes the contract ibc_channel_open endpoint + fn ibc_channel_open( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcChannelOpenMsg, + ) -> AnyResult { + Ok(IbcChannelOpenResponse::None) + } + + /// Executes the contract ibc_channel_connect endpoint + fn ibc_channel_connect( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcChannelConnectMsg, + ) -> AnyResult { + Ok(AppIbcBasicResponse::default()) + } + + /// Executes the contract ibc_channel_close endpoints + fn ibc_channel_close( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcChannelCloseMsg, + ) -> AnyResult { + Ok(AppIbcBasicResponse::default()) + } + + /// Executes the contract ibc_packet_receive endpoint + fn ibc_packet_receive( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketReceiveMsg, + ) -> AnyResult { + panic!("No ibc packet receive implemented"); + } + + /// Executes the contract ibc_packet_acknowledge endpoint + fn ibc_packet_acknowledge( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketAckMsg, + ) -> AnyResult { + panic!("No ibc packet acknowledgement implemented"); + } + + /// Executes the contract ibc_packet_timeout endpoint + fn ibc_packet_timeout( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketTimeoutMsg, + ) -> AnyResult { + panic!("No ibc packet timeout implemented"); + } } /// # Always failing module /// diff --git a/src/wasm.rs b/src/wasm.rs index 8c3ef7c1..90a18b36 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -4,6 +4,7 @@ use crate::checksums::{ChecksumGenerator, SimpleChecksumGenerator}; use crate::contracts::Contract; use crate::error::{bail, AnyContext, AnyError, AnyResult, Error}; use crate::executor::AppResponse; +use crate::ibc::types::{AppIbcBasicResponse, AppIbcReceiveResponse}; use crate::prefixed_storage::{prefixed, prefixed_read, PrefixedStorage, ReadonlyPrefixedStorage}; use crate::transactions::transactional; use cosmwasm_std::testing::mock_wasmd_attr; @@ -12,8 +13,11 @@ use cosmwasm_std::GovMsg; use cosmwasm_std::{ to_json_binary, Addr, Api, Attribute, BankMsg, Binary, BlockInfo, Checksum, Coin, ContractInfo, ContractInfoResponse, CosmosMsg, CustomMsg, CustomQuery, Deps, DepsMut, Env, Event, - MessageInfo, MsgResponse, Order, Querier, QuerierWrapper, Record, Reply, ReplyOn, Response, - StdResult, Storage, SubMsg, SubMsgResponse, SubMsgResult, TransactionInfo, WasmMsg, WasmQuery, + IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcChannelOpenResponse, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, + IbcReceiveResponse, MessageInfo, MsgResponse, Order, Querier, QuerierWrapper, Record, Reply, + ReplyOn, Response, StdResult, Storage, SubMsg, SubMsgResponse, SubMsgResult, TransactionInfo, + WasmMsg, WasmQuery, }; #[cfg(feature = "staking")] use cosmwasm_std::{DistributionMsg, StakingMsg}; @@ -169,6 +173,82 @@ pub trait Wasm { let storage = PrefixedStorage::multilevel(storage, &[NAMESPACE_WASM, &namespace]); Box::new(storage) } + /// Executes the contract ibc_channel_open endpoint + fn ibc_channel_open( + &self, + _api: &dyn Api, + _contract_addr: Addr, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcChannelOpenMsg, + ) -> AnyResult { + panic!("No ibc channel open implemented"); + } + /// Executes the contract ibc_channel_connect endpoint + fn ibc_channel_connect( + &self, + _api: &dyn Api, + _contract_addr: Addr, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcChannelConnectMsg, + ) -> AnyResult { + panic!("No ibc channel connect implemented"); + } + + /// Executes the contract ibc_channel_close endpoint + fn ibc_channel_close( + &self, + _api: &dyn Api, + _contract_addr: Addr, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcChannelCloseMsg, + ) -> AnyResult { + panic!("No ibc channel close implemented"); + } + + /// Executes the contract ibc_packet_receive endpoint + fn ibc_packet_receive( + &self, + _api: &dyn Api, + _contract_addr: Addr, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketReceiveMsg, + ) -> AnyResult { + panic!("No ibc packet receive implemented"); + } + + /// Executes the contract ibc_packet_acknowledge endpoint + fn ibc_packet_acknowledge( + &self, + _api: &dyn Api, + _contract_addr: Addr, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketAckMsg, + ) -> AnyResult { + panic!("No ibc packet acknowledgement implemented"); + } + + /// Executes the contract ibc_packet_timeout endpoint + fn ibc_packet_timeout( + &self, + _api: &dyn Api, + _contract_addr: Addr, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _request: IbcPacketTimeoutMsg, + ) -> AnyResult { + panic!("No ibc packet timeout implemented"); + } } /// A structure representing a default wasm keeper. @@ -339,6 +419,135 @@ where let storage = self.contract_storage(storage, address); storage.range(None, None, Order::Ascending).collect() } + + // The following ibc endpoints can only be used by the ibc module. + // For channels + fn ibc_channel_open( + &self, + api: &dyn Api, + contract: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + request: IbcChannelOpenMsg, + ) -> AnyResult { + // For channel open, we simply return the result directly to the ibc module + let contract_response = self.with_storage( + api, + storage, + router, + block, + contract, + |contract, deps, env| contract.ibc_channel_open(deps, env, request), + )?; + + Ok(contract_response) + } + + fn ibc_channel_connect( + &self, + api: &dyn Api, + contract_addr: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + request: IbcChannelConnectMsg, + ) -> AnyResult { + let res = Self::verify_ibc_response(self.with_storage( + api, + storage, + router, + block, + contract_addr.clone(), + |contract, deps, env| contract.ibc_channel_connect(deps, env, request), + )?)?; + + self.process_ibc_response(api, contract_addr, storage, router, block, res) + } + + fn ibc_channel_close( + &self, + api: &dyn Api, + contract_addr: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + request: IbcChannelCloseMsg, + ) -> AnyResult { + let res = Self::verify_ibc_response(self.with_storage( + api, + storage, + router, + block, + contract_addr.clone(), + |contract, deps, env| contract.ibc_channel_close(deps, env, request), + )?)?; + + self.process_ibc_response(api, contract_addr, storage, router, block, res) + } + + fn ibc_packet_receive( + &self, + api: &dyn Api, + contract_addr: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + request: IbcPacketReceiveMsg, + ) -> AnyResult { + let res = Self::verify_packet_response(self.with_storage( + api, + storage, + router, + block, + contract_addr.clone(), + |contract, deps, env| contract.ibc_packet_receive(deps, env, request), + )?)?; + + self.process_ibc_receive_response(api, contract_addr, storage, router, block, res) + } + + fn ibc_packet_acknowledge( + &self, + api: &dyn Api, + contract_addr: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + request: IbcPacketAckMsg, + ) -> AnyResult { + let res = Self::verify_ibc_response(self.with_storage( + api, + storage, + router, + block, + contract_addr.clone(), + |contract, deps, env| contract.ibc_packet_ack(deps, env, request), + )?)?; + + self.process_ibc_response(api, contract_addr, storage, router, block, res) + } + + fn ibc_packet_timeout( + &self, + api: &dyn Api, + contract_addr: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + request: IbcPacketTimeoutMsg, + ) -> AnyResult { + let res = Self::verify_ibc_response(self.with_storage( + api, + storage, + router, + block, + contract_addr.clone(), + |contract, deps, env| contract.ibc_packet_timeout(deps, env, request), + )?)?; + + self.process_ibc_response(api, contract_addr, storage, router, block, res) + } } impl WasmKeeper @@ -469,6 +678,42 @@ where Ok(()) } + fn verify_ibc_response(response: IbcBasicResponse) -> AnyResult> + where + T: Clone + Debug + PartialEq + JsonSchema, + { + Self::verify_attributes(&response.attributes)?; + + for event in &response.events { + Self::verify_attributes(&event.attributes)?; + let ty = event.ty.trim(); + if ty.len() < 2 { + bail!(Error::event_type_too_short(ty)); + } + } + + Ok(response) + } + + fn verify_packet_response( + response: IbcReceiveResponse, + ) -> AnyResult> + where + T: Clone + Debug + PartialEq + JsonSchema, + { + Self::verify_attributes(&response.attributes)?; + + for event in &response.events { + Self::verify_attributes(&event.attributes)?; + let ty = event.ty.trim(); + if ty.len() < 2 { + bail!(Error::event_type_too_short(ty)); + } + } + + Ok(response) + } + fn verify_response(response: Response) -> AnyResult> where T: CustomMsg, @@ -993,6 +1238,60 @@ where Ok(AppResponse { events, data }) } + fn process_ibc_response( + &self, + api: &dyn Api, + contract: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + res: IbcBasicResponse, + ) -> AnyResult { + // We format the events correctly because we are executing wasm + let contract_response = Response::new() + .add_submessages(res.messages) + .add_attributes(res.attributes) + .add_events(res.events); + + let (res, msgs) = self.build_app_response(&contract, Event::new("ibc"), contract_response); + + // We process eventual messages that were sent out with the response + let res = self.process_response(api, router, storage, block, contract, res, msgs)?; + + // We transfer back to an IbcBasicResponse + Ok(AppIbcBasicResponse { events: res.events }) + } + + fn process_ibc_receive_response( + &self, + api: &dyn Api, + contract: Addr, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + original_res: IbcReceiveResponse, + ) -> AnyResult { + // We format the events correctly because we are executing wasm + let contract_response = Response::new() + .add_submessages(original_res.messages) + .add_attributes(original_res.attributes) + .add_events(original_res.events); + + let (res, msgs) = self.build_app_response(&contract, Event::new("ibc"), contract_response); + + // We process eventual messages that were sent out with the response + let res = self.process_response(api, router, storage, block, contract, res, msgs)?; + + // If the data field was overwritten by the response propagation, we replace the ibc ack + let acknowledgement = res.data.or(original_res.acknowledgement); + + // We transfer back to an IbcBasicResponse + Ok(AppIbcReceiveResponse { + events: res.events, + acknowledgement, + }) + } + /// Creates a contract address and empty storage instance. /// Returns the new contract address. /// diff --git a/tests/mod.rs b/tests/mod.rs index 82ff037b..5cfa91c4 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -7,6 +7,7 @@ mod test_attributes; mod test_bank; mod test_contract_storage; mod test_empty_contract; +mod test_ibc; mod test_module; mod test_payload; mod test_prefixed_storage; diff --git a/tests/test_app_builder/test_with_ibc.rs b/tests/test_app_builder/test_with_ibc.rs index f61b4cd2..2d200b4e 100644 --- a/tests/test_app_builder/test_with_ibc.rs +++ b/tests/test_app_builder/test_with_ibc.rs @@ -1,8 +1,11 @@ use crate::test_app_builder::MyKeeper; -use cosmwasm_std::{Empty, IbcMsg, IbcQuery, QueryRequest}; -use cw_multi_test::{no_init, AppBuilder, Executor, Ibc}; +use anyhow::Result as AnyResult; +use cosmwasm_std::{IbcMsg, IbcQuery, QueryRequest}; +use cw_multi_test::ibc::relayer::{create_channel, create_connection}; +use cw_multi_test::ibc::{types::MockIbcQuery, IbcPacketRelayingMsg}; +use cw_multi_test::{no_init, AppBuilder, BasicApp, Executor, Ibc}; -type MyIbcKeeper = MyKeeper; +type MyIbcKeeper = MyKeeper; impl Ibc for MyIbcKeeper {} @@ -48,3 +51,38 @@ fn building_app_with_custom_ibc_should_work() { .to_string() ); } + +#[test] +fn create_channel_should_work_with_basic_app() -> AnyResult<()> { + let mut app1 = BasicApp::new(no_init); + let mut app2 = BasicApp::new(no_init); + + let (src_connection_id, _dst_connection) = create_connection(&mut app1, &mut app2)?; + + create_channel( + &mut app1, + &mut app2, + src_connection_id, + "transfer".to_string(), + "transfer".to_string(), + "ics20-1".to_string(), + cosmwasm_std::IbcOrder::Unordered, + )?; + + Ok(()) +} + +#[test] +fn create_channel_should_work_with_failing_keeper() -> AnyResult<()> { + // build custom ibc keeper (no sudo handling for ibc) + let ibc_keeper1 = MyIbcKeeper::new(EXECUTE_MSG, QUERY_MSG, ""); + let ibc_keeper2 = MyIbcKeeper::new(EXECUTE_MSG, QUERY_MSG, ""); + + // build the application with custom ibc keeper + let mut app1 = AppBuilder::default().with_ibc(ibc_keeper1).build(no_init); + let mut app2 = AppBuilder::default().with_ibc(ibc_keeper2).build(no_init); + + create_connection(&mut app1, &mut app2).unwrap_err(); + + Ok(()) +} diff --git a/tests/test_ibc/bank.rs b/tests/test_ibc/bank.rs new file mode 100644 index 00000000..5d66f497 --- /dev/null +++ b/tests/test_ibc/bank.rs @@ -0,0 +1,270 @@ +use cosmwasm_std::{ + coin, from_json, testing::MockApi, to_json_binary, AllBalanceResponse, BankQuery, CosmosMsg, + Empty, IbcMsg, IbcOrder, IbcTimeout, IbcTimeoutBlock, Querier, QueryRequest, +}; +use cw_multi_test::{ + ibc::{ + relayer::{create_channel, create_connection, relay_packets_in_tx, ChannelCreationResult}, + IbcSimpleModule, + }, + no_init, AppBuilder, Executor, +}; + +/// In this module, we are testing the bank module ibc capabilities +/// We try in the implementation to stay simple but as close as the real deal as possible + +#[test] +fn simple_transfer() -> anyhow::Result<()> { + let funds = coin(100_000, "ufund"); + + let mut app1 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("src")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + // We add a start balance for the owner + let fund_owner = app1.api().addr_make("owner"); + let fund_recipient = app2.api().addr_make("recipient"); + app1.init_modules(|router, _api, storage| { + router + .bank + .init_balance(storage, &fund_owner, vec![funds.clone()]) + .unwrap(); + }); + + let port1 = "transfer".to_string(); + let port2 = "transfer".to_string(); + + let (src_connection_id, _) = create_connection(&mut app1, &mut app2)?; + + // We start by creating channels + let ChannelCreationResult { + src_channel, + dst_channel, + .. + } = create_channel( + &mut app1, + &mut app2, + src_connection_id, + port1, + port2, + "ics20-1".to_string(), + IbcOrder::Ordered, + )?; + + // We send an IBC transfer Cosmos Msg on app 1 + let send_response = app1.execute( + fund_owner.clone(), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: src_channel, + to_address: fund_recipient.to_string(), + amount: funds.clone(), + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 1, + height: app2.block_info().height + 1, + }), + memo: None, + }), + )?; + + // We are relaying all packets found in the transaction. + relay_packets_in_tx(&mut app1, &mut app2, send_response)?; + + // We make sure the balance of the recipient has changed. + let balances = app2 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_recipient.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + + // The recipient has received exactly what they needs + assert_eq!(balances.amount.len(), 1); + assert_eq!(balances.amount[0].amount, funds.amount); + assert_eq!( + balances.amount[0].denom, + format!("ibc/{}/{}", dst_channel, funds.denom) + ); + + // We make sure the balance of the sender has changed as well + let balances = app1 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_owner.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + assert!(balances.amount.is_empty()); + + Ok(()) +} + +#[test] +fn transfer_and_back() -> anyhow::Result<()> { + let funds = coin(100_000, "ufund"); + + let port1 = "transfer".to_string(); + let port2 = "transfer".to_string(); + + let mut app1 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("src")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + // We add a start balance for the owner + let fund_owner = app1.api().addr_make("owner"); + let fund_recipient = app2.api().addr_make("recipient"); + app1.init_modules(|router, _api, storage| { + router + .bank + .init_balance(storage, &fund_owner, vec![funds.clone()]) + .unwrap(); + }); + + let (src_connection_id, _) = create_connection(&mut app1, &mut app2)?; + + // We start by creating channels + let ChannelCreationResult { + src_channel, + dst_channel, + .. + } = create_channel( + &mut app1, + &mut app2, + src_connection_id, + port1, + port2, + "ics20-1".to_string(), + IbcOrder::Ordered, + )?; + + // We send an IBC transfer Cosmos Msg on app 1 + let send_response = app1.execute( + fund_owner.clone(), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: src_channel, + to_address: fund_recipient.to_string(), + amount: funds.clone(), + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 1, + height: app2.block_info().height + 1, + }), + memo: None, + }), + )?; + + // We are relaying all packets found in the transaction. + relay_packets_in_tx(&mut app1, &mut app2, send_response)?; + + // TODO: We can't verify the funds are locked because the IBC_LOCK_MODULE_ADDRESS is not valid. + // let balances = app1 + // .raw_query( + // to_json_binary(&QueryRequest::::Bank(BankQuery::AllBalances { + // address: IBC_LOCK_MODULE_ADDRESS.to_string(), + // }))? + // .as_slice(), + // ) + // .into_result()? + // .unwrap(); + // let balances: AllBalanceResponse = from_json(balances)?; + // assert_eq!(balances.amount.len(), 1); + // assert_eq!(balances.amount[0].amount, funds.amount); + // assert_eq!(balances.amount[0].denom, funds.denom); + + let chain2_funds = coin( + funds.amount.u128(), + format!("ibc/{}/{}", dst_channel, funds.denom), + ); + // We send an IBC transfer back from app2 + let send_back_response = app2.execute( + fund_recipient.clone(), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: dst_channel, + to_address: fund_owner.to_string(), + amount: chain2_funds, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 1, + height: app2.block_info().height + 100, + }), + memo: None, + }), + )?; + + // We are relaying all packets found in the transaction. + relay_packets_in_tx(&mut app2, &mut app1, send_back_response)?; + + // We make sure the balance of the recipient has changed + let balances = app2 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_recipient.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + assert!(balances.amount.is_empty()); + + // We make sure the balance of the sender has changed as well. + let balances = app1 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_owner.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + + // The owner has back exactly what they need. + assert_eq!(balances.amount.len(), 1); + assert_eq!(balances.amount[0].amount, funds.amount); + assert_eq!(balances.amount[0].denom, funds.denom); + + // TODO: We can't verify the funds are locked because the IBC_LOCK_MODULE_ADDRESS is not valid. + // // Same for ibc lock address + // let balances = app1 + // .raw_query( + // to_json_binary(&QueryRequest::::Bank(BankQuery::AllBalances { + // address: IBC_LOCK_MODULE_ADDRESS.to_string(), + // }))? + // .as_slice(), + // ) + // .into_result()? + // .unwrap(); + // let balances: AllBalanceResponse = from_json(balances)?; + // assert_eq!(balances.amount.len(), 0); + + Ok(()) +} diff --git a/tests/test_ibc/mod.rs b/tests/test_ibc/mod.rs new file mode 100644 index 00000000..f201a944 --- /dev/null +++ b/tests/test_ibc/mod.rs @@ -0,0 +1,146 @@ +#![cfg(feature = "stargate")] + +mod bank; +mod timeout; + +use cosmwasm_std::{ + from_json, to_json_binary, ChannelResponse, Empty, IbcChannel, IbcEndpoint, IbcOrder, IbcQuery, + Querier, QueryRequest, +}; +use cw_multi_test::{ + ibc::{ + relayer::{create_channel, create_connection, ChannelCreationResult}, + IbcSimpleModule, + }, + no_init, AppBuilder, +}; + +#[test] +fn channel_creation() -> anyhow::Result<()> { + // Here we want to create a channel between 2 bank modules to make sure + // that we are able to create a channel correctly. + // This is a tracking test for all channel creation. + let mut app1 = AppBuilder::default() + .with_ibc(IbcSimpleModule) + .build(no_init); + let mut app2 = AppBuilder::default() + .with_ibc(IbcSimpleModule) + .build(no_init); + + app1.update_block(|block| block.chain_id = "mock_app_1".to_string()); + app2.update_block(|block| block.chain_id = "mock_app_2".to_string()); + + let src_port = "transfer".to_string(); + let dst_port = "transfer".to_string(); + let order = IbcOrder::Unordered; + let version = "ics20-1".to_string(); + + let (src_connection_id, _) = create_connection(&mut app1, &mut app2)?; + + let ChannelCreationResult { + src_channel, + dst_channel, + .. + } = create_channel( + &mut app1, + &mut app2, + src_connection_id, + src_port.clone(), + dst_port.clone(), + version.clone(), + order.clone(), + )?; + + let channel_query = app1 + .raw_query( + to_json_binary(&QueryRequest::::Ibc(IbcQuery::Channel { + channel_id: src_channel.clone(), + port_id: Some(src_port.clone()), + }))? + .as_slice(), + ) + .into_result()? + .unwrap(); + + let channel: ChannelResponse = from_json(channel_query)?; + + assert_eq!( + channel, + ChannelResponse::new(Some(IbcChannel::new( + IbcEndpoint { + port_id: src_port.clone(), + channel_id: src_channel.clone() + }, + IbcEndpoint { + port_id: dst_port.clone(), + channel_id: dst_channel.clone() + }, + order.clone(), + version.clone(), + "connection-0" + ))) + ); + + let channel_query = app2 + .raw_query( + to_json_binary(&QueryRequest::::Ibc(IbcQuery::Channel { + channel_id: dst_channel.clone(), + port_id: Some(dst_port.clone()), + }))? + .as_slice(), + ) + .into_result()? + .unwrap(); + + let channel: ChannelResponse = from_json(channel_query)?; + + assert_eq!( + channel, + ChannelResponse::new(Some(IbcChannel::new( + IbcEndpoint { + port_id: dst_port, + channel_id: dst_channel + }, + IbcEndpoint { + port_id: src_port, + channel_id: src_channel + }, + order, + version, + "connection-0" + ))) + ); + + Ok(()) +} + +#[test] +fn channel_unknown_port() -> anyhow::Result<()> { + // Here we want to create a channel between 2 bank modules to make sure + // that we are able to create a channel correctly. + // This is a tracking test for all channel creation. + let mut app1 = AppBuilder::default() + .with_ibc(IbcSimpleModule) + .build(no_init); + let mut app2 = AppBuilder::default() + .with_ibc(IbcSimpleModule) + .build(no_init); + + let port1 = "other-bad-port".to_string(); + let port2 = "bad-port".to_string(); + + let (src_connection_id, _) = create_connection(&mut app1, &mut app2)?; + + create_channel( + &mut app1, + &mut app2, + src_connection_id, + port1, + port2, + "ics20-1".to_string(), + IbcOrder::Ordered, + ) + .unwrap_err(); + + Ok(()) +} diff --git a/tests/test_ibc/timeout.rs b/tests/test_ibc/timeout.rs new file mode 100644 index 00000000..aa6eec03 --- /dev/null +++ b/tests/test_ibc/timeout.rs @@ -0,0 +1,242 @@ +use cosmwasm_std::{ + coin, from_json, testing::MockApi, to_json_binary, Addr, AllBalanceResponse, BankQuery, + CosmosMsg, Empty, IbcMsg, IbcOrder, IbcTimeout, IbcTimeoutBlock, Querier, QueryRequest, +}; +use cw_multi_test::{ + ibc::{ + events::TIMEOUT_RECEIVE_PACKET_EVENT, + relayer::{ + create_channel, create_connection, has_event, relay_packets_in_tx, + ChannelCreationResult, RelayingResult, + }, + types::{ChannelInfo, MockIbcQuery}, + IbcSimpleModule, + }, + no_init, AppBuilder, Executor, +}; + +#[test] +fn simple_transfer_timeout() -> anyhow::Result<()> { + let funds = coin(100_000, "ufund"); + + let mut app1 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("src")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + // We add a start balance for the owner. + let fund_owner = app1.api().addr_make("owner"); + let fund_recipient = app2.api().addr_make("recipient"); + app1.init_modules(|router, _api, storage| { + router + .bank + .init_balance(storage, &fund_owner, vec![funds.clone()]) + .unwrap(); + }); + + let port1 = "transfer".to_string(); + let port2 = "transfer".to_string(); + + let (src_connection_id, _) = create_connection(&mut app1, &mut app2)?; + + // We start by creating channels. + let ChannelCreationResult { src_channel, .. } = create_channel( + &mut app1, + &mut app2, + src_connection_id, + port1, + port2, + "ics20-1".to_string(), + IbcOrder::Ordered, + )?; + + // We send an IBC transfer Cosmos Msg on app 1. + let send_response = app1.execute( + fund_owner.clone(), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: src_channel, + to_address: fund_recipient.to_string(), + amount: funds.clone(), + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 1, + height: app2.block_info().height, // this will have the effect of a timeout when relaying the packets + }), + memo: None, + }), + )?; + + // We assert the sender balance is empty ! + + // We make sure the balance of the sender hasn't changed in the process. + let balances = app1 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_owner.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + assert!(balances.amount.is_empty()); + + // We are relaying all packets found in the transaction. + let resp = relay_packets_in_tx(&mut app1, &mut app2, send_response)?; + + // We make sure the response contains a timeout. + assert_eq!(resp.len(), 1); + if let RelayingResult::Acknowledgement { .. } = resp[0].result { + panic!("Expected a timeout"); + } + assert!(has_event(&resp[0].receive_tx, TIMEOUT_RECEIVE_PACKET_EVENT)); + + // We make sure the balance of the recipient has not changed. + let balances = app2 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_recipient.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + + // The recipient has exactly no balance, because it has timed out. + assert_eq!(balances.amount.len(), 0); + + // We make sure the balance of the sender hasn't changed in the process. + let balances = app1 + .raw_query( + to_json_binary(&QueryRequest::::Bank( + #[allow(deprecated)] + BankQuery::AllBalances { + address: fund_owner.to_string(), + }, + ))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + println!("{:?}", balances); + assert_eq!(balances.amount.len(), 1); + assert_eq!(balances.amount[0].amount, funds.amount); + assert_eq!(balances.amount[0].denom, funds.denom); + Ok(()) +} + +#[test] +fn simple_transfer_timeout_closes_channel() -> anyhow::Result<()> { + let funds = coin(100_000, "ufund"); + + let mut app1 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("src")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(no_init); + + // We add a start balance for the owner + let fund_owner = app1.api().addr_make("owner"); + let fund_recipient = app2.api().addr_make("recipient"); + app1.init_modules(|router, _api, storage| { + router + .bank + .init_balance(storage, &fund_owner, vec![funds.clone()]) + .unwrap(); + }); + + let port1 = "transfer".to_string(); + let port2 = "transfer".to_string(); + + let (src_connection_id, _) = create_connection(&mut app1, &mut app2)?; + + // We start by creating channels + let ChannelCreationResult { + src_channel, + dst_channel, + .. + } = create_channel( + &mut app1, + &mut app2, + src_connection_id, + port1.clone(), + port2.clone(), + "ics20-1".to_string(), + IbcOrder::Ordered, + )?; + + // We send an IBC transfer Cosmos Msg on app 1. + let send_response = app1.execute( + Addr::unchecked(fund_owner), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: src_channel.clone(), + to_address: fund_recipient.to_string(), + amount: funds.clone(), + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 1, + height: app2.block_info().height, // this will have the effect of a timeout when relaying the packets + }), + memo: None, + }), + )?; + + // We make sure the channel is open. + let channel_info: ChannelInfo = from_json(app1.ibc_query(MockIbcQuery::ChannelInfo { + port_id: port1.clone(), + channel_id: src_channel.clone(), + })?)?; + assert!(channel_info.open); + // We make sure the channel is open. + let channel_info: ChannelInfo = from_json(app2.ibc_query(MockIbcQuery::ChannelInfo { + port_id: port2.clone(), + channel_id: dst_channel.clone(), + })?)?; + assert!(channel_info.open); + + // We are relaying all packets found in the transaction. + let resp = relay_packets_in_tx(&mut app1, &mut app2, send_response)?; + + // We make sure the response contains a timeout. + assert_eq!(resp.len(), 1); + match resp[0].result.clone() { + RelayingResult::Acknowledgement { .. } => panic!("Expected a timeout"), + RelayingResult::Timeout { + close_channel_confirm, + .. + } => { + // We make sure the confirmation of close transaction was executed. + assert!(close_channel_confirm.is_some()) + } + } + + // We make sure the channel is closed. + let channel_info: ChannelInfo = from_json(app1.ibc_query(MockIbcQuery::ChannelInfo { + port_id: port1, + channel_id: src_channel, + })?)?; + assert!(!channel_info.open); + // We make sure the channel is closed. + let channel_info: ChannelInfo = from_json(app2.ibc_query(MockIbcQuery::ChannelInfo { + port_id: port2, + channel_id: dst_channel, + })?)?; + assert!(!channel_info.open); + + Ok(()) +}