How to Set Up Cross-Chain Messaging Between EVM and Cosmos

How to Set Up Cross-Chain Messaging Between EVM and Cosmos

Cross-chain messaging allows seamless communication and interaction between different blockchain ecosystems, from Ethereum Virtual Machine (EVM)-based chains to Cosmos-based chains.

Axelar has long been the best way to connect EVM and Cosmos chains via bridged assets. With General Message Passing, that connection goes beyond bridging, supporting a new generation of cross-chain applications that combine the best of Cosmos and EVM.

In this tutorial, you will learn how to:

  • Build and deploy an EVM smart contract on Avalanche

  • Build and deploy a CosmWasm contract on Osmosis

  • Send a message from a CosmWasm contract to Avalanche

  • Read a message from an EVM smart contract on Avalanche

  • Send a message from EVM to a CosmWasm contract on Osmosis

  • Read a message from a CosmWasm contract on Osmosis

Prerequisites

git clone https://github.com/osmosis-labs/osmosis.git
cd osmosis
make build
  • Create a Wallet

    • If you don't have a wallet yet, create one using the following command:
osmosisd keys add wallet

What is a CosmWasm contract?

CosmWasm is a smart contracting platform designed for the Cosmos ecosystem. In simple terms, it utilizes WebAssembly (Wasm) in the Cosmos (Cosm) way.

CosmWasm contracts provide advanced smart contract capabilities within the Cosmos ecosystem, leveraging the performance and security advantages of WebAssembly and Rust. This allows developers to create complex, interoperable, and secure decentralized applications across various Cosmos-based blockchains.

What is Axelar Cosmos general message passing (GMP)

General Message Passing (GMP) empowers developers with the ability to build interchain-native applications that make cross-chain function calls and synchronize state in a way that is completely abstracted for the user.

In simpler terms, Axelar GMP enables developers to build applications that integrate functions (such as smart contracts) hosted on various connected chains, similar to using Lego bricks.

Axelar has expanded General Message Passing (GMP) to support Cosmos blockchains. With Axelar, you can now send and receive messages on EVM chains and Cosmos chains. Messages sent to Cosmos chains can be received by CosmWasm smart contracts (on blockchains with CosmWasm support), or those messages can be received natively at the consensus layer as part of Go code.

Cosmos GMP works by sending and receiving through IBC’s memo field. Cosmos chains that support GMP should integrate the appropriate middleware and verify the message source.

Project setup and installation

To start the project setup and installation quickly, clone this project on GitHub using the following command:

git clone https://github.com/axelarnetwork/send-message-from-cosmos-to-evm-example.git

Make sure you're on the start branch using the following command:

git checkout start

Next, change the directory into the cloned folder and install the project locally using npm with the following command:

cd send-message-from-cosmos-to-evm-example && npm i

Create a .env file

Run the following command to create an .env file:

touch .env

Add your axl private key to .env

Export your osmo prefix address private key and add it to the .env file you just created:

PRIVATE_KEY= // Add your account private key here

Build and deploy a CosmWasm contract on Osmosis

The cloned project has the following folder structure that already contains the WASM contract; the next step you will be taking is to build and deploy the contract.

├── wasm
│ ├── src
│ │ ├── contract.rs
│ │ ├── error.rs
│ │ ├── ibc.rs
| │ ├── lib.rs
│ │ ├── msg.rs
│ │ ├── state.rs
| │ ├── unit_tests.rs
│ ├── cargo.lock
│ ├── corgo.toml
├── .env
├── node_modules
├── package.json
├── package-lock.json
├── README.md
└── .gitignore

In the contract.rs, the following was implemented:

  • Initialization: The instantiate function sets up the contract by saving the initial configuration and a placeholder message into the contract's storage.

  • Message Sending: The execute function handles various ExecuteMsg variants to send messages to EVM or Cosmos chains using Axelar GMP, constructing and dispatching messages with the necessary payload and metadata.

  • Message Reception: The contract includes functions (receive_message_evm and receive_message_cosmos) to receive and store messages from EVM and Cosmos chains, decoding the payload and saving it in the contract's state.

  • IBC Integration: It leverages Inter-Blockchain Communication (IBC) to send cross-chain messages, specifically designed to work with Axelar's GMP for transferring messages and funds across chains.

  • Query Stored Messages: The query function enables retrieval of stored messages via the GetStoredMessage query, returning the sender and message details stored in the contract.

