{"content":{"title":"实现一个简洁版的NFT交易所","body":"# 前言\r\n> 本文实现一个简单版的NFT交易所，主要包含上架、下架、更新价格、购买NFT等相关功能\r\n# NFT合约说明\r\n关于NFT合约的开发、测试、部署具体实现，可以查看另一篇：[快速实现一个标准的NFT合约（实操篇）](https://learnblockchain.cn/article/11241)\r\n# NFT交易所合约\r\n合约说明具备上架、下架、更新价格、购买NFT\r\n```\r\n// SPDX-License-Identifier: MIT\r\n// Compatible with OpenZeppelin Contracts ^5.0.0\r\npragma solidity ^0.8.22;\r\nimport \"hardhat/console.sol\";\r\nimport { IERC721 } from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\r\nimport {IERC721Receiver} from \"@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol\";\r\nimport \"./NFT.sol\";//实现的nft的合约\r\n\r\ncontract NFTSwap is IERC721Receiver {\r\n    event List(\r\n        address indexed seller,\r\n        address indexed nftAddr,\r\n        uint256 indexed tokenId,\r\n        uint256 price\r\n    );\r\n    event Purchase(\r\n        address indexed buyer,\r\n        address indexed nftAddr,\r\n        uint256 indexed tokenId,\r\n        uint256 price\r\n    );\r\n    event Revoke(\r\n        address indexed seller,\r\n        address indexed nftAddr,\r\n        uint256 indexed tokenId\r\n    );\r\n    event Update(\r\n        address indexed seller,\r\n        address indexed nftAddr,\r\n        uint256 indexed tokenId,\r\n        uint256 newPrice\r\n    );\r\n    receive() external payable {}//解决没有接受函数的问题\r\n    // 定义order结构体\r\n    struct Order {\r\n        address owner;\r\n        uint256 price;\r\n    }\r\n    // NFT Order映射\r\n    mapping(address => mapping(uint256 => Order)) public nftList;\r\n\r\n    fallback() external payable {}\r\n    \r\n    // 挂单: 卖家上架NFT，合约地址为_nftAddr，tokenId为_tokenId，价格_price为以太坊（单位是wei）\r\n    function list(address _nftAddr, uint256 _tokenId, uint256 _price) public {\r\n        IERC721 _nft = IERC721(_nftAddr); // 声明IERC721接口合约变量\r\n        require(_nft.getApproved(_tokenId) == address(this), \"Need Approval\"); // 合约得到授权\r\n        require(_price > 0); // 价格大于0\r\n\r\n        Order storage _order = nftList[_nftAddr][_tokenId]; //设置NFT持有人和价格\r\n        _order.owner = msg.sender;\r\n        _order.price = _price;\r\n        // 将NFT转账到合约\r\n        _nft.safeTransferFrom(msg.sender, address(this), _tokenId);\r\n\r\n        // 释放List事件\r\n        emit List(msg.sender, _nftAddr, _tokenId, _price);\r\n    }\r\n\r\n    // 购买: 买家购买NFT，合约为_nftAddr，tokenId为_tokenId，调用函数时要附带ETH\r\n    function purchase(address _nftAddr, uint256 _tokenId) public payable {\r\n        Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order\r\n        require(_order.price > 0, \"Invalid Price\"); // NFT价格大于0\r\n        require(msg.value >= _order.price, \"Increase price\"); // 购买价格大于标价\r\n        // 声明IERC721接口合约变量\r\n        IERC721 _nft = IERC721(_nftAddr);\r\n        require(_nft.ownerOf(_tokenId) == address(this), \"Invalid Order\"); // NFT在合约中\r\n\r\n        // 将NFT转给买家\r\n        _nft.safeTransferFrom(address(this), msg.sender, _tokenId);\r\n        // 将ETH转给卖家\r\n        payable(_order.owner).transfer(_order.price);\r\n        // 多余ETH给买家退款\r\n        if (msg.value > _order.price) {\r\n            payable(msg.sender).transfer(msg.value - _order.price);\r\n        }\r\n\r\n        // 释放Purchase事件\r\n        emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price);\r\n\r\n        delete nftList[_nftAddr][_tokenId]; // 删除order\r\n    }\r\n\r\n    // 撤单： 卖家取消挂单\r\n    function revoke(address _nftAddr, uint256 _tokenId) public {\r\n        Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order\r\n        require(_order.owner == msg.sender, \"Not Owner\"); // 必须由持有人发起\r\n        // 声明IERC721接口合约变量\r\n        IERC721 _nft = IERC721(_nftAddr);\r\n        require(_nft.ownerOf(_tokenId) == address(this), \"Invalid Order\"); // NFT在合约中\r\n\r\n        // 将NFT转给卖家\r\n        _nft.safeTransferFrom(address(this), msg.sender, _tokenId);\r\n        delete nftList[_nftAddr][_tokenId]; // 删除order\r\n\r\n        // 释放Revoke事件\r\n        emit Revoke(msg.sender, _nftAddr, _tokenId);\r\n    }\r\n\r\n    // 调整价格: 卖家调整挂单价格\r\n    function update(\r\n        address _nftAddr,\r\n        uint256 _tokenId,\r\n        uint256 _newPrice\r\n    ) public {\r\n        require(_newPrice > 0, \"Invalid Price\"); // NFT价格大于0\r\n        Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order\r\n        require(_order.owner == msg.sender, \"Not Owner\"); // 必须由持有人发起\r\n        // 声明IERC721接口合约变量\r\n        IERC721 _nft = IERC721(_nftAddr);\r\n        require(_nft.ownerOf(_tokenId) == address(this), \"Invalid Order\"); // NFT在合约中\r\n\r\n        // 调整NFT价格\r\n        _order.price = _newPrice;\r\n\r\n        // 释放Update事件\r\n        emit Update(msg.sender, _nftAddr, _tokenId, _newPrice);\r\n    }\r\n\r\n    // 实现{IERC721Receiver}的onERC721Received，能够接收ERC721代币\r\n    function onERC721Received(\r\n        address operator,\r\n        address from,\r\n        uint tokenId,\r\n        bytes calldata data\r\n    ) external override returns (bytes4) {\r\n        return IERC721Receiver.onERC721Received.selector;\r\n    }\r\n}\r\n```\r\n# NFT交易所测试\r\n```\r\nconst {ethers,getNamedAccounts,deployments} = require(\"hardhat\");\r\nconst { assert,expect } = require(\"chai\");\r\ndescribe(\"NFTSwap\",function(){\r\n    let nft;//合约\r\n    let nftswap;//合约\r\n    let addr1;\r\n    let addr2;\r\n    let firstAccount//第一个账户\r\n    let secondAccount//第二个账户\r\n    let mekadata='ipfs://QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB';//mekadata关于说明nft相关描述，属性的json文件\r\n    let mekadata1=\"ipfs://QmXzbsbjpWpbSGJkgGzmk6r6HLz1nvjpEtjFR6bVhMh3U9\";//同上\r\n    beforeEach(async function(){\r\n        await deployments.fixture([\"nft\",\"nftswap\"]);//部署nft和nft交易所\r\n        [addr1,addr2]=await ethers.getSigners();//获取账号（说明：这样获取的账号支持签名操作，例如：如果使用secondAccount就会报错VoidSigner cannot sign transactions）\r\n        firstAccount=(await getNamedAccounts()).firstAccount;\r\n        secondAccount=(await getNamedAccounts()).secondAccount;\r\n        const nftDeployment = await deployments.get(\"BoykaNFT\");\r\n        nft = await ethers.getContractAt(\"BoykaNFT\",nftDeployment.address);//已经部署的合约交互\r\n        const nftswapDeployment = await deployments.get(\"NFTSwap\");\r\n        nftswap = await ethers.getContractAt(\"NFTSwap\",nftswapDeployment.address);//已经部署的合约交互\r\n    })\r\n    describe(\"nftswap\",function(){\r\n        it(\"把nft授权给交易所swap\",async ()=>{\r\n            //铸造两个nft\r\n            await nft.safeMint(firstAccount,mekadata);\r\n            console.log(await nft.ownerOf(0))//查看持有者\r\n            await nft.safeMint(firstAccount,mekadata1);\r\n            console.log(await nft.ownerOf(1))//同上\r\n            //把铸造的nft授权给nft交易所\r\n            await nft.approve(nftswap.address,0);\r\n            await nft.approve(nftswap.address,1);\r\n            //上架nft\r\n            //把nft放到交易所 参数说明：nft的合约地址 ，nftid，nft的价格\r\n            await nftswap.list(nft.address,0,ethers.utils.parseEther(\"1\"));\r\n            await nftswap.list(nft.address,1,ethers.utils.parseEther(\"1\"));\r\n            //查看是否上架成功\r\n            console.log('上架成功',await nftswap.nftList(nft.address,0))\r\n            //如果不存在的nft会返回owner: '0x0000000000000000000000000000000000000000',\r\n            console.log(\"查看不存在的nft\",await nftswap.nftList(nft.address,3))//不存在的nft\r\n            //更新nft的价格\r\n            await nftswap.update(nft.address,0,ethers.utils.parseEther(\"2\"));//把nft的价格改成2eth\r\n            console.log(\"查看更新的价格\",await nftswap.nftList(nft.address,0))\r\n            await nftswap.update(nft.address,1,ethers.utils.parseEther(\"23\"));//同上\r\n            console.log(\"查看更新的价格\",await nftswap.nftList(nft.address,1))\r\n            //下架\r\n            //把nftid为1的nft下架\r\n            await nftswap.revoke(nft.address,1);\r\n            //返回owner为0x0000000000000000000000000000000000000000说明下架成功\r\n            console.log('下架成功',await nftswap.nftList(nft.address,1))\r\n            //购买nft\r\n            console.log(secondAccount)\r\n            //切换账号到addr2使用await ethers.getSigners() 添加ether足够的值\r\n            //说明参数：1.nft合约地址2.nft的id3.addr2的msg.value的eth，要大于nft的价格当前addr2的值为3\r\n           await nftswap.connect(addr2).purchase(nft.address,0,{value:ethers.utils.parseEther(\"3\")});\r\n           console.log('所有权时addr2',await nft.ownerOf(0));//返回的账号地址时addr2说明addr2购买成功了\r\n        })\r\n    })\r\n})\r\n# 测试指令\r\n# npx hardhat test ./test/xxx.js\r\n```\r\n# NFT交易所部署\r\n```\r\nmodule.exports = async function ({getNamedAccounts,deployments}) {\r\n    const  firstAccount= (await getNamedAccounts()).firstAccount;\r\n    const {deploy,log} = deployments;\r\n    const NftSwap=await deploy(\"NFTSwap\",{\r\n        from:firstAccount,\r\n        args: [],//参数\r\n        log: true,\r\n    })\r\n    console.log(\"nftswap合约\",NftSwap.address)\r\n\r\n}\r\nmodule.exports.tags = [\"all\",\"nftswap\"];\r\n# 部署指令\r\n# npx hardhat deploy\r\n```\r\n# 总结\r\n以上就是简洁版NFT交易所的整个实现过程，此交易所具备上架、下架、改价、购买的功能和一些验证的方法。"},"author":{"user":"https://learnblockchain.cn/people/18158","address":null},"history":null,"timestamp":1740560953,"version":1}