DApp设计与开发 课程笔记(四):NFT交易市场合约

笔记对应课程内容为成都信息工程大学区块链产业学院老师梁培利DApp 设计与开发 10-13 课

笔记中提到的名词不做过多解释 不懂就搜!

开发准备:

动手学

Windows 需要使用 Powershell,不能使用git bash

mkdir nft-market
cd nft-market/
npx hardhat init

npm install @openzeppelin/contracts
npm i @remix-project/remixd
remixd -s C:\Users\Administrator\Desktop\nft-market --remix-ide https://remix.ethereum.org

remix共享本地文件夹教程 - 走走停停走走 - 博客园 (cnblogs.com)

solcjs 和 hardhat在管理solidity编译版本上有什么区别?

solcjshardhat 都是与 Solidity 相关的工具,但在管理 Solidity 编译版本上有一些区别:

  1. solcjs:

    • 独立的 Solidity 编译器solcjs 是 Solidity 的官方编译器的 JavaScript 包装器。它允许你在 JavaScript 或 Node.js 环境中调用 Solidity 编译器,以便编译和部署智能合约。
    • 版本管理solcjs 可以通过 npm(Node.js 的包管理器)来安装不同版本的 Solidity 编译器。你可以选择安装并使用特定的 Solidity 版本来编译你的合约。
  2. Hardhat:

    • 开发框架Hardhat 是一个以太坊智能合约开发的工具套件和框架,它整合了多种功能,包括编译、测试、部署等。
    • 集成 solc:Hardhat 默认集成了 Solidity 编译器(通常使用的是 solc,不是 solcjs)。它通过自身的配置文件来管理 Solidity 版本和编译选项。
    • 版本管理:Hardhat 允许在项目配置中指定 Solidity 的版本,它会根据配置文件来选择合适的 Solidity 编译器版本进行编译。Hardhat 会自动下载和管理所需的 Solidity 版本,使得开发者可以在项目中轻松切换和管理不同的 Solidity 版本。

区别总结

  • solcjs 是一个单独的 JavaScript 包,主要用于直接调用 Solidity 编译器进行合约编译,需要手动安装和管理 Solidity 版本。
  • Hardhat 是一个完整的以太坊智能合约开发框架,集成了 Solidity 编译器,可以通过配置文件轻松管理 Solidity 版本,同时提供了测试、部署等功能,更适合大型和复杂的以太坊项目开发。

复制第三方模板合约库的智能合约代码:https://docs.openzeppelin.com/contracts/5.x/wizard

在你已经理解了NFT原理之后,就不用完全自己构建一个智能合约了,直接复制一个拿来就能用了

然后在remix部署合约

即可运行合约函数

但是交易市场的智能合约是没有拿来就能用的。

附上述代码:

erc20-usdt.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract cUSDT is ERC20 {
    constructor() ERC20("fake usdt in oktc", "cUSDT") {
        _mint(msg.sender, 1*10*8*10**18);
    }
}

erc-721-nft.sol

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
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 {
    constructor(address initialOwner)
        ERC721("NFTM", "NFTM")
        Ownable(initialOwner)
    {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://sample.onefly.top/";
    }

    function safeMint(address to, uint256 tokenId) public onlyOwner {
        _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);
    }
}

老师和我都推荐使用 github copilot来编程,学生免费申请可以看我之前的文章:2023.3申请github copilot x 学生认证以及Jetbrain专业版学生教育免费教程 - 知乎 (zhihu.com)