Build the CosmWasm contract

Navigate into the wasm folder and build the contract with the following command:

docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/rust-optimizer:0.12.13

Make sure you have Docker daemon running before running the command above.

You should see something similar to what is displayed below on your terminal if the build is successful:

Info: RUSTC_WRAPPER=sccache
Info: sccache stats before build
Compile requests                      0
Compile requests executed             0
Cache hits                            0
Cache misses                          0
Cache timeouts                        0
Cache read errors                     0
Forced recaches                       0
Cache write errors                    0
Compilation failures                  0
Cache errors                          0
Non-cacheable compilations            0
Non-cacheable calls                   0
Non-compilation calls                 0
Unsupported compiler calls            0
Average cache write               0.000 s
Average cache read miss           0.000 s
Average cache read hit            0.000 s
Failed distributed compilations       0
Cache location                  Local disk: "/root/.cache/sccache"
Cache size                            0 bytes
Max cache size                       10 GiB
Building contract in /code ...
    Finished release [optimized] target(s) in 0.44s
Creating intermediate hash for send_receive.wasm ...
ebea0ee2e90b34fe4d55bd011051213938b95d596d6b56853c6e3e9bda8630ad  ./target/wasm32-unknown-unknown/release/send_receive.wasm
Optimizing send_receive.wasm ...
Creating hashes ...
c255d85938b647f59fcb77fe9d52ceefef5e510cd71364c522ce9ce25d3de8cc  send_receive.wasm
Info: sccache stats after build
Compile requests                      0
Compile requests executed             0
Cache hits                            0
Cache misses                          0
Cache timeouts                        0
Cache read errors                     0
Forced recaches                       0
Cache write errors                    0
Compilation failures                  0
Cache errors                          0
Non-cacheable compilations            0
Non-cacheable calls                   0
Non-compilation calls                 0
Unsupported compiler calls            0
Average cache write               0.000 s
Average cache read miss           0.000 s
Average cache read hit            0.000 s
Failed distributed compilations       0
Cache location                  Local disk: "/root/.cache/sccache"
Cache size                            0 bytes
Max cache size                       10 GiB
done

Upload the CosmWasm contract

Next, you need to upload the contract using the following command:

osmosisd tx wasm store ./artifacts/send_receive.wasm --from wallet --gas-prices 0.4uosmo --gas auto --gas-adjustment 1.5 -y -b sync --output json --node https://rpc.osmotest5.osmosis.zone:443 --chain-id osmo-test-5

You should see something similar to what is displayed below on your terminal if the upload is successful:

gas estimate: 3150117
{
    "height":"0",
    "txhash":"339C8D4E8BB10E0DD337F79ACC26BB825C341E6DAFE803C5E8F8C2B42E9D5A33",
    "codespace":"","code":0,"data":"",
    "raw_log":"","logs":[],
    "info":"","gas_wanted":"0","gas_used":"0","tx":null,
    "timestamp":"","events":[]
}

Instantiate the CosmWasm contract

To instantiate the CosmWasm contract, you will need a CodeId. To retrieve that, copy the transaction hash from the terminal response and paste it on Mintscan; in the transaction details, you should see a CodeId similar to this.

Next, instantiate the contract with the following command:

osmosisd tx wasm instantiate <codeId> '{"channel":"channel-4118"}' --from wallet --label "send_receive" --gas-prices 0.1uosmo --gas auto --gas-adjustment 1.3 --no-admin -y -b sync --output json --node https://rpc.osmotest5.osmosis.zone:443 --chain-id osmo-test-5

Replace <codeId> with the actual code ID of your transaction. You should see something similar to what is displayed below on your terminal if the instantiation is successful:

gas estimate: 208330
{
    "height":"0",
    "txhash":"6F2FD21EF41267562826444DB29091CA163941BC31AB5FCC085A42B3F67E2317",
    "codespace":"","code":0,"data":"","raw_log":"","logs":[],
    "info":"","gas_wanted":"0","gas_used":"0",
    "tx":null,"timestamp":"","events":[]
    }

You have successfully built, uploaded, and instantiated the CosmWasm contract. In the next section, you will build and deploy the EVM smart contract on Avalanche in this example and then interact with the EVM contract from the CosmWasm contract.

