代码详情:https://github.com/langjiyunmie/Chainlink-CCIP.git chainlink学习的官方视频链接:https://www.bilibili.com/video/BV1RFsfe5Ek5/
源链 (Source Chain)
├── Sender (发送方合约)
├── Router (路由合约)
└── Token Pool (代币池)
目标链 (Destination Chain)
├── Receiver (接收方合约)
├── Router (路由合约)
└── Token Pool (代币池)
链下部分 (Offchain)
├── Committing DON (提交DON网络)
├── Executing DON (执行DON网络)
└── RMN (风险管理网络)
概括CCIP的三个部分的完整流程:
源链(Source Chain)
用户 → Sender合约 → Router → OnRamp → Token Pool
具体流程:
链下(Off-chain)
Committing DON → RMN → Executing DON
具体流程:
目标链(Destination Chain)
OffRamp → Token Pool → Router → 接收方合约
具体流程:
发起跨链请求:
用户使用sender合约,指定目标链,指定接收地址,准备要发送的代币,附带自定义消息数据
// 1. Sender发起跨链请求
contract Sender {
   function sendMessage(
       uint64 destinationChainSelector,
       address receiver,
       bytes memory data,
       TokenTransfer[] memory tokens
   ) external {
       // 构造CCIP消息
       Message memory message = Message({
           sender: msg.sender,
           receiver: receiver,
           data: data,
           tokens: tokens
       });
       // 计算费用
       uint256 fee = router.getFee(destinationChainSelector, message);
       // 调用Router
       router.ccipSend{value: fee}(destinationChainSelector, message);
   }
}  Router合约源链路由器,验证消息格式,计算费用,处理代币锁定
// 2. Router验证并转发到OnRamp
contract Router {
   function ccipSend(uint64 chainSelector, Message memory message) external {
       // 验证目标链
       validateDestination(chainSelector);
       // 获取对应的OnRamp
       OnRamp onRamp = getOnRamp(chainSelector);
       // 转发到OnRamp
       onRamp.forwardMessage(message);
   }
}OnRamp合约处理(源链):接收Router的请求,验证消息格式和费用,与Token Pool交互锁定代币,生成跨链消息的证明
// 3. OnRamp处理并与Token Pool交互
contract OnRamp {
   function forwardMessage(Message memory message) external {
       // 验证消息格式
       validateMessage(message);
       // 处理代币锁定
       for (TokenTransfer token : message.tokens) {
           tokenPool.lockTokens(
               token.token,
               token.amount,
               message.sender
           );
       }
       // 生成merkle叶子
       bytes32 leaf = generateLeaf(message);
       // 提交到Commit Store
       commitStore.addMessage(leaf);
       // 触发事件供DON监听
       emit MessageSent(message);
   }
}Token Pool: 代币池锁定源链代币,管理流动性
详细交互流程:
Committing DON 监听和处理
class CommittingDON {
   // 监听源链事件
   async listenToSourceChain() {
       sourceChain.on('MessageSent', async (event) => {
           const message = event.args.message;
           await this.processMessage(message);
       });
   }
   // 处理跨链消息
   async processMessage(message) {
       // 验证消息
       await this.validateMessage(message);
       // 构建 merkle 树
       const leaf = this.generateLeaf(message);
       const merkleTree = this.buildMerkleTree([leaf]);
       // 收集 DON 签名
       const signatures = await this.collectSignatures(merkleTree.root);
       // 提交到 RMN
       await this.submitToRMN(message, merkleTree, signatures);
   }
   // 生成 merkle 叶子
   generateLeaf(message) {
       return ethers.utils.keccak256(
           ethers.utils.defaultAbiCoder.encode(
               ['address', 'address', 'bytes', 'uint256'],
               [message.sender, message.receiver, message.data, message.nonce]
           )
       );
   }
}监听和收集:
验证流程:
数据整理:
Lane Manager(通道管理)
class LaneManager {
   constructor() {
       this.lanes = new Map();
       this.limits = new Map();
   }
   // 检查通道状态
   async checkLane(sourceChain, destChain) {
       const lane = this.getLane(sourceChain, destChain);
       // 检查通道容量
       if (lane.messageCount >= lane.maxCapacity) {
           throw new Error('Lane capacity exceeded');
       }
       // 检查限额
       if (lane.totalValue >= this.limits.get(lane.id)) {
           throw new Error('Lane value limit exceeded');
       }
       // 更新统计
       await this.updateLaneStats(lane);
   }
   // 更新通道统计
   async updateLaneStats(lane) {
       lane.messageCount++;
       lane.lastUpdated = Date.now();
       await this.persistLaneData(lane);
   }
}通道状态管理:
流量控制:
Price Feed(价格预言机):
class PriceFeed {
   // 获取实时价格
   async getPrice(token) {
       // 从多个源获取价格
       const prices = await Promise.all([
           this.getPriceFromSource1(token),
           this.getPriceFromSource2(token),
           this.getPriceFromSource3(token)
       ]);
       // 过滤和计算加权价格
       return this.calculateWeightedPrice(prices);
   }
   // 价格偏差检查
   async checkPriceDeviation(token, price) {
       const historicalPrice = await this.getHistoricalPrice(token);
       const deviation = Math.abs(price - historicalPrice) / historicalPrice;
       if (deviation > this.maxDeviation) {
           await this.triggerPriceProtection(token, price);
       }
   }
}价格数据服务:
价格保护机制:
Risk Management Network (RMN):
class RiskManagementNetwork {
   // 风险评估
   async assessRisk(message, context) {
       const riskScore = await this.calculateRiskScore({
           // 交易相关风险
           transactionRisk: await this.assessTransactionRisk(message),
           // 地址风险
           addressRisk: await this.assessAddressRisk(message.sender),
           // 网络风险
           networkRisk: await this.assessNetworkRisk(context),
           // 代币风险
           tokenRisk: await this.assessTokenRisk(message.tokens)
       });
       return this.evaluateRiskScore(riskScore);
   }
   // 流动性检查
   async checkLiquidity(message) {
       const liquidityData = await this.getLiquidityData(message.destChain);
       return {
           isLiquidityOk: liquidityData.available >= message.value,
           liquidityRatio: liquidityData.available / liquidityData.total
       };
   }
}风险评估:
安全检查:
流动性管理:
Executing DON:
class ExecutingDON {
   // 准备执行
   async prepareExecution(message, proof) {
       // 验证所有条件
       await this.validateExecutionConditions(message);
       // 准备执行数据
       const executionData = await this.prepareExecutionData(message);
       // 收集执行签名
       const signatures = await this.collectExecutionSignatures(executionData);
       return { executionData, signatures };
   }
   // 执行共识
   async reachConsensus(executionData) {
       const nodes = await this.getActiveNodes();
       const votes = await this.collectNodeVotes(nodes, executionData);
       if (this.hasConsensus(votes)) {
           return this.prepareConsensusProof(votes);
       }
       throw new Error('Consensus not reached');
   }
}执行准备:
共识过程:
执行触发:
跨链消息传递流程:
消息封装:
验证层级:
基础验证
共识验证
风险验证
监控和优化系统:
class MonitoringSystem {
   // 性能监控
   async monitorPerformance() {
       const metrics = {
           nodeLatency: await this.measureNodeLatency(),
           messageQueueSize: await this.getQueueSize(),
           processingTime: await this.getAverageProcessingTime(),
           resourceUsage: await this.getResourceMetrics()
       };
       await this.analyzeMetrics(metrics);
   }
   // 异常检测
   async detectAnomalies() {
       const patterns = await this.analyzePatterns();
       if (patterns.hasAnomaly) {
           await this.triggerAlert(patterns.anomalyType);
       }
   }
}性能监控:
数据分析:
系统调优:
应急响应机制:
class EmergencyHandler {
   // 处理异常
   async handleEmergency(error) {
       // 记录错误
       await this.logError(error);
       // 执行应急预案
       const plan = await this.selectEmergencyPlan(error);
       await this.executeEmergencyPlan(plan);
       // 通知相关方
       await this.notifyStakeholders(error, plan);
   }
   // 恢复服务
   async recoverService() {
       // 检查系统状态
       const status = await this.checkSystemStatus();
       // 执行恢复步骤
       if (status.needsRecovery) {
           await this.executeRecoverySteps(status);
       }
       // 验证恢复结果
       await this.verifyRecovery();
   }
}异常处理:
故障恢复:
OffRamp接收和验证:
这是目标链上的入口合约,负责接收和处理来自链下DON网络的跨链消息。它会验证消息的有效性,包括检查消息证明和DON签名。一旦验证通过,它会协调TokenPool进行代币释放,并通过Router将消息转发给最终的接收方。可以把它理解为跨链消息在目标链上的"报关处",负责验证和清关。
class OffRamp {
   async processIncomingMessage(message, proof) {
       // 1. 验证消息和证明
       await this.validateMessage(message, proof);
       // 2. 检查执行条件
       const executionContext = {
           message,
           proof,
           timestamp: await this.getBlockTimestamp(),
           gasPrice: await this.getGasPrice()
       };
       // 3. 准备执行
       await this.prepareExecution(executionContext);
   }
   async validateMessage(message, proof) {
       // 验证merkle证明
       const isValidProof = await this.verifyMerkleProof(
           message.leaf,
           proof.root,
           proof.path
       );
       // 验证DON签名
       const isValidSignature = await this.verifyDONSignatures(
           message,
           proof.signatures
       );
       if (!isValidProof || !isValidSignature) {
           throw new Error('Invalid message or proof');
       }
   }
}TokenPool合约:
这是代币管理合约,管理着目标链上用于跨链的代币流动性池。当OffRamp确认跨链消息有效后,TokenPool负责将对应数量的代币释放给接收方。它还管理流动性提供者的存款和取款,确保池中始终有足够的代币来满足跨链需求。这就像是一个银行金库,负责资金的安全存管和分发。
contract TokenPool {
   // 代币余额映射
   mapping(address => uint256) public poolBalance;
   mapping(address => uint256) public lockedAmount;
   // 释放代币给接收方
   function releaseTokens(
       address token,
       address receiver,
       uint256 amount
   ) external onlyOffRamp {
       require(
           poolBalance[token] >= amount,
           "Insufficient liquidity"
       );
       poolBalance[token] -= amount;
       IERC20(token).transfer(receiver, amount);
       emit TokensReleased(token, receiver, amount);
   }
   // 添加流动性
   function addLiquidity(
       address token,
       uint256 amount
   ) external {
       IERC20(token).transferFrom(
           msg.sender,
           address(this),
           amount
       );
       poolBalance[token] += amount;
       emit LiquidityAdded(token, amount);
   }
}Router合约
Router是消息路由合约,负责将验证过的跨链消息传递给正确的接收方合约。它会检查接收方是否是有效的合约地址,并调用接收方的ccipReceive函数。如果消息执行失败,Router还负责处理失败情况。它就像是一个邮递员,确保消息准确送达指定接收方。
contract Router {
   // 路由表
   mapping(address => bool) public whitelistedOffRamps;
   // 执行消息
   function routeMessage(
       OffRamp.CCIPMessage memory message
   ) external onlyOffRamp {
       // 验证接收方合约
       require(
           _isContract(message.receiver),
           "Receiver must be a contract"
       );
       // 调用接收方的ccipReceive函数
       try ICCIPReceiver(message.receiver).ccipReceive(
           message.sourceChainSelector,
           message.sender,
           message.data
       ) {
           emit MessageRouted(message.messageId);
       } catch Error(string memory reason) {
           emit MessageFailed(message.messageId, reason);
           _handleFailure(message);
       }
   }
}CommitStore合约:
这是消息存储和验证合约,存储了所有经过DON网络确认的消息根。当OffRamp收到跨链消息时,会向CommitStore验证该消息是否已经得到了DON网络的确认。它维护着一个可信消息的数据库,确保只有经过验证的消息才能被执行。这像是一个公证处,负责验证消息的真实性
contract CommitStore {
   // 存储已确认的消息根
   mapping(bytes32 => bool) public committedRoots;
   // DON签名者
   mapping(address => bool) public allowedSigners;
   // 验证并存储消息证明
   function verifyMessage(
       OffRamp.CCIPMessage memory message,
       bytes memory proof
   ) external returns (bool) {
       bytes32 root = _computeRoot(message);
       require(
           committedRoots[root],
           "Unknown message root"
       );
       require(
           _verifyProof(message, proof),
           "Invalid proof"
       );
       return true;
   }
}接收方合约接口:
这是最终接收和处理跨链消息的合约,需要实现ccipReceive接口。当Router转发消息时,接收方合约会被调用,然后根据收到的消息执行相应的业务逻辑。这可以是任何需要接收跨链消息的智能合约,比如跨链桥、跨链交易所等。
interface ICCIPReceiver {
   function ccipReceive(
       uint64 sourceChainSelector,
       address sender,
       bytes calldata data
   ) external;
}
// 示例实现
contract ExampleReceiver is ICCIPReceiver {
   // 只允许Router调用
   modifier onlyRouter() {
       require(
           msg.sender == address(router),
           "Only router can call"
       );
       _;
   }
   function ccipReceive(
       uint64 sourceChainSelector,
       address sender,
       bytes calldata data
   ) external override onlyRouter {
       // 解码数据
       (uint256 amount, bytes memory payload) = abi.decode(
           data,
           (uint256, bytes)
       );
       // 处理业务逻辑
       _handleBusinessLogic(sender, amount, payload);
   }
}权限控制合约:
contract AccessControl {
   // 角色定义
   bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
   bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
   // 角色管理
   mapping(bytes32 => mapping(address => bool)) public roles;
   modifier onlyRole(bytes32 role) {
       require(
           roles[role][msg.sender],
           "Caller is not authorized"
       );
       _;
   }
}目标链流程
消息。验证通过后,如果消息包含代币转移,OffRamp会通知TokenPool释放相应的代币。然后OffRamp将消息交给Router,Router负责将消息路由到正确的接收方合约。在这个过程中,CommitStore提供消息验证服务,确保只有经过DON网络确认的消息才会被处理。

