Writing Cross-Chain Contract
Go to https://github.com/akshatcoder-hash/x-mail and create a repository using this template.
Clone the repository and set up the starter code in your workspace:
git clone git@github.com:<your username>/x-mail.git
cd x-mail/
npm i
We have already written up the tests and deploy scripts for you. In this tutorial, we will go through how you can write a cross-chain contract.
Let’s write the contract first and then we’ll see what happens behind the scenes.
Step 1: Go to the src
directory and create a new file named HelloWormhole.sol
.
Here’s the initial structure of the contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract HelloWormhole {
constructor() {
// We'll add some code here soon
}
}
A couple notes on this:
-
// SPDX-License-Identifier: UNLICENSED
: This is a special comment known as a “SPDX license identifier”. You can look it up for more details. -
pragma solidity ^0.8.13;
: This specifies the version of the Solidity compiler we want our contract to use. It essentially means “I only want to use version 0.8.13 of the Solidity compiler, nothing lower”.
Step 2: Now let’s import the Wormhole SDK interfaces. These are necessary for our contract to interact with Wormhole bridge.
import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol";
import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol";
Step 3: Implement the IWormholeReceiver
interface in our HelloWormhole
contract:
contract HelloWormhole is IWormholeReceiver {
event GreetingReceived(string greeting, uint16 senderChain, address sender);
uint256 constant GAS_LIMIT = 50_000;
IWormholeRelayer public immutable wormholeRelayer;
string public latestGreeting;
// More to come
}
Step 4: Next, let’s add the constructor. This will run once when the contract is deployed. It sets the address of the Wormhole relayer:
constructor(address _wormholeRelayer) {
wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
}
Step 5: Let’s define the quoteCrossChainGreeting
function. This function will quote the cost of sending a message to a given chain:
function quoteCrossChainGreeting(uint16 targetChain) public view returns (uint256 cost) {
(cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT);
}
This function calls the quoteEVMDeliveryPrice
method of our Wormhole relayer and returns the cost.
function sendCrossChainGreeting(uint16 targetChain, address targetAddress, string memory greeting) public payable {
uint256 cost = quoteCrossChainGreeting(targetChain);
require(msg.value == cost);
wormholeRelayer.sendPayloadToEvm{value: cost}(
targetChain,
targetAddress,
abi.encode(greeting, msg.sender), // payload
0, // no receiver value needed since we're just passing a message
GAS_LIMIT
);
}
This function first gets the cost of sending the message, then checks if the sender has sent enough ETH to cover the cost. If they have, it sends the message through the Wormhole relayer.
Step 8: Lastly, we’ll implement the receiveWormholeMessages
function which is required by the IWormholeReceiver
interface:
function receiveWormholeMessages(
bytes memory payload,
bytes[] memory, // additionalVaas
bytes32, // address that called 'sendPayloadToEvm' (HelloWormhole contract address)
uint16 sourceChain,
bytes32 // deliveryHash - this can be stored in a mapping deliveryHash => bool to prevent duplicate deliveries
) public payable override {
require(msg.sender == address(wormholeRelayer), "Only relayer allowed");
(string memory greeting, address sender) = abi.decode(payload, (string, address));
latestGreeting = greeting;
emit GreetingReceived(latestGreeting, sourceChain, sender);
}
Summing up, here’s how your final code would look like:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol";
import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol";
contract HelloWormhole is IWormholeReceiver {
event GreetingReceived(string greeting, uint16 senderChain, address sender);
uint256 constant GAS_LIMIT = 50_000;
IWormholeRelayer public immutable wormholeRelayer;
string public latestGreeting;
constructor(address _wormholeRelayer) {
wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
}
function quoteCrossChainGreeting(uint16 targetChain) public view returns (uint256 cost) {
(cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT);
}
function sendCrossChainGreeting(uint16 targetChain, address targetAddress, string memory greeting) public payable {
uint256 cost = quoteCrossChainGreeting(targetChain);
require(msg.value == cost);
wormholeRelayer.sendPayloadToEvm{value: cost}(
targetChain,
targetAddress,
abi.encode(greeting, msg.sender), // payload
0, // no receiver value needed since we're just passing a message
GAS_LIMIT
);
}
function receiveWormholeMessages(
bytes memory payload,
bytes[] memory, // additionalVaas
bytes32, // address that called 'sendPayloadToEvm' (HelloWormhole contract address)
uint16 sourceChain,
bytes32 // deliveryHash - this can be stored in a mapping deliveryHash => bool to prevent duplicate deliveries
) public payable override {
require(msg.sender == address(wormholeRelayer), "Only relayer allowed");
(string memory greeting, address sender) = abi.decode(payload, (string, address));
latestGreeting = greeting;
emit GreetingReceived(latestGreeting, sourceChain, sender);
}
}