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");
3、当前合约部署脚本获取之前合约的地址
//当前合约中设置变量,获取之前已经部署的合约的deployment
const tokenDeployment = await deployments.get("MyToken");
//通过deloyment.address获取合约地址
const tokenAddr = await tokenDeployment.address;
4、一个完整的部署脚本(参考用02_deploy_pool_lock_and_release.js)
const{ getNamedAccounts } = require("hardhat")
moudle.exports = async({getNamedAccounts, deployments}) => {const {firstAccount} = getNameAccounts()const {deploy,log} = deploymentslog("NFTPoolLockAndRelease contract deploying...")//合约部署需要参数_router、_link、_nftAddrconst 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.addressawait deploy("NFTPoolLockAndRelease",{cotract: "NFTPoolLockAndRelease",from: firstAccount,log: true,//这里的传参数_router、_link、_nftAddrargs:[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--是否成功mintit("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.targetconsole.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新的wnftit("test if user can get a wrapped nft in dest chain",async function(){//当源链完成lockAndSendNFT后,会通过CCIP发送消息给目标链,目标链上就会mint一个wnft//所以只要验证目标链上是否有id为0的wnft存在,即owner不是空值,且owner为firstAccountconst 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需要获取approveawait wnft.approve(NFTPoolBurnAndMint.target,0)//需要消耗feesawait ccipSimulator.requestLinkFromFaucet(NFTPoolBurnAndMint,ethers.parseEther("10"))//调用burnAndSendNFT(),传参为tokenId, newOwner, chainSelector, revceiverawait NFTPoolBurnAndMint.burnAndSendNFT(0,firstAccount,chainSelector,NFTPoolLockAndRelease.target)//执行完burnAndSendNFT后,目标链的池子中就没有wnft了,此时totalSupply应该为0const totalSupply = await wnft.totalSupply()expect(totalSupply).to.equal(0)})//test5-源链接收到信息后,nft被unlockit("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; }
10、如何确认合约函数调用时链上交易所需的gas费用--待更新
11、会存在修改Mytoken.sol合约后需要重新部署,而重新部署后合约的地址就会更改,旧代币无法同步到新合约中,如何避免这个问题呢,https://t.me/gtokentool 。