原始模型
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IRouterClient} from "@chainlink/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "node_modules/@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "node_modules/@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {CCIPReceiver} from "@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.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.
 */
/// @title - A simple messenger contract for sending/receving string data across chains.
contract Messenger is CCIPReceiver, OwnerIsCreator {
    using SafeERC20 for IERC20;
    // Custom errors to provide more descriptive revert messages.
    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.
    error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
    error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.
    // Event emitted when a message is sent to another chain.
    event MessageSent(
        bytes32 indexed messageId, // The unique ID of the CCIP message.
        uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
        address receiver, // The address of the receiver on the destination chain.
        bytes text, // The text being sent.
        address feeToken, // the token address used to pay CCIP fees.
        uint256 fees // The fees paid for sending the CCIP message.
    );
    bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
    string private s_lastReceivedText; // Store the last received text.
    IERC20 private s_linkToken;
    // remember to add visibility for the variable 
    MyToken public nft;
    struct RequestData{
        uint256 tokenId;
        address newOwner;
    }
    /// @notice Constructor initializes the contract with the router address.
    /// @param _router The address of the router contract.
    /// @param _link The address of the link contract.
    constructor(address _router, address _link, address nftAddr) CCIPReceiver(_router) {
        s_linkToken = IERC20(_link);
        nft = MyToken(nftAddr);
    }
    /// @notice Sends data to receiver on the destination chain.
    /// @notice Pay for fees in LINK.
    /// @dev Assumes your contract has sufficient LINK.
    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
    /// @param _receiver The address of the recipient on the destination blockchain.
    /// @param _payload The data to be sent.
    /// @return messageId The ID of the CCIP message that was sent.
    function sendMessagePayLINK(
        uint64 _destinationChainSelector,
        address _receiver,
        bytes memory _payload
    )
        internal
        returns (bytes32 messageId)
    {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver,
            _payload,
            address(s_linkToken)
        );
        // Initialize a router client instance to interact with cross-chain router
        IRouterClient router = IRouterClient(this.getRouter());
        // Get the fee required to send the CCIP message
        uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
        if (fees > s_linkToken.balanceOf(address(this)))
            revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
        // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
        s_linkToken.approve(address(router), fees);
        // Send the CCIP message through the router and store the returned CCIP message ID
        messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
        // Emit an event with message details
        emit MessageSent(
            messageId,
            _destinationChainSelector,
            _receiver,
            _payload,
            address(s_linkToken),
            fees
        );
        // Return the CCIP message ID
        return messageId;
    }
    /// handle a received message
    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    )
        internal
        override
    {
        s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
        RequestData memory requestData = abi.decode(any2EvmMessage.data, (RequestData));
        uint256 tokenId = requestData.tokenId;
        address newOwner = requestData.newOwner;
        require(tokenLocked[tokenId], "the NFT is not locked");
        nft.transferFrom(address(this), newOwner, tokenId);
        emit TokenUnlocked(tokenId, newOwner);
    }
    /// @notice Construct a CCIP message.
    /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for sending a text.
    /// @param _receiver The address of the receiver.
    /// @param _payload The string data to be sent.
    /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas.
    /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message.
    function _buildCCIPMessage(
        address _receiver,
        bytes memory _payload,
        address _feeTokenAddress
    ) private pure returns (Client.EVM2AnyMessage memory) {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        return
            Client.EVM2AnyMessage({
                receiver: abi.encode(_receiver), // ABI-encoded receiver address
                data: _payload, // ABI-encoded string
                tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
                extraArgs: Client._argsToBytes(
                    // Additional arguments, setting gas limit
                    Client.EVMExtraArgsV1({gasLimit: 200_000})
                ),
                // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
                feeToken: _feeTokenAddress
            });
    }
    /// @notice Fetches the details of the last received message.
    /// @return messageId The ID of the last received message.
    /// @return text The last received text.
    function getLastReceivedMessageDetails()
        external
        view
        returns (bytes32 messageId, string memory text)
    {
        return (s_lastReceivedMessageId, s_lastReceivedText);
    }
    /// @notice Fallback function to allow the contract to receive Ether.
    /// @dev This function has no function body, making it a default function for receiving Ether.
    /// It is automatically called when Ether is sent to the contract without any data.
    receive() external payable {}
    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
    /// It should only be callable by the owner of the contract.
    /// @param _beneficiary The address to which the Ether should be sent.
    function withdraw(address _beneficiary) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = address(this).balance;
        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();
        // Attempt to send the funds, capturing the success status and discarding any return data
        (bool sent, ) = _beneficiary.call{value: amount}("");
        // Revert if the send failed, with information about the attempted transfer
        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
    }
    /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
    /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
    /// @param _beneficiary The address to which the tokens will be sent.
    /// @param _token The contract address of the ERC20 token to be withdrawn.
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));
        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();
        IERC20(_token).safeTransfer(_beneficiary, amount);
    }
}// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, ERC721Burnable, Ownable {
    string constant public METADATA_URI = "ipfs://QmXw7TEAJWKjKifvLE25Z9yjvowWk2NWY3WgnZPUto9XoA";
    uint256 private _nextTokenId;
    constructor(string memory tokenName, string memory tokenSymbol)
        ERC721(tokenName, tokenSymbol)
        Ownable(msg.sender)
    {}
    function safeMint(address to)
        public
    {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, METADATA_URI);
    }
    // The following functions are overrides required by Solidity.
    function _update(address to, uint256 tokenId, address auth)
        internal
        override(ERC721, ERC721Enumerable)
        returns (address)
    {
        return super._update(to, tokenId, auth);
    }
    function _increaseBalance(address account, uint128 value)
        internal
        override(ERC721, ERC721Enumerable)
    {
        super._increaseBalance(account, value);
    }
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory )
    {
        return super.tokenURI(tokenId);
    }
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IRouterClient} from "@chainlink/contracts/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "node_modules/@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "node_modules/@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {CCIPReceiver} from "@chainlink/contracts/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {IERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "node_modules/@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";
import {MyNFT} from "./MyNFT.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.
 */
/// @title - A simple messenger contract for sending/receving string data across chains.
contract NFTPoolLockAndRelease is CCIPReceiver, OwnerIsCreator {
    using SafeERC20 for IERC20;
    // Custom errors to provide more descriptive revert messages.
    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.
    error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
    error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.
    // Event emitted when a message is sent to another chain.
    event MessageSent(
        bytes32 indexed messageId, // The unique ID of the CCIP message.
        uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
        address receiver, // The address of the receiver on the destination chain.
        bytes text, // The text being sent.
        address feeToken, // the token address used to pay CCIP fees.
        uint256 fees // The fees paid for sending the CCIP message.
    );
    bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
    string private s_lastReceivedText; // Store the last received text.
    IERC20 private s_linkToken;
    MyNFT public nft;//第一步先实例化一个nft对象,同时需要在构造函数中初始化
    // remember to add visibility for the variable 
    MyToken public nft;
    struct RequestData{
        uint256 tokenId;
        address newOwner;
    }
    /// @notice Constructor initializes the contract with the router address.
    /// @param _router The address of the router contract.
    /// @param _link The address of the link contract.
    constructor(address _router, address _link, address nftAddr) CCIPReceiver(_router) {
        s_linkToken = IERC20(_link);
        nft = MyToken(nftAddr);
    }
    function lockAndSendNFT(
        uint256 tokenId,
        address newOwner,
        uint64 chainSelector,
        address receiver) public returns(bytes32 messageId){
        //transfer
        nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址
        //发送跨链消息:需要传入receiver地址和tokenid给到链下的ccip组件
        //通过加密,打包两个参数
        bytes memory payload = abi.encode(tokenId,newOwner);
        //发送消息,使用link支付
        bytes32 messageId = sendMessagePayLINK(chainSelector,receiver,payload);
    }
    /// @notice Sends data to receiver on the destination chain.
    /// @notice Pay for fees in LINK.
    /// @dev Assumes your contract has sufficient LINK.
    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
    /// @param _receiver The address of the recipient on the destination blockchain.
    /// @param _payload The data to be sent.
    /// @return messageId The ID of the CCIP message that was sent.
    function sendMessagePayLINK(
        uint64 _destinationChainSelector,
        address _receiver,
        bytes memory _payload
    )
        internal
        returns (bytes32 messageId)
    {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver,
            _payload,
            address(s_linkToken)
        );
        // Initialize a router client instance to interact with cross-chain router
        IRouterClient router = IRouterClient(this.getRouter());
        // Get the fee required to send the CCIP message
        uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
        if (fees > s_linkToken.balanceOf(address(this)))
            revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
        // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
        s_linkToken.approve(address(router), fees);
        // Send the CCIP message through the router and store the returned CCIP message ID
        messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
        // Emit an event with message details
        emit MessageSent(
            messageId,
            _destinationChainSelector,
            _receiver,
            _payload,
            address(s_linkToken),
            fees
        );
        // Return the CCIP message ID
        return messageId;
    }
    /// handle a received message
    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    )
        internal
        override
    {
        s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
        RequestData memory requestData = abi.decode(any2EvmMessage.data, (RequestData));
        uint256 tokenId = requestData.tokenId;
        address newOwner = requestData.newOwner;
        require(tokenLocked[tokenId], "the NFT is not locked");
        nft.transferFrom(address(this), newOwner, tokenId);
        emit TokenUnlocked(tokenId, newOwner);
    }
    /// @notice Construct a CCIP message.
    /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for sending a text.
    /// @param _receiver The address of the receiver.
    /// @param _payload The string data to be sent.
    /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas.
    /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message.
    function _buildCCIPMessage(
        address _receiver,
        bytes memory _payload,
        address _feeTokenAddress
    ) private pure returns (Client.EVM2AnyMessage memory) {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        return
            Client.EVM2AnyMessage({
                receiver: abi.encode(_receiver), // ABI-encoded receiver address
                data: _payload, // ABI-encoded string
                tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
                extraArgs: Client._argsToBytes(
                    // Additional arguments, setting gas limit
                    Client.EVMExtraArgsV1({gasLimit: 200_000})
                ),
                // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
                feeToken: _feeTokenAddress
            });
    }
    /// @notice Fetches the details of the last received message.
    /// @return messageId The ID of the last received message.
    /// @return text The last received text.
    function getLastReceivedMessageDetails()
        external
        view
        returns (bytes32 messageId, string memory text)
    {
        return (s_lastReceivedMessageId, s_lastReceivedText);
    }
    /// @notice Fallback function to allow the contract to receive Ether.
    /// @dev This function has no function body, making it a default function for receiving Ether.
    /// It is automatically called when Ether is sent to the contract without any data.
    receive() external payable {}
    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
    /// It should only be callable by the owner of the contract.
    /// @param _beneficiary The address to which the Ether should be sent.
    function withdraw(address _beneficiary) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = address(this).balance;
        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();
        // Attempt to send the funds, capturing the success status and discarding any return data
        (bool sent, ) = _beneficiary.call{value: amount}("");
        // Revert if the send failed, with information about the attempted transfer
        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
    }
    /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
    /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
    /// @param _beneficiary The address to which the tokens will be sent.
    /// @param _token The contract address of the ERC20 token to be withdrawn.
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));
        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();
        IERC20(_token).safeTransfer(_beneficiary, amount);
    }
}传入参数
function lockAndSendNFT(
       uint256 tokenId,
       address newOwner,
       uint64 chainSelector,
       address receiver) public{
           //transfer
   }
