DApp设计与开发 课程笔记(四):NFT交易市场合约
笔记对应课程内容为成都信息工程大学区块链产业学院老师梁培利的DApp 设计与开发 10-13 课
笔记中提到的名词不做过多解释 不懂就搜!
开发准备:
- 编辑IDE环境:
Vscode
+Hardhat
- 调试:
Remix
- ERC标准智能合约生成巫师:https://docs.openzeppelin.com/contracts/5.x/wizard
动手学
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
solcjs 和 hardhat在管理solidity编译版本上有什么区别?
solcjs
和hardhat
都是与 Solidity 相关的工具,但在管理 Solidity 编译版本上有一些区别:
solcjs:
- 独立的 Solidity 编译器:
solcjs
是 Solidity 的官方编译器的 JavaScript 包装器。它允许你在 JavaScript 或 Node.js 环境中调用 Solidity 编译器,以便编译和部署智能合约。- 版本管理:
solcjs
可以通过 npm(Node.js 的包管理器)来安装不同版本的 Solidity 编译器。你可以选择安装并使用特定的 Solidity 版本来编译你的合约。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 合约代码。