09.NFT市场交易开发
NFT市场交易开发¶
初始化hardhat工程¶
mkdir nft-market-cbi02
cd mkdir nft-market-cbi02
npx hardhat init
生成ERC20代码¶
在 https://docs.openzeppelin.com/contracts/5.x/wizard中,生成ERC20代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract cUSDT is ERC20, Ownable, ERC20Permit {
constructor(address initialOwner)
ERC20("fake usdt in CBI", "cUSDT")
Ownable(initialOwner)
ERC20Permit("MyToken")
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
//
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract cUSDT is ERC20, Ownable, ERC20Permit {
constructor(address initialOwner)
ERC20("fake usdt in CBI", "cUSDT")
Ownable(initialOwner)
ERC20Permit("MyToken")
{
_mint(msg.sender, 1*10**8*10**18);//18亿
}
}
放到之前建立的项目的contracts下
如果openzeppelin报错,安装这个库
npm install @openzeppelin/contracts
部署测试看
remix连接本地项目¶
yarn global add @remix-project/remixd
remixd -s D:/Workplace/web3.0/Demo/nft-market-cbi02 --remix-ide https://remix.ethereum.org
部署测试是否运行成功
ERC721¶
同理测试ERC721的代码,放入项目中
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTM is ERC721, ERC721Enumerable, Ownable {
uint256 private _nextTokenId;
constructor(address initialOwner)
ERC721("NFTM", "NFTM")
Ownable(initialOwner)
{}
function _baseURI() internal pure override returns (string memory) {
return "https://cbisample.com/";
}
function safeMint(address to) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
}
// 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 supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
NFT交易市场智能合约¶
难点,上架,用户把NFT转移给市场,市场帮忙自动上架,
safeTransferfrom和onERC721Received
onERC721Received可以理解为一个钩子hook,调用的时候,验证to是不是合约地址,如果是,就会尝试调用这个地址的合约的onERC721Received方法,会有一个返回值,必须是约定好的值
这个case中,用户调用safeTransferfrom,传入参数时,自动调用market的onERC721Received,要在这里完成自动上架。
价格以bytes4的形式编码到data里面
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/interfaces/IERC721.sol";
contract Market{
// 写address也可以,但是后续transfer不方便
// address public erc20;
// address public erc721;
IERC20 public erc20;
IERC721 public erc721;
bytes4 internal constant MAGIC_ON_ERC721_RECEIVED=0x150b7a02;
struct Order{
address seller;
uint256 tokenId;
uint256 price;
}
// token id -> order
mapping (uint256=>Order) public orderOfId;
Order[]public orders;
// token id ->index in orders
mapping (uint256=>uint256) public idToOrderIndex;
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 newPrice);
event OrderCancelled(address seller, uint256 tokenId);
constructor(address _erc20,address _erc721){
require(_erc20!=address(0),"zero address");
require(_erc721!=address(0),"zero address");
erc20=IERC20(_erc20);
erc721=IERC721(_erc721);
}
// A用户放一个NFT在market里面, B用户用USDT购买
//那么
// B把USDT从B的账户转到A的账户
// NFT从合约账户转到B庄户
function buy(uint256 _tokenId) external {
address seller=orderOfId[_tokenId].seller;
address buyer=msg.sender;
uint256 price=orderOfId[_tokenId].price;
//转账
require(erc20.transferFrom(buyer, seller, price),"transfer not successful");
erc721.safeTransferFrom(address(this), buyer, _tokenId);
removeOrder(_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);
removeOrder(_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 storage order=orders[idToOrderIndex[_tokenId]];
order.price=_price;
emit PriceChanged(seller, _tokenId, previousPrice,_price);
}
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();
delete orderOfId[_tokenId];
delete idToOrderIndex[_tokenId];
}
// 解析价格,格式转换
function toUint256(bytes memory _bytes, uint256 _start) internal 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;
}
function getOrderLength() external view returns (uint256){
return orders.length;
}
function getAllNFTs() external view returns (Order[]memory){
return orders;
}
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;
}
}
remix部署测试
部署ERC20, ERC721.
调用NFT的safeTransferFrom
from: 自己的地址
to: market地址
tokenId: I
data: 0x0000000000000000000000000000000000000000000000000001c6bf52634000
成功上架
测试¶
现在remix中手动跑一遍检测
再写测试代码
测试代码尽量保证100%覆盖率
在test下创建js文件测试
一部分测试代码
const {expect} =require('chai');
const {ethers} =require('hardhat');
describe('Market', function(){
let usdt,myNft,market,accountA,accountB;
// 每个测试开始前都执行一次
this.beforeEach(async()=>{
// 分配账号
[accountA,accountB]=await ethers.getSigners();
// 部署合约
const USDT=await ethers.getContractFactory('cUSDT');
usdt=await USDT.deploy(accountA.address);
const MyNFT=await ethers.getContractFactory('NFTM');
myNft=await MyNFT.deploy(accountA.address);
const Market=await ethers.getContractFactory('Market');
market=await Market.deploy(usdt.target,myNft.target);
// 给B挖币并且approve
await myNft.safeMint(accountB.address);
await myNft.safeMint(accountB.address);
await usdt.approve(market.target,"1000000000000000000000000");
});
// 验证合约状态
it('its erc20 address should be usdt',async function(){
// usdt.target可以理解为usdt合约地址
expect(await market.erc20()).to.equal(usdt.target);
})
it('its erc721 address should be myNft',async function(){
expect(await market.erc721()).to.equal(myNft.target);
})
// 检测余额
it('accountB should have 2 nfts',async function(){
expect(await myNft.balanceOf(accountB.address)).to.equal(2);
})
it('accountA should have usdt',async function(){
expect(await usdt.balanceOf(accountA.address)).to.equal("100000000000000000000000000");
})
// ...
})
npx hardhat test
Market
✔ its erc20 address should be usdt
✔ its erc721 address should be myNft
✔ accountB should have 2 nfts
✔ accountA should have usdt
4 passing (1s)
npx hardhat coverage
Version
=======
> solidity-coverage: v0.8.5
Instrumenting for coverage...
=============================
> erc20-usdt.sol
> erc721-usdt.sol
> nft-market.sol
Compilation:
============
Compiled 31 Solidity files successfully (evm target: paris).
Network Info
============
> HardhatEVM: v2.19.4
> network: hardhat
Market
✔ its erc20 address should be usdt
✔ its erc721 address should be myNft
✔ accountB should have 2 nfts
✔ accountA should have usdt
4 passing (471ms)
------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
------------------|----------|----------|----------|----------|----------------|
contracts\ | 12.5 | 13.64 | 27.78 | 13.11 | |
erc20-usdt.sol | 100 | 100 | 100 | 100 | |
erc721-usdt.sol | 50 | 50 | 50 | 50 | 17,39,48 |
nft-market.sol | 4.88 | 10 | 9.09 | 7.41 |... 127,130,134 |
------------------|----------|----------|----------|----------|----------------|
All files | 12.5 | 13.64 | 27.78 | 13.11 | |
------------------|----------|----------|----------|----------|----------------|
> Istanbul reports written to ./coverage/ and ./coverage.json