先将NFT锁入当前合约并检查
先创建一个nft的实例,前面我们已经创建了好了一个MyNFT的合约
import {MyNFT} from "./MyNFT.sol";MyNFT public nft;//第一步先实例化一个nft对象,同时需要在构造函数中初始化之后将自己拥有的NFT转移到当前的合约中
nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址发送跨链消息,首先两个主要函数起到主要作用
 /// @notice Sends data to receiver on the destination chain.
    /// @notice Pay for fees in LINK.
    /// @dev Assumes your contract has sufficient LINK.
    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.
    /// @param _receiver The address of the recipient on the destination blockchain.
    /// @param _payload The data to be sent.
    /// @return messageId The ID of the CCIP message that was sent.
    function sendMessagePayLINK(
        uint64 _destinationChainSelector,
        address _receiver,
        bytes memory _payload
    )
        internal
        returns (bytes32 messageId)
    {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        // evm to AnyMessage, 这个消息时从evm链上发送到链下的
        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
            _receiver,
            _payload,
            address(s_linkToken)
        );
        // Initialize a router client instance to interact with cross-chain router
        //Router验证请求并计算gas费用
        IRouterClient router = IRouterClient(this.getRouter());
        // Get the fee required to send the CCIP message
        //计算发送消息的gas费
        uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);
        if (fees > s_linkToken.balanceOf(address(this)))
            revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
        // approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
        //授权link给router合约 to 发送消息
        s_linkToken.approve(address(router), fees);
        // Send the CCIP message through the router and store the returned CCIP message ID
        //通过router合约发送ccip消息,并将CCIP message ID返回
        messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);
        // Emit an event with message details
        // 释放事件
        emit MessageSent(
            messageId,
            _destinationChainSelector,
            _receiver,
            _payload,
            address(s_linkToken),
            fees
        );
        // Return the CCIP message ID
        return messageId;
    }该函数的主要目的是构建一个用于跨链消息传递的 EVM2AnyMessage 结构体。这个结构体包含了发送跨链消息所需的所有信息
 /// @notice Construct a CCIP message.
    /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for sending a text.
    /// @param _receiver The address of the receiver.
    /// @param _payload The string data to be sent.
    /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas.
    /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message.
    function _buildCCIPMessage(
        address _receiver,
        bytes memory _payload,
        address _feeTokenAddress
    ) private pure returns (Client.EVM2AnyMessage memory) {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        return
            Client.EVM2AnyMessage({
                receiver: abi.encode(_receiver), // ABI-encoded receiver address
                data: _payload, // ABI-encoded string
                tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array aas no tokens are transferred
                extraArgs: Client._argsToBytes(
                    // Additional arguments, setting gas limit
                    Client.EVMExtraArgsV1({gasLimit: 200_000})
                ),
                // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
                feeToken: _feeTokenAddress
            });
    }
