Chainlink--CCIP--NFT 讲解

ccip主要组件

三个领域

源链 (Source Chain)
├── Sender (发送方合约)
├── Router (路由合约)
└── Token Pool (代币池)

目标链 (Destination Chain)
├── Receiver (接收方合约)
├── Router (路由合约)
└── Token Pool (代币池)

链下部分 (Offchain)
├── Committing DON (提交DON网络)
├── Executing DON (执行DON网络)
└── RMN (风险管理网络)

工作流程

image-20250118090921151.png

概括CCIP的三个部分的完整流程:

  1. 源链(Source Chain)

    用户 → Sender合约 → Router → OnRamp → Token Pool

    具体流程:

    1. 用户调用Sender合约发起跨链请求
    2. Router验证请求并计算费用
    3. OnRamp准备跨链数据
    4. Token Pool锁定用户代币
    5. 生成消息证明并等待DON处理
    6. 链下(Off-chain)
  2. 链下(Off-chain)

    Committing DON → RMN → Executing DON

    具体流程:

    1. Committing DON监听并验证源链消息
    2. 构建merkle树和收集DON签名
    3. RMN进行风险评估和安全检查
    4. 通过后,Executing DON准备目标链执行数据
    5. 将执行包发送到目标链
    6. 目标链(Destination Chain)
  3. 目标链(Destination Chain)

    OffRamp → Token Pool → Router → 接收方合约

    具体流程:

    1. OffRamp接收并验证DON发来的执行包
    2. Token Pool释放对应代币给接收方
    3. Router将消息路由到接收方合约
    4. 接收方合约执行最终的业务逻辑

源链

  1. 发起跨链请求:

    用户使用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: 代币池锁定源链代币,管理流动性

    详细交互流程

    • 第一阶段:源链发起
    • 用户通过Sender合约发起跨链请求
    • Router接收请求并验证基本参数
    • Fee Manager计算所需费用
    • Price Registry检查代币价格
    • ARM进行初步风险评估
    • OnRamp准备跨链数据
    • Token Pool锁定相应代币
    • 生成merkle叶子并提交到Commit Store

链下

  1. 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]
               )
           );
       }
    }

    监听和收集:

    • 监听源链上的MessageSent事件
    • 收集交易详情和证明
    • 验证交易状态

    验证流程:

    • 验证消息格式
    • 检查签名有效性
    • 验证代币锁定状态
    • 确认费用支付

    数据整理:

    • 构建merkle树
    • 生成merkle证明
    • 准跨链数据包
  2. 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);
       }
    }

    通道状态管理:

    • 检查源链-目标链通道状态
    • 验证通道容量和限额
    • 监控通道拥堵情况
    • 更新通道统计数据

    流量控制:

    • 管理消息队列
    • 控制处理速率
    • 优化资源分配
    • 负载均衡
  3. 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);
           }
       }
    }

    价格数据服务:

    • 收集多源价格数据
    • 过滤异常价格
    • 计算加权平均价
    • 提供实时价格更新

    价格保护机制:

    • 监控价格波动
    • 设置价格偏差阈值
    • 触发价格保护
    • 更新价格预警
  4. 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
           };
       }
    }

    风险评估:

    • 交易规模分析
    • 地址行为评估
    • 网络状态监控
    • 代币风险评级

    安全检查:

    • 重放攻击检测
    • 异常行为识别
    • 流动性风险评估
    • 网络安全监控

    流动性管理:

    • 检查目标链流动性
    • 评估Token Pool状态
    • 监控资金流向
    • 预警异常提现
  5. 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');
       }
    }

    执行准备:

    • 接收RMN确认信息
    • 验证所有必要条件
    • 准备执行数据包
    • 构建执行证明

    共识过程:

    • DON节点投票
    • 收集执行签名
    • 达成执行共识
    • 确认执行决策

    执行触发:

    • 准备目标链交易
    • 构建执行参数
    • 发送执行指令
    • 监控执行状态
  6. 跨链消息传递流程:

    消息封装:

    • 源链交易信息
    • 代币转移数据
    • 执行指令
    • 证明数据

    验证层级:

    1. 基础验证

      • 格式检查
      • 参数验证
      • 签名确认
    2. 共识验证

      • DON节点验证
      • 多重签名
      • 阈值确认
    3. 风险验证

      • RMN评估
      • 安全检查
      • 异常识别
  7. 监控和优化系统:

    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);
           }
       }
    }

    性能监控:

    • 节点响应时间
    • 网络延迟
    • 处理队列状态
    • 资源使用率

    数据分析:

    • 交易模式分析
    • 风险模型更新
    • 性能瓶颈识别
    • 优化建议生成

    系统调优:

    • 动态参数调整
    • 资源分配优化
    • 处理策略更新
    • 性能优化
  8. 应急响应机制:

    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();
       }
    }

    异常处理:

    • 检测异常情况
    • 触发应急预案
    • 执行恢复流程
    • 记录事件日志

    故障恢复:

    • 节点故障切换
    • 数据同步修复
    • 状态一致性检查
    • 服务恢复确认

