diff --git a/Cargo.lock b/Cargo.lock index 66622ae9..90b5556f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -120,9 +120,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.99" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema" -version = "2.0.4" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101d0739564bd34cba9b84bf73665f0822487ae3b29b2dd59930608ed3aafd43" +checksum = "d403dea1175a5b20fd2d29dda180fa9f1391dd46f354a8639391d1e549a99e5e" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "2.0.4" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4be75f60158478da2c5d319ed59295bca1687ad50c18215a0485aa91a995ea" +checksum = "e3c3153038e91080ded2e2554689e802be2a34a24c6e49c039ae94810c99a680" dependencies = [ "proc-macro2", "quote", @@ -248,23 +248,42 @@ dependencies = [ "zeroize", ] +[[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", +] + [[package]] name = "cw-multi-test" version = "2.0.1" dependencies = [ "anyhow", "bech32 0.11.0", + "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", "cw-utils", + "cw20-ics20", "derivative", "hex", "hex-literal", "itertools 0.13.0", + "log", "once_cell", "prost", "schemars", "serde", + "serde_json", "sha2 0.10.8", "thiserror", ] @@ -293,6 +312,53 @@ dependencies = [ "thiserror", ] +[[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", +] + +[[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", +] + [[package]] name = "der" version = "0.7.9" @@ -372,9 +438,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "elliptic-curve" @@ -424,9 +490,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -435,9 +501,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "group" @@ -520,9 +586,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" @@ -532,18 +604,18 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "object" -version = "0.36.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -572,9 +644,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -599,7 +671,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.60", ] [[package]] @@ -638,15 +710,15 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schemars" @@ -669,7 +741,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.66", + "syn 2.0.60", ] [[package]] @@ -686,6 +758,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.203" @@ -712,25 +790,25 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.60", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -806,9 +884,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -832,7 +910,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.60", ] [[package]] @@ -861,6 +939,6 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "zeroize" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index 5635c814..ec60a139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "cw-multi-test" version = "2.0.1" authors = [ "Ethan Frey ", - "Dariusz Depta " + "Dariusz Depta ", ] description = "Testing tools for multi-contract interactions" repository = "https://github.com/CosmWasm/cw-multi-test" @@ -34,6 +34,12 @@ serde = "1.0.203" sha2 = "0.10.8" thiserror = "1.0.61" +serde_json = "1.0.40" +cosmwasm-schema = "2.0.3" +log = "0.4.20" +cw20-ics20 = "2.0.0" +hex = "0.4.3" + [dev-dependencies] hex = "0.4.3" hex-literal = "0.4.1" diff --git a/src/app.rs b/src/app.rs index 39540a69..01cbcc85 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,10 @@ use crate::contracts::Contract; use crate::error::{bail, AnyResult}; use crate::executor::{AppResponse, Executor}; 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, @@ -11,7 +14,7 @@ use crate::prefixed_storage::{ use crate::staking::{Distribution, DistributionKeeper, StakeKeeper, Staking, StakingSudo}; 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, @@ -39,7 +42,7 @@ pub type BasicApp = App< WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >; @@ -56,7 +59,7 @@ pub struct App< Wasm = WasmKeeper, Staking = StakeKeeper, Distr = DistributionKeeper, - Ibc = IbcFailingModule, + Ibc = IbcSimpleModule, Gov = GovFailingModule, Stargate = StargateFailing, > { @@ -92,7 +95,7 @@ impl BasicApp { WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >, @@ -117,7 +120,7 @@ where WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >, @@ -483,6 +486,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. @@ -565,6 +582,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 { @@ -584,6 +603,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. @@ -622,6 +655,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 @@ -687,7 +729,7 @@ where QueryRequest::Bank(req) => self.bank.query(api, storage, &querier, block, req), QueryRequest::Custom(req) => self.custom.query(api, storage, &querier, block, req), QueryRequest::Staking(req) => self.staking.query(api, storage, &querier, block, req), - 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)] QueryRequest::Stargate { path, data } => self .stargate @@ -710,6 +752,96 @@ where SudoMsg::Bank(msg) => self.bank.sudo(api, storage, self, block, msg), SudoMsg::Staking(msg) => self.staking.sudo(api, storage, self, block, msg), SudoMsg::Custom(_) => unimplemented!(), + SudoMsg::Ibc(msg) => self.ibc.sudo(api, storage, self, block, msg), + } + } + + 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), + }, } } } @@ -769,6 +901,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 ad9b0dc5..6b2a5781 100644 --- a/src/app_builder.rs +++ b/src/app_builder.rs @@ -1,9 +1,9 @@ //! AppBuilder helps you set up your test blockchain environment step by step [App]. +use crate::ibc::IbcSimpleModule; use crate::{ App, Bank, BankKeeper, Distribution, DistributionKeeper, FailingModule, Gov, GovFailingModule, - Ibc, IbcFailingModule, Module, Router, StakeKeeper, Staking, Stargate, StargateFailing, Wasm, - WasmKeeper, + Ibc, Module, Router, StakeKeeper, Staking, Stargate, StargateFailing, Wasm, WasmKeeper, }; use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; use cosmwasm_std::{Api, BlockInfo, CustomMsg, CustomQuery, Empty, Storage}; @@ -36,7 +36,7 @@ pub type BasicAppBuilder = AppBuilder< WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, >; @@ -66,7 +66,7 @@ impl Default WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, > @@ -85,7 +85,7 @@ impl WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, > @@ -101,7 +101,7 @@ impl custom: FailingModule::new(), staking: StakeKeeper::new(), distribution: DistributionKeeper::new(), - ibc: IbcFailingModule::new(), + ibc: IbcSimpleModule, gov: GovFailingModule::new(), stargate: StargateFailing, } @@ -117,7 +117,7 @@ impl WasmKeeper, StakeKeeper, DistributionKeeper, - IbcFailingModule, + IbcSimpleModule, GovFailingModule, StargateFailing, > @@ -137,7 +137,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 756254ef..50a69b91 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)] @@ -230,6 +236,7 @@ impl Module for BankKeeper { let res = AllBalanceResponse::new(amount); to_json_binary(&res).map_err(Into::into) } + BankQuery::Balance { address, denom } => { let address = api.addr_validate(&address)?; let all_amounts = self.get_balance(&bank_storage, &address)?; @@ -282,6 +289,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 e3feb726..97ddeaa4 100644 --- a/src/contracts.rs +++ b/src/contracts.rs @@ -5,6 +5,11 @@ use cosmwasm_std::{ from_json, Binary, CosmosMsg, CustomMsg, CustomQuery, Deps, DepsMut, Empty, Env, 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; @@ -33,6 +38,72 @@ where /// Evaluates contract's `migrate` entry-point. fn migrate(&self, deps: DepsMut, env: Env, msg: Vec) -> AnyResult>; + + /// Executes the contract ibc_channel_open endpoint + #[allow(unused)] + fn ibc_channel_open( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelOpenMsg, + ) -> AnyResult { + bail!("No Ibc capabilities on this contract") + } + + /// Executes the contract ibc_channel_connect endpoint + #[allow(unused)] + fn ibc_channel_connect( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelConnectMsg, + ) -> AnyResult> { + bail!("No Ibc capabilities on this contract") + } + + /// Executes the contract ibc_channel_close endpoint + #[allow(unused)] + fn ibc_channel_close( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelCloseMsg, + ) -> AnyResult> { + bail!("No Ibc capabilities on this contract") + } + + /// Executes the contract ibc_packet_receive endpoint + #[allow(unused)] + fn ibc_packet_receive( + &self, + deps: DepsMut, + env: Env, + msg: IbcPacketReceiveMsg, + ) -> AnyResult> { + bail!("No Ibc capabilities on this contract") + } + + /// Executes the contract ibc_packet_acknowledge endpoint + #[allow(unused)] + fn ibc_packet_acknowledge( + &self, + deps: DepsMut, + env: Env, + msg: IbcPacketAckMsg, + ) -> AnyResult> { + bail!("No Ibc capabilities on this contract") + } + + /// Executes the contract ibc_packet_timeout endpoint + #[allow(unused)] + fn ibc_packet_timeout( + &self, + deps: DepsMut, + env: Env, + msg: IbcPacketTimeoutMsg, + ) -> AnyResult> { + bail!("No Ibc capabilities on this contract") + } } #[rustfmt::skip] @@ -40,12 +111,16 @@ mod closures { use super::*; // function types + pub type IbcFn = fn(deps: DepsMut, env: Env, msg: T) -> Result; + 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; // closure types + pub type IbcClosure = Box,Env, T) -> Result>; + 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>>; @@ -135,6 +210,12 @@ pub struct ContractWrapper< E5 = AnyError, T6 = Empty, E6 = AnyError, + E7 = AnyError, + E8 = AnyError, + E9 = AnyError, + E10 = AnyError, + E11 = AnyError, + E12 = AnyError, > where T1: DeserializeOwned, // Type of message passed to `execute` entry-point. T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. @@ -147,7 +228,13 @@ pub struct ContractWrapper< 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`. + E7: Display + Debug + Send + Sync, // Type of error returned from `channel_open` entry-point. + E8: Display + Debug + Send + Sync, // Type of error returned from `channel_connect` entry-point. + E9: Display + Debug + Send + Sync, // Type of error returned from `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. + 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, @@ -156,6 +243,14 @@ pub struct ContractWrapper< sudo_fn: Option>, reply_fn: Option>, migrate_fn: Option>, + + channel_open_fn: Option>, + channel_connect_fn: Option, E8, Q>>, + 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>>, } impl ContractWrapper @@ -182,6 +277,14 @@ where sudo_fn: None, reply_fn: None, migrate_fn: None, + + channel_open_fn: None, + channel_connect_fn: None, + channel_close_fn: None, + + ibc_packet_receive_fn: None, + ibc_packet_ack_fn: None, + ibc_packet_timeout_fn: None, } } @@ -199,6 +302,14 @@ where sudo_fn: None, reply_fn: None, migrate_fn: None, + + channel_open_fn: None, + channel_connect_fn: None, + channel_close_fn: None, + + ibc_packet_receive_fn: None, + ibc_packet_ack_fn: None, + ibc_packet_timeout_fn: None, } } } @@ -237,6 +348,14 @@ where sudo_fn: Some(Box::new(sudo_fn)), reply_fn: self.reply_fn, migrate_fn: self.migrate_fn, + + channel_open_fn: self.channel_open_fn, + channel_connect_fn: self.channel_connect_fn, + channel_close_fn: self.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, } } @@ -256,6 +375,14 @@ where sudo_fn: Some(customize_permissioned_fn(sudo_fn)), reply_fn: self.reply_fn, migrate_fn: self.migrate_fn, + + channel_open_fn: self.channel_open_fn, + channel_connect_fn: self.channel_connect_fn, + channel_close_fn: self.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, } } @@ -274,6 +401,14 @@ where sudo_fn: self.sudo_fn, reply_fn: Some(Box::new(reply_fn)), migrate_fn: self.migrate_fn, + + channel_open_fn: self.channel_open_fn, + channel_connect_fn: self.channel_connect_fn, + channel_close_fn: self.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, } } @@ -292,6 +427,14 @@ where sudo_fn: self.sudo_fn, reply_fn: Some(customize_permissioned_fn(reply_fn)), migrate_fn: self.migrate_fn, + + channel_open_fn: self.channel_open_fn, + channel_connect_fn: self.channel_connect_fn, + channel_close_fn: self.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, } } @@ -311,6 +454,14 @@ where sudo_fn: self.sudo_fn, reply_fn: self.reply_fn, migrate_fn: Some(Box::new(migrate_fn)), + + channel_open_fn: self.channel_open_fn, + channel_connect_fn: self.channel_connect_fn, + channel_close_fn: self.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, } } @@ -330,6 +481,71 @@ where sudo_fn: self.sudo_fn, reply_fn: self.reply_fn, migrate_fn: Some(customize_permissioned_fn(migrate_fn)), + + channel_open_fn: self.channel_open_fn, + channel_connect_fn: self.channel_connect_fn, + channel_close_fn: self.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, + } + } + + /// Adding IBC endpoint 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>, + ) -> ContractWrapper< + T1, + T2, + T3, + E1, + E2, + E3, + C, + Q, + T4, + E4, + E5, + T6, + E6, + E7A, + E8A, + E9A, + E10A, + E11A, + E12A, + > + 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, + { + 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, + + channel_open_fn: Some(Box::new(channel_open_fn)), + channel_connect_fn: Some(Box::new(channel_connect_fn)), + 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)), } } } @@ -443,8 +659,8 @@ where } } -impl Contract - for ContractWrapper +impl Contract + for ContractWrapper where T1: DeserializeOwned, // Type of message passed to `execute` entry-point. T2: DeserializeOwned, // Type of message passed to `instantiate` entry-point. @@ -457,6 +673,12 @@ 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. 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. { @@ -531,4 +753,72 @@ where None => bail!("migrate is not implemented for contract"), } } + + fn ibc_channel_open( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelOpenMsg, + ) -> AnyResult { + match &self.channel_open_fn { + Some(channel_open) => channel_open(deps, env, msg).map_err(|err| anyhow!(err)), + None => bail!("channel open not implemented for contract"), + } + } + fn ibc_channel_connect( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelConnectMsg, + ) -> AnyResult> { + match &self.channel_connect_fn { + Some(channel_connect) => channel_connect(deps, env, msg).map_err(|err| anyhow!(err)), + None => bail!("channel connect not implemented for contract"), + } + } + fn ibc_channel_close( + &self, + deps: DepsMut, + env: Env, + msg: IbcChannelCloseMsg, + ) -> AnyResult> { + match &self.channel_close_fn { + Some(channel_close) => channel_close(deps, env, msg).map_err(|err| anyhow!(err)), + None => bail!("channel close not implemented for contract"), + } + } + + 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| anyhow!(err)), + None => bail!("packet receive not implemented for contract"), + } + } + fn ibc_packet_acknowledge( + &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| anyhow!(err)), + None => bail!("packet ack not implemented for contract"), + } + } + 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| anyhow!(err)), + None => bail!("packet timeout not implemented for contract"), + } + } } diff --git a/src/ibc.rs b/src/ibc.rs index e8442c2b..bdbf321e 100644 --- a/src/ibc.rs +++ b/src/ibc.rs @@ -1,18 +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}; + +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; + +/// 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), + + /// 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 {} +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; +pub type IbcAcceptingModule = AcceptingModule; 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; +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..8639f967 --- /dev/null +++ b/src/ibc/relayer/channel.rs @@ -0,0 +1,232 @@ +use anyhow::Result as AnyResult; +use cosmwasm_std::{from_json, Api, CustomMsg, CustomQuery, IbcEndpoint, IbcOrder, Storage}; +use serde::de::DeserializeOwned; + +use crate::{ + ibc::{ + types::{Connection, MockIbcQuery}, + IbcPacketRelayingMsg, + }, + App, AppResponse, Bank, Distribution, Gov, Ibc, Module, Staking, Wasm, +}; + +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))?; + + log::debug!("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))?; + log::debug!("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))?; + log::debug!("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))?; + log::debug!("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..cb230e13 --- /dev/null +++ b/src/ibc/relayer/packet.rs @@ -0,0 +1,217 @@ +use anyhow::Result as AnyResult; +use cosmwasm_std::{from_json, Api, Binary, CustomMsg, CustomQuery, Storage}; +use serde::de::DeserializeOwned; + +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, Distribution, Gov, Ibc, Module, Staking, SudoMsg, Wasm, +}; + +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(hex::decode(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..3613d653 --- /dev/null +++ b/src/ibc/simple_ibc.rs @@ -0,0 +1,1250 @@ +use anyhow::{anyhow, bail}; +use cosmwasm_std::{ + ensure_eq, 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; + +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; + +#[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", hex::encode(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 its 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", hex::encode(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", hex::encode(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", hex::encode(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(|a| hex::encode(a.as_slice())) + .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 aknowledge, 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 an 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 cosmwasm_std::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 cosmwasm_std::Storage, + router: &dyn crate::CosmosRouter, + block: &cosmwasm_std::BlockInfo, + sender: cosmwasm_std::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 sudo( + &self, + api: &dyn cosmwasm_std::Api, + storage: &mut dyn cosmwasm_std::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) + } + + fn query( + &self, + _api: &dyn cosmwasm_std::Api, + storage: &dyn cosmwasm_std::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 specificed 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), + ))?) + } + 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)?) + } + } + } + + //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..e1f868f6 --- /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<'a> IndexList for ConnectionIndexes<'a> { + 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..7d1de65f --- /dev/null +++ b/src/ibc/types.rs @@ -0,0 +1,252 @@ +use anyhow::bail; +use cosmwasm_std::{ + Addr, Binary, Event, IbcChannel, IbcChannelOpenResponse, IbcEndpoint, IbcOrder, IbcQuery, + IbcTimeout, +}; +use std::{fmt::Display, str::FromStr}; + +use crate::app::IbcModule; + +#[cosmwasm_schema::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, +} + +#[cosmwasm_schema::cw_serde] +#[derive(Default)] +/// IBC Port Info +pub struct PortInfo { + /// Channel id of the next opened channel + pub next_channel_id: u64, +} + +#[cosmwasm_schema::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, +} + +#[cosmwasm_schema::cw_serde] +pub enum ChannelHandshakeState { + Init, + Try, + Ack, + Confirm, +} + +#[cosmwasm_schema::cw_serde] +pub struct ChannelInfo { + pub next_packet_id: u64, + pub last_packet_relayed: u64, + + pub info: IbcChannel, + + pub open: bool, +} + +#[cosmwasm_schema::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 + ) + } +} + +#[cosmwasm_schema::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, +} + +#[cosmwasm_schema::cw_serde] +pub struct IbcPacketReceived { + pub data: IbcPacketData, + /// Indicates wether the packet was received with a timeout + pub timeout: bool, +} + +#[cosmwasm_schema::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 environement +#[cosmwasm_schema::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 +#[cosmwasm_schema::cw_serde] +pub enum IbcResponse { + Open(IbcChannelOpenResponse), + Basic(AppIbcBasicResponse), + Receive(AppIbcReceiveResponse), +} + +#[cosmwasm_schema::cw_serde] +#[derive(Default)] +pub struct AppIbcBasicResponse { + pub events: Vec, +} + +#[cosmwasm_schema::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) + } +} + +#[cosmwasm_schema::cw_serde] +// This extends the cosmwasm std IBC query type with internal tools needed +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 e5d3f24c..9bbac9e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,7 +135,7 @@ pub mod custom_handler; pub mod error; mod executor; mod gov; -mod ibc; +pub mod ibc; mod module; mod prefixed_storage; mod 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 04493cd2..ef5eb876 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; @@ -13,6 +14,11 @@ use cosmwasm_std::{ Querier, QuerierWrapper, Record, Reply, ReplyOn, Response, StdResult, Storage, SubMsg, SubMsgResponse, SubMsgResult, TransactionInfo, WasmMsg, WasmQuery, }; +use cosmwasm_std::{ + IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcChannelOpenResponse, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, + IbcReceiveResponse, +}; use cw_storage_plus::Map; use prost::Message; use schemars::JsonSchema; @@ -167,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. @@ -281,6 +363,134 @@ where self.process_response(api, router, storage, block, msg.contract_addr, res, msgs) } + // 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_acknowledge(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) + } + /// Stores the contract's code in the in-memory lookup table. /// Returns an identifier of the stored contract code. fn store_code(&mut self, creator: Addr, code: Box>) -> u64 { @@ -393,6 +603,41 @@ impl WasmKeeper { Ok(response) } + fn verify_ibc_response(response: IbcBasicResponse) -> AnyResult> + where + T: Clone + std::fmt::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 + std::fmt::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 save_code( &mut self, code_id: u64, @@ -957,6 +1202,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 9001d161..18adb825 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -5,6 +5,7 @@ mod test_app; mod test_app_builder; mod test_attributes; mod test_contract_storage; +mod test_ibc; mod test_module; mod test_prefixed_storage; mod test_staking; diff --git a/tests/test_app_builder/test_with_ibc.rs b/tests/test_app_builder/test_with_ibc.rs index a39ec0a5..ba50ccfc 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, NO_MESSAGE}; -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 {} @@ -46,3 +49,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, NO_MESSAGE); + let ibc_keeper2 = MyIbcKeeper::new(EXECUTE_MSG, QUERY_MSG, NO_MESSAGE); + + // 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..31e0c2a9 --- /dev/null +++ b/tests/test_ibc/bank.rs @@ -0,0 +1,259 @@ +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, + }, + 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(|_, _, _| {}); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(|_, _, _| {}); + + // 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 relaying all packets found in the transaction + relay_packets_in_tx(&mut app1, &mut app2, send_response)?; + + // We make sure the balance of the reciepient has changed + let balances = app2 + .raw_query( + to_json_binary(&QueryRequest::::Bank(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(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(|_, _, _| {}); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(|_, _, _| {}); + + // 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 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 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 reciepient has changed + let balances = app2 + .raw_query( + to_json_binary(&QueryRequest::::Bank(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(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..9fce6769 --- /dev/null +++ b/tests/test_ibc/mod.rs @@ -0,0 +1,144 @@ +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, + }, + AppBuilder, +}; + +mod bank; +mod timeout; + +#[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(|_, _, _| {}); + let mut app2 = AppBuilder::default() + .with_ibc(IbcSimpleModule) + .build(|_, _, _| {}); + + 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(|_, _, _| {}); + let mut app2 = AppBuilder::default() + .with_ibc(IbcSimpleModule) + .build(|_, _, _| {}); + + 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..b15af3b7 --- /dev/null +++ b/tests/test_ibc/timeout.rs @@ -0,0 +1,234 @@ +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, + }, + 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(|_, _, _| {}); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(|_, _, _| {}); + + // 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(BankQuery::AllBalances { + address: fund_owner.to_string(), + }))? + .as_slice(), + ) + .into_result()? + .unwrap(); + let balances: AllBalanceResponse = from_json(balances)?; + assert!(balances.amount.is_empty()); + + // We 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(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(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(|_, _, _| {}); + + let mut app2 = AppBuilder::default() + .with_api(MockApi::default().with_prefix("dst")) + .with_ibc(IbcSimpleModule) + .build(|_, _, _| {}); + + // 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 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 confirm 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(()) +}