Skip to content

Cosmwasm and IBC

Carlos Rodriguez edited this page Dec 13, 2022 · 7 revisions

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.

Table of contents

Resources

Usefult sections of the documentation to understand at a high level how to write a contract are these:

Enable IBC in a contract

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 the MsgChannelOpenInit and MsgChannelOpenTry steps of the channel handshake.
  • ibc_channel_connect to handle the MsgChannelOpenAck and MsgChannelOpenConfirm steps of the channel handshake.
  • ibc_channel_close to handle channel closure.
  • ibc_packet_receive to handle MsgRecvPacket.
  • ibc_packet_ack to handle MsgAcknowledgement.
  • ibc_packet_timeout to handle MsgTimeout.

CosmWasm also provide an IbcMsg::Transfer that can be used to do ICS20 transfers in a contract.

Sample IBC contracts

CW20-ICS20

We are going to deploy the CW20-ICS20 contract and send CW20 tokens to a counterparty ICS20 transfer application module.

Setup

  • Two chains: chain1 runs wasmd and chain2 runs ib-go's simd binary.
  • chain1's RPC endpoint runs on http://localhost:27000 and REST API runs on http://localhost:27001.
  • chain2's RPC endpoint runs on http://localhost:27010 and REST API runs on http://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 with module = ibc - see this issue in hermes repository for more details).

At the time of writing the versions used were:

Building the contract

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.

Deploying the contract

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.

Instantiating the contract

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.

Interacting with the contract

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"

Contract code

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).

IBC counter

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.

State

The state of the contract is simply a map with the channel ID as key and the counter for that channel as value.

Messages

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.

Queries

It is possible to query the contract instance to retrieve the current value of the counter.

IBC functions

The IBC functions are implemented here. In ibc_channel_open and ibc_channel_connect some simple channel ordering and channel version validation is performed.

Sending a packet

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.

Receiving and acknowledging a packet

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.

Processing the acknowledgement

The processing of the acknowledgment happens in ibc_packet_ack.

IBC messenger

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!

Contract code

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"] }

src/state.rs

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");

src/msg.rs

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>,
}

src/error.rs

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 },
}

src/ack.rs

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()
}

src/ibc.rs

#[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(())
}

src/contract.rs

#[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 {}

src/lib.rs

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;

Building the contract

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.

Deploying the contract

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.

Instantiating the contract

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

Interacting with the contract

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.

Querying the contract

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).