目标链

  1. 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');
           }
       }
    }
  2. 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);
       }
    }
  3. 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);
           }
       }
    }
  4. 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;
       }
    }
  5. 接收方合约接口:

    这是最终接收和处理跨链消息的合约,需要实现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);
       }
    }
  6. 权限控制合约:

    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网络确认的消息才会被处理。

跨链NFT

image-20250119151854398.png

源链发送消息

原始模型

// 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);
    }
}

NFT合约

// 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);
    }
}

NFT-Locked-Release

// 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);
    }

}
lockAndSendNFT
  1. 传入参数

    function lockAndSendNFT(
           uint256 tokenId,
           address newOwner,
           uint64 chainSelector,
           address receiver) public{
               //transfer
    
       }
    
  2. 先将NFT锁入当前合约并检查

    • 先创建一个nft的实例,前面我们已经创建了好了一个MyNFT的合约

      import {MyNFT} from "./MyNFT.sol";
      MyNFT public nft;//第一步先实例化一个nft对象,同时需要在构造函数中初始化

      之后将自己拥有的NFT转移到当前的合约中

      nft.transferFrom(msg.sender,address(this),tokenId);//从持有人地址转入当前地址

      发送跨链消息,首先两个主要函数起到主要作用

      1. sendMessagePayLINK
       /// @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;
          }
      1. _buildCCIPMessage

      该函数的主要目的是构建一个用于跨链消息传递的 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
                  });
          }
      

      参数:

      • address _receiver:接收者的地址,表示消息将发送到哪个地址。
      • bytes memory _payload:要发送的数据,通常是经过编码的参数。
      • address _feeTokenAddress:用于支付费用的代币地址。如果使用原生代币支付,则可以设置为 address(0)。

      构建 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
          });
        • receiver:使用 abi.encode 对接收者地址进行编码,以确保其格式正确。
        • data:直接使用传入的 _payload,这是要发送的消息内容。
        • tokenAmounts:初始化为空数组,因为在此消息中不涉及代币转移。
        • extraArgs:使用 Client._argsToBytes 函数设置额外参数,这里主要是设置了 gasLimit,确保跨链消息有足够的 gas 进行处理。
        • feeToken:设置为传入的费用代币地址,指明将使用哪个代币支付跨链消息的费用。

        并将构建好的消息返回给 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);
        }
        }
        

NFT-Burn-Mint

image-20250119152806603.png

MINT--_ccipReceive

来自目标链上的合约接收链下的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
          );
      }
BURN--BurnAndReturn
  • 首先需要的参数与 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框架来说,部署的时候,主要用到两个工具,getNamedAccountsdeployments

  • 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"];//输出标签

    使用异步函数,原因是需要先等待关键参数的获取,区别于按顺序执行命令,没有等待时间

    • 使用 getNamedAccounts() 方法时,需要在 hardhat-config 文件中进行配置,需要先声明 firstAccount 对象
    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,
        }
      }
    };
    
    • deploy 和 log 是 deployment 的方法
      await deploy("CCIPLocalSimulator",{
            contract: "CCIPLocalSimulator",
            from: firstAccount,
            log:true,
            args:[] //构造函数需要传入的参数
        });
    
        log("CCIPSimulator contract deployed successfully");
    • deployments 方法的使用
    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
        );
      }

测试

test --流程测试

//源链 --> 目标链
//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)
            }
        )
    }
)

task测试

网络配置文件

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
}
部署脚本更改
  1. 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"

  2. 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 的配置中去获取的

  3. 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"];
task的工具使用
// 定义一个名为 "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")  // 自动包含所有任务
任务脚本
  1. 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 仍然会返回一个交易对象,这让我们可以:

      1. 获取交易哈希
      2. 等待交易确认
      3. 检查交易状态
      4. 获取事件日志

      这是以太坊交易机制的一部分,所有状态改变都通过交易完成

    • 为什么需要这样子写 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 值
  2. 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 = {}
  3. 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() 需要交易对象才能监听确认

      必须先有交易才能等待它的确认

  4. 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 = {}
  5. 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 = {}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
浪迹陨灭
浪迹陨灭
0x0c37...a92b
专注于solidity智能合约的开发