ethers.js与solidity智能合约交互(hardhat项目)

  • zsjayy
  • 更新于 2024-12-06 09:53
  • 阅读 282

1、test脚本中如何获取合约中的状态变量//合约中public类型的状态变量支持getter()特性,可以直接使用部署合约的实例调用如:vault.token()contractVault{//这里的token属性是public,自带getter()方法IERC20p

1、test脚本中如何获取合约中的状态变量

//合约中public类型的状态变量支持getter()特性,可以直接使用部署合约的实例调用如:vault.token()
contract Vault {
    //这里的token属性是public,自带getter()方法
    IERC20 public immutable token;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor(address _token) {
        token = IERC20(_token);
    }

2、test脚本中环境设置(包括部署合约、获取账户信息及创建合约实例)

//这行代码是获取合约部署的相关信息,包含abi、address等等
const tokenDeployment = await deployments.get("Mytoken");

alt text

3、当前合约部署脚本获取之前合约的地址

//当前合约中设置变量,获取之前已经部署的合约的deployment
const tokenDeployment = await deployments.get("MyToken");
//通过deloyment.address获取合约地址
const tokenAddr = await tokenDeployment.address;

alt text

4、一个完整的部署脚本(参考用02_deploy_pool_lock_and_release.js)

const{ getNamedAccounts } = require("hardhat")
moudle.exports = async({getNamedAccounts, deployments}) => {
    const {firstAccount} = getNameAccounts()
    const {deploy,log} = deployments

    log("NFTPoolLockAndRelease contract deploying...")
    //合约部署需要参数_router、_link、_nftAddr
    const ccipSimulatorDeployment = await deployments.get("CCIPLocalSimulator")
    //获得CCIP的对象(就是在0_deploy_ccip_simulator.js部署后才能获得),方便后面调用CCIP中的函数
    const ccipSimulator = await ethers.getContractAt("CCIPLocalSimulator",ccipSimulatorDeployment.address)
    //下面开始调用CCIP中的函数,获取需要的东西
    const ccipConfig = await ccipSimulator.configuretion()
    const sourceChainRouter = ccipConfig.sourceRouter_
    const linkTokenAddr = ccipConfig.linkToken_
    const nftDeployment = await deployments.get("MyToken")
    const nftAddr = nftDeployment.address
    await deploy("NFTPoolLockAndRelease",{
        cotract: "NFTPoolLockAndRelease",
        from: firstAccount,
        log: true,
        //这里的传参数_router、_link、_nftAddr
        args:[sourceChainRouter,linkTokenAddr,nftAddr]
    })
    log("NFTPoolLockAndRelease contract deployed")
}

moudle.exports.tags = ["sourcechain","all"]

5、一个完成的测试脚本

const { getNamedAccounts, ethers, deployments } = require("hardhat");
const { expect } = require("chai");

//把变量提取出来,方便后面的测试函数调用
let firstAccount
let ccipSimulator
let nft
let NFTPoolLockAndRelease
let wnft
let NFTPoolBurnAndMint
let chainSelector
before(async function(){
    //准备变量--账号
    firstAccount = (await getNamedAccounts()).firstAccount
    //准备变量--合约,通过tag,部署所有合约
    await deployments.fixture(["all"])
    ccipSimulator = await ethers.getContract("CCIPLocalSimulator",firstAccount)
    nft = await ethers.getContract("MyToken",firstAccount)
    NFTPoolLockAndRelease = await ethers.getContract("NFTPoolLockAndRelease",firstAccount)
    wnft = await ethers.getContract("WrappedMyToken",firstAccount)
    NFTPoolBurnAndMint = await ethers.getContract("NFTPoolBurnAndMint",firstAccount)
    const ccipConfig = await ccipSimulator.configuration()
    console.log("ccipConfig:",ccipConfig)
    chainSelector = ccipConfig.chainSelector_
    console.log("chainSelector:",chainSelector)

})

//第一步:源链sourcechain--》目标链destchain
describe("source chain -> dest chain test", async function(){
    //test1--是否成功mint
    it("test if user can mint one nft from MyToken contract successfully",
        async function () {
            await nft.safeMint(firstAccount)
            const owner = await nft.ownerOf(0)
            expect(owner).to.equal(firstAccount)    
        }
    )

    //test2--是否将nft已经lock在源链的pool中,并通过ccip将message发送给目标链
    it("test if nft has locked in source pool and send message to dest pool successfully",
        async function(){
            //await nft.transferFrom(firstAccount,NFTPoolLockAndRelease.target,0),不能直接这么用
            //这是在测试NFTPoolLockAndRelease合约中lockAndSendNFT()函数,该函数中使用的nft.transferFrom(),调用的是MyToken合约中的transferFrom()
            //所以NFTPoolLockAndRelease合约本身不具备转移nft的权限
            //先授权--将id为0的nft授权给NFTPoolLockAndRelease合约(执行lockAndSendNFT所需条件一)
            await nft.approve(NFTPoolLockAndRelease.target,0)
            console.log("nft's approval:",await nft.approve(NFTPoolLockAndRelease.target,0))
            //执行lockAndSentNFT需要fee(执行lockAndSendNFT所需条件二)
            await ccipSimulator.requestLinkFromFaucet(NFTPoolLockAndRelease, ethers.parseEther("10"))

            //参考合约中的入参进行赋值uint256 tokenId, newOwner, chainSelector, revceiver
            //lockAndSendNFT包含两个步骤:1.将nft从firstAccount转移到NFTPoolLockAndRelease合约;2.通过ccip发送消息
            console.log("newOwner:",firstAccount)
            console.log("chainSelector:",chainSelector)

            const receiverAddr = NFTPoolBurnAndMint.target
            console.log("receiver:",receiverAddr)
            await NFTPoolLockAndRelease.lockAndSendNFT(0,firstAccount,chainSelector,receiverAddr)
            //检查是不是完成了第一步的转移
            const owner = await nft.ownerOf(0)
            console.log("newOwner:",owner)
            expect(owner).to.equal(NFTPoolLockAndRelease.target)
        }
    )

    //test3--目标链接收到并mint新的wnft
    it("test if user can get a wrapped nft in dest chain",
        async function(){
            //当源链完成lockAndSendNFT后,会通过CCIP发送消息给目标链,目标链上就会mint一个wnft
            //所以只要验证目标链上是否有id为0的wnft存在,即owner不是空值,且owner为firstAccount
            const owner = await wnft.ownerOf(0)
            expect(owner).to.equal(firstAccount)

    })
})

//第二步:目标链destchain--》源链sourcechain
describe("dest chain->source chain test", async function(){
    //test4-目标链的wnft被burn掉,并通过ccip发送message给源链
    it("test if dest chain burn wnft and send message successfully",
        async function() {
            //wnft当前的owner是firstAccount,合约NFTPoolBurnAndMint想要burn掉wnft需要获取approve
            await wnft.approve(NFTPoolBurnAndMint.target,0)
            //需要消耗fees
            await ccipSimulator.requestLinkFromFaucet(NFTPoolBurnAndMint,ethers.parseEther("10"))
            //调用burnAndSendNFT(),传参为tokenId, newOwner, chainSelector, revceiver
            await NFTPoolBurnAndMint.burnAndSendNFT(0,firstAccount,chainSelector,NFTPoolLockAndRelease.target)
            //执行完burnAndSendNFT后,目标链的池子中就没有wnft了,此时totalSupply应该为0
            const totalSupply = await wnft.totalSupply()
            expect(totalSupply).to.equal(0)
        }
    )
    //test5-源链接收到信息后,nft被unlock
    it("test if source nft has unlocked", 
        async function(){
            //检查源链当中的nft是否被unlock释放出来
            const owner = await nft.ownerOf(0)
            expect(owner).to.equal(firstAccount) 
    })
})

6、部署脚本中的ethers.getContractAt()和测试脚本中的ethers.getCotract()有什么区别 ethers.getContractAt()是用于获取已经部署的合约实例args(name,address),与其进行交互,比如部署脚本中获取前一个部署合约的地址

//用于获取前面已经部署的MyToken合约,并填入传参args:合约名,合约地址
const nftDeployment = await deployments.get("MyToken")
const nft = await ethers.getContractAt("MyToken", nftDeployment.address)

ethers.getContract()是用于部署新的合约实例,相当于ethers.getContractFactory(),即通过合约工厂部署一个新的合约实例

//谁去部署的
const nft = await ethers.getContract("Mytoken", firstAccount)
//相当于
const contractFactory = await ethers.getContractFactory("MyContract");  
const contract = await contractFactory.deploy(); // 部署合约并获得实例

7、部署脚本deploy和测试脚本test中如何获取合约地址 部署脚本deploy

//先创建一个合约实例
const nftDeployment = await deployments.get("MyToken")
//获取合约地址
const nftAddr = nftDeployment.address

测试脚本test

//先创建一个合约实例
const nftDeployment = await ethers.getContract("MyToken",firstAccout)
const nftAddr = nftDeployment.target

8、获取当前用户的账户余额,检查是否够gas费用

const [account] = await ethers.getSigners()
const accountBalance = await ethers.provider.getBalance(account.address)
或者
const accountBalance = await ethers.provider.getBalance(firstAccount) -- 即账户的地址

9、如何获取mint的tokenId--在dev分支上尝试 问题:由于burn掉的代币tokenId没有被重置,所以再次mint时tokenId会进行累加 解决:如何获取tokenId 方案一:通过修改MyToken.sol合约中safemint方法return tokenId来实现,如:

    function safeMint(address to) public returns(uint256){
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, META_DATA);
        isTokenIdExitStill[tokenId] = true;
        emit Minted(to,tokenId);
        return tokenId;
    }