Save the CosmWasm contract address

You will need the CosmWasm contract address later in this tutorial. You can find it in the contract details on Mintscan by pasting the transaction hash and saving it somewhere. In this example, the contract address is osmo1vqgrchlfuymkjrzmrjznpam3xtzfemthzue43yt8l4ug046rtvwqarcl8r.

Build and deploy an EVM smart contract on Avalanche

In this session, you will build and deploy an EVM smart contract to Avalanche, a contract you will interact with from the CosmWasm you deployed.

To quickly build and deploy the EVM smart contract, you can use remix.ethereum.org, a powerful toolset for developing, deploying, debugging, and testing Ethereum and EVM-compatible smart contracts.

Build contract

Create a new file titled SendReceive.sol inside the contracts folder and copy the code from this gist into it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";
import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";
import {IAxelarGasService} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";
import {StringToAddress, AddressToString} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/AddressString.sol";

contract SendReceive is AxelarExecutable {
    using StringToAddress for string;
    using AddressToString for address;

    IAxelarGasService public immutable gasService;
    string public chainName; // name of the chain this contract is deployed to

    struct Message {
        string sender;
        string message;
    }

    Message public storedMessage; // message received from _execute

    constructor(
        address gateway_,
        address gasReceiver_,
        string memory chainName_
    ) AxelarExecutable(gateway_) {
        gasService = IAxelarGasService(gasReceiver_);
        chainName = chainName_;
    }

    function send(
        string calldata destinationChain,
        string calldata destinationAddress,
        string calldata message
    ) external payable {
        require(msg.value > 0, 'Gas payment is required');

        // 1. Generate GMP payload
        bytes memory executeMsgPayload = abi.encode(msg.sender.toString(), message);
        bytes memory payload = _encodePayloadToCosmWasm(executeMsgPayload);

        // 2. Pay for gas
        gasService.payNativeGasForContractCall{value: msg.value}(
            address(this),
            destinationChain,
            destinationAddress,
            payload,
            msg.sender
        );

        // 3. Make GMP call
        gateway.callContract(destinationChain, destinationAddress, payload);
    }

    function _encodePayloadToCosmWasm(bytes memory executeMsgPayload) internal view returns (bytes memory) {
        //   Schema
        //   bytes4  version number (0x00000001)
        //   bytes   ABI-encoded payload, indicating function name and arguments:
        //   string                   CosmWasm contract method name
        //   dynamic array of string  CosmWasm contract argument name array
        //   dynamic array of string  argument abi type array
        //   bytes                    abi encoded argument values

        // contract call arguments for ExecuteMsg::receive_message_evm{ source_chain, source_address, payload }
        bytes memory argValues = abi.encode(
            chainName,
            address(this).toString(),
            executeMsgPayload
        );

        string[] memory argumentNameArray = new string[](3);
        argumentNameArray[0] = "source_chain";
        argumentNameArray[1] = "source_address";
        argumentNameArray[2] = "payload";

        string[] memory abiTypeArray = new string[](3);
        abiTypeArray[0] = "string";
        abiTypeArray[1] = "string";
        abiTypeArray[2] = "bytes";

        bytes memory gmpPayload;
        gmpPayload = abi.encode(
            "receive_message_evm",
            argumentNameArray,
            abiTypeArray,
            argValues
        );

        return abi.encodePacked(
            bytes4(0x00000001),
            gmpPayload
        );
    }

    function _execute(
        string calldata /*sourceChain*/,
        string calldata /*sourceAddress*/,
        bytes calldata payload
    ) internal override {
        (string memory sender, string memory message) = abi.decode(payload, (string, string));
        storedMessage = Message(sender, message);
    }
}

In the contract above,

  • Contract Initialization: The SendReceive contract inherits from AxelarExecutable and initializes the Axelar gas service and chain name in its constructor

  • Sending Messages: The send function allows users to send messages to a specified destination chain and address, requiring a gas payment. It generates a General Message Passing (GMP) payload, pays for the gas, and makes the call through the Axelar gateway

  • Payload Encoding: The _encodePayloadToCosmWasm internal function encodes the message payload into a specific format required for CosmWasm contracts, including schema versioning and argument encoding

  • Message Reception: The _execute function decodes the received message payload and stores the message in the contract's state, handling incoming messages from other chains

