本文将通过一个实际示例,演示如何通过 Hyperlane 协议发送跨链消息 。
你将学习如何:
• 部署与 Hyperlane Mailbox 兼容的合约
• 部署可信中继器 ISM ( 跨链安全模块 )
• 运行一个简单的中继器
• 发送跨链消息
在此示例中,您将在两个不同的链上部署类似的合约。链 A 上的合约将使用 Hyperlane 协议 向链 B 发送一条消息,链 B 上的合约将处理此消息并回复链 A。
合约主要功能:
• sendPing:通过调用 Hyperlane 的方法 来发起跨链消息 IMailbox.dispatch。
• enrollRemoteRouter:在继承的 Router 合约中,注册来自另一条链的合约。
• setInterchainSecurityModule:设置合同的 ISM。
• _handle:处理来自邮箱的传入消息(继承的 Router 合约 handle 函数调用的内部函数)。
1. 创建并导航到新目录:
mkdir hyperlane-pingpong && cd hyperlane-pingpong
2. 初始化 Hardhat 项目并安装依赖项:
npx hardhat init
3. 添加@hyperlane-xyz/core 为依赖项:
npm install -D @hyperlane-xyz/core
确保你有足够的测试令牌 Arbitrum Sepolia,并且 Sapphire Testnet
获取更多信息:
• Sapphire Testnet 来自 Oasis Faucet 的测试代币。
• Arbitrum Sepolia 来自 Alchemy 的 Faucet 的 ETH 代币。
打开 hardhat.config.ts 并添加 Arbitrum Sepolia 和 Sapphire 测试网。
hardhat.config.ts
import { HardhatUserConfig } from"hardhat/config";
import"@nomicfoundation/hardhat-toolbox";
const accounts = process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [];
constconfig: HardhatUserConfig = {
solidity: "0.8.24",
networks: {
'arbitrum-sepolia': {
url: 'https://arbitrum-sepolia-rpc.publicnode.com',
chainId: 421614,
accounts,
},
'sapphire-testnet': {
url: "https://testnet.sapphire.oasis.io",
accounts,
chainId: 23295, // 0x5aff
},
},
};
exportdefault config;
在此示例中,我们利用了 HyperlaneRouter 中的包装器。这具有以下优势:
• 合约与 Hyperlane 的 MailboxClient 和 IMessageRecipient 接口兼容 。
• 支持注册其他链的路由器。
• 支持设置自定义 ISM。
1. Ping.sol 创建一个名为 Arbitrum Sepolia 的新文件
2. 将以下合同粘贴到其中:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
// ============ External Imports ============
import {Router} from"@hyperlane-xyz/core/contracts/client/Router.sol";
/*
* @title Ping
* @dev You can use this simple app as a starting point for your own application.
*/
contract Ping is Router {
// A generous upper bound on the amount of gas to use in the handle
// function when a message is processed. Used for paying for gas.
uint256 public constant HANDLE_GAS_AMOUNT = 50_000;
// A counter of how many messages have been sent from this contract.
uint256 public sent;
// A counter of how many messages have been received by this contract.
uint256 public received;
// Keyed by domain, a counter of how many messages that have been sent
// from this contract to the domain.
mapping(uint32 => uint256) public sentTo;
// Keyed by domain, a counter of how many messages that have been received
// by this contract from the domain.
mapping(uint32 => uint256) public receivedFrom;
// ============ Events ============
event SentPing(
uint32 indexed origin,
uint32 indexed destination,
string message
);
event ReceivedPing(
uint32 indexed origin,
uint32 indexed destination,
bytes32 sender,
string message
);
event HandleGasAmountSet(
uint32 indexed destination,
uint256 handleGasAmount
);
constructor(address _mailbox) Router(_mailbox) {
// Transfer ownership of the contract to deployer
_transferOwnership(msg.sender);
setHook(address(0));
}
// ============ External functions ============
/**
* @notice Sends a message to the _destinationDomain. Any msg.value is
* used as interchain gas payment.
* @param _destinationDomain The destination domain to send the message to.
* @param _message The message to send.
*/
functionsendPing(
uint32 _destinationDomain,
string calldata _message
) public payable {
sent += 1;
sentTo[_destinationDomain] += 1;
_dispatch(_destinationDomain, bytes(_message));
emit SentPing(
mailbox.localDomain(),
_destinationDomain,
_message
);
}
/**
* @notice Fetches the amount of gas that will be used when a message is
* dispatched to the given domain.
*/
functionquoteDispatch(
uint32 _destinationDomain,
bytes calldata _message
) external view returns (uint256) {
return_quoteDispatch(_destinationDomain, _message);
}
// ============ Internal functions ============
/**
* @notice Handles a message from a remote router.
* @dev Only called for messages sent from a remote router, as enforced by Router.sol.
* @param _origin The domain of the origin of the message.
* @param _sender The sender of the message.
* @param _message The message body.
*/
function_handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) internal override {
received += 1;
receivedFrom[_origin] += 1;
emit ReceivedPing(
_origin,
mailbox.localDomain(),
_sender,
string(_message)
);
}
}
3. Pong.sol 创建一个名为 Sapphire Testnet 的新文件
4. 将以下合同粘贴到其中:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
// ============ External Imports ============
import {Router} from"@hyperlane-xyz/core/contracts/client/Router.sol";
/*
* @title Pong
* @dev You can use this simple app as a starting point for your own application.
*/
contract Pong is Router {
// A generous upper bound on the amount of gas to use in the handle
// function when a message is processed. Used for paying for gas.
uint256 public constant HANDLE_GAS_AMOUNT = 50_000;
// A counter of how many messages have been sent from this contract.
uint256 public sent;
// A counter of how many messages have been received by this contract.
uint256 public received;
// Keyed by domain, a counter of how many messages that have been sent
// from this contract to the domain.
mapping(uint32 => uint256) public sentTo;
// Keyed by domain, a counter of how many messages that have been received
// by this contract from the domain.
mapping(uint32 => uint256) public receivedFrom;
// ============ Events ============
event SentPing(
uint32 indexed origin,
uint32 indexed destination,
string message
);
event ReceivedPing(
uint32 indexed origin,
uint32 indexed destination,
bytes32 sender,
string message
);
event HandleGasAmountSet(
uint32 indexed destination,
uint256 handleGasAmount
);
constructor(address _mailbox) Router(_mailbox) {
// Transfer ownership of the contract to deployer
_transferOwnership(msg.sender);
setHook(address(0));
}
// ============ External functions ============
/**
* @notice Sends a message to the _destinationDomain. Any msg.value is
* used as interchain gas payment.
* @param _destinationDomain The destination domain to send the message to.
* @param _message The message to send.
*/
functionsendPing(
uint32 _destinationDomain,
string calldata _message
) public payable {
sent += 1;
sentTo[_destinationDomain] += 1;
_dispatch(_destinationDomain, bytes(_message));
emit SentPing(
mailbox.localDomain(),
_destinationDomain,
_message
);
}
/**
* @notice Fetches the amount of gas that will be used when a message is
* dispatched to the given domain.
*/
functionquoteDispatch(
uint32 _destinationDomain,
bytes calldata _message
) external view returns (uint256) {
return_quoteDispatch(_destinationDomain, _message);
}
// ============ Internal functions ============
/**
* @notice Handles a message from a remote router.
* @dev Only called for messages sent from a remote router, as enforced by Router.sol.
* @param _origin The domain of the origin of the message.
* @param _sender The sender of the message.
* @param _message The message body.
*/
function_handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) internal override {
received += 1;
receivedFrom[_origin] += 1;
emit ReceivedPing(
_origin,
mailbox.localDomain(),
_sender,
string(_message)
);
// send return message
sendPing(
_origin,
string(_message)
);
}
}
在当前状态下,如果您从 Sapphire Testnet 向 Arbitrum Sepolia 发送消息,邮箱的默认 ISM 将不会接受该消息。您可以在 Arbitrum Sepolia 上部署和注册自定义 ISM 以使其正常工作。
Hyperlane 的一个简单默认 ISM 是 TrustedRelayerISM,它在传递消息之前检查中继器地址。
1. 创建一个名为 TrustedRelayerIsm.sol
2. 将以下合同粘贴到其中:
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from"@hyperlane-xyz/core/contracts/interfaces/IInterchainSecurityModule.sol";
import {Address} from"@openzeppelin/contracts/utils/Address.sol";
import {Message} from"@hyperlane-xyz/core/contracts/libs/Message.sol";
import {Mailbox} from"@hyperlane-xyz/core/contracts/Mailbox.sol";
import {PackageVersioned} from"@hyperlane-xyz/core/contracts/PackageVersioned.sol";
contract TrustedRelayerIsm is IInterchainSecurityModule, PackageVersioned {
usingMessagefor bytes;
uint8 public immutable moduleType = uint8(Types.NULL);
Mailboxpublic immutable mailbox;
address public immutable trustedRelayer;
constructor(address _mailbox, address _trustedRelayer) {
require(
_trustedRelayer != address(0),
"TrustedRelayerIsm: invalid relayer"
);
require(
Address.isContract(_mailbox),
"TrustedRelayerIsm: invalid mailbox"
);
mailbox = Mailbox(_mailbox);
trustedRelayer = _trustedRelayer;
}
functionverify(
bytes calldata,
bytes calldata message
) external view returns (bool) {
return mailbox.processor(message.id()) == trustedRelayer;
}
}
分别在 Sapphire Testnet 和 Arbitrum Sepolia 两个不同的链上部署 Ping 和 Pong 合约。
将 Pong 部署到 Sapphire Testnet
1. deploypong.ts 在以下位置创建部署脚本 scripts/:
import { ethers } from"hardhat";
asyncfunctionmain() {
// deployed mailbox on Sapphire Testnet
const mailbox = "0x79d3ECb26619B968A68CE9337DfE016aeA471435";
constPongFactory = await hre.ethers.getContractFactory("Pong");
const pong = awaitPongFactory.deploy(mailbox);
const pongAddr = await pong.waitForDeployment();
console.log(`Pong deployed at: ${pongAddr.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行部署:
pnpm hardhat run scripts/deploypong.ts --network sapphire-testnet
1. deployping.ts 在以下位置创建部署脚本 scripts/:
import { ethers } from"hardhat";
asyncfunctionmain() {
// default mailbox on Arbitrum Sepolia
const mailbox = "0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8";
constPingFactory = await ethers.getContractFactory("Ping");
const ping = awaitPingFactory.deploy(mailbox);
const pingAddr = await ping.waitForDeployment();
console.log(`Ping deployed at: ${pingAddr.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行部署:
pnpm hardhat run scripts/deployping.ts --network arbitrum-sepolia
1. deployISM.ts 在以下位置创建部署脚本 scripts/:
import { ethers } from"hardhat";
asyncfunctionmain() {
// default mailbox on Arbitrum Sepolia
const mailbox = "0x598facE78a4302f11E3de0bee1894Da0b2Cb71F8";
const trustedRelayer = "0x<your relayer address>";
const trustedRelayerISM = await ethers.deployContract(
"TrustedRelayerIsm",
[mailbox, trustedRelayer]
);
await trustedRelayerISM.waitForDeployment();
console.log(`TrustedRelayerISM deployed to ${trustedRelayerISM.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行部署:
pnpm hardhat run scripts/deployISM.ts --network arbitrum-sepolia
由于我们将路由器包装器用于我们的 Ping Pong 合约,因此我们需要登记对方合约的合约地址。
在 Sapphireestnet 上注册 Router
1. enroll.ts 在文件夹中创建一个名为 /scripts
import { ethers } from"hardhat";
asyncfunctionmain() {
let pingpongArbitrum = "0x<your contract deployed before>";
let pingpongSapphire = "0x<your contract deployed before>";
let arbId = "421614";
const signer = await ethers.provider.getSigner();
const contract = await ethers.getContractAt("Pong", sapphireAddr, signer);
await contract.enrollRemoteRouter(arbId, ethers.zeroPadValue(pingpongArbitrum, 32));
const arbRouter = await contract.routers(arbId);
console.log(`remote router adr for ${arbId}: ${arbRouter}`)
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行部署
pnpm hardhat run scripts/enroll.ts --network sapphire-testnet
在 Arbitrum Sepolia 上注册 Router
1. enroll.ts 在文件夹中创建一个名为 /scripts
import { ethers } from"hardhat";
asyncfunctionmain() {
let pingpongArbitrum = "0x<your contract deployed before>";
let pingpongSapphire = "0x<your contract deployed before>";
let sapphireId = "23295";
const signer = await ethers.provider.getSigner();
const contract = await ethers.getContractAt("Ping", pingpongArbitrum, signer);
await contract.enrollRemoteRouter(sapphireId, ethers.zeroPadValue(pingpongSapphire, 32));
const arbRouter = await contract.routers(sapphireId);
console.log(`remote router adr for ${sapphireId}: ${arbRouter}`)
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行部署
pnpm hardhat run scripts/enroll.ts --network arbitrum-sepolia
在 Arbitrum Sepolia 注册 ISM
1. registerIsm.ts 在文件夹中创建一个名为 /scripts
import { ethers } from"hardhat";
asyncfunctionmain() {
let pingpongArbitrum = "0x<your contract deployed before>";
let ismAddr = "0x<your ISM contract deployed before>";
const signer = await ethers.provider.getSigner();
const contract = await ethers.getContractAt("Ping", pingpongArbitrum, signer);
await contract.setInterchainSecurityModule(ismAddr);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行部署
pnpm hardhat run scripts/registerIsm.ts --network arbitrum-sepolia
在开始测试 Ping Pong 之前,请确保你在 Arbitrum Sepolia 和 Sapphire Testnet 上运行了一个 Relayer。
运行中继器的最简单方法是使用 Hyperlane CLI。
1. 导出要与 CLI 一起使用的私钥
export HYP_KEY='<YOUR_PRIVATE_KEY>'
2. 启动一个中继器 监视 Arbitrum Sepolia & SapphireTestnet
hyperlane relayer --chains sapphiretestnet,arbitrumsepolia
链配置
Sapphire Testnet 已在 Hyperlane Registry 中注册,如果你自己部署 Hyperlane CoreSapphire Testnet,请确保你拥有 类似于下面的 Hyperlane$HOME/.hyperlane/chains/sapphiretestnet 配置文件。
• metadata.yaml
# yaml-language-server: $schema=../schema.json
blockExplorers:
-apiUrl:https://nexus.oasis.io/v1/
family:other
name:OasisExplorer
url:https://explorer.oasis.io/testnet/sapphire
chainId:23295
displayName:SapphireTestnet
domainId:23295
isTestnet:true
name:sapphiretestnet
nativeToken:
decimals:18
name:TEST
symbol:TEST
protocol:ethereum
rpcUrls:
-http:https://testnet.sapphire.oasis.io
technicalStack: other
• addresses.yaml
domainRoutingIsmFactory: "0x3497967f8E5041f486eC559E6B760d8f051A034C"
interchainAccountIsm:"0xD84DE931A0EDA06Af3944a4e9933c24f3B56DCaC"
interchainAccountRouter:"0xFdca43771912CE5F5B4D869B0c05df0b6eF8aEFc"
mailbox:"0x79d3ECb26619B968A68CE9337DfE016aeA471435"
proxyAdmin:"0x5Ed8004e3352df333901b0B2E98Bd98C3B4AA59A"
staticAggregationHookFactory:"0x212c232Ee07E187CF9b4497A30A3a4D034aAC4D6"
staticAggregationIsmFactory:"0xE25A539AdCa1Aac56549997f2bB88272c5D9498c"
staticMerkleRootMultisigIsmFactory:"0x9851EC4C62943E9974370E87E93CE552abE7705E"
staticMerkleRootWeightedMultisigIsmFactory:"0x688dE6d0aBcb60a711f149c274014c865446b49D"
staticMessageIdMultisigIsmFactory:"0xFE0937b1369Bbba59211c4119B91984FF450ccf1"
staticMessageIdWeightedMultisigIsmFactory:"0x1de05675c8cd512A30c17Ea0a3491d74eF290994"
testRecipient:"0x7bf548104F8f500C563Aa6DC7FbF3b1ad93E4E03"
validatorAnnounce: "0xB119f96a106919489b6495128f30e7088e55B05c"
对于更复杂的验证器和中继器设置,请查看 Hyperlane 的 本地代理指南 或更适合生产的 代理操作员指南。
要执行,您将调用 Ping.sol 合约的 sendPing 函数。
在示例中,demo-opl 您可以使用提供的 send-ping 任务,也可以使用以下脚本:
1. sendping.ts 在文件夹中创建一个名为 /scripts
import { ethers } from"hardhat";
asyncfunctionmain() {
const destChainId = "23295";
const message = "Hello OPL"
const pingpongArbitrum = "0x<your pingpong contract deployed on arbitrum sepolia"
const signer = await ethers.provider.getSigner();
const contract = await ethers.getContractAt("Ping", pingpongArbitrum, signer);
console.log("Calculating fee...");
let fee = await contract.quoteDispatch(
destChainId,
hre.ethers.toUtf8Bytes(message));
console.log(`Fee: ${hre.ethers.formatEther(fee)} ETH`);
console.log("Sending message...");
const tx = await contract.sendPing(destChainId, message, {value: fee});
await tx.wait();
console.log("Message sent");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行脚本
pnpm hardhat run scripts/sendping.ts --network arbitrum-sepolia
验证跨链消息传递的最简单方法是检查区块链浏览器并检查已部署 Ping 合约上的交易。
如果您想直接监视事件,可以使用以下脚本:
1. verifyping.ts 在文件夹中创建一个名为 /scripts
import { ethers } from"hardhat";
asyncfunctionmain() {
const contractAddr = "0x<your deployed ping contract on arbitrum sepolia>"
const signer = await ethers.provider.getSigner();
const contract = await ethers.getContractAt("Ping", contractAddr, signer);
const spinner = ['-', '\\', '|', '/'];
let spinnerIndex = 0;
const interval = setInterval(() => {
process.stdout.write(`\rListing for event... ${spinner[spinnerIndex]}`);
current = (spinnerIndex + 1) % spinner.length;
}, 150);
let events;
do {
const block = await ethers.provider.getBlockNumber();
events = await contract.queryFilter('ReceivedPing', block - 10, 'latest');
if (events.length === 0) {
awaitnewPromise(resolve =>setTimeout(resolve, 60 * 1000));
}
} while (events.length === 0);
clearInterval(interval);
process.stdout.write(`\r`);
process.stdout.clearLine(0);
const parsedEvent = contract.interface.parseLog(events[0]);
const message = parsedEvent?.args?.message;
console.log(`Message received with: ${message}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
2. 运行脚本
pnpm hardhat run scripts/verifyping.ts --network sapphire-testnet
查看:
• enrollRemoteRouter 您已通过以下方式注册了对面的路由器合约:ping.routers(oppositeChainId)
• Arbitrum Sepolia 您已通过调用以下方式在 Ping 合约上设置了自定义 ISMping.interchainSecurityModule
• 中继器地址与受信任的 ISM 配置相匹配。
查看:
• 您的 Hardhat RPC 允许历史事件查询。
• 如果有必要,请使用专用的 RPC 提供程序(例如 Alchemy)。
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。