对应js脚本的调用为:

//尝试获取tokenId
const tokenId = await nft.safeMint(firstAccount)
console(`mint出来的tokenId为${tokenId}`)

结果日志打印出来是:mint出来的tokenId为[object][object] 疑问:为什么mint函数返回的是个对象呢? 解答: 1)在智能合约中函数方法可以分为两种:状态改变型函数(写入函数)状态只读型函数 状态改变型函数(写入函数):如转账、铸币等。它们通常返回一个包含交易信息的对象(transactionresponse),而不是直接返回执行结果 2)智能合约的写入型函数调用涉及到区块链的交易处理 所以本合约中的safeMint函数返回类型是 uint256,它在只能合约中确实返回了tokenId。但是,智能合约函数的调用在 JavaScript 中通常是异步的,返回的是一个交易对象,而不是直接的返回值 由此引出另外两种获取tokenId的解决方案:1、交易日志中获取;2、合约中写一个读取tokenId的只读型函数 优化方案1:交易日志中获取

//尝试1:通过交易日志查询到tokenId
const mintTx = await nft.safeMint(firstAccount)
const mintReceipt = await mintTx.wait()
const mintReceiptString = JSON.stringify(mintReceipt,null,2)
console.log(`合约交易信息内容是:${mintReceiptString}`)
const tokenId = await mintReceiptString.logs[0].args.tokenId