参数:
构建 EVM2AnyMessage 结构体:
return
  Client.EVM2AnyMessage({
      receiver: abi.encode(_receiver), // ABI-encoded receiver address
      data: _payload, // ABI-encoded string
      tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array as no tokens are transferred
      extraArgs: Client._argsToBytes(
          // Additional arguments, setting gas limit
          Client.EVMExtraArgsV1({gasLimit: 200_000})
      ),
      // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees
      feeToken: _feeTokenAddress
  });并将构建好的消息返回给 sendMessagePayLINK 函数
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
      Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(
          _receiver,
          _payload,
          address(s_linkToken)
      );这里重新写了函数 lockAndSendNFT ,将 tokenId ,newOwner 编码打包
function lockAndSendNFT(
      uint256 tokenId,
      address newOwner,
      uint64 chainSelector,
      address receiver) public returns(bytes32 messageId){
      //transfer
      nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址
      //发送跨链消息:需要传入receiver地址和tokenid给到链下的ccip组件
      //通过加密,打包两个参数
      bytes memory payload = abi.encode(tokenId,newOwner);
      //发送消息,使用link支付
      bytes32 messageId = sendMessagePayLINK(chainSelector,receiver,payload);
  } 其实对于结构体的参数结构,Client 库里面定义了
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// End consumer library.
library Client {
/// @dev RMN depends on this struct, if changing, please notify the RMN maintainers.
struct EVMTokenAmount {
  address token; // token address on the local chain.
  uint256 amount; // Amount of tokens.
}
struct Any2EVMMessage {
  bytes32 messageId; // MessageId corresponding to ccipSend on source.
  uint64 sourceChainSelector; // Source chain selector.
  bytes sender; // abi.decode(sender) if coming from an EVM chain.
  bytes data; // payload sent in original message.
  EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation.
}
// If extraArgs is empty bytes, the default is 200k gas limit.
struct EVM2AnyMessage {
  bytes receiver; // abi.encode(receiver address) for dest EVM chains
  bytes data; // Data payload
  EVMTokenAmount[] tokenAmounts; // Token transfers
  address feeToken; // Address of feeToken. address(0) means you will send msg.value.
  bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV2)
}
// bytes4(keccak256("CCIP EVMExtraArgsV1"));
bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9;
struct EVMExtraArgsV1 {
  uint256 gasLimit;
}
function _argsToBytes(
  EVMExtraArgsV1 memory extraArgs
) internal pure returns (bytes memory bts) {
  return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);
}
// bytes4(keccak256("CCIP EVMExtraArgsV2"));
bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10;
/// @param gasLimit: gas limit for the callback on the destination chain.
/// @param allowOutOfOrderExecution: if true, it indicates that the message can be executed in any order relative to other messages from the same sender.
/// This value's default varies by chain. On some chains, a particular value is enforced, meaning if the expected value
/// is not set, the message request will revert.
struct EVMExtraArgsV2 {
  uint256 gasLimit;
  bool allowOutOfOrderExecution;
}
function _argsToBytes(
  EVMExtraArgsV2 memory extraArgs
) internal pure returns (bytes memory bts) {
  return abi.encodeWithSelector(EVM_EXTRA_ARGS_V2_TAG, extraArgs);
}
}

