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;
            }
        
      2. _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 条评论

请先 登录 后评论