2.1)问题:这个打印的mintReceiptString里面没有看到tokenId的相关信息,(即使追加event没有对应信息)

event Minted(address indexed to, uint256 indexed tokenId);

function safeMint(address to) public returns(uint256){
    uint256 tokenId = _nextTokenId++;
    _safeMint(to, tokenId);
    _setTokenURI(tokenId, META_DATA);
    isTokenIdExitStill[tokenId] = true;
    emit Minted(to,tokenId);
    return tokenId;
    }

打印输出结果:

receipt的打印输出为:{
  "_type": "TransactionReceipt",
  "blockHash": "0x7b712ac81f2ca097a18ce7167c49d670e10057169cf85fca38fd7f3746205405",
  "blockNumber": 2,
  "contractAddress": null,
  "cumulativeGasUsed": "216130",
  "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "gasPrice": "1786313340",
  "blobGasUsed": null,
  "blobGasPrice": null,
  "gasUsed": "216130",
  "hash": "0x8ef224ccbe646fbc7c5bb89e5ab0a0e663abdb5174e212e2e78932d3ea762f0a",
  "index": 0,
  "logs": [
    {
      "_type": "log",
      "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
      "blockHash": "0x7b712ac81f2ca097a18ce7167c49d670e10057169cf85fca38fd7f3746205405",
      "blockNumber": 2,
      "data": "0x",
      "index": 0,
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
        "0x0000000000000000000000000000000000000000000000000000000000000000",
        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
        "0x0000000000000000000000000000000000000000000000000000000000000000"
      ],
      "transactionHash": "0x8ef224ccbe646fbc7c5bb89e5ab0a0e663abdb5174e212e2e78932d3ea762f0a",
      "transactionIndex": 0
    },
    {
      "_type": "log",
      "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
      "blockHash": "0x7b712ac81f2ca097a18ce7167c49d670e10057169cf85fca38fd7f3746205405",
      "blockNumber": 2,
      "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "index": 1,
      "topics": [
        "0xf8e1a15aba9398e019f0b49df1a4fde98ee17ae345cb5f6b5e2c27f5033e8ce7"
      ],
      "transactionHash": "0x8ef224ccbe646fbc7c5bb89e5ab0a0e663abdb5174e212e2e78932d3ea762f0a",
      "transactionIndex": 0
    },
    {
      "_type": "log",
      "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
      "blockHash": "0x7b712ac81f2ca097a18ce7167c49d670e10057169cf85fca38fd7f3746205405",
      "blockNumber": 2,
      "data": "0x",
      "index": 2,
      "topics": [
        "0x30385c845b448a36257a6a1716e6ad2e1bc2cbe333cde1e69fe849ad6511adfe",
        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
        "0x0000000000000000000000000000000000000000000000000000000000000000"
      ],
      "transactionHash": "0x8ef224ccbe646fbc7c5bb89e5ab0a0e663abdb5174e212e2e78932d3ea762f0a",
      "transactionIndex": 0
    }
  ],
  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000040020000000000000100000800000000000000000080000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000002000000000000000000000000000008000000042000000200000000000000000000000002040000000000000000020000000000000000000200000000000000000000000000000000000000000000000",
  "status": 1,
  "to": "0x5FbDB2315678afecb367f032d93F642f64180aa3"
}

