Skip to content

Create Cross-Chain Messaging Contracts

Introduction

Wormhole's cross-chain messaging allows smart contracts to interact seamlessly across multiple blockchains. This enables developers to build decentralized applications that leverage the strengths of different networks, whether it's Avalanche, Celo, Ethereum, or beyond. In this tutorial, we'll explore using Wormhole's Solidity SDK to create cross-chain contracts to send and receive messages across chains.

Wormhole's messaging infrastructure simplifies data transmission, event triggering, and transaction initiation across blockchains. In this tutorial, we'll guide you through a simple yet powerful hands-on demonstration that showcases this practical capability. We'll deploy contracts on two Testnets—Avalanche Fuji and Celo Alfajores—and send messages from one chain to another. This tutorial is perfect for those new to cross-chain development and seeking hands-on experience with Wormhole's powerful toolkit.

By the end of this tutorial, you will have not only built a fully functioning cross-chain message sender and receiver using Solidity but also developed a comprehensive understanding of how to interact with the Wormhole relayer, manage cross-chain costs, and ensure your smart contracts are configured correctly on both source and target chains.

This tutorial assumes a basic understanding of Solidity and smart contract development. Before diving in, it may be helpful to review the basics of Wormhole to familiarize yourself with the protocol.

Wormhole Overview

We'll interact with two key Wormhole components: the Wormhole relayer and the Wormhole Core Contracts. The relayer handles cross-chain message delivery and ensures the message is accurately received on the target chain. This allows smart contracts to communicate across blockchains without developers worrying about the underlying complexity.

Additionally, we'll rely on the Wormhole relayer to automatically determine cross-chain transaction costs and facilitate payments. This feature simplifies cross-chain development by allowing you to specify only the target chain and the message. The relayer handles the rest, ensuring that the message is transmitted with the appropriate fee.

Wormhole architecture detailed diagram: source to target chain communication.

Prerequisites

Before starting this tutorial, ensure you have the following:

Build Cross-Chain Messaging Contracts

In this section, we'll deploy two smart contracts: one to send a message from Avalanche Fuji and another to receive it on Celo Alfajores. The contracts interact with the Wormhole relayer to transmit messages across chains.

At a high level, our contracts will:

  1. Send a message from Avalanche to Celo using the Wormhole relayer
  2. Receive and process the message on Celo, logging the content of the message

Before diving into the deployment steps, let's first break down key parts of the contracts.

Sender Contract: MessageSender

The MessageSender contract is responsible for quoting the cost of sending a message cross-chain and then sending that message.

Key functions include:

  • quoteCrossChainCost - calculates the cost of delivering a message to the target chain using the Wormhole relayer
  • sendMessage - encodes the message and sends it to the target chain and contract address using the Wormhole relayer

Here's the core of the contract:

    function sendMessage(
        uint16 targetChain,
        address targetAddress,
        string memory message
    ) external payable {
        uint256 cost = quoteCrossChainCost(targetChain);

        require(
            msg.value >= cost,
            "Insufficient funds for cross-chain delivery"
        );

        wormholeRelayer.sendPayloadToEvm{value: cost}(
            targetChain,
            targetAddress,
            abi.encode(message, msg.sender),
            0,
            GAS_LIMIT
        );
    }

You can find the full code for the MessageSender.sol below.

MessageSender.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "lib/wormhole-solidity-sdk/src/interfaces/IWormholeRelayer.sol";