Compile contract

You can proceed to compile the smart contract. Click the compile icon, and then the compile buttons are shown below.

Deploy contract

Deploying the smart contract to Avalanche testnets requires specifying both the Axelar Gateway Service and the Gas Service contract in the argument. Here is the list of Axelar Gas Service and Gateway contracts for all the chains currently supported by Axelar.

This contract you want to deploy requires the Gas Service, Gateway, and Chain Name, which will be passed on to the constructor before deployment, as shown below.

Click the deploy icon on Remix, as shown below.

Select the deployment target, Injected Provider - MetaMask.

Add the Axelar Gateway: 0xC249632c2D40b9001FE907806902f63038B737Ab and the Gas Service: 0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6 contract address deployed on Avalanche and chain name: Avalanche.

Next, click the transact button.

Copy contract address

Copy the contract address and save it somewhere; you will need it in the following section to send a message from the Coswasm contract to Avalanche.

Address 0x113BD07720a5Ae9104C5d54fBDfA83F792afc8e0 Details - Avalanche Testnet.

Send a message from a CosmWasm contract to Avalanche

You are all set to send a message from the CosmWasm contract you deployed to Avalanche. Create a new file with the name index.js in the root directory.

Import dependencies

To import the required dependencies add the following code snippet.

require("dotenv").config();
const { GasPrice } = require("@cosmjs/stargate");
const { SigningCosmWasmClient } = require("@cosmjs/cosmwasm-stargate");
const { AxelarQueryAPI } = require("@axelar-network/axelarjs-sdk");
const { DirectSecp256k1Wallet } = require("@cosmjs/proto-signing");
const { fromHex } = require("@cosmjs/encoding");
  • The script uses dotenv to load environment variables and imports essential modules from @cosmjs/stargate, @cosmjs/cosmwasm-stargate, @axelar-network/axelarjs-sdk, and @cosmjs/proto-signing to interact with Cosmos and Axelar networks

  • The fromHex function from @cosmjs/encoding is imported for decoding hex strings, which is crucial for handling private keys and other hexadecimal data formats

Set up the environment, API settings, token, and contract details

You need to define a few variables to be used in the script for the environment, API settings, token, and contract details. To do that, add the following code snippet.

//...

// Environment and API settings
const privateKey = process.env.PRIVATE_KEY;
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone";
const chainId = "osmosis-7";
const testnetEnvironment = "testnet";

// Token and contract details
const aUSDC =
  "ibc/1587E7B54FC9EFDA2350DC690EC2F9B9ECEB6FC31CF11884F9C0C5207ABE3921"; // aUSDC IBC address
const osmoDenom = "uosmo";
const gasPriceString = `0.4${osmoDenom}`;
const wasmContractAddress =
  "osmo1vqgrchlfuymkjrzmrjznpam3xtzfemthzue43yt8l4ug046rtvwqarcl8r";

Add EVM contract details

//...

// Message details
const destinationChain = "Avalanche";
const destinationAddress = "0x113BD07720a5Ae9104C5d54fBDfA83F792afc8e0"; // Replace with the Avalanche address you deployed
const messageToSend = "Hello world from Osmosis!";

Replace 0x113BD07720a5Ae9104C5d54fBDfA83F792afc8e0 with the contract address you copied from Remix earlier when you deployed on Avalanche.

Decode the Private Key and Create a Wallet

Now, decode your private key and create a wallet:

//...

(async () => {
  try {
    // Decode the private key from hex
    const decodedPrivateKey = fromHex(privateKey);

    // Create a wallet from the private key
    const wallet = await DirectSecp256k1Wallet.fromKey(
      decodedPrivateKey,
      "osmo",
    );
    const [{ address }] = await wallet.getAccounts();
  } catch (error) {
    console.error("An error occurred:", error);
  }
})();

Connect to the Client

Set up the client to interact with the blockchain:

//...

// Connect to the client
const gasPrice = GasPrice.fromString(gasPriceString);
const client = await SigningCosmWasmClient.connectWithSigner(
  rpcEndpoint,
  wallet,
  { gasPrice },
);

Retrieve and Log Balances

Retrieve the balances of aUSDC and OSMO tokens and log them:

//...

// Retrieve balances
const balanceUsdc = await client.getBalance(address, aUSDC);
const balanceOsmo = await client.getBalance(address, osmoDenom);