原因: hardhat测试框架中,默认情况下,交易回执只显示原始的日志(logs),不会自动解码事件。你需要手动解码事件日志。

    nft = await ethers.getContract("MyToken",firstAccount)
    contractABI = [
        "event Minted(address indexed to, uint256 indexed tokenId)",
        "function safeMint(address to) public returns (uint256)"
    ];
    // iface = new ethers.utils.Interface(contractABI);  由于导入包依赖的问题,这一步无法正确执行

    const filter = nft.filters.Minted(null, null); // 监听所有 Minted 事件
    const logs = await nft.queryFilter(filter);
    console.log("Minted事件的日志: ", logs);

    logs.forEach((log) => {
    const parsedLog = iface.parseLog(log);
    console.log("解析后的事件:", parsedLog);
    });

优化方案2:合约中写一个读取tokenId的只读型函数

// 新增函数以获取指定地址的所有 Token IDs  
function getTokenIdsByOwner(address owner) public view returns (uint256[] memory) {  
    uint256 balance = balanceOf(owner);  
    uint256[] memory tokenIds = new uint256[](balance);  

    for (uint256 i = 0; i < balance; i++) {  
        tokenIds[i] = tokenOfOwnerByIndex(owner, i);  
    }  

    return tokenIds;  
} 

alt text 10、如何确认合约函数调用时链上交易所需的gas费用--待更新

11、会存在修改Mytoken.sol合约后需要重新部署,而重新部署后合约的地址就会更改,旧代币无法同步到新合约中,如何避免这个问题呢? 代理合约

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zsjayy
zsjayy
江湖只有他的大名,没有他的介绍。