contract MessageSender {
    IWormholeRelayer public wormholeRelayer;
    uint256 constant GAS_LIMIT = 50000;

    constructor(address _wormholeRelayer) {
        wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
    }

    function quoteCrossChainCost(
        uint16 targetChain
    ) public view returns (uint256 cost) {
        (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice(
            targetChain,
            0,
            GAS_LIMIT
        );
    }

    function sendMessage(
        uint16 targetChain,
        address targetAddress,
        string memory message
    ) external payable {
        uint256 cost = quoteCrossChainCost(targetChain);

        require(
            msg.value >= cost,
            "Insufficient funds for cross-chain delivery"
        );

        wormholeRelayer.sendPayloadToEvm{value: cost}(
            targetChain,
            targetAddress,
            abi.encode(message, msg.sender),
            0,
            GAS_LIMIT
        );
    }
}

Receiver Contract: MessageReceiver

The MessageReceiver contract handles incoming cross-chain messages. When a message arrives, it decodes the payload and logs the message content. It ensures that only authorized contracts can send and process messages, adding an extra layer of security in cross-chain communication.

Emitter Validation and Registration

In cross-chain messaging, validating the sender is essential to prevent unauthorized contracts from sending messages. The isRegisteredSender modifier ensures that messages can only be processed if they come from the registered contract on the source chain. This guards against malicious messages and enhances security.

Key implementation details include:

  • registeredSender - stores the address of the registered sender contract
  • setRegisteredSender - registers the sender's contract address on the source chain. It ensures that only registered contracts can send messages, preventing unauthorized senders
  • isRegisteredSender - restricts the processing of messages to only those from registered senders, preventing unauthorized cross-chain communication
    mapping(uint16 => bytes32) public registeredSenders;

    modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) {
        require(
            registeredSenders[sourceChain] == sourceAddress,
            "Not registered sender"
        );
        _;
    }

    function setRegisteredSender(
        uint16 sourceChain,
        bytes32 sourceAddress
    ) public {
        require(
            msg.sender == registrationOwner,
            "Not allowed to set registered sender"
        );
        registeredSenders[sourceChain] = sourceAddress;
    }

Message Processing

The receiveWormholeMessages is the core function that processes the received message. It checks that the Wormhole relayer sent the message, decodes the payload, and emits an event with the message content. It is essential to verify the message sender to prevent unauthorized messages.

    function receiveWormholeMessages(
        bytes memory payload,
        bytes[] memory,
        bytes32 sourceAddress,
        uint16 sourceChain,
        bytes32
    ) public payable override isRegisteredSender(sourceChain, sourceAddress) {
        require(
            msg.sender == address(wormholeRelayer),
            "Only the Wormhole relayer can call this function"
        );

        // Decode the payload to extract the message
        string memory message = abi.decode(payload, (string));

        // Example use of sourceChain for logging
        if (sourceChain != 0) {
            emit SourceChainLogged(sourceChain);
        }

        // Emit an event with the received message
        emit MessageReceived(message);
    }

You can find the full code for the MessageReceiver.sol below.

MessageReceiver.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "lib/wormhole-solidity-sdk/src/interfaces/IWormholeRelayer.sol";
import "lib/wormhole-solidity-sdk/src/interfaces/IWormholeReceiver.sol";

contract MessageReceiver is IWormholeReceiver {
    IWormholeRelayer public wormholeRelayer;
    address public registrationOwner;

    // Mapping to store registered senders for each chain
    mapping(uint16 => bytes32) public registeredSenders;

    event MessageReceived(string message);
    event SourceChainLogged(uint16 sourceChain);

    constructor(address _wormholeRelayer) {
        wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
        registrationOwner = msg.sender; // Set contract deployer as the owner
    }

    modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) {
        require(
            registeredSenders[sourceChain] == sourceAddress,
            "Not registered sender"
        );
        _;
    }

    function setRegisteredSender(
        uint16 sourceChain,
        bytes32 sourceAddress
    ) public {
        require(
            msg.sender == registrationOwner,
            "Not allowed to set registered sender"
        );
        registeredSenders[sourceChain] = sourceAddress;
    }

    // Update receiveWormholeMessages to include the source address check
    function receiveWormholeMessages(
        bytes memory payload,
        bytes[] memory,
        bytes32 sourceAddress,
        uint16 sourceChain,
        bytes32
    ) public payable override isRegisteredSender(sourceChain, sourceAddress) {
        require(
            msg.sender == address(wormholeRelayer),
            "Only the Wormhole relayer can call this function"
        );

        // Decode the payload to extract the message
        string memory message = abi.decode(payload, (string));

        // Example use of sourceChain for logging
        if (sourceChain != 0) {
            emit SourceChainLogged(sourceChain);
        }

        // Emit an event with the received message
        emit MessageReceived(message);
    }
}

Deploy Contracts

This section will guide you through deploying the cross-chain messaging contracts on the Avalanche Fuji and Celo Alfajores Testnets. Follow these steps to get your contracts up and running.

Deployment Tools

