-
Notifications
You must be signed in to change notification settings - Fork 588
Cosmwasm and IBC
This is not an in-depth tutorial of CosmWasm, just a collection of (hopefully) useful resources to dive into it and some examples to understand how CosmWasm interacts with IBC.
- CosmWasm documentation.
- CosmWasm resources.
- Talk by Ethan Frey at HackATOM Seoul 2022.
- CosmWasm zero to hero.
- CosmWasm Plus.
Usefult sections of the documentation to understand at a high level how to write a contract are these:
This document explains what interfaces need to be implemented in a contract to enable IBC communication. Basically you need to expose 6 entry points:
-
ibc_channel_open
to handle theMsgChannelOpenInit
andMsgChannelOpenTry
steps of the channel handshake. -
ibc_channel_connect
to handle theMsgChannelOpenAck
andMsgChannelOpenConfirm
steps of the channel handshake. -
ibc_channel_close
to handle channel closure. -
ibc_packet_receive
to handleMsgRecvPacket
. -
ibc_packet_ack
to handleMsgAcknowledgement
. -
ibc_packet_timeout
to handleMsgTimeout
.
CosmWasm also provide an IbcMsg::Transfer
that can be used to do ICS20 transfers in a contract.
We are going to deploy the CW20-ICS20 contract and send CW20 tokens to a counterparty ICS20 transfer
application module.
- Two chains:
chain1
runs wasmd andchain2
runs ib-go'ssimd
binary. -
chain1
's RPC endpoint runs onhttp://localhost:27000
and REST API runs onhttp://localhost:27001
. -
chain2
's RPC endpoint runs onhttp://localhost:27010
and REST API runs onhttp://localhost:27011
. - hermes relayer (configured with
clear_interval = 1
because at the time of writing hermes wouldn't relay the packets sent from a CosmWasm contract because they don't emit a web socket event tagged withmodule = ibc
- see this issue in hermes repository for more details).
At the time of writing the versions used were:
Use the instructions here to build it. The file produced is cw20_ics20.wasm
. Next we are going to deploy, instantiate and interact with the contract. The steps followed in the next sections follow pretty much what's described here.
We use the address wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u
on the wasmd chain to deploy the contract.
> wasmd tx wasm store cw20_ics20.wasm \
--from wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u \
--gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \
--keyring-backend test \
--chain-id chain1 \
--home ../../gm/chain1 \
--node http://localhost:27000
Once deployed we can query the list of codes that have been uploaded to the chain:
> wasmd query wasm list-code --node http://localhost:27000
code_infos:
- code_id: "1"
creator: wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u
data_hash: 212211560D8280F7C699059E62B9A5C64673A032CB6AAE4D33148FAA013CDED5
instantiate_permission:
address: ""
addresses: []
permission: Everybody
pagination:
next_key: null
total: "0"
We see that our contract has code_id
1 and was created by wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u
. We can retrieve more information for this particular code_id
:
> wasmd query wasm list-contract-by-code 1 --node http://localhost:27000
contracts: []
pagination:
next_key: null
total: "0"
The response contains an empty list of contracts
since we have not instantiated any contract yet. We can now create an instance of the wasm contract. Following the instantiation, we can repeat this query and receive non-empty responses.
We instantiate the contract by executing the InitMsg
:
# Prepare the instantiation message
INIT='{"default_timeout":300,"gov_contract":"wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u","allowlist":[]}'
# Instantiate the contract
> wasmd tx wasm instantiate 1 "$INIT" \
--label "cw20-ics20" \
--no-admin \
--from wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u \
--keyring-backend test \
--chain-id chain1 \
--home ../../gm/chain1 \
--node http://localhost:27000
Once instantiated we can confirm that the contract has one instance:
> wasmd query wasm list-contract-by-code 1 \
--chain-id chain1 \
--home ../../gm/chain1 \
--node http://localhost:27000
contracts:
- wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
pagination:
next_key: null
total: "0"
The address of the instance of the contract is wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
.
We can retrieve more information about this instance:
# See the contract details
> wasmd query wasm contract wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--node http://localhost:27000
address: wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
contract_info:
admin: ""
code_id: "1"
created: null
creator: wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u
extension: null
ibc_port_id: wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
label: cw20-ics20
Since this is an IBC-enabled contract it has an ibc_port_id
.
First we create a channel between the instance of the contract on chain1
and the ICS20 application on chain2
.
> hermes --config config.toml create channel \
--a-chain chain1 --b-chain chain2 \
--a-port wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--b-port transfer \
--channel-version 'ics20-1' \
--new-client-connection
The channel created on both chains has channel ID channel-0
.
Once the channel is created we can send 100samoleans
to a destination address on chain2
by executing the TransferMsg
:
# Prepare the transfer message
EXECUTE='{"transfer":{"channel":"channel-0","remote_address":"cosmos17jaf89qy8lhhukhde9kptwdj65qzz5q8nqasar"}}'
# fund the contract with 100samoleans and execute the message
./wasmd tx wasm execute wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d "$EXECUTE" \
--amount 100samoleans \
--from wasm1knws0ku0x7hxd3z5vd7scz5qcjce6ts48nu29u \
--keyring-backend test \
--chain-id chain1 \
--node http://localhost:27000 \
--home ../../gm/chain1
We now start hermes to relay the message:
./hermes --config config.toml start
After the message is relayed we can query the balance of the destination address on chain2
:
> simd q bank balances cosmos17jaf89qy8lhhukhde9kptwdj65qzz5q8nqasar --node http://localhost:27010
balances:
- amount: "100"
denom: ibc/27A6394C3F9FF9C9DCF5DFFADF9BB5FE9A37C7E92B006199894CF1824DF9AC7C
- amount: "100000000"
denom: samoleans
- amount: "99998862"
denom: stake
pagination:
next_key: null
total: "0"
And it has 100ibc/27A6394C3F9FF9C9DCF5DFFADF9BB5FE9A37C7E92B006199894CF1824DF9AC7C
. This vouchers correspond to the tokens sent from chain1
:
>simd q ibc-transfer denom-trace ibc/27A6394C3F9FF9C9DCF5DFFADF9BB5FE9A37C7E92B006199894CF1824DF9AC7C \
--node http://localhost:27010
denom_trace:
base_denom: samoleans
path: transfer/channel-0
And now if we want we can also send the vouchers back to the contract:
./simd tx ibc-transfer transfer transfer channel-0 wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d 100ibc/27A6394C3F9FF9C9DCF5DFFADF9BB5FE9A37C7E92B006199894CF1824DF9AC7C \
--from cosmos17jaf89qy8lhhukhde9kptwdj65qzz5q8nqasar \
--keyring-backend test \
--home ../../gm/chain2 \
--chain-id chain2 \
--node http://localhost:27010
Wait for the packet to be relayed and check the balance of the account on chain2
:
> simd q bank balances cosmos17jaf89qy8lhhukhde9kptwdj65qzz5q8nqasar
--node http://localhost:27010
balances:
- amount: "100000000"
denom: samoleans
- amount: "99998708"
denom: stake
pagination:
next_key: null
total: "0"
The vouchers are gone. We can check the balance of the contract:
> wasmd query bank balances wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--node http://localhost:27000
balances:
- amount: "100"
denom: samoleans
pagination:
next_key: null
total: "0"
Without going into much details, the part that I think it's most interesting is the function that executes the TransferMsg
. In that function an Ics20Packet
is prepared and included as the binary data in the SendPacket
message (similar to what a Cosmos-SDK IBC application would do by calling SendPacket
and passing the packet data).
This is a simple contract that should be deployed on two chains. When prompted on one chain it will send a message to the other chain to increment a counter associated with a specific channel opened between both contracts. Let's explore a bit in the next sections how it works.
The state of the contract is simply a map with the channel ID as key and the counter for that channel as value.
The messages to instantiate the contract, execute the increment message on the contract, send the increment message to the counterparty and query the contract are defined here.
It is possible to query the contract instance to retrieve the current value of the counter.
The IBC functions are implemented here. In ibc_channel_open
and ibc_channel_connect
some simple channel ordering and channel version validation is performed.
This contract only sends one type of packet data (i.e. the Increment
message). When the Increment
message for a particular channel is executed on a contract instance, a response is created that contains an IbcMsg::SendPacket
message with the Increment
message to execute on the counterparty.
When the contract instance on the counterparty chain receives the Increment
message, the ibc_packet_receive
function is executed. Eventually the code tries to increment the counter in the contract instance's state and returns an IbcReceiveResponse
that sets the acknowledgement.
The processing of the acknowledgment happens in ibc_packet_ack
.
This is a contract written to gain hands-on experience implementing a simple IBC contract. The contract should be deployed on two chains and it allows to send text messages from one contract to the other. The latest message received is stored in state. The state of the contract also tracks the number of successfully messages sent and received. The state of the contract instance can be queried for a particular channel ID.
Disclaimer: None of the code shown here is production ready!
First we generate a boilerplate contract project:
cargo install cargo-generate --features vendored-openssl
cargo generate --git https://github.com/CosmWasm/cw-template.git --name ibc-messenger
Then in Cargo.toml
it is important to the stargate
feature of the cosmwasm-std
dependency:
cosmwasm-std = { version = "1.1.3", features = ["stargate"] }
use cosmwasm_schema::cw_serde;
use cw_storage_plus::Map;
#[cw_serde]
#[derive(Default)]
pub struct State {
// count of messages successfully sent and received by counterparty
pub count_sent: u32,
// count of messages received
pub count_received: u32,
// latest received message
pub latest_message: Option<String>,
}
// map with channel_id as key and State as value
pub const CHANNEL_STATE: Map<String, State> = Map::new("channel_state");
use cosmwasm_schema::{cw_serde, QueryResponses};
#[cw_serde]
pub struct InstantiateMsg {}
#[cw_serde]
pub enum ExecuteMsg {
SendMessage { channel: String, message: String },
}
#[cw_serde]
pub enum IbcExecuteMsg {
Message { message: String },
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
/// Return the state for a particular channel
#[returns(GetStateResponse)]
GetState {
// the ID of the channel to query its state
channel: String,
},
}
#[cw_serde]
pub struct GetStateResponse {
pub count_sent: u32,
pub count_received: u32,
pub latest_message: Option<String>,
}
use cosmwasm_std::StdError;
use thiserror::Error;
/// Never is a placeholder to ensure we don't return any errors
#[derive(Error, Debug)]
pub enum Never {}
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("no_state")]
NoState {},
#[error("only unordered channels are supported")]
OrderedChannel {},
#[error("invalid IBC channel version. Got ({actual}), expected ({expected})")]
InvalidVersion { actual: String, expected: String },
}
use cosmwasm_std::{to_binary, Binary};
use cosmwasm_schema::cw_serde;
#[cw_serde]
pub enum Ack {
Result(Binary),
Error(String),
}
pub fn make_ack_success() -> Binary {
let res = Ack::Result(b"1".into());
to_binary(&res).unwrap()
}
pub fn make_ack_fail(err: String) -> Binary {
let res = Ack::Error(err);
to_binary(&res).unwrap()
}
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
from_binary, DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg,
IbcChannelConnectMsg, IbcChannelOpenMsg, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg,
IbcPacketTimeoutMsg, IbcReceiveResponse,
};
use crate::{
ack::{make_ack_fail, make_ack_success, Ack},
contract::execute::try_receive_message,
error::Never,
msg::IbcExecuteMsg,
ContractError,
state::{State, CHANNEL_STATE},
};
pub const IBC_VERSION: &str = "messenger-1";
/// Handles the `OpenInit` and `OpenTry` parts of the IBC handshake
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
_deps: DepsMut,
_env: Env,
msg: IbcChannelOpenMsg,
) -> Result<(), ContractError> {
validate_order_and_version(msg.channel(), msg.counterparty_version())
}
/// Handles the `OpenAck` and `OpenConfirm` parts of the IBC handshake
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_connect(
deps: DepsMut,
_env: Env,
msg: IbcChannelConnectMsg,
) -> Result<IbcBasicResponse, ContractError> {
validate_order_and_version(msg.channel(), msg.counterparty_version())?;
// initialize the state for this channel
let channel = msg.channel().endpoint.channel_id.clone();
let state = State {
count_sent: 0,
count_received: 0,
latest_message: None,
};
CHANNEL_STATE.save(deps.storage, channel.clone(), &state)?;
Ok(IbcBasicResponse::new()
.add_attribute("action", "channel_connect")
.add_attribute("channel_id", channel.clone()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_close(
_deps: DepsMut,
_env: Env,
_msg: IbcChannelCloseMsg,
) -> Result<IbcBasicResponse, ContractError> {
unimplemented!();
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(
deps: DepsMut,
env: Env,
msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, Never> {
match do_ibc_packet_receive(deps, env, msg) {
Ok(response) => Ok(response),
Err(error) => Ok(IbcReceiveResponse::new()
.add_attribute("action", "packet_receive")
.add_attribute("error", error.to_string())
// on error write an error ack
.set_ack(make_ack_fail(error.to_string()))),
}
}
pub fn do_ibc_packet_receive(
deps: DepsMut,
_env: Env,
msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, ContractError> {
// the channel ID this packet is being relayed along on this chain
let channel = msg.packet.dest.channel_id.clone();
let msg: IbcExecuteMsg = from_binary(&msg.packet.data)?;
match msg {
IbcExecuteMsg::Message { message } => execute_receive_message(deps, channel, message),
}
}
fn execute_receive_message(deps: DepsMut, channel: String, message: String) -> Result<IbcReceiveResponse, ContractError> {
let state = try_receive_message(deps, channel.clone(), message)?;
Ok(IbcReceiveResponse::new()
.add_attribute("action", "receive_message")
.add_attribute("channel_id", channel.clone())
.add_attribute("count_received", state.count_received.to_string())
.add_attribute("latest_message", state.latest_message.unwrap_or_default())
// write a success ack
.set_ack(make_ack_success()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_ack(
deps: DepsMut,
_env: Env,
ack: IbcPacketAckMsg,
) -> Result<IbcBasicResponse, ContractError> {
// load channel state
let channel = ack.original_packet.src.channel_id.clone();
let mut state = CHANNEL_STATE.load(deps.storage, channel.clone())?;
// check acknowledgement data
let acknowledgement: Ack = from_binary(&ack.acknowledgement.data)?;
match acknowledgement {
// for a success ack we increment the count of sent messages and save state
Ack::Result(_) => {
state.count_sent += 1;
CHANNEL_STATE.save(deps.storage, channel.clone(), &state)?;
},
// for an error ack we don't do anything and let the count of sent messages as it was
Ack::Error(_) => {},
}
Ok(IbcBasicResponse::new()
.add_attribute("action", "acknowledge")
.add_attribute("channel_id", channel.clone())
.add_attribute("count_sent", state.count_sent.to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_timeout(
_deps: DepsMut,
_env: Env,
_msg: IbcPacketTimeoutMsg,
) -> Result<IbcBasicResponse, ContractError> {
Ok(IbcBasicResponse::new().add_attribute("action", "timeout"))
}
pub fn validate_order_and_version(
channel: &IbcChannel,
counterparty_version: Option<&str>,
) -> Result<(), ContractError> {
// We expect an unordered channel here. Ordered channels have the
// property that if a message is lost the entire channel will stop
// working until you start it again.
if channel.order != IbcOrder::Unordered {
return Err(ContractError::OrderedChannel {});
}
if channel.version != IBC_VERSION {
return Err(ContractError::InvalidVersion {
actual: channel.version.to_string(),
expected: IBC_VERSION.to_string(),
});
}
// Make sure that we're talking with a counterparty who speaks the
// same "protocol" as us.
//
// For a connection between chain A and chain B being established
// by chain A, chain B knows counterparty information during
// `OpenTry` and chain A knows counterparty information during
// `OpenAck`. We verify it when we have it but when we don't it's
// alright.
if let Some(counterparty_version) = counterparty_version {
if counterparty_version != IBC_VERSION {
return Err(ContractError::InvalidVersion {
actual: counterparty_version.to_string(),
expected: IBC_VERSION.to_string(),
});
}
}
Ok(())
}
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
to_binary, Binary, Deps, DepsMut, Env,
IbcMsg, IbcTimeout, MessageInfo, Response, StdResult
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{InstantiateMsg, ExecuteMsg, IbcExecuteMsg, QueryMsg, GetStateResponse};
use crate::state::{State, CHANNEL_STATE};
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:ibc-messenger";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new()
.add_attribute("action", "instantiate"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::SendMessage { channel, message} => execute::try_send_message(deps, env, channel, message),
}
}
pub mod execute {
use super::*;
pub fn try_send_message(_deps: DepsMut, env: Env, channel: String, message: String) -> Result<Response, ContractError> {
Ok(Response::new()
.add_attribute("action", "send_message")
.add_attribute("channel_id", channel.clone())
// outbound IBC message, where packet is then received on other chain
.add_message(IbcMsg::SendPacket {
channel_id: channel,
data: to_binary(&IbcExecuteMsg::Message { message })?,
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(300)),
}))
}
/// Called on IBC packet receive in other chain
pub fn try_receive_message(deps: DepsMut, channel: String, message: String) -> Result<State, ContractError> {
CHANNEL_STATE.update(deps.storage, channel, |state| -> Result<_, ContractError> {
match state {
Some(mut s) => {
s.count_received += 1;
s.latest_message = Some(message);
Ok(s)
}
None => Err(ContractError::NoState {}),
}
})
}
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetState { channel } => to_binary(&query_state(deps, channel.clone())?),
}
}
pub fn query_state(deps: Deps, channel: String) -> StdResult<GetStateResponse> {
let state = CHANNEL_STATE.load(deps.storage, channel.clone())?;
Ok(GetStateResponse {
count_sent: state.count_sent,
count_received: state.count_received,
latest_message: state.latest_message
})
}
#[cfg(test)]
mod tests {}
pub mod ack;
pub mod contract;
mod error;
pub mod helpers;
pub mod ibc;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
Execute from the root of the project the following command:
> RUSTFLAGS='-C link-arg=-s' cargo wasm
The build process generates a file called ibc_messenger.wasm
.
We deploy this contract on both chain1
and chain2
. Both chains are now running the wasmd
binary.
On chain1
:
> wasmd tx wasm store ibc_messenger.wasm \
--from wasm1yduh76rkea0sjh3u944dakzm2x7kwfc3fyvat6 \
--gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \
--keyring-backend test
--chain-id chain1
--home ../../gm/chain1
--node http://localhost:27000
On chain2
:
> wasmd tx wasm store ibc_messenger.wasm \
--from wasm1d0uuqn0q2lv80m5t0tz9cu8sr63drejpwwrkug \
--gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \
--keyring-backend test
--chain-id chain2
--home ../../gm/chain2
--node http://localhost:27010
The code_id
for both contracts is 1 and at the moment they are no instances of these contracts yet.
The initialization message does not contain any parameters, so it is just '{}'
.
On chain1
:
# Instantiate the contract
> wasmd tx wasm instantiate 1 '{}' \
--label "ibc-messenger" \
--no-admin \
--from wasm1yduh76rkea0sjh3u944dakzm2x7kwfc3fyvat6 \
--keyring-backend test
--chain-id chain1
--home ../../gm/chain1
--node http://localhost:27000
Query the contract by code ID:
> wasmd query wasm list-contract-by-code 1 \
--chain-id chain1 \
--home ../../gm/chain1 \
--node http://localhost:27000
contracts:
- wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
pagination:
next_key: null
total: "0"
And the query the contract instance:
> wasmd query wasm contract wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--node http://localhost:27000
address: wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
contract_info:
admin: ""
code_id: "1"
created: null
creator: wasm1yduh76rkea0sjh3u944dakzm2x7kwfc3fyvat6
extension: null
ibc_port_id: wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
label: ibc-messenger
We see that the contract has an ibc_port_id
.
On chain2
:
> wasmd tx wasm instantiate 1 '{}'
--label "ibc-messenger" \
--no-admin \
--from wasm1d0uuqn0q2lv80m5t0tz9cu8sr63drejpwwrkug
--keyring-backend test \
--chain-id chain2 \
--home ../../gm/chain2 \
--node http://localhost:27010
The contract instance has the same address and ibc_port_id
as the contract on chain1
:
> wasmd query wasm list-contract-by-code 1 \
--chain-id chain2 \
--home ../../gm/chain2 \
--node http://localhost:27010
contracts:
- wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
pagination:
next_key: null
total: "0"
> wasmd query wasm contract wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--node http://localhost:27010
address: wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
contract_info:
admin: ""
code_id: "1"
created: null
creator: wasm1d0uuqn0q2lv80m5t0tz9cu8sr63drejpwwrkug
extension: null
ibc_port_id: wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d
label: ibc-messenger
First we create a channel between the instance of the contract on chain1
and the instance on chain2
.
> hermes --config config.toml create channel \
--a-chain chain1 --b-chain chain2 \
--a-port wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--b-port wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d \
--channel-version 'messenger-1' \
--new-client-connection
Once the channel is created (with channel ID channel-0
on both chains) we can execute a message to send a text message from chain1
to chain2
:
# Prepare the send message
EXECUTE='{"send_message":{"channel":"channel-0","message":"hello IBC"}}'
.> wasmd tx wasm execute wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d "$EXECUTE" \
--from wasm1yduh76rkea0sjh3u944dakzm2x7kwfc3fyvat6 \
--keyring-backend test \
--chain-id chain1 \
--home ../../gm/chain1
--node http://localhost:27000 \
We now start hermes to relay the message:
./hermes --config config.toml start
After the message is relayed we can query the state of both instances.
The query message is the same for both chains:
QUERY='{"get_state":{"channel": "channel-0"}}'
On chain1
:
> wasmd query wasm contract-state smart wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d "$QUERY" \
--node http://localhost:27000
data:
count_received: 0
count_sent: 1
latest_message: null
We see that the count_sent
is 1, which means that the message was successfully received on chain2
and the acknowledgement was processed on chain1
. latest_message
is null
because the contract on chain1
has not received any message yet.
On chain2
:
> wasmd query wasm contract-state smart wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d "$QUERY" \
--node http://localhost:27010
data:
count_received: 1
count_sent: 0
latest_message: hello IBC
We see that count_received
is 1, which means that chain2
successfully processed the message and stored the text message in state (as latest_message
being hello IBC
confirms).