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} = 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; }
10、如何确认合约函数调用时链上交易所需的gas费用--待更新
11、会存在修改Mytoken.sol合约后需要重新部署,而重新部署后合约的地址就会更改,旧代币无法同步到新合约中,如何避免这个问题呢,https://t.me/gtokentool 。