来自目标链上的合约接收链下的ccip的组件的消息
首先接收的到消息需要先进行decode解码 any2EvmMessage,获取需要的信息,信息结构需要进行实例化
RequestData memory message = abi.decode(any2EvmMessage.data,(RequestData));
uint256 tokenId = message.tokenId;
address newOwner = message.newOwner;将 wnft 转给新的owner地址
//mint the NFT ,注意这里是mint一个nft,而不是直接进行transferFrom
 wnft.ResetTokenId(newOwner,tokenId);补充
接收的这个信息结构由Client库提供
/// handle a received message
  function _ccipReceive(
      Client.Any2EVMMessage memory any2EvmMessage
  )
      internal
      override
  {
      RequestData memory message = abi.decode(any2EvmMessage.data,(RequestData));
      uint256 tokenId = message.tokenId;
      address newOwner = message.newOwner;
      //mint the NFT
      wnft.ResetTokenId(newOwner,tokenId);
      emit MessageReceived(
          any2EvmMessage.messageId,
          any2EvmMessage.sourceChainSelector,
          abi.decode(any2EvmMessage.sender, (address)),
          tokenId,
          newOwner
      );
  }首先需要的参数与 lockAndSendNFT的函数是一样的,因为需要使用 sendMessagePayLINK 函数去发送消息
function BurnAndReturn(
      uint256 _tokenId, 
      address newOwner, 
      uint64 destChainSelector, 
      address receiver) public {}将 wnft 从owner地址转移到pool地址,用burn函数烧毁
// transfer NFT to the pool
wnft.transferFrom(msg.sender, address(this), _tokenId);
// burn the NFT
wnft.burn(_tokenId);使用encode打包消息,提供 payload 给 sendMessagePayLINK函数
// send transaction to the destination chain
bytes memory payload = abi.encode(_tokenId, newOwner);
sendMessagePayLINK(destChainSelector, receiver, payload);BurnAndReturn函数
function BurnAndReturn(
      uint256 _tokenId, 
      address newOwner, 
      uint64 destChainSelector, 
      address receiver) public {
          // verify if the sender is the owner of NFT
          // comment this because the check is already performed by ERC721
          // require(wnft.ownerOf(_tokenId) == msg.sender, "you are not the owner of the NFT");
          // transfer NFT to the pool
          wnft.transferFrom(msg.sender, address(this), _tokenId);
          // burn the NFT
          wnft.burn(_tokenId);
          // send transaction to the destination chain
          bytes memory payload = abi.encode(_tokenId, newOwner);
          sendMessagePayLINK(destChainSelector, receiver, payload);
  }
对于hardhat框架来说,部署的时候,主要用到两个工具,getNamedAccounts 和 deployments
getNamedAccounts
const {getNamedAccounts,deployments} = require("hardhat");
module.exports = async({getNamedAccounts,deployments}) => {
  const {firstAccount} = await getNamedAccounts();
  const {deploy,log} = deployments;
  log("Deploying CCIP Simulator...");
  await deploy("CCIPLocalSimulator",{
      contract: "CCIPLocalSimulator",
      from: firstAccount,
      log:true,
      args:[]
  });
  log("CCIPSimulator contract deployed successfully");
}
module.exports.tags = ["testlocal","all"];//输出标签使用异步函数,原因是需要先等待关键参数的获取,区别于按顺序执行命令,没有等待时间
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-ethers");
require("hardhat-deploy");
require("hardhat-deploy-ethers");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    compilers: [
      {
        version: "0.8.28",
        settings: {
          optimizer: {
            enabled: true,
            runs: 200
          }
        }
      }
    ]
  },
  namedAccounts: {
    firstAccount: {
      default: 0,
    }
  }
};
  await deploy("CCIPLocalSimulator",{
        contract: "CCIPLocalSimulator",
        from: firstAccount,
        log:true,
        args:[] //构造函数需要传入的参数
    });
    log("CCIPSimulator contract deployed successfully");const {getNamedAccounts,deployments, ethers} = require("hardhat");
module.exports = async({getNamedAccounts,deployments}) => {
    const {firstAccount} = await getNamedAccounts();
    const {deploy,log} = deployments;
    log("Deploying NFTPoolLockAndRelease contract...");
    //  1. 先获取部署信息
    const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator");
    // 2. 获取合约实例
    const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
    // 3. 调用configuration函数
    const ccipConfig = await ccipSimulator.configuration();
    // 4. 获取router,link的地址,nft的地址
    const sourceChainRouter = ccipConfig.sourceRouter_;
    const sourceChainlink = ccipConfig.linkToken_;
    const nftAddrDeployment = await deployments.get("MyNFT");
    const nftAddr = await nftAddrDeployment.address;
    // 5. 部署NFTPoolLockAndRelease合约
    await deploy("NFTPoolLockAndRelease",{
        contract: "NFTPoolLockAndRelease",
        from: firstAccount,
        log:true,
        //需要传入的参数: address _router, address _link, address nftAddr
        args:[sourceChainRouter,sourceChainlink,nftAddr]
    });
    log("NFTPoolLockAndRelease contract deployed successfully");
}
module.exports.tags = ["SourceChain","all"];deployment.get 方法 -- 获取部署的信息 -- 查找合约在哪
ethers.getContractAt -- 创建合约实例
const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);传入合约名字 和 地址信息,创建合约实例 -- ccipSimulator
调用configuration函数 --- 这个是 ccip-local 的函数
返回参数
/**
* @dev Returns the configuration of the CCIP simulator
*/
function configuration() 
  public 
  view 
  returns (
      uint64 chainSelector,
      IRouterClient sourceRouter,
      IRouterClient destinationRouter,
      WETH9 wrappedNative,
      LinkToken linkToken,
      BurnMintERC677Helper ccipBnM,
      BurnMintERC677Helper ccipLnM
  )
{
  // 返回所有配置参数
  return (
      chainSelector,
      sourceRouter,
      destinationRouter,
      wrappedNative,
      linkToken,
      ccipBnM,
      ccipLnM
  );
}//源链 --> 目标链
//mint 一个 nft 到源链
//将nft 锁定在源链, 发送跨链消息
//在目标链得到 mint的 wnft
//目标链 --> 源链
//将目标链的 wnft烧掉,发送跨链消息
//将源链的nft解锁,得到nft
//验证nft是否正确ethers.getContractAt 与 ethers.getContract 方法
ethers.getContractAt 用于任何地址的合约,包括其他项目的合约。需要传入合约地址进行调用
// 需要指定具体的合约地址
const myNFT = await ethers.getContractAt(
  "MyNFT",
  "0x1234..."  // 具体的合约地址
);
// 适用场景:
- 与已经部署的合约交互
- 与其他项目的合约交互
- 需要指定特定版本的合约ethers.getContract 适用于同一个项目
// 直接用合约名获取最新部署的合约
const myNFT = await ethers.getContract("MyNFT");
// 多传入一个signer参数,带 signer 的用法:
// 需要发送交易的场景
const contract = await ethers.getContract("ContractName", signer);
await contract.mint(tokenId);          // 铸造 NFT
await contract.transfer(to, amount);   // 转账
await contract.approve(spender, id);   // 授权
await contract.setBaseURI(uri);        // 设置 URI
// 适用场景:
- 在同一个项目中
- 合约刚刚部署完
- 想要获取最新部署的合约实例Chai工具
Chai 是一个用于测试的断言库,它让我们可以写出更易读的测试代码
const { expect } = require("chai");
describe("NFT Contract", function() {
    it("Should mint NFT correctly", async function() {
        const nft = await ethers.getContract("MyNFT");
        const [owner] = await ethers.getSigners();
        // Chai 的断言方法
        expect(await nft.balanceOf(owner.address)).to.equal(0);  // 检查初始余额
        await nft.mint(owner.address, 1);
        expect(await nft.balanceOf(owner.address)).to.equal(1);  // 检查铸造后余额
        expect(await nft.ownerOf(1)).to.equal(owner.address);    // 检查所有权
    });
});
常用方法
// 相等判断
expect(value).to.equal(expectedValue);
expect(value).to.be.equal(expectedValue);
// 大小比较
expect(value).to.be.gt(5);       // 大于
expect(value).to.be.gte(5);      // 大于等于
expect(value).to.be.lt(10);      // 小于
expect(value).to.be.lte(10);     // 小于等于
// 包含判断
expect(array).to.include(item);
expect(string).to.contain("text");
// 事件测试
await expect(contract.function())
    .to.emit(contract, "EventName")
    .withArgs(arg1, arg2);