// Log wallet information
console.log("----- Wallet Info -----");
console.log(`Wallet address: ${address}`);
console.log(`aUSDC balance: ${balanceUsdc.amount / 1e6} aUSDC`);
console.log(`Osmo balance: ${balanceOsmo.amount / 1e6} OSMO\\n`);

Estimate Gas Fee

Estimate the gas fee for sending the message:

//...

// Estimate gas fee
const api = new AxelarQueryAPI({ environment: testnetEnvironment });
const gasAmount = await api.estimateGasFee(
  chainId,
  destinationChain,
  100000,
  "auto",
  "aUSDC",
);
console.log(`Estimated gas fee: ${parseInt(gasAmount) / 1e6} aUSDC`);

Check for Sufficient Balances

Ensure you have enough balances to cover the gas fees:

//...

// Check for sufficient balances
if (balanceUsdc.amount < gasAmount) {
  console.error("Insufficient aUSDC balance to pay for gas fee");
  return process.exit(0);
}

if (balanceOsmo.amount < 1e6) {
  console.error("Insufficient OSMO balance to pay for gas fee");
  return process.exit(0);
}

Query Message from the Osmosis Contract

Retrieve a stored message from the Osmosis contract:

//...

// Query message from the osmosis contract
const response = await client.queryContractSmart(wasmContractAddress, {
  get_stored_message: {},
});
console.log("Message from Osmosis contract:", response.message);

Prepare and Send the Message

Prepare the payload and send the message to the Osmosis contract:

//...

// Prepare payload to send message to osmosis contract
const payload = {
  send_message_evm: {
    destination_chain: destinationChain,
    destination_address: destinationAddress,
    message: messageToSend,
  },
};

const fee = {
  amount: gasAmount,
  denom: aUSDC,
};

console.log("Sending message to Osmosis contract...");

// Execute transaction
const result = await client.execute(
  address,
  wasmContractAddress,
  payload,
  "auto",
  undefined,
  [fee],
);

console.log("Sent:", result.transactionHash);

Test sending a message from Coswasm contract to Avalanche

You have successfully implemented sending messages from the CosmWasm contract you deployed earlier. It's time to test the implementation. Use the following command to run the script and ensure you are in the project's root directory.

node index.js

You should see something similar to what is displayed below on your terminal if the test is successful:

----- Wallet Info -----
Wallet address: osmo16m62wcd7dyednttjgayc08n22z509a70vcekuu
aUSDC balance: 48.553691 aUSDC
Osmo balance: 109.698386 OSMO

Estimated gas fee: 0.344874 aUSDC
Message from Osmosis contract: none
Sending message to Osmosis contract...
Sent: F19AB19C94794CF5BD2E480E1CEFD893D1E74E7CC709ADC8260736BCC253AD27

In case you run into an error like the following:

Error: Client network socket disconnected before secure TLS connection was established
    at connResetException (node:internal/errors:705:14)
    at TLSSocket.onConnectEnd (node:_tls_wrap:1594:19)
    at TLSSocket.emit (node:events:525:35)
    at endReadableNT (node:internal/streams/readable:1358:12)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
    code: 'ECONNRESET',
    path: null,
    host: 'rpc.osmotest5.osmosis.zone',
    port: 443,
    localAddress: undefined
}

Wait a bit and retry.

If you encounter the error "Insufficient aUSDC balance to pay for gas fee," please proceed to the Axelar Discord server. In the faucet channel, kindly paste your axl wallet address in this format: !faucet axelar16m62wcd7dyednttjgayc08n22z509a70qduwp0, and then conduct an IBC transfer to your osmo wallet address.

Check transaction on Axelarscan testnet.

If you do not have any errors, you can copy the transaction hash logged on the console, head over to the Axelarscan testnet, and paste it; you should be able to see how the message is sent from Osmosis to Avalanche via Axelar. You can find an example of the transaction here.

Read a message from an EVM smart contract on Avalanche

You have successfully sent a message from your CosmWasm contract to Avalanche; let's confirm that the message was actually sent and arrived on Avalanche.

Head back to https://remix.ethereum.org/, find the contract you deployed earlier, and click on the storedMessage button, as shown below. You should see "Hello world from Osmosis!" logged, which indicates that your message was successfully sent from the CosmWasm contract and received on the Avalanche contract.