We use Foundry to deploy our smart contracts. However, you can use any tool you're comfortable with, such as:

  • Remix for a browser-based IDE
  • Hardhat for a more extensive JavaScript/TypeScript workflow
  • Foundry for a CLI-focused experience with built-in scripting and testing features

The contracts and deployment steps remain the same regardless of your preferred tool. The key is to ensure you have the necessary Testnet funds and are deploying to the right networks.

Repository Setup

To get started with cross-chain messaging using Wormhole, first clone the GitHub repository. This repository includes everything you need to deploy, interact, and test the message flow between chains.

This demo focuses on using the scripts, so it's best to take a look at them, starting with deploySender.js, deployReceiver.js, and sendMessage.js.

To configure the dependencies properly, run the following command:

npm install

The repository includes:

  • Two Solidity contracts:

    • MessageSender.sol - contract that sends the cross-chain message from Avalanche
    • MessageReceiver.sol - contract that receives the cross-chain message on Celo
  • Deployment scripts located in the script directory:

    • deploySender.js - deploys the MessageSender contract to Avalanche
    • deployReceiver.js - deploys the MessageReceiver contract to Celo
    • sendMessage.js - sends a message from Avalanche to Celo
  • Configuration files and ABI JSON files for easy deployment and interaction:

    • chains.json - configuration file that stores key information for the supported Testnets, including the Wormhole relayer addresses, RPC URLs, and chain IDs. You likely won't need to modify this file unless you're working with different networks

Important Setup Steps

  1. Add your private key - create a .env file in the root of the project and add your private key:

    touch .env
    

    Inside .env, add your private key in the following format:

    PRIVATE_KEY=INSERT_PRIVATE_KEY
    
  2. Compile the contracts - ensure everything is set up correctly by compiling the contracts:

    forge build
    

The expected output should be similar to this:

forge build > [⠒] Compiling... > [⠰] Compiling 30 files with 0.8.23 [⠔] Solc 0.8.23 finished in 2.29s Compiler run successful!

Deployment Process

Both deployment scripts, deploySender.js and deployReceiver.js, perform the following key tasks:

  1. Load configuration and contract details - each script begins by loading the necessary configuration details, such as the network's RPC URL and the contract's ABI and bytecode. This information is essential for deploying the contract to the correct blockchain network

    {
        "chains": [
            {
                "description": "Avalanche testnet fuji",
                "chainId": 6,
                "rpc": "https://api.avax-test.network/ext/bc/C/rpc",
                "tokenBridge": "0x61E44E506Ca5659E6c0bba9b678586fA2d729756",
                "wormholeRelayer": "0xA3cF45939bD6260bcFe3D66bc73d60f19e49a8BB",
                "wormhole": "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"
            },
            {
                "description": "Celo Testnet",
                "chainId": 14,
                "rpc": "https://alfajores-forno.celo-testnet.org",
                "tokenBridge": "0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153",
                "wormholeRelayer": "0x306B68267Deb7c5DfCDa3619E22E9Ca39C374f84",
                "wormhole": "0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56"
            }
        ]
    }
    
      // Load the chain configuration from JSON
      const chains = JSON.parse(
        fs.readFileSync(path.resolve(__dirname, '../deploy-config/chains.json'))
      );
    
      // Get the Avalanche Fuji configuration
      const avalancheChain = chains.chains.find((chain) =>
        chain.description.includes('Avalanche testnet')
      );
    
      // Load the chain configuration from the JSON file
      const chains = JSON.parse(
        fs.readFileSync(path.resolve(__dirname, '../deploy-config/chains.json'))
      );
    
      // Get the Celo Testnet configuration
      const celoChain = chains.chains.find((chain) =>
        chain.description.includes('Celo Testnet')
      );
    

    Note

    The chains.json file contains the configuration details for the Avalanche Fuji and Celo Alfajores Testnets. You can modify this file to add more networks if needed. For a complete list of contract addresses, visit the reference page.

  2. Set up provider and wallet - the scripts establish a connection to the blockchain using a provider and create a wallet instance using a private key. This wallet is responsible for signing the deployment transaction

      const provider = new ethers.JsonRpcProvider(avalancheChain.rpc);
      const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
    
      const provider = new ethers.JsonRpcProvider(celoChain.rpc);
      const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
    
  3. Deploy the contract - the contract is deployed to the network specified in the configuration. Upon successful deployment, the contract address is returned, which is crucial for interacting with the contract later on

      const senderContract = await MessageSender.deploy(
        avalancheChain.wormholeRelayer
      );
      await senderContract.waitForDeployment();
    
      const receiverContract = await MessageReceiver.deploy(
        celoChain.wormholeRelayer
      );
      await receiverContract.waitForDeployment();
    
  4. Register the MessageSender on the target chain - after you deploy the MessageReceiver contract on the Celo Alfajores network, the sender contract address from Avalanche Fuji needs to be registered. This ensures that only messages from the registered MessageSender contract are processed

    This additional step is essential to enforce emitter validation, preventing unauthorized senders from delivering messages to the MessageReceiver contract

      // Retrieve the address of the MessageSender from the deployedContracts.json file
      const avalancheSenderAddress = deployedContracts.avalanche.MessageSender;
    
      // Define the source chain ID for Avalanche Fuji
      const sourceChainId = 6;
    
      // Call setRegisteredSender on the MessageReceiver contract
      const tx = await receiverContract.setRegisteredSender(
        sourceChainId,
        ethers.zeroPadValue(avalancheSenderAddress, 32)
      );
      await tx.wait();
    