// 错误测试
await expect(contract.function())
    .to.be.revertedWith("error message");Mocha
是一个功能强大的 JavaScript 测试框架,主要用于 Node.js 应用程序的单元测试和集成测试。它提供了一个灵活的测试环境,支持异步测试,并且可以与其他断言库(如 Chai)结合使用。
describe 块:
describe("Mint NFT,source chain --> destination chain",async function(){//一个注释,一个函数参数,JavaScript 的语法需要一个函数来包含代码块,函数参数就是来形成闭包的
   it("Mint NFT",async function(){
    await nft.mint(firstAccount.user1.address,1);
   })
})
//变量准备
const {getNamedAccounts,deployments, ethers} = require("hardhat");
const {expect} = require("chai");
let firstAccount;
let ccipSimulator;
let nft;
let wnft;
let NFTPoolLockAndRelease;
let NFTPoolBurnAndMint;
let chainSelector;
before(async function () {
    firstAccount = (await getNamedAccounts()).firstAccount;
    // 部署所有带 "all" 标签的合约并创建快照
    await deployments.fixture(["all"]);
    ccipSimulator = await ethers.getContract("CCIPLocalSimulator",firstAccount);
    nft = await ethers.getContract("MyNFT",firstAccount);
    wnft = await ethers.getContract("WrappedNFT",firstAccount);
    NFTPoolLockAndRelease = await ethers.getContract("NFTPoolLockAndRelease",firstAccount);
    NFTPoolBurnAndMint = await ethers.getContract("NFTPoolBurnAndMint",firstAccount);
    chainSelector = (await ccipSimulator.configuration()).chainSelector_;
})describe("source chain --> dest chain",
    async function () {
            it("mint nft and test the owner is minter",
                async function () {
                    // get nft 
                    await nft.safeMint(firstAccount);
                    const ownerOfNft = await nft.ownerOf(0);
                    expect(ownerOfNft).to.equal(firstAccount);
                    console.log("owner address is",firstAccount);
                }
            )
            it("transfer NFT from source chain to dest chain, check if the nft is locked",
                async function() {
                    await ccipSimulator.requestLinkFromFaucet(NFTPoolLockAndRelease.target, ethers.parseEther("10"))
                    // lock and send with CCIP
                    await nft.approve(NFTPoolLockAndRelease.target, 0)
                    await NFTPoolLockAndRelease.lockAndSendNFT(0, firstAccount, chainSelector, NFTPoolBurnAndMint.target)
                    // check if owner of nft is pool's address
                    const newOwner = await nft.ownerOf(0)
                    console.log("test")
                    expect(newOwner).to.equal(NFTPoolLockAndRelease.target)
                    // check if the nft is locked
                    const isLocked = await NFTPoolLockAndRelease.tokenLocked(0)
                    expect(isLocked).to.equal(true)
                }
            )
            it("check if the nft is minted on dest chain",
                async function() {
                    const ownerOfNft = await wnft.ownerOf(0)
                    expect(ownerOfNft).to.equal(firstAccount)
                }
            )
})
describe("dest chain --> source chain",
    async function () {
        it("burn nft and check the nft owner is firstAccount",
            async function() {
                await wnft.approve(NFTPoolBurnAndMint.target,0)
                await NFTPoolBurnAndMint.BurnAndReturn(0, firstAccount, chainSelector, NFTPoolLockAndRelease.target)
                const ownerOfNft = await nft.ownerOf(0)
                expect(ownerOfNft).to.equal(firstAccount)
            }
        )
    }
)网络配置文件
developmentChains = ["hardhat", "localhost"]
const networkConfig = {
    11155111: {
        name: "sepolia",
        router: "0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59",
        linkToken: "0x779877A7B0D9E8603169DdbD7836e478b4624789",
        companionChainSelector: "16281711391670634445"
    },
    80002: {
        name: "amoy",
        router: "0x9C32fCB86BF0f4a1A8921a9Fe46de3198bb884B2",
        linkToken: "0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904",
        companionChainSelector: "16015286601757825753"
    }
}
module.exports ={
    developmentChains,
    networkConfig
}deploy--ccipsimulator
如果network.name是 hardhat 或者 localhost,就运行该脚本
const {getNamedAccounts,deployments, network} = require("hardhat");
const {ethers} = require("hardhat");
const {developmentChains} = require("helper-hardhat-config.js")
module.exports = async({getNamedAccounts,deployments}) => {
   if(developmentChains.includes(network.name)){
       const {firstAccount} = await getNamedAccounts();
       const {deploy,log} = deployments;
       log("Deploying CCIP Simulator...");
       const ccipSimulator = await deploy("CCIPLocalSimulator",{
           contract: "CCIPLocalSimulator",
           from: firstAccount,
           log:true,
           args:[]
       });
   }
}
module.exports.tags = ["testlocal","all"];developmentChains.includes(network.name)
在 Hardhat 部署脚本中,network.name 会返回当前运行网络的名称。
比如:
当你运行 hardhat deploy 时,network.name 会是 "hardhat"
当你运行 hardhat deploy --network localhost 时,会是 "localhost"
deploy--NFTPoolLockAndRelease
const {getNamedAccounts,deployments, ethers, network} = require("hardhat");
const {developmentChains,networkConfig} = require("helper-hardhat-config.js")
module.exports = async({getNamedAccounts,deployments}) => {
   const {firstAccount} = await getNamedAccounts();
   const {deploy,log} = deployments;
   let sourceChainRouter
   let linkTokenAddr
   if(developmentChains.includes(network.name)){
       //  1. 先获取部署信息
       const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator");
       // 2. 获取合约实例
       const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
       // 3. 调用configuration函数
       const ccipConfig = await ccipSimulator.configuration();
       // 4. 获取router,link的地址,nft的地址
       sourceChainRouter = ccipConfig.sourceRouter_;
       linkTokenAddr = ccipConfig.linkToken_;
   }
   else{
       //network.config 是 Hardhat 提供的,用来获取当前运行网络的配置
       //network.config 从 hardhat.config.js 获取网络配置(比如 chainId)
       //用这个 chainId 去 helper-hardhat-config.js 中查找对应的合约配置
       sourceChainRouter = networkConfig[network.config.chainId].router
       linkTokenAddr = networkConfig[network.config.chainId].linkToken
   }
   log("Deploying NFTPoolLockAndRelease contract...");
   const nftAddrDeployment = await deployments.get("MyNFT");
   const nftAddr = await nftAddrDeployment.address;
   // 5. 部署NFTPoolLockAndRelease合约
   await deploy("NFTPoolLockAndRelease",{
       contract: "NFTPoolLockAndRelease",
       from: firstAccount,
       log:true,
       //需要传入的参数: address _router, address _link, address nftAddr
       args:[sourceChainRouter,sourceChainlink,nftAddr]
   });
   log("NFTPoolLockAndRelease contract deployed successfully");
}
module.exports.tags = ["SourceChain","all"];sourceChainRouter = networkConfig[network.config.chainId].router这种方法的使用例子
// 2. 使用数字作为键的对象
const networkConfig = {
 11155111: {
     name: "sepolia",
     router: "0x0BF3..."
 },
 80002: {
     name: "amoy",
     router: "0x9C32..."
 }
}
// 假设现在 network.config.chainId 是 11155111
// 这三种写法是等价的:
console.log(networkConfig[11155111].router)                  // "0x0BF3..."
console.log(networkConfig["11155111"].router)                // "0x0BF3..."
console.log(networkConfig[network.config.chainId].router)    // "0x0BF3..."而 network.config.chainId 是从 hardhat.config.js 的配置中去获取的
deploy--NFTPoolBurnAndMint
const {getNamedAccounts,deployments, ethers, network} = require("hardhat");
const {developmentChains,networkConfig} = require("../helper-hardhat-config.js")
module.exports = async({getNamedAccounts,deployments}) => {
   const {firstAccount} = await getNamedAccounts();
   const {deploy,log} = deployments;
   let destChainRouter;
   let linkTokenAddr;
   if(developmentChains.includes(network.name)){
       // 1. 获取部署信息
       const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator");
       // 2. 获取合约实例
       const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address);
       // 3. 获取router,link,wnft的地址
       const ccipConfig = await ccipSimulator.configuration();
       destChainRouter = ccipConfig.destinationRouter_;
       linkTokenAddr = ccipConfig.linkToken_;
   }
   else{
       destChainRouter = networkConfig[network.config.chainId].router
       linkTokenAddr = networkConfig[network.config.chainId].linkToken
   }
   log("Deploying NFTPoolBurnAndMint contract...");
   const wnftAddrDeployment = await deployments.get("WrappedNFT");
   const wnftAddr = wnftAddrDeployment.address;
   // 4. 部署NFTPoolBurnAndMint合约
   await deploy("NFTPoolBurnAndMint",{
       contract:"NFTPoolBurnAndMint",
       from:firstAccount,
       log:true,
       args:[destChainRouter,linkTokenAddr,wnftAddr]
   })
}
module.exports.tags = ["destChain","all"];// 定义一个名为 "check-nft" 的任务
task("check-nft")
    // 添加参数(可选)
    .addParam("address", "NFT contract address")
    // 添加可选参数(可选)
    .addOptionalParam("tokenId", "Token ID to check")
    // 设置任务描述(可选)
    .setDescription("Check NFT information")
    // 设置任务执行的操作
    .setAction(async (taskArgs, hre) => {
        // taskArgs: taskArgs 就是用来接收通过 addParam 和 addOptionalParam 定义的参数
        // hre: Hardhat Runtime Environment,包含 ethers, network 等工具
        // 任务逻辑
        const nftContract = await hre.ethers.getContractAt("YourNFT", taskArgs.address);
        console.log("Checking NFT...");
    });使用实例