nft-market.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract Market {
    IERC20 public erc20;
    IERC721 public erc721;

    bytes4 private constant Magic_On_Erc721_Received = 0x150b7a02;

    struct Order {
        address seller;
        uint256 tokenId;
        uint256 price;
    }
    mapping(uint256 => Order) public orderOfId; // token id => order
    Order[] public orders;
    mapping(uint256 => uint256) public idToOrderIndex; // token id => order id

    event Deal(address seller, address buyer, uint256 tokenId, uint256 price); //事件是合约与外部世界通信的唯一方式
    event NewOrder(uint256 tokenId, uint256 price);
    event PriceChanged(address seller, uint256 tokenId, uint256 previousPrice, uint256 price);
    event OrderCancled(address seller, uint256 tokenId);

    constructor(address _erc20, address _erc721) {
        require(_erc20 != address(0) && _erc721 != address(0), "invalid zero address");
        erc20 = IERC20(_erc20);
        erc721 = IERC721(_erc721);
    }

    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 failed");//ierc20包装的erc20 transferFrom方法
        erc721.safeTransferFrom(address(this), buyer, _tokenId); //address(this)是合约本身地址
        
        //清除订单
        emit Deal(seller, buyer, _tokenId, price);//emit关键字用于触发事件
    }   

    function cancelOrder(uint256 _tokenId) external {
        address seller = orderOfId[_tokenId].seller;
        require(msg.sender == seller, "only seller can cancel order");

        erc721.safeTransferFrom(address(this), seller, _tokenId);

        uint256 orderId = idToOrderIndex[_tokenId];
        //清除订单

        emit OrderCancled(seller, _tokenId);
    }

    function changePrice(uint256 _tokenId, uint256 _price) external {
        address seller = orderOfId[_tokenId].seller;
        require(msg.sender == seller, "only seller can change price");

        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( //ERC721回调函数 难点
        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(tokenId, price);
            return Magic_On_Erc721_Received;
        }

    function toUint256(
        bytes memory _bytes, 
        uint256 _start) internal pure returns (uint256) {
        require(_bytes.length >= (_start + 32), "toUint256 out of bounds");
        uint256 tempUint;

        assembly {
            tempUint := mload(add(add(_bytes, 0x20), _start))
        }

        return tempUint;
    }

    function removeOrder(uint256 _tokenId) internal {
        uint256 orderId = idToOrderIndex[_tokenId];
        uint256 lastOrderId = orders.length - 1;
        if (orderId != lastOrderId) {
            Order storage lastOrder = orders[lastOrderId];
            orders[orderId] = lastOrder;
            idToOrderIndex[lastOrder.tokenId] = orderId;
        }
        orders.pop();
        delete orderOfId[_tokenId];
        delete idToOrderIndex[_tokenId];
    }

}

测试

在remix提供的手动测试按钮测试基本功能后,我们可以进一步利用hardhat使用js代码进行测试。

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Market", function () {
    let usdt, market, myNft, accountA, accountB;

    beforeEach(async () => {
        [accountA, accountB] = await ethers.getSigners();
        const USDT = await ethers.getContractFactory("cUSDT");
        usdt = await USDT.deploy();
        const MyNFT = await ethers.getContractFactory("MyNFT");
        myNft = await MyNFT.deploy(accountA.address);        
        const Market = await ethers.getContractFactory("Market");
        market = await Market.deploy(usdt.target, myNft.target);

        await myNft.safeMint(accountA.address);
        await myNft.safeMint(accountB.address);

        await usdt.approve(market.target, 10**18);
    });

    it("its erc20 address should be usdt", async() => {
        expect(await market.erc20()).to.equal(usdt.target);
    });

    it("its nft address should be myNft", async() => {
        expect(await market.erc721()).to.equal(myNft.target);
    });

    it("accountB shuold have 2 nfts", async() => {
        expect(await myNft.balanceOf(accountB.address)).to.equal(2);
    });

    it("accountA should have usdt", async() => {
        expect(await usdt.balanceOf(accountA.address)).to.equal(10**18);
    });

    // expect(await myNft['safeTransferFrom(address,address,uint256,bytes)'](accountB.address,market.target,0,price)).to.emit (market,"Neworder");
    // expect(await myNft['safeTransferFrom(address,address,uint256,bytes)'](accountB.address,market.target,1,price)).to.emit (market,"Neworder");
  
    // expect(await myNft.balanceOf(accountB.address)).to.equal(0);
    // expect(await myNft.balanceOf(market.target)).to.equal(2);
    // expect(await market.orders(0)).to.equal(true);
    // expect(await market.orders(1)).to.equal(true);

    // 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);

});    

一些小技巧

hardhat-abi-explore

安装

要安装 hardhat-abi-explore,可以使用 npm 或者 yarn:

npm install --save-dev hardhat-abi-explore

或者

yarn add --dev hardhat-abi-explore

配置

在 Hardhat 项目中,你需要在 hardhat.config.js 文件中添加以下内容:

require('hardhat-abi-explore');

使用命令

安装并配置后,你可以使用以下命令来生成 ABI 目录:

npx hardhat abi:explore

这将会根据你的合约代码自动创建一个 ABI 目录,并将每个合约的 ABI 存储在其中。

flatten

安装

同样地,使用 npm 或 yarn 来安装 truffle-flattener

npm install -g truffle-flattener

或者

yarn global add truffle-flattener

使用命令

安装完成后,你可以使用以下命令来扁平化你的合约代码:

truffle-flattener <ContractName.sol> > FlatContract.sol

这将会将指定的合约及其所有依赖的代码全部合并到一个文件中,输出为 FlatContract.sol 文件。

通过这些步骤,你可以更方便地管理和部署 Solidity 合约代码。

posted @ 2024-07-01 23:02  孤飞  阅读(18)  评论(0编辑  收藏  举报