You can find the full code for the deploySender.js and deployReceiver.js below.

deploySender.js
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

async function main() {
  // Load the chain configuration from JSON
  const chains = JSON.parse(
    fs.readFileSync(path.resolve(__dirname, '../deploy-config/chains.json'))
  );

  // Get the Avalanche Fuji configuration
  const avalancheChain = chains.chains.find((chain) =>
    chain.description.includes('Avalanche testnet')
  );

  // Set up the provider and wallet
  const provider = new ethers.JsonRpcProvider(avalancheChain.rpc);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  // Load the ABI and bytecode of the MessageSender contract
  const messageSenderJson = JSON.parse(
    fs.readFileSync(
      path.resolve(__dirname, '../out/MessageSender.sol/MessageSender.json'),
      'utf8'
    )
  );

  const abi = messageSenderJson.abi;
  const bytecode = messageSenderJson.bytecode;

  // Create a ContractFactory for MessageSender
  const MessageSender = new ethers.ContractFactory(abi, bytecode, wallet);

  // Deploy the contract using the Wormhole Relayer address for Avalanche Fuji
  const senderContract = await MessageSender.deploy(
    avalancheChain.wormholeRelayer
  );
  await senderContract.waitForDeployment();

  console.log('MessageSender deployed to:', senderContract.target);

  // Update the deployedContracts.json file
  const deployedContractsPath = path.resolve(
    __dirname,
    '../deploy-config/deployedContracts.json'
  );
  const deployedContracts = JSON.parse(
    fs.readFileSync(deployedContractsPath, 'utf8')
  );

  deployedContracts.avalanche = {
    MessageSender: senderContract.target,
    deployedAt: new Date().toISOString(),
  };

  fs.writeFileSync(
    deployedContractsPath,
    JSON.stringify(deployedContracts, null, 2)
  );
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
deployReceiver.js
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

async function main() {
  // Load the chain configuration from the JSON file
  const chains = JSON.parse(
    fs.readFileSync(path.resolve(__dirname, '../deploy-config/chains.json'))
  );

  // Get the Celo Testnet configuration
  const celoChain = chains.chains.find((chain) =>
    chain.description.includes('Celo Testnet')
  );

  // Set up the provider and wallet
  const provider = new ethers.JsonRpcProvider(celoChain.rpc);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  // Load the ABI and bytecode of the MessageReceiver contract
  const messageReceiverJson = JSON.parse(
    fs.readFileSync(
      path.resolve(
        __dirname,
        '../out/MessageReceiver.sol/MessageReceiver.json'
      ),
      'utf8'
    )
  );

  const abi = messageReceiverJson.abi;
  const bytecode = messageReceiverJson.bytecode;

  // Create a ContractFactory for MessageReceiver
  const MessageReceiver = new ethers.ContractFactory(abi, bytecode, wallet);

  // Deploy the contract using the Wormhole Relayer address for Celo Testnet
  const receiverContract = await MessageReceiver.deploy(
    celoChain.wormholeRelayer
  );
  await receiverContract.waitForDeployment();

  console.log('MessageReceiver deployed to:', receiverContract.target); // `target` is the contract address in ethers.js v6

  // Update the deployedContracts.json file
  const deployedContractsPath = path.resolve(
    __dirname,
    '../deploy-config/deployedContracts.json'
  );
  const deployedContracts = JSON.parse(
    fs.readFileSync(deployedContractsPath, 'utf8')
  );

  // Retrieve the address of the MessageSender from the deployedContracts.json file
  const avalancheSenderAddress = deployedContracts.avalanche.MessageSender;

  // Define the source chain ID for Avalanche Fuji
  const sourceChainId = 6;

  // Call setRegisteredSender on the MessageReceiver contract
  const tx = await receiverContract.setRegisteredSender(
    sourceChainId,
    ethers.zeroPadValue(avalancheSenderAddress, 32)
  );
  await tx.wait();

  console.log(
    `Registered MessageSender (${avalancheSenderAddress}) for Avalanche chain (${sourceChainId})`
  );

  deployedContracts.celo = {
    MessageReceiver: receiverContract.target,
    deployedAt: new Date().toISOString(),
  };

  fs.writeFileSync(
    deployedContractsPath,
    JSON.stringify(deployedContracts, null, 2)
  );
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Deploy the Sender Contract

The sender contract will handle quoting and sending messages cross-chain.

  1. Run the following command to deploy the sender contract:

    npm run deploy:sender
    
  2. Once deployed, the contract address will be displayed. You may check the contract on the Avalanche Fuji Explorer

npm run deploy:sender > wormhole-cross-chain@1.0.0 deploy:sender > node script/deploySender.js MessageSender deployed to: 0xf5c474f335fFf617fA6FD04DCBb17E20ee0cEfb1

Deploy the Receiver Contract

The receiver contract listens for cross-chain messages and logs them when received.

  1. Deploy the receiver contract with this command:

    npm run deploy:receiver
    
  2. After deployment, note down the contract address. You may check the contract on the Celo Alfajores Explorer.

Send a Cross-Chain Message

Now that both the sender and receiver contracts are deployed, let's move on to the next exciting step: sending a cross-chain message from Avalanche Fuji to Celo Alfajores.

In this example, we will use the sendMessage.js script to transmit a message from the sender contract on Avalanche to the receiver contract on Celo. The script uses Ethers.js to interact with the deployed contracts, calculate the cross-chain cost dynamically, and handle the transaction.

Let's break down the script step by step.

  1. Load configuration files

    1. chains.json - contains details about the supported Testnet chains, such as RPC URLs and relayer addresses
    2. deployedContracts.json - stores the addresses of the deployed sender and receiver contracts. This file is dynamically updated when contracts are deployed, but users can also manually add their own deployed contract addresses if needed
      const chains = JSON.parse(
        fs.readFileSync(path.resolve(__dirname, '../deploy-config/chains.json'))
      );
    
      const deployedContracts = JSON.parse(
        fs.readFileSync(
          path.resolve(__dirname, '../deploy-config/deployedContracts.json')
        )
      );
    
  2. Configure the provider and signer - the script first reads the chain configurations and extracts the contract addresses. One essential step in interacting with a blockchain is setting up a provider. A provider is your connection to the blockchain network. It allows your script to interact with the blockchain, retrieve data, and send transactions. In this case, we're using a JSON-RPC provider

    Next, we configure the wallet, which will be used to sign transactions. The wallet is created using the private key and the provider. This ensures that all transactions sent from this wallet are broadcast to the Avalanche Fuji network:

      const provider = new ethers.JsonRpcProvider(avalancheChain.rpc);
      const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
    

    After setting up the wallet, the script loads the ABI for the MessageSender.sol contract and creates an instance of it:

      const messageSenderJson = JSON.parse(
        fs.readFileSync(
          path.resolve(__dirname, '../out/MessageSender.sol/MessageSender.json'),
          'utf8'
        )
      );
    
  3. Set up the message details - the next part of the script defines the target chain (Celo) and the target address (the receiver contract on Celo):

      const targetChain = 14; // Wormhole chain ID for Celo Alfajores
      const targetAddress = deployedContracts.celo.MessageReceiver;
    

    You can customize the message that will be sent across chains:

      const message = 'Hello from Avalanche to Celo!';
    
  4. Estimate cross-chain cost - before sending the message, we dynamically calculate the cross-chain cost using the quoteCrossChainCost function:

      const txCost = await MessageSender.quoteCrossChainCost(targetChain);
    

    This ensures that the transaction includes enough funds to cover the gas fees for the cross-chain message.

  5. Send a message - with everything set up, the message is sent using the sendMessage function:

      const tx = await MessageSender.sendMessage(
        targetChain,
        targetAddress,
        message,
        {
          value: txCost,
        }
      );
    

    After sending, the script waits for the transaction to be confirmed:

      await tx.wait();
    
  6. Run the script - to send the message, run the following command:

    npm run send:message
    

If everything is set up correctly, the message will be sent from the Avalanche Fuji Testnet to the Celo Alfajores Testnet. You can monitor the transaction and verify that the message was received on Celo using the Wormhole Explorer.

The console should output something similar to this:

npm run send:message > wormhole-cross-chain@1.0.0 send:message > node script/sendMessage.js Sender Contract Address: 0xD720BFF42a0960cfF1118454A907a44dB358f2b1 Receiver Contract Address: 0x692550997C252cC5044742D1A2BD91E4f4b46D39 ... Transaction sent, waiting for confirmation... ... Message sent! Transaction hash: 0x9d359a66ba42baced80062229c0b02b4f523fe304aff3473dcf53117aee13fb6 You may see the transaction status on the Wormhole Explorer: https://wormholescan.io/#/tx/0x9d359a66ba42baced80062229c0b02b4f523fe304aff3473dcf53117aee13fb6?network=TESTNET

You can find the full code for the sendMessage.js below.

sendMessage.js
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

async function main() {
  // Load the chain configuration and deployed contract addresses
  const chains = JSON.parse(
    fs.readFileSync(path.resolve(__dirname, '../deploy-config/chains.json'))
  );

  const deployedContracts = JSON.parse(
    fs.readFileSync(
      path.resolve(__dirname, '../deploy-config/deployedContracts.json')
    )
  );

  console.log(
    'Sender Contract Address: ',
    deployedContracts.avalanche.MessageSender
  );
  console.log(
    'Receiver Contract Address: ',
    deployedContracts.celo.MessageReceiver
  );
  console.log('...');

  // Get the Avalanche Fuji configuration
  const avalancheChain = chains.chains.find((chain) =>
    chain.description.includes('Avalanche testnet')
  );

  // Set up the provider and wallet
  const provider = new ethers.JsonRpcProvider(avalancheChain.rpc);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  // Load the ABI of the MessageSender contract
  const messageSenderJson = JSON.parse(
    fs.readFileSync(
      path.resolve(__dirname, '../out/MessageSender.sol/MessageSender.json'),
      'utf8'
    )
  );

  const abi = messageSenderJson.abi;

  // Create a contract instance for MessageSender
  const MessageSender = new ethers.Contract(
    deployedContracts.avalanche.MessageSender,
    abi,
    wallet
  );

  // Define the target chain and target address (the Celo receiver contract)
  const targetChain = 14; // Wormhole chain ID for Celo Alfajores
  const targetAddress = deployedContracts.celo.MessageReceiver;

  // The message you want to send
  const message = 'Hello from Avalanche to Celo!';

  // Dynamically quote the cross-chain cost
  const txCost = await MessageSender.quoteCrossChainCost(targetChain);

  // Send the message (make sure to send enough gas in the transaction)
  const tx = await MessageSender.sendMessage(
    targetChain,
    targetAddress,
    message,
    {
      value: txCost,
    }
  );

  console.log('Transaction sent, waiting for confirmation...');
  await tx.wait();
  console.log('...');

  console.log('Message sent! Transaction hash:', tx.hash);
  console.log(
    `You may see the transaction status on the Wormhole Explorer: https://wormholescan.io/#/tx/${tx.hash}?network=TESTNET`
  );
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Conclusion

You're now fully equipped to build cross-chain contracts using the Wormhole protocol! With this tutorial, you've learned how to:

  • Deploy sender and receiver contracts on different Testnets
  • Send a cross-chain message from one blockchain to another
  • Monitor the status of your cross-chain transactions using the Wormhole Explorer and Wormhole-Solidity-SDK

Got any questions?

Find out more