编写脚本文件进行测试
- nft合约代码
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/interfaces/IERC721.sol";
contract Market {
//将erc20初始化为一个IERC20类型的实例,但具体的地址尚未赋值,后续的构造函数会使用传入的参数来设置erc20,从而与实际的ERC20代币合约进行交互
IERC20 public erc20;
IERC721 public erc721;
bytes4 internal constant MAGIC_ON_ERC721_RECEIVED = 0x150b7a02;
//包含代币售卖地址,代币Id,代币价格
struct Order {
address seller;
uint256 tokenId;
uint256 price;
}
mapping(uint256 => Order) public orderOfId; //根据tokenId查询Order
Order[] public orders; //定义一个Order类型的数组存放所有的Order
mapping(uint256 => uint256) public idToOrderIndex; //根据tokenId查询在数组中的索引
event Deal(address seller, address buyer, uint256 tokenId, uint256 price); //交易信息
event NewOrder(address seller, uint256 tokenId, uint256 price); //新的卖家信息
event PriceChanged(
address seller,
uint256 tokenId,
uint256 previousPrice,
uint256 price
); //价格修改信息
event OrderCancelled(address seller, uint256 tokenId); //卖家退出交易市场信息
//获取ERC20和ERC721的合约地址,这俩合约地址在部署合约时,在详细信息里面可以找到,合约地址不能时零地址
constructor(address _erc20, address _erc721) {
require(_erc20 != address(0), "zero address");
require(_erc721 != address(0), "zero address");
//将传入的地址_erc20转换为IERC20类型的实例,并将其赋值给变量erc20
erc20 = IERC20(_erc20);
erc721 = IERC721(_erc721);
}
//购买函数
function buy(uint256 _tokenId) external {
//获取该tokenId的卖家地址
address seller = orderOfId[_tokenId].seller;
//获取该tokenI的售卖价格
uint256 price = orderOfId[_tokenId].price;
//买方地址即当前调用合约的地址
address buyer = msg.sender;
//调用ERC20里面的授权转账函数进行转账
/*关于这个授权转账函数,最开始就有一点疑惑,按道理应该是购买者给市场合约授权才能有权限去转走购买者的币给卖家,具体这个该函数我在ERC20学习里面讲过
假如A授权给B一定量的币允许它使用,B要给C转账,因此就是B调用合约,而里面的from就是A,to就是C,即使A给B授权,但是币仍然在A手中,因此这里的买家相当于
A,市场合约相当于B,而卖家相当于C,因此买家2需要给市场合约授权才能完成购买
*/
require(
erc20.transferFrom(buyer, seller, price),
"transfer not successful"
);
//调用了ERC721代币合约的safeTransferFrom函数,将指定的ERC721代币(由_tokenId标识)从当前合约地址(address(this))安全地转移到买家账户(buyer)。
erc721.safeTransferFrom(address(this), buyer, _tokenId);
emit Deal(seller, buyer, _tokenId, price); //释放交易信息
}
//下架函数
function cancelOrder(uint256 _tokenId) external {
//获取卖家地址
address seller = orderOfId[_tokenId].seller;
//查看当前地址是不是卖家,因为商品只能由卖家自己下架
require(msg.sender == seller, "not seller");
erc721.safeTransferFrom(address(this), seller, _tokenId);
emit OrderCancelled(seller, _tokenId);
}
//改价函数
function changePrice(uint256 _tokenId, uint256 _price) external {
address seller = orderOfId[_tokenId].seller;
require(msg.sender == seller, "not seller");
uint256 previousPrice = orderOfId[_tokenId].price;
orderOfId[_tokenId].price = _price;
//注意,不仅要修改Order的价格,orders数组里面存储的该地址的代币价格也需要修改
//memory修改不会存储在链上,用于存储临时数据,storage修改会存储在链上,用于永久保存
Order storage order = orders[idToOrderIndex[_tokenId]];
order.price = _price;
emit PriceChanged(seller, _tokenId, previousPrice, _price);
}
//上架函数
//用于处理ERC721代币的接收事件,当其它合约或用户向该合约发送ERC721代币时,会触发该函数,可以在该函数中编写逻辑来处理接收到的代币
function onERC721Received(
address operator, //执行操作的地址
address from, //发送代币的地址
uint256 tokenId, //接收到的代币的唯一标识符
bytes calldata data //附加数据,可以是任意字节数组
) external returns (bytes4) {
//调用格式转换函数得到价格
uint256 price = toUint256(data, 0);
require(price > 0, "price must be greater than 0");
//上架
orders.push(Order(from, tokenId, price));
orderOfId[tokenId] = Order(from, tokenId, price);
idToOrderIndex[tokenId] = orders.length - 1;
emit NewOrder(from, tokenId, price);
//返回该值,这样发送方的合约就可以指导接收方是否已经正确处理了代币转移
return MAGIC_ON_ERC721_RECEIVED;
}
//下架函数
function removeOrder(uint256 _tokenId) internal {
//先从数组中把他删除掉
uint256 index = idToOrderIndex[_tokenId];
uint256 lastIndex = orders.length - 1;
if (index != lastIndex) {
Order storage lastOrder = orders[lastIndex];
orders[index] = lastOrder;
idToOrderIndex[lastOrder.tokenId] = index;
}
orders.pop();
//从mapping里面删除
delete orderOfId[_tokenId];
delete idToOrderIndex[_tokenId];
}
//格式转换函数
function toUint256(
bytes memory _bytes,
uint256 _start
) public pure returns (uint256) {
require(_start + 32 >= _start, "Market: toUint256_overflow");
require(_bytes.length >= _start + 32, "Market: toUint256_outOfBounds");
uint256 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _start))
}
return tempUint;
}
//验证该tokenId的售卖地址不是零地址
function isListed(uint256 _tokenId) public view returns (bool) {
return orderOfId[_tokenId].seller != address(0);
}
//获取数组订单长度
function getOrderLength() external view returns (uint256) {
return orders.length;
}
//获取所有上架的NFT
function getAllNFTs() external view returns (Order[] memory) {
return orders;
}
//获取用户自己上架的NFT
function getMyNFTs() external view returns (Order[] memory) {
Order[] memory myOrders = new Order[](orders.length);
uint256 count = 0;
for (uint256 i = 0; i < orders.length; i++) {
if (orders[i].seller == msg.sender) {
myOrders[count] = orders[i];
count++;
}
}
return myOrders;
}
}
- 在hardhat框架下的test文件夹下新建一个名为market.js的文件,删除掉原有的js文件
javascript
const { expect } = require('chai');
const { ethers } = require('hardhat');
//测试的步骤就跟我们之前在remix里面编译部署测试类似
describe('Market', async function () {
let usdt, nft, market, accountA, accountB;
//测试框架中的钩子函数,每个测试用例执行之前都会运行该函数
beforeEach(async () => {
//使用ethers.getSigners()方法获取当前连接的以太坊网络中的前两个账户,也就是我们之前编译部署的时候用到的地址
[accountA, accountB] = await ethers.getSigners();
//获取erc20合约并部署
const USDT = await ethers.getContractFactory('cUSDT');
usdt = await USDT.deploy();
//获取erc721合约并部署
const NFT = await ethers.getContractFactory('NFTM');
nft = await NFT.deploy(accountA.address);
//获取nft合约并部署
const Market = await ethers.getContractFactory('Market');
market = await Market.deploy(usdt.target, nft.target);
//给 B地址mint两个NFT
await nft.safeMint(accountB.address);
await nft.safeMint(accountB.address);
//B允许A花费自己所有的NFT
await nft.connect(accountB).setApprovalForAll(accountA.address, true);
//调用erc20合约里面的approve函数给市场合约授权一亿个USDT币
await usdt.approve(market.target, "100000000000000000000000");
});
//查看市场合约的erc20地址是否和erc20合约的地址一样
it('its erc20 address should be usdt', async function () {
expect(await market.erc20()).to.equal(usdt.target);
});
//查看市场合约的erc721地址是否和erc721合约的地址一样
it('its erc721 address should be nft', async function () {
expect(await market.erc721()).to.equal(nft.target);
});
//验证B账户是否有两个NFT
it('accountB should have 2 nfts', async function () {
expect(await nft.balanceOf(accountB.address)).to.equal(2);
});
//验证A账户有很多USDT
it('accountA should have usdt', async function () {
expect(await usdt.balanceOf(accountA.address)).to.equal("100000000000000000000000000");
});
//B账户可以上架两个NFT,
it('accountB can list two nfts to market', async function () {
const price = "0x0000000000000000000000000000000000000000000000000001c6bf52634000";
//调用erc721的safeTransferFrom函数将B账户的两个NFT上架到市场合约,并释放新的卖家信息
expect(await nft['safeTransferFrom(address, address, uint256, bytes)'](accountB.address, market.target, 0, price)).to.emit(market, "NewOrder");
expect(await nft['safeTransferFrom(address, address, uint256, bytes)'](accountB.address, market.target, 1, price)).to.emit(market, "NewOrder");
//已经上架了,查看他的余额是否正确
expect(await nft.balanceOf(accountB.address)).to.equal(0);
expect(await nft.balanceOf(market.target)).to.equal(2);
expect(await market.isListed(0)).to.equal(true);
expect(await market.isListed(1)).to.equal(true);
//调用市场合约的getALLNFTs()函数验证该数组的对应位置的值是否与B账户上架的两个NFT的值相匹配
//expect((await market.getALLNFTs())[0][0]).to.equal(accountB.address); //验证地址
//expect((await market.getALLNFTs())[0][1]).to.equal(0); //验证tokenId
//expect((await market.getALLNFTs())[0][2]).to.equal(price); //代币价格
//expect((await market.getALLNFTs())[1][0]).to.equal(accountB.address); //验证地址
//expect((await market.getALLNFTs())[1][1]).to.equal(1); //验证tokenId
//expect((await market.getALLNFTs())[1][2]).to.equal(price); //代币价格
//查看数组长度是否是2
expect(await market.getOrderLength()).to.equal(2);
expect((await market.connect(accountB).getMyNFTs())[0][0]).to.equal(accountB.address);
expect((await market.connect(accountB).getMyNFTs())[0][1]).to.equal(0);
expect((await market.connect(accountB).getMyNFTs())[0][2]).to.equal(price);
})
});
-
可以运行
npx hardhat test
命令测试一下
-
在这里查找abi
-
安装abi插件
npm install hardhat-abi-exporter
-
然后在hardhat.config.js文件中添加
-
输入
npx hardhat export-abi --no-compile
命令运行,然后就可以看到abi文件夹如下图所示