Send a message from EVM to a CosmWasm contract on Osmosis

You have been able to send the message successfully in the previous step; now, proceed to send a message from Avalanche to the same CosmWasm contract on Osmosis. To do that, on Remix, click on the arrow icon beside the “Send” button and enter the following details.

destinationChain: osmosis-7

destinationAddress: osmo1vqgrchlfuymkjrzmrjznpam3xtzfemthzue43yt8l4ug046rtvwqarcl8r

message: Hello from Avalanche

Make sure to replace osmo1vqgrchlfuymkjrzmrjznpam3xtzfemthzue43yt8l4ug046rtvwqarcl8r with your contract address on Osmosis.

Next, pass a gas value in Gwei, in this example we passed 10000000 Gwei.

After confirming the transaction on the MetaMask popup, you can proceed to verify the transaction onchain on the Axelarscan testnet by copying the transaction hash and pasting it. Here is an example.

Read a message from a CosmWasm contract on Osmosis

In the previous step, you have successe=fully sent a message from Avalanche to Osmosis, in this step you will read the message from Osmosi to confirm it was truly received on Osmosis. To do that, create a new file, read.js, in the root folder of your project and add the following code snippet.

require("dotenv").config();
const { GasPrice } = require("@cosmjs/stargate");
const { SigningCosmWasmClient } = require("@cosmjs/cosmwasm-stargate");
const { DirectSecp256k1Wallet } = require("@cosmjs/proto-signing");
const { fromHex } = require("@cosmjs/encoding");

// Environment and API settings
const privateKey = process.env.PRIVATE_KEY;
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone";

// Token and contract details
const aUSDC =
  "ibc/1587E7B54FC9EFDA2350DC690EC2F9B9ECEB6FC31CF11884F9C0C5207ABE3921";
const osmoDenom = "uosmo";
const gasPriceString = `0.4${osmoDenom}`;
const wasmContractAddress =
  "osmo1vqgrchlfuymkjrzmrjznpam3xtzfemthzue43yt8l4ug046rtvwqarcl8r";

(async () => {
  try {
    // Decode the private key from hex
    const decodedPrivateKey = fromHex(privateKey);

    // Create a wallet from the private key
    const wallet = await DirectSecp256k1Wallet.fromKey(
      decodedPrivateKey,
      "osmo",
    );
    const [{ address }] = await wallet.getAccounts();

    // Connect to the client
    const gasPrice = GasPrice.fromString(gasPriceString);
    const client = await SigningCosmWasmClient.connectWithSigner(
      rpcEndpoint,
      wallet,
      { gasPrice },
    );

    // Retrieve balances
    const balanceUsdc = await client.getBalance(address, aUSDC);
    const balanceOsmo = await client.getBalance(address, osmoDenom);

    // Log wallet information
    console.log("----- Wallet Info -----");
    console.log(`Wallet address: ${address}`);
    console.log(`aUSDC balance: ${balanceUsdc.amount / 1e6} aUSDC`);
    console.log(`Osmo balance: ${balanceOsmo.amount / 1e6} OSMO\n`);

    // Query message from the osmosis contract
    const response = await client.queryContractSmart(wasmContractAddress, {
      get_stored_message: {},
    });
    console.log("Message from Osmosis contract:", response.message);
  } catch (error) {
    console.error("An error occurred:", error);
  }
})();

You should see something similar to what is displayed below on your terminal if the test is successful:

----- Wallet Info -----
Wallet address: osmo16m62wcd7dyednttjgayc08n22z509a70vcekuu
aUSDC balance: 48.208817 aUSDC
Osmo balance: 109.58592 OSMO

Message from Osmosis contract: Hello from Avalanche

Woohoo! If you made it this far, congratulations.

Conclusion

In this tutorial, you have learned how to build and deploy an EVM smart contract on Avalanche, build and deploy a CosmWasm contract on Osmosis, send a message from a CosmWasm contract to Avalanche, read a message from an EVM smart contract on Avalanche, send a message from EVM to a CosmWasm contract on Osmosis, and read a message from a CosmWasm contract on Osmosis.

By mastering these steps, you can effectively create interoperable decentralized applications that leverage the strengths of both EVM-based and Cosmos-based blockchain networks.

Reference