task("check-nft")
    .addParam("address", "NFT contract address")
    .addOptionalParam("tokenId", "Token ID to check", "0")
    .setAction(async (taskArgs, hre) => {
        // 获取合约实例
        const nftContract = await hre.ethers.getContractAt("YourNFT", taskArgs.address);
        // 获取 NFT 信息
        const owner = await nftContract.ownerOf(taskArgs.tokenId);
        const uri = await nftContract.tokenURI(taskArgs.tokenId);
        console.log(`Token ${taskArgs.tokenId}:`);
        console.log(`Owner: ${owner}`);
        console.log(`URI: ${uri}`);
    });任务运行
# 基本使用
npx hardhat check-nft --address 0x123... --network sepolia
# 带可选参数
npx hardhat check-nft --address 0x123... --token-id 1 --network sepolia其他用法
task("task-name")
    // 添加必需参数
    .addParam("param1", "描述")
    // 添加可选参数
    .addOptionalParam("param2", "描述", "默认值")
    // 添加标志参数
    .addFlag("flag", "描述")
    // 添加位置参数
    .addPositionalParam("pos", "描述")
    // 设置描述
    .setDescription("任务描述")
    // 设置执行操作
    .setAction(async (taskArgs, hre) => {
        // 任务逻辑
    });文件组织结构
在 Hardhat 中,任务(Task)系统的文件组织采用了模块化的结构:每个具体任务都在独立的文件中定义(如 check-nft.js),然后通过一个中心化的 index.js 文件统一导出所有任务,最后在 hardhat.config.js 中只需要一行代码就能导入所有任务。这种结构使得代码更容易维护和扩展,同时保持了项目结构的清晰性。当需要添加新任务时,只需创建新的任务文件并在 index.js 中添加导出即可,而不需要修改配置文件。
如果你后来添加了新任务:
task("mint-nft").setAction(async(taskArgs,hre)=>{
    // 铸造 NFT 的逻辑
})
module.exports = {}只需要在 index.js 中添加:
exports.checkNft = require("./check-nft")
exports.mintNft = require("./mint-nft")  // 添加新任务hardhat.config.js 不需要改变:
require("./task")  // 自动包含所有任务mint-nft
const {task} = require("hardhat/config")
task("mint-nft").setAction(async(taskArgs,hre)=>{
   try {
       // 1. 检查网络
       const network = await hre.ethers.provider.getNetwork();
       console.log("Current network:", network.name, network.chainId);
       // 2. 检查账户
       const {firstAccount} = await hre.getNamedAccounts();
       console.log("Account:", firstAccount);
       // 3. 检查部署
       const deployments = await hre.deployments.all();
       console.log("Available deployments:", Object.keys(deployments));
       // 4. 获取合约
       console.log("Getting contract...");
       const MyNFT = await hre.deployments.get("MyNFT");
       console.log("Contract address:", MyNFT.address);
       // 5. 创建合约实例
       const nft = await hre.ethers.getContractAt(
           "MyNFT",
           MyNFT.address,
           await hre.ethers.getSigner(firstAccount)
       );
       // 6. 铸造 NFT
       console.log("Minting NFT...");
       const mintTx = await nft.safeMint(firstAccount);
       console.log("Waiting for confirmation...");
       await mintTx.wait(6);
       const tokenAmount = await nft.totalSupply();
       const tokenId = tokenAmount - 1n;
       console.log(`Mint successful! TokenId:${tokenId}, Amount:${tokenAmount}, Owner:${firstAccount}`);
   } catch (error) {
       console.error("Detailed error:");
       console.error(error);
       // 检查特定错误
       if (error.code === 'INVALID_ARGUMENT') {
           console.error("Contract deployment not found. Please ensure the contract is deployed to Sepolia.");
       }
   }
})
module.exports = {}为什么safeMint函数没有返回值,却可以赋值给 mintTx ?
在以太坊智能合约中,当你调用一个写入函数(比如 safeMint)时,它会返回一个 Transaction 对象,即使函数本身没有返回值。这是因为所有改变状态的操作都需要发送交易。
// 1. 调用 safeMint 函数会返回一个待处理的交易对象
const mintTx = await nft.safeMint(firstAccount)
// mintTx 包含了交易的信息,例如:
// {
//     hash: "0x...",          // 交易哈希
//     from: "0x...",          // 发送者地址
//     to: "0x...",            // 合约地址
//     nonce: 1,               // 交易序号
//     gasLimit: BigNumber,    // gas 限制
//     data: "0x...",          // 调用数据
//     value: BigNumber,       // 发送的以太币数量
//     ...
// }
// 2. wait(6) 等待 6 个区块确认
await mintTx.wait(6)  // 返回交易收据
// 交易收据包含:
// {
//     transactionHash: "0x...",
//     blockNumber: 123,
//     blockHash: "0x...",
//     status: 1,              // 1 表示成功
//     events: [...],          // 包含事件日志
//     ...
// }ethers.js 仍然会返回一个交易对象,这让我们可以:
这是以太坊交易机制的一部分,所有状态改变都通过交易完成
为什么需要这样子写 const tokenId = tokenAmount - BigInt(1) ?
1n 是 JavaScript 中的 BigInt 字面量表示法。在以太坊开发中,我们经常需要处理大数字,特别是当与智能合约交互时。
合约返回的数字通常是 BigNumber 或 BigInt,BigInt 的范围是无限的,只受限于系统的内存
JavaScript 的普通数字(Number)只能安全表示到 2^53 - 1
与 BigInt 类型的数字运算时,必须使用 BigInt 类型
例子
// ❌ 错误:不能混合 BigInt 和 Number
const tokenId = tokenAmount - 1    // TypeError
// ✅ 正确:使用 BigInt
const tokenId = tokenAmount - 1n   // 正确
const tokenId = tokenAmount - BigInt(1)  // 也正确
// 其他 BigInt 字面量例子
const a = 1n
const b = 100n
const c = 1000000000000000000n    // 1 ETH 的 wei 值check-nft
//引入task工具
const {task} = require("hardhat/config")
task("check-nft").setAction(async(taskArgs,hre)=>{
   const {firstAccount } = (await getNamedAccounts()).firstAccount;
   const nft = await hre.ethers.getContract("MyNFT",firstAccount)
   const totalSupply = await nft.totalSupply()
   console.log("check-nft status:")
   for(let tokenId=0; tokenId< totalSupply; tokenId++){
      const owner = await nft.ownerOf(tokenId)
      console.log(`tokenId:${tokenId},owner:${owner}`)
   }
})
module.exports = {}lock-and-cross
const {task} = require("hardhat/config");
const { networkConfig } = require("../helper-hardhat-config");
const { networks } = require("../hardhat.config");
task("lock-and-cross")
   .addParam("tokenid", "tokenid to lock and cross")
   .addOptionalParam("chainselector", "chainSelector of destination chain")
   .addOptionalParam("receiver", "receiver in destination chain")
   .setAction(async(taskArgs, hre) => {
       //get tokenid
       const tokenId = taskArgs.tokenid
       //get deployer
       const {firstAccount} = await hre.getNamedAccounts();
       console.log("deployer is:", firstAccount)
       //get chainSelector
       let destChainSelector
       if(taskArgs.chainselector){
           destChainSelector = taskArgs.chainselector
       }else{
           destChainSelector = networkConfig[hre.network.config.chainId].companionChainSelector
       }
       console.log("destination chainSelector is:", destChainSelector)
       //get receiver
       let destReceiver
       if(taskArgs.receiver){
           destReceiver = taskArgs.receiver
       }else{
           const nftBurnAndMint = await hre.companionNetworks["destChain"].deployments.get("NFTPoolBurnAndMint")
           destReceiver = nftBurnAndMint.address
       }
       console.log("destination receiver is:", destReceiver)
       //get link token
       const linkTokenAddr = networkConfig[hre.network.config.chainId].linkToken
       const linkToken = await hre.ethers.getContractAt("LinkToken", linkTokenAddr)
       console.log("link token is:", linkTokenAddr)
       //get nft pool
       const nftPoolLockAndRelease = await hre.ethers.getContract("NFTPoolLockAndRelease", firstAccount)
       console.log("nft pool is:", nftPoolLockAndRelease.target)
       //Transfer link token to nft pool
       const balanceBefore = await linkToken.balanceOf(nftPoolLockAndRelease.target)
       console.log("balance before is:", balanceBefore)
       const transferLinkTx = await linkToken.transfer(nftPoolLockAndRelease.target, hre.ethers.parseEther("0"))
       await transferLinkTx.wait(6)
       const balanceAfter = await linkToken.balanceOf(nftPoolLockAndRelease.target)
       console.log("balance after is:", balanceAfter)
       //get nft and approve
       const nft = await hre.ethers.getContract("MyNFT", firstAccount)
       await nft.approve(nftPoolLockAndRelease.target, tokenId)
       console.log("nft approved successfully")
       //lock nft
       console.log("locking nft...")
       console.log(`tokenId: ${tokenId}`, `owner: ${firstAccount}`, `destChainSelector: ${destChainSelector}`, `destReceiver: ${destReceiver}`)
       const lockAndCrossTx = await nftPoolLockAndRelease.lockAndSendNFT(tokenId, firstAccount , destChainSelector, destReceiver)
       await lockAndCrossTx.wait(6)
       console.log("nft locked and sent successfully")
        // provide the transaction hash
        console.log(`NFT locked and crossed, transaction hash is ${lockAndCrossTx.hash}`)
        //messageId
        console.log(`messageId is ${lockAndCrossTx.value}`)
   })注意这里要使用 addOptionalParam,保证参数是可选项,而不是 addParam
