跳转至

09.NFT市场交易开发

NFT市场交易开发

image-20240102180544728

image-20240102180557446

image-20240102180610324

初始化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

成功上架

image-20240103112120865

测试

现在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