本文详细介绍了如何通过Chainlink的跨链互操作协议(CCIP)在不同区块链上铸造NFT。内容涵盖了从跨链互操作性和Chainlink CCIP核心功能的解释,到基于示例代码的开发步骤,包括项目设置、合约部署及跨链NFT的铸造过程,为开发者提供了实用的参考。
喜欢视频演示吗?和Sahil一起了解如何铸造跨链NFT。
如何使用Chainlink的CCIP铸造跨链NFT (ERC-721)
在快速发展的web3生态系统中,区块链互操作性已成为弥合孤立区块链网络之间差距的关键。本指南将引导你了解如何在一个区块链上铸造NFT,方法是使用Chainlink的跨链互操作性协议 (CCIP) 在另一个区块链上发送交易。当你想要在Polygon上购买NFT,而你的资金在Avalanche上时,CCIP使你能够通过Avalanche发送交易来完成购买。到最后,你将对如何利用CCIP进行跨链NFT铸造有一个扎实的理解。
在本指南中,你将学习:
在开始之前,请确保你具备以下条件:
依赖 | 版本 |
---|---|
node.js | >8.9.4 |
智能合约的采用最初集中在以太坊,因为它在支持可编程智能合约方面的开创性作用。然而,随着以太坊交易费用的上升,用户开始寻找更具成本效益的替代方案,导致多链生态系统的快速增长。
该生态系统包括各种第一层区块链、侧链、子网和第二层汇总,每个提供独特的优势和用例。一些区块链在游戏应用方面表现出色,而另一些则更适合DeFi或NFT项目。跨链互操作性在这一格局中发挥着至关重要的作用,促进这些多样化区块链网络之间的无缝通信和数据共享。这种连通性增强了去中心化应用程序的灵活性和功能,通过弥合它们之间的差距,为开发者和用户打开了新的可能性。
Chainlink CCIP,即Chainlink跨链互操作性协议,是一种提供一种安全高效的方式,使去中心化应用程序(dApps)和web3企业家在不同区块链之间无缝互动的协议。CCIP促进Token转移和任意消息传递,使开发人员能够在接收智能合约上触发操作,例如铸造NFT、重新平衡指数或执行自定义函数。
Chainlink CCIP目前处于“早期访问”阶段,这意味着Chainlink CCIP目前具有正在开发的功能,可能在未来版本中进行更改。
跨链互操作性为开发者和用户打开了一个世界的可能性。一些常见的跨链dApps用例包括:
现在,让我们深入探讨使用CCIP进行跨链NFT铸造的技术方面。在本节中,我们将提供逐步说明和代码示例,引导你完成开发过程。你将学习如何设置所需的基础设施,编写智能合约,以及执行跨链NFT铸造交易。
本指南中使用的所有智能合约和相关文件,包括任务,都可以在GitHub上公开获取,相关的创作者获得了信用。
在开始编写代码之前,让我们决定我们铸造的NFT的规范,例如名称、描述和外观。然后,将所有内容存储在一个JSON元数据文件中。
我们将使用以下JSON文件来确定元数据。我们的NFT图像和元数据存储在IPFS上。因此,你不需要创建任何JSON文件。不过,如果你想上传自己的图像,你需要将图像和元数据上传到IPFS。要了解有关使用IPFS的更多信息,请查看我们的如何使用QuickNode创建和托管IPFS博客和如何创建和部署ERC-721(NFT)指南。
{
"name": "QN CCIP NFT",
"description": "Cross Chain NFT",
"image": "https://ipfs.io/ipfs/Qme68BnvU3Y3fYymZRXgqjYsVLe7MogeM11mXYzsD8gAmo/qn_pixel.png",
"attributes": [\
{\
"trait_type": "Year",\
"value": "2023"\
},\
{\
"trait_type": "Quality",\
"value": "98"\
},\
{\
"trait_type": "Type",\
"value": "Pixel"\
},\
]
}
我们的NFT元数据存储在IPFS上。我们将在后续部分使用这个URL。
在本指南中,我们将使用Polygon Mumbai Testnet和Avalanche Fuji Testnet。因此,你需要获取一些MATIC(在Mumbai)和AVAX(在Fuji)测试Token。
要获取你的测试Token:
Hardhat是一个用于编译、部署和测试EVM基础区块链智能合约的开发环境。我们将使用Hardhat来编译、部署和测试本指南的智能合约。
在你想要的任何目录中打开终端,运行以下代码。
此代码创建一个名为ccip-nft-project的文件夹,初始化项目,安装Hardhat,然后分别运行Hardhat。
mkdir ccip-nft-project && cd ccip-nft-project
npm init --yes
npm install --save-dev hardhat
npx hardhat init
选择创建一个TypeScript项目
,并按照说明操作。
👷 欢迎使用Hardhat v2.17.4 👷
✔ 你想要做什么? · 创建一个TypeScript项目
✔ Hardhat项目根目录: ·
✔ 你想要添加一个 .gitignore 吗? (Y/n) · y
✔ 你想用npm安装这个示例项目的依赖项吗(@nomicfoundation/hardhat-toolbox)? (Y/n) · y
然后,安装以下软件包。
npm install --save-dev @chainlink/contracts@^0.6.1 @chainlink/contracts-ccip@^0.7.3 @chainlink/env-enc@^1.0.5 @nomicfoundation/hardhat-toolbox@^2.0.1 ethers@^5.7.2
要在Polygon Mumbai和Avalanche Fuji链上部署智能合约,你需要API端点与网络进行通信。你可以使用公共节点或部署并管理你自己的基础设施;但是,如果你希望获得更快的响应时间,可以将繁重的工作留给我们。可以在此处注册免费账户。
登录后,点击创建端点按钮,然后选择Polygon链和Mumbai网络。
创建端点后,复制HTTP Provider链接并保留,以便后续使用。
如果你的账户支持多个端点,请为Avalanche Fuji重复相同的过程。如果不支持,你可以使用以下公共端点。
https://api.avax-test.network/ext/bc/C/rpc
首先,创建环境变量以确定如私钥和Polygon Mumbai和Avalanche Fuji的RPC端点URL等变量,这些变量在与区块链交互时是必需的。
如果你不知道如何获取私钥,请点击这里。
获取你的私钥:
为了进一步保护,我们将利用@chainlink/env-enc
包。通过建立一个新的.env.enc
文件,它将敏感数据加密,而不是将其作为纯文本保存在.env
文件中。
1. 设置用于加密和解密环境变量文件的密码。
npx env-enc set-pw
2. 设置以下环境变量:PRIVATE_KEY
、POLYGON_MUMBAI_RPC_URL
和AVALANCHE_FUJI_RPC_URL
。运行下面的命令并按照说明进行操作。
npx env-enc set
你的控制台输出应如下所示:
> npx env-enc set
请输入变量名称(或直接按 ENTER 完成):
PRIVATE_KEY
请输入变量值(输入将被隐藏):
****************************************************************
你是否想设置另一个变量?请输入变量名称(或直接按 ENTER 完成):
POLYGON_MUMBAI_RPC_URL
请输入变量值(输入将被隐藏):
****************************************************************************************************
你是否想设置另一个变量?请输入变量名称(或直接按 ENTER 完成):
AVALANCHE_FUJI_RPC_URL
请输入变量值(输入将被隐藏):
******************************************
.env.enc
文件将自动生成。在本节中,我们修改Hardhat项目的配置文件。它设置了项目的配置,包括Solidity版本以及Hardhat网络、Polygon Mumbai和Avalanche Fuji网络的设置。它使用dotenvenc
加载敏感信息,例如私钥和RPC URL。
用代码编辑器打开hardhat.config.ts
,并按如下更改。
import * as dotenvenc from '@chainlink/env-enc'
dotenvenc.config();
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
// import './tasks'
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const POLYGON_MUMBAI_RPC_URL = process.env.POLYGON_MUMBAI_RPC_URL;
const AVALANCHE_FUJI_RPC_URL = process.env.AVALANCHE_FUJI_RPC_URL;
const config: HardhatUserConfig = {
solidity: "0.8.19",
networks: {
hardhat: {
chainId: 31337,
},
polygonMumbai: {
url: POLYGON_MUMBAI_RPC_URL !== undefined ? POLYGON_MUMBAI_RPC_URL : "",
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 80001,
},
avalancheFuji: {
url: AVALANCHE_FUJI_RPC_URL !== undefined ? AVALANCHE_FUJI_RPC_URL : "",
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 43113,
allowUnlimitedContractSize: true,
},
},
};
export default config;
在本教程中,我们将在Avalanche Fuji Testnet上发送跨链铸造交易,以便在Polygon Mumbai Testnet上铸造NFT。因此,我们的目标区块链将是Polygon Mumbai,而Avalanche Fuji将是源区块链。
在跨链NFT铸造过程中,我们需要以下智能合约。
SourceMinter.sol
提取以太币和ERC-20代币的功能。每个合约都作为示例提供,不应在生产环境中使用,因其使用了硬编码值和未审核的代码。你可以在这里查看跨链铸造的仓库。
通过运行以下代码,创建智能合约的文件结构和文件。
mkdir contracts/utils
echo > contracts/DestinationMinter.sol
echo > contracts/MyNFT.sol
echo > contracts/SourceMinter.sol
echo > contracts/utils/Withdraw.sol
你可以删除自动生成的样本智能合约Lock.sol。
MyNFT.sol合约继承自ERC721URIStorage
以管理NFT元数据,并继承Ownable
以控制所有权。它允许合约所有者向指定地址铸造NFT,每个NFT都有一个独特的TokenURI。
打开MyNFT.sol文件并按如下方式修改。
如你所知,定义NFT元数据的IPFS URL用于定义TOKEN_URI
。如果要使用自己的NFT元数据,请相应修改。有关如何使用QuickNode上传文件到IPFS的更多信息,请查看我们的指南,在这里。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* 这是一个示例合约,使用硬编码值以便于理解。
* 这是一个示例合约,使用未审核的代码。
* 不要在生产环境中使用此代码。
*/
contract MyNFT is ERC721URIStorage, Ownable {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmTKNJyrRpzbnB4XUyKKRDxtTaLhvc5sXbcdWz5tSg9sEM/qn_ccip_nft.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
DestinationMinter.sol合约的主要目的是接收并执行跨链铸造请求。当接收到消息时,它尝试调用MyNFT合约中的相应功能,使得在成功的情况下发出MintCallSuccessfull
事件。
用代码编辑器打开DestinationMinter.sol文件,并按如下方式修改。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {MyNFT} from "./MyNFT.sol";
/**
* 这是一个示例合约,使用硬编码值以便于理解。
* 这是一个示例合约,使用未审核的代码。
* 不要在生产环境中使用此代码。
*/
contract DestinationMinter is CCIPReceiver {
MyNFT nft;
event MintCallSuccessfull();
constructor(address router, address nftAddress) CCIPReceiver(router) {
nft = MyNFT(nftAddress);
}
function _ccipReceive(
Client.Any2EVMMessage memory message
) internal override {
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
Withdraw.sol合约包含允许合约所有者从其持有的ERC-20代币和Ether中提取的功能。
打开Withdraw.sol文件并按如下方式修改。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
/**
* 这是一个示例合约,使用硬编码值以便于理解。
* 这是一个示例合约,使用未审核的代码。
* 不要在生产环境中使用此代码。
*/
contract Withdraw is OwnerIsCreator {
error FailedToWithdrawEth(address owner, address target, uint256 value);
function withdraw(address beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
(bool sent, ) = beneficiary.call{value: amount}("");
if (!sent) revert FailedToWithdrawEth(msg.sender, beneficiary, amount);
}
function withdrawToken(
address beneficiary,
address token
) public onlyOwner {
uint256 amount = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(beneficiary, amount);
}
}
SourceMinter.sol合约旨在进行跨链NFT铸造,并与Chainlink的跨链互操作性协议(CCIP)进行交互。该合约允许用户通过发送CCIP消息在目标链上启动铸造过程,并处理原生代币或链上LINK代币的费用。
用如下代码修改SourceMinter.sol。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {Withdraw} from "./utils/Withdraw.sol";
/**
* 这是一个示例合约,使用硬编码值以便于理解。
* 这是一个示例合约,使用未审核的代码。
* 不要在生产环境中使用此代码。
*/
contract SourceMinter is Withdraw {
enum PayFeesIn {
Native,
LINK
}
address immutable i_router;
address immutable i_link;
event MessageSent(bytes32 messageId);
constructor(address router, address link) {
i_router = router;
i_link = link;
LinkTokenInterface(i_link).approve(i_router, type(uint256).max);
}
receive() external payable {}
function mint(
uint64 destinationChainSelector,
address receiver,
PayFeesIn payFeesIn
) external {
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(receiver),
data: abi.encodeWithSignature("mint(address)", msg.sender),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: "",
feeToken: payFeesIn == PayFeesIn.LINK ? i_link : address(0)
});
uint256 fee = IRouterClient(i_router).getFee(
destinationChainSelector,
message
);
bytes32 messageId;
if (payFeesIn == PayFeesIn.LINK) {
// LinkTokenInterface(i_link).approve(i_router, fee);
messageId = IRouterClient(i_router).ccipSend(
destinationChainSelector,
message
);
} else {
messageId = IRouterClient(i_router).ccipSend{value: fee}(
destinationChainSelector,
message
);
}
emit MessageSent(messageId);
}
}
在移动到其他部分之前,运行以下命令以为智能合约创建Typechain类型定义。
Typechain是一个开发工具,用于为以太坊智能合约生成TypeScript类型定义,方便开发人员通过自动生成的TypeScript接口和类型检查更轻松地与这些合约进行交互。
npx hardhat typechain
输出应类似于下面所示。
成功编译了26个Solidity文件
现在,我们将创建一个Spinner,它是控制台中一个简单的文本旋转动画。它启动和停止动画,在某些异步任务期间为用户提供可视反馈。尽管它不影响铸造过程,但对于开发者体验非常有帮助。
通过运行以下代码创建utils文件夹和spinner.ts文件。
mkdir utils
echo > utils/spinner.ts
打开spinner.ts,并按如下方式修改。
export class Spinner {
private line = { interval: 130, frames: ['-', '\\', '|', '/'] }
private spin: any;
start() {
const start = 0;
const end = this.line.frames.length;
let i = start;```
process.stdout.write('\x1B[?25l');\
this.spin = setInterval(() => {\
process.stdout.cursorTo(0);\
process.stdout.write(this.line.frames[i]);\
i == end - 1 ? i = start : i++;\
}, this.line.interval);\
}\
\
stop() {\
process.stdout.clearLine(0);\
process.stdout.cursorTo(0);\
clearInterval(this.spin);\
process.stdout.write('\x1B[?25h');\
}\
}\
\
```\
\
现在,是时候创建 Hardhat 任务,这些任务对部署智能合约和铸造 NFT 非常有帮助。\
\
### 任务 \
\
任务是一个附带某些信息的异步函数。 Hardhat 使用这些信息自动化某些任务。参数解析、有效性和帮助消息都由它管理。Hardhat 包含一些预定义任务,如 **compile**、**run** 和 **test**。你可以运行 **npx hardhat** 来查看所有可用任务。\
\
我们将创建一些额外的任务和帮助文件,以便简化跨链 NFT 铸造过程。\
\
```\
mkdir tasks\
\
echo > tasks/constants.ts\
echo > tasks/utils.ts\
echo > tasks/helpers.ts\
echo > tasks/balance-of.ts\
echo > tasks/cross-chain-mint.ts\
echo > tasks/deploy-destination-minter.ts\
echo > tasks/deploy-source-minter.ts\
echo > tasks/fill-sender.ts\
echo > tasks/withdraw.ts\
echo > tasks/index.ts\
\
```\
\
#### constants.ts \
\
此代码定义了与区块链地址、代币数量、费用支付选项及支持的网络相关的 TypeScript 类型和常量。它还包括一个配置对象(`routerConfig`),该对象针对各种区块链网络(如以太坊 Sepolia、Optimism Goerli、Arbitrum Testnet、Avalanche Fuji 和 Polygon Mumbai)提供相关的合约地址和费用代币信息。你可以从 [Chainlink 的文档页面](https://docs.chain.link/ccip/supported-networks) 查看这些合约地址。\
\
打开 **constants.ts** 文件,并根据下面的内容进行修改。\
\
```\
export type AddressMap = { [blockchain: string]: string };\
export type TokenAmounts = { token: string, amount: string }\
\
export enum PayFeesIn {\
Native,\
LINK\
}\
\
export const supportedNetworks = [\
`ethereumSepolia`,\
`optimismGoerli`,\
`arbitrumTestnet`,\
`avalancheFuji`,\
`polygonMumbai`,\
];\
\
export const LINK_ADDRESSES: AddressMap = {\
[`ethereumSepolia`]: `0x779877A7B0D9E8603169DdbD7836e478b4624789`,\
[`polygonMumbai`]: `0x326C977E6efc84E512bB9C30f76E30c160eD06FB`,\
[`optimismGoerli`]: `0xdc2CC710e42857672E7907CF474a69B63B93089f`,\
[`arbitrumTestnet`]: `0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28`,\
[`avalancheFuji`]: `0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846`\
};\
\
export const routerConfig = {\
ethereumSepolia: {\
address: `0xd0daae2231e9cb96b94c8512223533293c3693bf`,\
chainSelector: `16015286601757825753`,\
feeTokens: [LINK_ADDRESSES[`ethereumSepolia`], `0x097D90c9d3E0B50Ca60e1ae45F6A81010f9FB534`]\
},\
optimismGoerli: {\
address: `0xeb52e9ae4a9fb37172978642d4c141ef53876f26`,\
chainSelector: `2664363617261496610`,\
feeTokens: [LINK_ADDRESSES[`optimismGoerli`], `0x4200000000000000000000000000000000000006`]\
},\
avalancheFuji: {\
address: `0x554472a2720e5e7d5d3c817529aba05eed5f82d8`,\
chainSelector: `14767482510784806043`,\
feeTokens: [LINK_ADDRESSES[`avalancheFuji`], `0xd00ae08403B9bbb9124bB305C09058E32C39A48c`]\
},\
arbitrumTestnet: {\
address: `0x88e492127709447a5abefdab8788a15b4567589e`,\
chainSelector: `6101244977088475029`,\
feeTokens: [LINK_ADDRESSES[`arbitrumTestnet`], `0x32d5D5978905d9c6c2D4C417F0E06Fe768a4FB5a`]\
},\
polygonMumbai: {\
address: `0x70499c328e1e2a3c41108bd3730f6670a44595d1`,\
chainSelector: `12532609583862916517`,\
feeTokens: [LINK_ADDRESSES[`polygonMumbai`], `0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889`]\
}\
}\
\
```\
\
#### utils.ts \
\
此代码提供了用于根据网络名称和费用支付选择检索各种配置参数的实用函数:\
\
- **getProviderRpcUrl(network):** 根据环境变量检索给定网络名称(例如 Avalanche Fuji 或 Polygon Mumbai)的 RPC URL。\
\
- **getPrivateKey():** 从环境变量中检索与网络交互的私钥。\
\
- **getRouterConfig(network):** 从 routerConfig 中检索给定网络名称(例如 Avalanche Fuji 或 Avalanche Fuji)的路由器配置。\
\
- **getPayFeesIn(payFeesIn):** 根据提供的字符串输入(例如 "Native" 或 "LINK")确定费用支付选择(即原生货币或 LINK 代币)。\
\
打开 **utils.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { PayFeesIn, routerConfig } from "./constants";\
\
export const getProviderRpcUrl = (network: string) => {\
let rpcUrl;\
\
switch (network) {\
case "ethereumSepolia":\
rpcUrl = process.env.ETHEREUM_SEPOLIA_RPC_URL;\
break;\
case "optimismGoerli":\
rpcUrl = process.env.OPTIMISM_GOERLI_RPC_URL;\
break;\
case "arbitrumTestnet":\
rpcUrl = process.env.ARBITRUM_TESTNET_RPC_URL;\
break;\
case "avalancheFuji":\
rpcUrl = process.env.AVALANCHE_FUJI_RPC_URL;\
break;\
case "polygonMumbai":\
rpcUrl = process.env.POLYGON_MUMBAI_RPC_URL;\
break;\
default:\
throw new Error("Unknown network: " + network);\
}\
\
if (!rpcUrl)\
throw new Error(\
`rpcUrl empty for network ${network} - check your environment variables`\
);\
\
return rpcUrl;\
};\
\
export const getPrivateKey = () => {\
const privateKey = process.env.PRIVATE_KEY;\
\
if (!privateKey)\
throw new Error(\
"private key not provided - check your environment variables"\
);\
\
return privateKey;\
};\
\
export const getRouterConfig = (network: string) => {\
switch (network) {\
case "ethereumSepolia":\
return routerConfig.ethereumSepolia;\
case "optimismGoerli":\
return routerConfig.optimismGoerli;\
case "arbitrumTestnet":\
return routerConfig.arbitrumTestnet;\
case "avalancheFuji":\
return routerConfig.avalancheFuji;\
case "polygonMumbai":\
return routerConfig.polygonMumbai;\
default:\
throw new Error("Unknown network: " + network);\
}\
};\
\
export const getPayFeesIn = (payFeesIn: string) => {\
let fees;\
\
switch (payFeesIn) {\
case "Native":\
fees = PayFeesIn.Native;\
break;\
case "native":\
fees = PayFeesIn.Native;\
break;\
case "LINK":\
fees = PayFeesIn.LINK;\
break;\
case "link":\
fees = PayFeesIn.LINK;\
break;\
default:\
fees = PayFeesIn.Native;\
break;\
}\
\
return fees;\
}\
\
```\
\
#### helpers.ts \
\
此代码定义了一个函数 `getCcipMessageId`,它通过模拟调用路由器合约来从交易凭证中提取跨链互操作协议(CCIP)消息 ID,并使用交易的数据和相关信息。检索到的消息 ID 记录下来,以便通过 CCIP Explorer 监控 token 转移的状态。\
\
打开 **helpers.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { ContractReceipt, ContractTransaction, providers } from "ethers";\
\
export const getCcipMessageId = async (tx: ContractTransaction, receipt: ContractReceipt, provider: providers.JsonRpcProvider) => {\
// 模拟调用路由器来获取 messageID\
const call = {\
from: tx.from,\
to: tx.to,\
data: tx.data,\
gasLimit: tx.gasLimit,\
gasPrice: tx.gasPrice,\
value: tx.value,\
};\
\
// 在交易发生的区块之前使用交易数据模拟合约调用\
const messageId = await provider.call(call, receipt.blockNumber - 1);\
\
console.log(`✅ 现在你可以通过 CCIP Explorer 监控 token 转移状态,检索 CCIP 消息 ID: ${messageId}`);\
}\
\
```\
\
#### balance-of.ts \
\
此脚本是一个 Hardhat 任务,用于检查与特定地址相关的 **MyNFTs** 在给定区块链上的余额。它利用 Hardhat 框架,与 JSON-RPC 提供者交互,并在连接到 MyNFT 智能合约后输出余额信息。在铸造过程之后,我们将使用此任务来检查我们地址的 NFT 智能合约余额。\
\
样本用法如下所示。我们 **不** 现在运行此任务。\
\
```\
npx hardhat balance-of --my-nft <MY_NFT_CONTRACT_ADDRESS> --blockchain polygonMumbai --owner <WALLET_ADDRESS>\
\
```\
\
打开 **balance-of.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { task } from "hardhat/config";\
import { TaskArguments } from "hardhat/types";\
import { getProviderRpcUrl } from "./utils";\
import { providers } from "ethers";\
import { MyNFT, MyNFT__factory } from "../typechain-types";\
import { Spinner } from "../utils/spinner";\
\
task('balance-of', '获取提供的地址的 MyNFTs 的余额')\
.addParam(`myNft`, `MyNFT 智能合约的地址`)\
.addParam(`blockchain`, `MyNFT 智能合约部署的区块链`)\
.addParam(`owner`, `检查 MyNFTs 的余额的地址`)\
.setAction(async (taskArguments: TaskArguments) => {\
const rpcProviderUrl = getProviderRpcUrl(taskArguments.blockchain);\
const provider = new providers.JsonRpcProvider(rpcProviderUrl);\
\
const spinner: Spinner = new Spinner();\
\
const myNft: MyNFT = MyNFT__factory.connect(taskArguments.myNft, provider);\
\
console.log(`ℹ️ 尝试检查 ${taskArguments.owner} 账户的 MyNFTs(${taskArguments.myNft})的余额`);\
spinner.start();\
\
const balanceOf = await myNft.balanceOf(taskArguments.owner);\
\
spinner.stop();\
console.log(`ℹ️ ${taskArguments.owner} 账户的 MyNFTs 余额为 ${balanceOf.toNumber()}`);\
})\
\
```\
\
#### cross-chain-mint.ts \
\
此代码定义了一个名为 `cross-chain-mint` 的 Hardhat 任务,负责通过发送跨链消息铸造一个新的 NFT。它接受各种参数,如源和目标区块链、源和目标智能合约地址,以及选择以原生代币或 LINK 代币支付费用。任务操作内部,它连接到源区块链,使用提供的参数启动铸造过程,并处理任务的执行,包括日志信息和完成跨链铸造操作。\
\
样本用法如下所示。我们 **不** 现在运行此任务。\
\
```\
npx hardhat cross-chain-mint --source-minter <SOURCE_MINTER_ADDRESS> --source-blockchain avalancheFuji --destination-blockchain polygonMumbai --destination-minter <DESTINATION_MINTER_ADDRESS> --pay-fees-in Native\
\
```\
\
```\
\
import { task } from "hardhat/config";\
import { TaskArguments } from "hardhat/types";\
import { getPayFeesIn, getPrivateKey, getProviderRpcUrl, getRouterConfig } from "./utils";\
import { Wallet, providers } from "ethers";\
import { SourceMinter, SourceMinter__factory } from "../typechain-types";\
import { Spinner } from "../utils/spinner";\
import { getCcipMessageId } from "./helpers";\
\
task(`cross-chain-mint`, `通过发送跨链消息铸造新 NFT`)\
.addParam(`sourceBlockchain`, `源区块链的名称(例如 ethereumSepolia)`)\
.addParam(`sourceMinter`, `源区块链上 SourceMinter.sol 智能合约的地址`)\
.addParam(`destinationBlockchain`, `目标区块链的名称(例如 polygonMumbai)`)\
.addParam(`destinationMinter`, `目标区块链上 DestinationMinter.sol 智能合约的地址`)\
.addParam(`payFeesIn`, `选择 'Native' 或 'LINK'`)\
.setAction(async (taskArguments: TaskArguments) => {\
const { sourceBlockchain, sourceMinter, destinationBlockchain, destinationMinter, payFeesIn } = taskArguments;\
\
const privateKey = getPrivateKey();\
const sourceRpcProviderUrl = getProviderRpcUrl(sourceBlockchain);\
\
const sourceProvider = new providers.JsonRpcProvider(sourceRpcProviderUrl);\
const wallet = new Wallet(privateKey);\
const signer = wallet.connect(sourceProvider);\
\
const spinner: Spinner = new Spinner();\
\
const sourceMinterContract: SourceMinter = SourceMinter__factory.connect(sourceMinter, signer)\
\
const destinationChainSelector = getRouterConfig(destinationBlockchain).chainSelector;\
const fees = getPayFeesIn(payFeesIn);\
\
console.log(`ℹ️ 尝试调用 ${sourceBlockchain} 上的 SourceMinter.sol 智能合约的铸造函数,来自 ${signer.address} 账户`);\
spinner.start();\
\
const tx = await sourceMinterContract.mint(\
destinationChainSelector,\
destinationMinter,\
fees,\
);\
\
const receipt = await tx.wait();\
\
spinner.stop();\
console.log(`✅ 发送铸造请求,交易哈希: ${tx.hash}`);\
\
await getCcipMessageId(tx, receipt, sourceProvider);\
\
console.log(`✅ 跨链铸造任务执行完毕`);\
})\
\
```\
\
#### deploy-destination-minter.ts \
\
此代码定义了一个名为 `deploy-destination-minter` 的 Hardhat 任务,负责在指定的区块链上部署两个智能合约,**MyNFT.sol** 和 **DestinationMinter.sol**,该区块链应为目标区块链。\
\
首先使用提供的参数或默认值连接到区块链,部署 **MyNFT** 合约,使用路由器地址和已部署的 **MyNFT** 合约作为构造函数参数部署 **DestinationMinter** 合约,并最终将铸造者角色授予 **DestinationMinter** 合约,允许其铸造 MyNFTs。该任务在部署过程中记录各种信息,并提供已部署合约地址和交易哈希的反馈。\
\
样本用法如下所示。我们 **不** 现在运行此任务。\
\
```\
npx hardhat deploy-destination-minter --network polygonMumbai\
\
```\
\
打开 **deploy-destination-minter.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { task } from "hardhat/config";\
import { HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types";\
import { getPrivateKey, getProviderRpcUrl, getRouterConfig } from "./utils";\
import { Wallet, providers } from "ethers";\
import { DestinationMinter, DestinationMinter__factory, MyNFT, MyNFT__factory } from "../typechain-types";\
import { Spinner } from "../utils/spinner";\
\
task(`deploy-destination-minter`, `部署 MyNFT.sol 和 DestinationMinter.sol 智能合约`)\
.addOptionalParam(`router`, `目标区块链上路由器合约的地址`)\
.setAction(async (taskArguments: TaskArguments, hre: HardhatRuntimeEnvironment) => {\
const routerAddress = taskArguments.router ? taskArguments.router : getRouterConfig(hre.network.name).address;\
\
const privateKey = getPrivateKey();\
const rpcProviderUrl = getProviderRpcUrl(hre.network.name);\
\
const provider = new providers.JsonRpcProvider(rpcProviderUrl);\
const wallet = new Wallet(privateKey);\
const deployer = wallet.connect(provider);\
\
const spinner: Spinner = new Spinner();\
\
console.log(`ℹ️ 尝试在 ${hre.network.name} 区块链上使用 ${deployer.address} 地址部署 MyNFT 智能合约`);\
spinner.start();\
\
const myNftFactory: MyNFT__factory = await hre.ethers.getContractFactory('MyNFT') as MyNFT__factory;\
const myNft: MyNFT = await myNftFactory.deploy();\
await myNft.deployed();\
\
spinner.stop();\
console.log(`✅ MyNFT 合约已在 ${hre.network.name} 区块链的地址 ${myNft.address} 上部署`)\
\
console.log(`ℹ️ 尝试在 ${hre.network.name} 区块链上使用 ${deployer.address} 地址部署 DestinationMinter 智能合约,并提供 Router 地址 ${routerAddress} 作为构造函数参数`);\
spinner.start();\
\
const destinationMinterFactory: DestinationMinter__factory = await hre.ethers.getContractFactory('DestinationMinter') as DestinationMinter__factory;\
const destinationMinter: DestinationMinter = await destinationMinterFactory.deploy(routerAddress, myNft.address);\
await destinationMinter.deployed();\
\
spinner.stop();\
console.log(`✅ DestinationMinter 合约已在 ${hre.network.name} 区块链的地址 ${destinationMinter.address} 上部署`);\
\
console.log(`ℹ️ 尝试将铸造者角色授予 DestinationMinter 智能合约`);\
spinner.start();\
\
const tx = await myNft.transferOwnership(destinationMinter.address);\
await tx.wait();\
\
spinner.stop();\
console.log(`✅ DestinationMinter 现在可以铸造 MyNFTs。交易哈希: ${tx.hash}`);\
})\
\
```\
\
#### deploy-source-minter.ts \
\
此代码定义了一个名为 `deploy-source-minter` 的 Hardhat 任务,负责在指定区块链上部署 **SourceMinter.sol** 智能合约,该区块链应为源区块链。它使用提供的参数或默认值连接到区块链,使用路由器和 LINK 代币地址作为构造函数参数部署 **SourceMinter** 合约,并记录部署状态,包括合约地址。\
\
样本用法如下所示。我们 **不** 现在运行此任务。\
\
```\
npx hardhat deploy-source-minter --network avalancheFuji\
\
```\
\
打开 **deploy-source-minter.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { task } from "hardhat/config";\
import { HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types";\
import { getPrivateKey, getProviderRpcUrl, getRouterConfig } from "./utils";\
import { Wallet, providers } from "ethers";\
import { SourceMinter, SourceMinter__factory } from "../typechain-types";\
import { Spinner } from "../utils/spinner";\
import { LINK_ADDRESSES } from "./constants";\
\
task(`deploy-source-minter`, `部署 SourceMinter.sol 智能合约`)\
.addOptionalParam(`router`, `源区块链上路由器合约的地址`)\
.setAction(async (taskArguments: TaskArguments, hre: HardhatRuntimeEnvironment) => {\
const routerAddress = taskArguments.router ? taskArguments.router : getRouterConfig(hre.network.name).address;\
const linkAddress = taskArguments.link ? taskArguments.link : LINK_ADDRESSES[hre.network.name]\
\
const privateKey = getPrivateKey();\
const rpcProviderUrl = getProviderRpcUrl(hre.network.name);\
\
const provider = new providers.JsonRpcProvider(rpcProviderUrl);\
const wallet = new Wallet(privateKey);\
const deployer = wallet.connect(provider);\
\
const spinner: Spinner = new Spinner();\
\
console.log(`ℹ️ 尝试在 ${hre.network.name} 区块链上使用 ${deployer.address} 地址部署 SourceMinter 智能合约,并提供 Router 地址 ${routerAddress} 和 LINK 地址 ${linkAddress} 作为构造函数参数`);\
spinner.start();\
\
const sourceMinterFactory: SourceMinter__factory = await hre.ethers.getContractFactory('SourceMinter') as SourceMinter__factory;\
const sourceMinter: SourceMinter = await sourceMinterFactory.deploy(routerAddress, linkAddress);\
await sourceMinter.deployed();\
\
spinner.stop();\
console.log(`✅ SourceMinter 合约已在 ${hre.network.name} 区块链的地址 ${sourceMinter.address} 上部署`);\
})\
\
```\
\
#### fill-sender.ts \
\
此代码定义了一个名为 `fill-sender` 的 Hardhat 任务,该任务方便将 LINK 代币或原生货币转移到指定的发送方合约上,位于给定的区块链。任务使用提供的参数连接到区块链,执行转帐,并记录交易状态及其哈希。选择以原生货币或 LINK 代币支付费用由 `payFeesIn` 参数确定。\
\
此任务可用于为 **SourceMinter** 合约提供资金以支付 CCIP 费用。资助流程可以通过在 MetaMask 中发送代币或使用 **fill-sender** 任务完成。\
\
样本用法如下所示。我们 **不** 现在运行此任务。请记住,`amount` 的单位是 wei。\
\
```\
npx hardhat fill-sender --sender-address <SOURCE_MINTER_ADDRESS> --blockchain avalancheFuji --amount 10000000000000000 --pay-fees-in Native\
\
```\
\
打开 **fill-sender.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { task } from "hardhat/config";\
import { TaskArguments } from "hardhat/types";\
import { getPrivateKey, getProviderRpcUrl, getPayFeesIn } from "./utils";\
import { Wallet, providers } from "ethers";\
import { IERC20, IERC20__factory } from "../typechain-types";\
import { LINK_ADDRESSES, PayFeesIn } from "./constants";\
import { Spinner } from "../utils/spinner";\
\
task(`fill-sender`, `将提供的 LINK 代币或原生货币金额转移到发送方合约以支付 CCIP 费用`)\
.addParam(`senderAddress`, `源区块链上发送方合约的地址`)\
.addParam(`blockchain`, `区块链的名称(例如 ethereumSepolia)`)\
.addParam(`amount`, `要发送的金额`)\
.addParam(`payFeesIn`, `选择 'Native' 或 'LINK'`)\
.setAction(async (taskArguments: TaskArguments) => {\
const { senderAddress, blockchain, amount, payFeesIn } = taskArguments;\
\
const privateKey = getPrivateKey();\
const rpcProviderUrl = getProviderRpcUrl(blockchain);\
\
const provider = new providers.JsonRpcProvider(rpcProviderUrl);\
const wallet = new Wallet(privateKey);\
const signer = wallet.connect(provider);\
\
const fees = getPayFeesIn(payFeesIn);\
\
const spinner: Spinner = new Spinner();\
\
if (fees === PayFeesIn.Native) {\
console.log(`ℹ️ 尝试将 ${amount} 的 ${blockchain} 原生货币从 ${signer.address} 转移到 ${senderAddress}`);\
spinner.start();\
\
const tx = await signer.sendTransaction({ to: senderAddress, value: amount });\
await tx.wait();\
\
spinner.stop();\
console.log(`✅ 币种已发送,交易哈希: ${tx.hash}`)\
} else {\
const link: IERC20 = IERC20__factory.connect(LINK_ADDRESSES[blockchain], signer);\
\
console.log(`ℹ️ 尝试将 ${amount} 的 ${link.address} 代币从 ${signer.address} 转移到 ${senderAddress}`);\
spinner.start();\
\
const tx = await link.transfer(senderAddress, amount);\
await tx.wait();\
\
spinner.stop();\
console.log(`✅ LINK 代币已发送,交易哈希: ${tx.hash}`)\
}\
})\
\
```\
\
#### withdraw.ts \
\
此代码定义了一个名为 `withdraw` 的 Hardhat 任务,允许所有者从一个名为 **Withdraw.sol** 的智能合约中提取代币或货币,位于指定的区块链上。它使用提供的区块链名称、发送方地址("from")、受益人地址,并可选择指定代币地址来执行提取。\
\
它可以用于从 **SourceMinter.sol** 智能合约提取 Chainlink CCIP 费用的代币。\
\
样本用法如下所示。我们 **不** 现在运行此任务。\
\
```\
npx hardhat withdraw --beneficiary <BENEFICIARY_ADDRESS> --blockchain avalancheFuji --from <SOURCE_MINTER_ADDRESS>\
\
```\
\
打开 **withdraw.ts** 文件,并根据下面的内容进行修改。\
\
```\
import { task } from "hardhat/config";\
import { TaskArguments } from "hardhat/types";\
import { getPrivateKey, getProviderRpcUrl } from "./utils";\
import { Wallet, providers } from "ethers";\
import { Spinner } from "../utils/spinner";\
import { Withdraw, Withdraw__factory } from "../typechain-types";\
\
task(`withdraw`, `从 Withdraw.sol 提取代币和货币。必须由所有者调用,否则将会回滚`)\
.addParam(`blockchain`, `区块链的名称(例如 ethereumSepolia)`)\
.addParam(`from`, `应提取资金的 Withdraw.sol 智能合约的地址`)\
.addParam(`beneficiary`, `提取的目标地址`)\
.addOptionalParam(`tokenAddress`, `要提取的代币地址`)\
.setAction(async (taskArguments: TaskArguments) => {\
const { blockchain, from, beneficiary, tokenAddress } = taskArguments;\
\
const privateKey = getPrivateKey();\
const rpcProviderUrl = getProviderRpcUrl(blockchain);\
\
const provider = new providers.JsonRpcProvider(rpcProviderUrl);\
const wallet = new Wallet(privateKey);\
const signer = wallet.connect(provider);\
\
const withdraw: Withdraw = Withdraw__factory.connect(from, signer);\
\
const spinner: Spinner = new Spinner();\
\
if (tokenAddress) {\
console.log(`ℹ️ 尝试将 ${tokenAddress} 代币从 ${from} 提取到 ${beneficiary}`);\
spinner.start();\
\
const withdrawalTx = await withdraw.withdrawToken(beneficiary, tokenAddress);\
await withdrawalTx.wait();\
\
spinner.stop();\
console.log(`✅ 提取成功,交易哈希: ${withdrawalTx.hash}`);\
} else {\
console.log(`ℹ️ 尝试将货币从 ${from} 提取到 ${beneficiary}`);\
spinner.start();\
\
const withdrawalTx = await withdraw.withdraw(beneficiary);\
await withdrawalTx.wait();\
\
spinner.stop();\
console.log(`✅ 提取成功,交易哈希: ${withdrawalTx.hash}`);\
}\
})\
\
```\
\
#### index.ts \
\
此代码似乎导入了已定义的任务。\
\
打开 **index.ts** 文件,并根据下面的内容进行修改。\
\
```\
import './balance-of'\
import './cross-chain-mint'\
import './deploy-destination-minter'\
import './deploy-source-minter'\
import './fill-sender'\
import './withdraw'\
\
```\
\
在进入下个部分之前,由于所有任务已经创建,打开 `hardhat.config.ts` 并取消注释下面的高亮行,以便能使用这些任务。\
\
```\
import * as dotenvenc from '@chainlink/env-enc'\
dotenvenc.config();\
\
import { HardhatUserConfig } from "hardhat/config";\
import "@nomicfoundation/hardhat-toolbox";\
import './tasks'\
\
const PRIVATE_KEY = process.env.PRIVATE_KEY;\
const POLYGON_MUMBAI_RPC_URL = process.env.POLYGON_MUMBAI_RPC_URL;\
const AVALANCHE_FUJI_RPC_URL = process.env.AVALANCHE_FUJI_RPC_URL;\
\
const config: HardhatUserConfig = {\
solidity: "0.8.19",\
networks: {\
hardhat: {\
chainId: 31337,\
},\
\
polygonMumbai: {\
url: POLYGON_MUMBAI_RPC_URL !== undefined ? POLYGON_MUMBAI_RPC_URL : "",\
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],\
chainId: 80001,\
},\
avalancheFuji: {\
url: AVALANCHE_FUJI_RPC_URL !== undefined ? AVALANCHE_FUJI_RPC_URL : "",\
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],\
chainId: 43113,\
allowUnlimitedContractSize: true,\
},\
},\
};\
\
export default config;\
\
```\
\
最终,编码部分完成!现在,我们可以运行任务来在 **Polygon Mumbai** 上铸造 NFT,方法是在 **Avalanche Fuji** 上发送交易。\
\
## 部署 \
\
### 在目标区块链上的部署 \
\
在这个示例中,我们将使用 **Polygon Mumbai** 作为目标区块链。因此,我们的 NFT 将在该区块链上。\
\
运行下面的命令,以最接近目标区块链的 `MyNFT.sol` 和 `DestinationMinter.sol` 智能合约进行部署,然后将 NFT 的铸造者角色授予 `DestinationMinter` 智能合约。\
\
```\
npx hardhat deploy-destination-minter --network polygonMumbai\
\
```\
\
输出应与以下内容类似。\
\
```\
> npx hardhat deploy-destination-minter --network polygonMumbai\
ℹ️ 尝试在 polygonMumbai 区块链上使用 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8 地址部署 MyNFT 智能合约\
✅ MyNFT 合约已在 polygonMumbai 区块链的地址 0xeB9c3F2eb9a5fAef70E578D82a9bB0CBEDde94A6 上部署\
ℹ️ 尝试在 polygonMumbai 区块链上使用 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8 地址部署 DestinationMinter 智能合约,并提供 Router 地址 0x70499c328e1e2a3c41108bd3730f6670a44595d1 作为构造函数参数\
✅ DestinationMinter 合约已在 polygonMumbai 区块链的地址 0xc9f0E298C520773A7c9cc509767E4F5b466FA412 上部署\
ℹ️ 尝试将铸造者角色授予 DestinationMinter 智能合约\
✅ DestinationMinter 现在可以铸造 MyNFTs。交易哈希: 0x480c3bda80c098f61861b91d5a1a3762989991d9072fb15b2f44c5eacb7bb081\
\
```\
\
请保持 **MyNFT** 和 **DestinationMinter** 合约地址,以便在接下来的步骤中使用。\
\
### 在源区块链上的部署 \
\
在此示例中,**Avalanche Fuji** 是源区块链,用于发送铸造交易。\
\
运行下面的命令,以便在源区块链上部署 **SourceMinter.sol** 智能合约。\
\
```\
npx hardhat deploy-source-minter --network avalancheFuji\
\
```\
\
输出应与以下内容类似。\
\
```\
> npx hardhat deploy-source-minter --network avalancheFuji\
ℹ️ 尝试在 avalancheFuji 区块链上使用 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8 地址部署 SourceMinter 智能合约,并提供 Router 地址 0x554472a2720e5e7d5d3c817529aba05eed5f82d8 和 LINK 地址 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 作为构造函数参数\
✅ SourceMinter 合约已在 avalancheFuji 区块链的地址 0xbD6d3ad9f5b7032c8e33E07D458a0eb1b25dE54E 上部署\
\
```\
\
### 为 SourceMinter 提供资金 \
```对于 CCIP 费用,**SourceMinter** 合约应该使用原生代币或 LINK 代币进行资金注入。如前所述,资金注入过程可以通过在 MetaMask 上发送代币或使用 `fill-sender` 任务完成。在本指南中,我们将使用一个钱包发送代币。
- 打开你的钱包(例如,MetaMask)
- 确保你已连接到源区块链(Avalanche Fuji)
- 向你的 **SourceMinter** 合约发送 0.1 Fuji AVAX;这足以支付 CCIP 费用。

### 铸造 NFT
现在,运行以下代码,以通过调用在源区块链上的 **SourceMinter** 合约,在目标区块链上铸造 NFT。
npx hardhat cross-chain-mint --source-minter SOURCE_MINTER_ADDRESS --source-blockchain avalancheFuji --destination-minter DESTINATION_MINTER_ADDRESS --destination-blockchain polygonMumbai --pay-fees-in Native
> 将 `SOURCE_MINTER_ADDRESS` 和 `DESTINATION_MINTER_ADDRESS` 替换为 SourceMinter(在 Avalanche Fuji 上)和 DestinationMinter(在 Polygon Mumbai 上)的地址。
输出应类似于以下内容。
npx hardhat cross-chain-mint --source-minter 0xbD6d3ad9f5b7032c8e33E07D458a0eb1b25dE54E --source-blockchain avalancheFuji --destination-minter 0xc9f0E298C520773A7c9cc509767E4F5b466FA412 --destination-blockchain polygonMumbai --pay-fees-in Native ℹ️ 尝试从 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8 账户调用 SourceMinter.sol 智能合约的铸造功能 ✅ 铸造请求已发送,交易哈希:0x9bab186a9312afc279e9faf7b3f173011434112e9eb370e83363b15a6b1b51e1 ✅ 现在可以通过 CCIP Explorer 监控代币转移状态,搜索 CCIP 消息 ID: 0x ✅ 任务 cross-chain-mint 执行完毕
现在,让我们在 CCIP Explorer 上检查交易哈希。
如你所记得,我们创建了 balance-of 任务来检查 NFT 智能合约对特定地址的余额。
运行以下命令,以获取 MyNFT.sol 合约的 balanceOf
函数的结果,参数为你的钱包地址。
npx hardhat balance-of --my-nft MYNFT_ADDRESS --blockchain polygonMumbai --owner YOUR_WALLET_ADDRESS
将 MYNFT_ADDRESS 和 YOUR_WALLET_ADDRESS 替换为 MyNFT 合约地址和你的钱包地址。
输出应如下所示。这意味着该地址有 1 个 MyNFT。
> npx hardhat balance-of --my-nft 0xeB9c3F2eb9a5fAef70E578D82a9bB0CBEDde94A6 --blockchain polygonMumbai --owner 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8
ℹ️ 尝试检查 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8 账户的 MyNFTs(0xeB9c3F2eb9a5fAef70E578D82a9bB0CBEDde94A6)的余额
ℹ️ 0x58D09ecd499A1d6F2a0269f361Ee6DbbaBa44eF8 账户的 MyNFTs 的余额为 1
但是,如果我们想检查 NFT 的外观怎么办?
要查看它,我们可以使用支持测试网的 NFT 市场,如 Rarible 和 OpenSea。
如果你不想连接钱包,可以通过将以下 URL 中的 MYNFT_ADDRESS 替换为你的 MyNFT 合约地址来修改。
https://testnet.rarible.com/token/polygon/MYNFT_ADDRESS:0
正如你所见,NFT 在 Rarible 上显示,并且它是在 Polygon Mumbai 测试网上。
🎉 恭喜你!在这段旅程中,你已经进入了去中心化跨链技术的世界。你了解了 CCIP,创建了任务,部署了智能合约,并在不同的区块链网络上铸造了跨链 NFT。
区块链互操作性,借助像 Chainlink CCIP 这样的协议实现,是 web3 生态系统的游戏规则改变者。它使开发者和创业者能够利用不同区块链的优势,创造创新的跨链应用程序。通过掌握使用 CCIP 进行跨链 NFT 铸造,你将更好地装备自己,探索去中心化应用程序和区块链技术的世界新领域。
如果你有任何问题,随时可以使用我们在 Discord 上的专用频道或者通过下面的表单提供反馈。通过关注我们的 Twitter 和我们的 Telegram 宣布频道,保持更新最新动态。
让我们知道 如果你有任何反馈或新主题的请求。我们很乐意听到你的想法。
- 原文链接: quicknode.com/guides/cro...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!