companionNetworks["destChain"].deployments.get("NFTPoolBurnAndMint"),我们执行命令的时候会使用 network --sepolia 这样的参数,这个参数可以让 hardhat 识别我们config文件里面的网络配置
不管你的函数是否有返回值,根据以太坊的规则,都会有交易对象返回,比如这里
const lockAndCrossTx = await nftPoolLockAndRelease.lockAndSendNFT(tokenId, firstAccount , destChainSelector, destReceiver)
     await lockAndCrossTx.wait(6)
     console.log("nft locked and sent successfully")
      // provide the transaction hash
      console.log(`NFT locked and crossed, transaction hash is ${lockAndCrossTx.hash}`)
      //messageId
      console.log(`messageId is ${lockAndCrossTx.value}`)lockAndSendNFT 是会返回一个 bytes32 类型的数据的,但是 lockAndCrossTx 并不是 bytes32 类型的数据,而是一个交易对象,交易对象类似json数据
const transferTx = await linkToken.transfer(...)
// transferTx = {
//     hash: "0x...",          // 交易哈希
//     from: "0x...",          // 发送者地址
//     to: "0x...",           // 接收者地址
//     data: "0x...",         // 交易数据
//     ...
// }还有一点,这里是先返回对象再进行 wait ,wait() 需要交易对象才能监听确认
必须先有交易才能等待它的确认
check-wnft
const { task } = require("hardhat/config")
task("check-wrapped-nft")
   .addParam("tokenid", "tokenid to check")
   .setAction(async(taskArgs, hre) => {
   const tokenId = taskArgs.tokenid
   const {firstAccount} = await getNamedAccounts()
   const wnft = await ethers.getContract("WrappedNFT", firstAccount)
   console.log("checking status of ERC-721")
   const totalSupply = await wnft.totalSupply()
   console.log(`there are ${totalSupply} tokens under the collection`)
   const owner = await wnft.ownerOf(tokenId)
   console.log(`TokenId: ${tokenId}, Owner is ${owner}`)
})
module.exports = {}burn-and-cross
const { task } = require("hardhat/config")
const { networkConfig } = require("../helper-hardhat-config")
task("burn-and-cross")
   .addParam("tokenid", "token id to be burned and crossed")
   .addOptionalParam("chainselector", "chain selector of destination chain")
   .addOptionalParam("receiver", "receiver in the destination chain")
   .setAction(async(taskArgs, hre) => {
       const { firstAccount } = await getNamedAccounts()
       // get token id from parameter
       const tokenId = taskArgs.tokenid
       const wnft = await ethers.getContract("WrappedNFT", firstAccount)
       const nftPoolBurnAndMint = await ethers.getContract("NFTPoolBurnAndMint", firstAccount)
       // approve the pool have the permision to transfer deployer's token
       const approveTx = await wnft.approve(nftPoolBurnAndMint.target, tokenId)
       await approveTx.wait(6)
       // transfer 10 LINK token from deployer to pool
       console.log("transfering 10 LINK token to NFTPoolBurnAndMint contract")
       const linkAddr = networkConfig[network.config.chainId].linkToken
       const linkToken = await ethers.getContractAt("LinkToken", linkAddr)
       const transferTx = await linkToken.transfer(nftPoolBurnAndMint.target, ethers.parseEther("0"))
       await transferTx.wait(6)
       // get chain selector
       let chainSelector
       if(taskArgs.chainselector) {
           chainSelector = taskArgs.chainselector
       } else {
           chainSelector = networkConfig[network.config.chainId].companionChainSelector
       }
       // get receiver
       let receiver
       if(taskArgs.receiver) {
           receiver = taskArgs.receiver
       } else {
           receiver = (await hre.companionNetworks["destChain"].deployments.get("NFTPoolLockAndRelease")).address
       }
       // burn and cross
       const burnAndCrossTx = await nftPoolBurnAndMint.BurnAndReturn(tokenId, firstAccount, chainSelector, receiver)
       console.log(`NFT burned and crossed with txhash ${burnAndCrossTx.hash}`)
})
module.exports = {} 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!