练习 2: 创建你的首个跨链NFT
代码时间 🎉
CCIP 配置参数
Chain Selector
Copy
Copy
CCIP 路由合约地址
0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165
0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59
LINK Token i之
0xb1D4538B4571d411F07960EF2838Ce337FE1E80E
0x779877A7B0D9E8603169DdbD7836e478b4624789
开始使用
您可以使用任何区块链开发框架来使用Chainlink CCIP。对于本次训练营,我们准备了Hardhat、Foundry 和 Remix IDE 的步骤说明。
让我们创建一个新项目。
确保你已安装Foundry。使用下列命令进行检查:
forge --version
创建一个新的文件夹并命名为ccip-masterclass-3
mkdir ccip-masterclass-3
进入该文件夹
cd ccip-masterclass-3
运行下面命令来创建一个新的Foundry工程:
forge init
进入网站 https://remix.ethereum.org/ 并点击“Create new Workspace”按钮。选择“Blank”模板并将工作区命名为“CCIP Bootcamp day2”。
或者你可以克隆:
安装@chainlink/contracts-ccip NPM包
要使用Chainlink CCIP,您需要与 @chainlink/contracts-ccip NPM 包中的一些Chainlink CCIP特定合约进行交互。
安装此包,请按照您用于本x的开发环境进行操作。
npm i @chainlink/contracts-ccip --save-dev
我们还需要一个标准的 @chainlink/contracts NPM包用于本模块,所以在这里我们也可以通过运行以下命令来安装它:
npm i @chainlink/contracts --save-dev
最后,对于本次练习,我们还需要安装 @openzeppelin/contracts NPM包。为此,请运行以下命令:
npm i @openzeppelin/contracts --save-dev
运行:
forge install smartcontractkit/ccip@ccip-develop
然后在您的 foundry.toml
或 remappings.txt
文件中设置 remappings 为
# foundry.toml
remappings = [
'@chainlink/contracts-ccip/=lib/ccip/contracts/'
]
我们还需要一个标准的 @chainlink/contracts NPM包用于本模块,所以趁现在我们通过运行以下命令来安装它:
forge install smartcontractkit/chainlink
并设置remappings为:
# foundry.toml
remappings = [
'@chainlink/contracts/=lib/chainlink/contracts/'
]
最后对于本次练习,我们还需要安装 @openzeppelin/contracts NPM包。为此,请运行以下命令:
forge install OpenZeppelin/openzeppelin-contracts
并在 foundry.toml
或 remappings.txt
文件中将 remappings 设置为 @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/。
# foundry.toml
remappings = [
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
]
创建一个新的 Solidity 文件,并粘贴上以下内容。这是一个空合约,只导入了 @chainlink/contracts-ccip
包中的一个合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Empty {}
编译它。如果成功编译并生成了 .deps/npm/@chainlink/contracts-ccip
、.deps/npm/@chainlink/contracts
和 .deps/npm/@openzeppelin/contracts
文件夹,意味着我们已将所有必要的包导入到了 Remix IDE 工作区。
水龙头
您可以使用 LINK 代币或给定区块链上的原生代币/wrapped原生代币来支付 CCIP 费用。在本次练习中,我们需要至少 1 个 LINK 或 Arbitrum Sepolia 测试网代币。要获取它,请前往https://faucets.chain.link/arbitrum-sepolia
开发xNFT智能合约
在contracts
文件夹中创建新文件XNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(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));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}
运行以下指令来编译:
npx hardhat compile
在src
文件夹中创建新文件XNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(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));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}
点击“Create new file”按钮来创建新的Solidity文件XNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(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));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}
准备部署
按照步骤添加必要的环境变量,以便部署这些合约并发送您的第一个 CCIP 消息。
该合约需要至少 0.8.20 的 Solidity 版本。因为在 0.8.20 版本的 Solc 中,默认的 EVM 版本为“上海”。在上海升级中,一个新的操作码 PUSH0 被添加到以太坊虚拟机中。
然而,除了以太坊外的大多数区块链尚未支持 PUSH0 操作码。
这意味着 PUSH0 操作码现在是合约字节码的一部分,如果您正在使用的链不支持它,它将会报出“无效操作码”错误。
为了了解更多信息,我们强烈建议您查看此 StackOverflow 的回复:
我们将使用 @chainlink/env-enc
包来提高安全性。它通过创建一个新的 .env.enc
文件来加密敏感数据,而不是将其以明文形式存储在 .env 文件中。虽然不建议将此文件上传到网络,但如果意外发生,您的机密信息仍然会被加密。 通过运行以下命令来安装该包:
npm i @chainlink/env-enc --save-dev
设置一个密码用于加密和解密环境变量文件。您可以稍后通过输入相同的命令来更改密码。
npx env-enc set-pw
现在设置以下环境变量:PRIVATE_KEY、源区块链的 RPC URL、目标区块链的 RPC URL。在此示例中,我们将使用 Arbitrum Sepolia 和 Ethereum Sepolia。
PRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""
要设置以上变量,请输入以下命令并按照终端中的说明进行操作:
npx env-enc set
完成以上操作后,.env.enc 文件将自动生成。如果您想验证您的输入,可以随时运行以下命令:
npx env-enc view
最后,扩展 hardhat.config
文件以支持这两个网络:
import * as dotenvenc from '@chainlink/env-enc'
dotenvenc.config();
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const ARBITRUM_SEPOLIA_RPC_URL = process.env.ARBITRUM_SEPOLIA_RPC_URL;
const ETHEREUM_SEPOLIA_RPC_URL = process.env.ETHEREUM_SEPOLIA_RPC_URL;
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: '0.8.20',
settings: {
evmVersion: 'paris'
}
}
]
},
networks: {
hardhat: {
chainId: 31337
},
arbitrumSepolia: {
url: ARBITRUM_SEPOLIA_RPC_URL !== undefined ? ARBITRUM_SEPOLIA_RPC_URL : '',
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 421614
},
ethereumSepolia: {
url: ETHEREUM_SEPOLIA_RPC_URL !== undefined ? ETHEREUM_SEPOLIA_RPC_URL : '',
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 11155111
},
}
};
export default config;
创建一个新文件.env
。在其中填写您的钱包的 PRIVATE_KEY 以及至少两个区块链的 RPC URL。在此示例中,我们将使用 Arbitrum Sepolia 和 Ethereum Sepolia。
PRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""
完成后,为了加载 .env 文件中的变量,请运行以下命令:
source .env
最后,扩展 foundry.toml
文件以支持这两个网络:
[profile.default]
src = 'src'
out = 'out'
remappings = [
'@chainlink/contracts/=lib/chainlink/contracts',
'@chainlink/contracts-ccip/=lib/chainlink/contracts-ccip',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
]
solc = '0.8.20'
evm_version = 'paris'
[rpc_endpoints]
arbitrumSepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
ethereumSepolia = "${ETHEREUM_SEPOLIA_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
点击进入到“Solidity compiler”选项卡
展开“Advanced Configurations”下拉菜单
展开“EVM VERSION”下拉菜单并选择“paris”而不是“default”
点击进入“Deploy & run transactions”选项卡,并从“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。
如果您使用的是 Metamask 钱包,其中已经预装了Ethereum Sepolia 网络。确保您添加了 Arbitrum Sepolia 网络。
访问 Chainlist.org 并搜索“arbitrum sepolia”。一旦看到 Chain ID 为 421614 的网络,点击“Add to Metamask”按钮。
步骤1)在Ethereum Sepolia上部署XNFT.sol
准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
进入scripts
文件夹并创建一个名为 deployXNFT.ts
的新文件。
// deployXNFT.ts
import { ethers, network } from "hardhat";
async function main() {
const ccipRouterAddressEthereumSepolia = `0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59`;
const linkTokenAddressEthereumSepolia = `0x779877A7B0D9E8603169DdbD7836e478b4624789`;
const chainIdEthereumSepolia = `16015286601757825753`;
const xNft = await ethers.deployContract("XNFT", [
ccipRouterAddressEthereumSepolia,
linkTokenAddressEthereumSepolia,
chainIdEthereumSepolia
]);
await xNft.waitForDeployment();
console.log(`XNFT deployed on ${network.name} with address ${xNft.target}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行部署脚本:
npx hardhat run ./scripts/deployXNFT.ts --network ethereumSepolia
准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
选项 1)
通过运行以下命令部署 XNFT.sol
forge create --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59 0x779877A7B0D9E8603169DdbD7836e478b4624789 16015286601757825753
选项 2)
在 scripts
文件夹下创建文件 XNFT.s.sol
请注意,此示例中的XNFT 的部署是直接硬编码 Ethereum Sepolia 的相关配置,但您可以重构以下部署脚本以支持其他网络。这里可参考 CCIP Starter Kit (Foundry version) 。
// script/XNFT.s.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "forge-std/Script.sol";
import {XNFT} from "../src/XNFT.sol";
contract DeployXNFT is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
address ccipRouterAddressEthereumSepolia = 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59;
address linkTokenAddressEthereumSepolia = 0x779877A7B0D9E8603169DdbD7836e478b4624789;
uint64 chainSelectorEthereumSepolia = 16015286601757825753;
XNFT xNft = new XNFT(
ccipRouterAddressEthereumSepolia,
linkTokenAddressEthereumSepolia,
chainSelectorEthereumSepolia
);
console.log(
"XNFT deployed to ",
address(xNft)
);
vm.stopBroadcast();
}
}
运行以下指令来部署XNFT.sol
:
forge script ./script/XNFT.s.sol:XNFT -vvv --broadcast --rpc-url ethereumSepolia
准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
打开 Metamask 钱包,切换到 Ethereum Sepolia 网络。
打开 XNFT.sol 文件。
打开“Solidity Compiler”选项卡,然后点击“Compile XNFT.sol”按钮。
打开“Deploy & run Transaction”选项卡,然后在“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。确保`chainId`切换为11155111(如果没有切换,你需要在浏览器中刷新Remix IDE页面)
在“Contract”下拉菜单中,确保被选中的是“XNFT - XNFT.sol”。
找到橘黄色的按钮“Deploy”。在 ccipRouterAddress
中填入0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59
,在 linkTokenAddress
中填入 0x779877A7B0D9E8603169DdbD7836e478b4624789
,在 currentChainSelector
中填入 16015286601757825753
。
点击橘黄色的“Deploy/Transact”按钮。
Metamask 通知会弹出来,对交易进行签名。
步骤 2) 在Arbitrum Sepolia上部署XNFT.sol
准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
进入scripts
文件夹并创建一个名为 deployXNFTArbitrum.ts
的新文件。
// deployXNFTArbitrum.ts
import { ethers, network } from "hardhat";
async function main() {
const ccipRouterAddressArbitrumSepolia = `0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165`;
const linkTokenAddressArbitrumSepolia = `0xb1D4538B4571d411F07960EF2838Ce337FE1E80E`;
const chainIdArbitrumSepolia = `3478487238524512106`;
const xNft = await ethers.deployContract("XNFT", [
ccipRouterAddressArbitrumSepolia,
linkTokenAddressArbitrumSepolia,
chainIdArbitrumSepolia
]);
await xNft.waitForDeployment();
console.log(`XNFT deployed on ${network.name} with address ${xNft.target}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行部署脚本:
npx hardhat run ./scripts/deployXNFTArbitrum.ts --network arbitrumSepolia
准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
选项 1)
通过运行以下命令部署 XNFT.sol
forge create --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E 3478487238524512106
选项 2)
在 scripts
文件夹下创建文件 XNFTArbitrum.s.sol
请注意,此示例中的XNFT 的部署是直接硬编码 Arbitrum Sepolia 的相关配置,但您可以重构以下部署脚本以支持其他网络。这里可参考 CCIP Starter Kit (Foundry version) 。
// script/XNFTArbitrum.s.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "forge-std/Script.sol";
import {XNFT} from "../src/XNFT.sol";
contract DeployXNFTArbitrum is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
address ccipRouterAddressArbitrumSepolia = 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165;
address linkTokenAddressArbitrumSepolia = 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E;
uint64 chainSelectorArbitrumSepolia = 3478487238524512106;
XNFT xNft = new XNFT(
ccipRouterAddressArbitrumSepolia,
linkTokenAddressArbitrumSepolia,
chainSelectorArbitrumSepolia
);
console.log(
"XNFT deployed to ",
address(xNft)
);
vm.stopBroadcast();
}
}
运行以下指令来部署XNFT.sol
:
forge script ./script/XNFTArbitrum.s.sol:XNFT -vvv --broadcast --rpc-url arbitrumSepolia
准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
打开您的 Metamask 钱包并切换到 Arbitrum Sepolia 网络。
打开 XNFT.sol 文件。
点击进入“Solidity Compiler”选项卡并点击“Compile XNFT.sol”按钮。
点击进入“Deploy & run transactions”选项卡,并从“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。确保 chainId 切换到 421614(如果没有,您可能需要刷新浏览器中的 Remix IDE 页面)。
在“Contract”下拉菜单中,确保选择了“XNFT - XNFT.sol”。
找到橙色的“Deploy”按钮。提供以下信息:
ccipRouterAddress:0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165
linkTokenAddress:0xb1D4538B4571d411F07960EF2838Ce337FE1E80E
currentChainSelector:3478487238524512106
点击橙色的“Deploy”/“Transact”按钮。
Metamask 通知将弹出。签署交易。
步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法
需要准备:
您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址;
您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;
chainSelector
参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
如果您想自己计算这个值,可以重用以下辅助智能合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}
在scripts
文件夹中创建TypeScript文件enableChain.ts
:
// scripts/enableChain.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `ethereumSepolia`) {
console.error(`Must be called from Ethereum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressEthereumSepolia = `PUT XNFT ADDRESS ON ETHEREUM SEPOLIA HERE`;
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const chainSelectorArbitrumSepolia = `3478487238524512106`;
const ccipExtraArgs = `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressEthereumSepolia, signer);
const tx = await xNft.enableChain(
chainSelectorArbitrumSepolia,
xNftAddressArbitrumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令来调用enableChain方法:
npx hardhat run ./scripts/enableChain.ts --network ethereumSepolia
需要准备:
您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址;
您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;
chainSelector
参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
如果您想自己计算这个值,可以重用以下辅助智能合约。在scripts
文件夹内创建文件EncodeExtraArgs.s.sol
并在其中粘贴以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}
运行:
cast send <XNFT_ADDRESS_ON_ETHEREUM_SEPOLIA> --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY "enableChain(uint64,address,bytes)" 3478487238524512106 <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
在“Deployed Contracts”部分,您应该能找到之前部署到 Ethereum Sepolia 的 XNFT.sol 合约。找到 enableChain
函数并提供以下参数:
chainSelector
参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址,作为 xNftAddress 参数;
ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
点击橙色的“Transact”按钮。 如果您想自己计算这个值,可以重用以下辅助智能合约。创建 EncodeExtraArgs.sol
文件并粘贴以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}
步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法
需要准备:
您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;
您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址,作为 xNftAddress 参数;
chainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 网络的 CCIP Chain Selector;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
如果您想自己计算这个值,可以重用以下辅助智能合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}
在scripts
文件夹中创建TypeScript文件enableChainArbitrum.ts
:
// scripts/enableChainArbitrum.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const xNftAddressEthereumSepolia = `PUT XNFT ADDRESS ON ETHEREUM SEPOLIA HERE`;
const chainSelectorEthereumSepolia = `16015286601757825753`;
const ccipExtraArgs = `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.enableChain(
chainSelectorEthereumSepolia,
xNftAddressEthereumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令来调用enableChain方法:
npx hardhat run ./scripts/enableChainArbitrum.ts --network arbitrumSepolia
需要准备:
您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;
您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址,作为参数
xNftAddress
;chainSelector
参数:16015286601757825753
,这是 Ethereum Sepolia 网络的 CCIP Chain Selector;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
如果您想自己计算这个值,可以重用以下辅助智能合约。在scripts
文件夹内创建文件EncodeExtraArgs.s.sol
并在其中粘贴以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}
运行:
cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "enableChain(uint64,address,bytes)" 16015286601757825753 <XNFT_ADDRESS_ON_ETHEREUM_SEPOLIA> 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
在“Deployed Contracts”部分,您应该能找到之前部署到 Arbitrum Sepolia 的 XNFT.sol
合约。找到 enableChain
函数并提供以下参数:
chainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 网络的 CCIP Chain Selector;您之前部署到 Ethereum Sepolia 的
XNFT.sol
智能合约地址,作为xNftAddress
参数;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
点击橙色的“Transact”按钮。 如果您想自己计算这个值,可以重用以下辅助智能合约。创建 EncodeExtraArgs.sol
文件并粘贴以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
extraArgsBytes = Client._argsToBytes(extraArgs);
}
}
步骤 5) 在Arbitrum Sepolia网络中, 为XNFT.sol充值3 LINK
为支付 CCIP 费用,向XNFT.sol
充值一定数量的 LINK。3 个 LINK 对于此演示应该绰绰有余。当然,为了实现完整的功能,您还应在其他区块链上为XNFT.sol
智能合约充值,以便在所有区块链之间执行跨链转账。
步骤 6) 在Arbitrum Sepolia网络中, 铸造新的xNFT
在scripts文件夹中创建
TypeScript文件mint.ts:
// scripts/mint.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.mint();
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令调用mint方法:
npx hardhat run ./scripts/mint.ts --network arbitrumSepolia
执行:
cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "mint()"
在“Deployed Contracts”部分,您应该能找到之前部署到 Arbitrum Sepolia 的 XNFT.sol
合约。找到 mint
函数,然后点击橙色的“Transact”按钮。
步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT
需要准备:
from
参数:您的 EOA 地址;to
参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;tokenId
参数:您要跨链转移的 xNFT 的 ID;destinationChainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;payFeesIn
参数:1,表示我们用 LINK 支付 CCIP 费用。
在 scripts
文件夹下创建一个 TypeScript 文件crossChainTransferFrom.ts
// scripts/crossChainTransfer.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const from = `PUT YOUR EOA ADDRESS HERE`;
const to = `PUT RECEIVER's ADDRESS HERE`;
const tokenId = 0; // put NFT token id here
const destinationChainSelector = `16015286601757825753`;
const payFeesIn = 1; // 0 - Native, 1 - LINK
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.crossChainTransferFrom(
from,
to,
tokenId,
destinationChainSelector,
payFeesIn
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令调用crossChainTransferFrom方法:
npx hardhat run ./scripts/crossChainTransfer.ts --network arbitrumSepolia
需要准备:
from
参数:您的 EOA 地址;to
参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;tokenId
参数:您要跨链转移的 xNFT 的 ID;destinationChainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;payFeesIn
参数:1,表示我们用 LINK 支付 CCIP 费用。
运行:
cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "crossChainTransferFrom(address,address,uint256,uint64,uint8)" <YOUR_EOA_ADDRESS> <RECEIVER_ADDRESS> 0 16015286601757825753 1
在“Deployed Contracts”部分,您应该能找到之前部署到 Arbitrum Sepolia 的 XNFT.sol
合约。找到 crossChainTransferFrom
函数并提供以下参数:
from
参数:您的 EOA 地址;to
参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;tokenId
参数:您要跨链转移的 xNFT 的 ID;destinationChainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;payFeesIn
参数:1,表示我们用 LINK 支付 CCIP 费用。
点击橙色的“Transact”按钮。
您现在可以在 CCIP Explorer 页面监控此次NFT的跨链转移。
一旦跨链 NFT 到达 Ethereum Sepolia,您可以在 Metamask 钱包中手动显示它。点击进入“NFT”选项卡并点击“Import NFT”按钮。
然后填写 Ethereum Sepolia 上的 XNFT.sol
智能合约地址和您收到的代币 ID(0)。
最后,您的 NFT 将显示在 Metamask 钱包中。
Last updated