学习视频:《DApp 设计与开发》课程简介_哔哩哔哩_bilibili
开发流程:
1.分析需求
2.本地开发
3.测试审计
4.部署测试网
5.部署项目
用户与区块链交互
以太坊 API | IPFS API 和网关 | ETH 节点即服务 (infura.io)
环境配置
合约使用hardhat和remix进行编写运行调试
合约环境
前置要求:安装node.js [建议安装18及以上]
1.新建合约文件夹 nft-contract
2.在文件夹路径cmd 开始配置环境
命令行输入:
npm init
npm install hardhat
命令行输入:
npx hardhat init
合约拷贝:
在OpenZeppelin Contracts Wizard中使用封装好的合约,在contrasts中新建sol文件保存
合约代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract cUSDT is ERC20 {
constructor() ERC20("fake usdt in cbi", "cUSDT") {
_mint(msg.sender, 1 * 10 ** 8 * 10 ** 18);
}
//没有approval,就会导致nftmarket的buy不成功,因为没有授权
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
constructor() ERC721("MyNFT", "NFT") Ownable(msg.sender) {}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
// 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 tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function supportsInterface(
bytes4 interfaceId
)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol";
import "@openzeppelin/contracts/interfaces/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC20.sol";
/**
* @title NFTMarket contract that allows atomic swaps of ERC20 and ERC721
*/
contract Market is IERC721Receiver {
IERC20 public erc20;
IERC721 public erc721;
bytes4 internal constant MAGIC_ON_ERC721_RECEIVED = 0x150b7a02;
struct Order {
address seller;
uint256 tokenId;
uint256 price;
}
mapping(uint256 => Order) public orderOfId; // token id to order
Order[] public orders;
mapping(uint256 => uint256) public idToOrderIndex;
event Deal(address buyer, address seller, uint256 tokenId, uint256 price);
event NewOrder(address seller, uint256 tokenId, uint256 price);
event CancelOrder(address seller, uint256 tokenId);
event ChangePrice(
address seller,
uint256 tokenId,
uint256 previousPrice,
uint256 price
);
constructor(IERC20 _erc20, IERC721 _erc721) {
require(
address(_erc20) != address(0),
"Market: IERC20 contract address must be non-null"
);
require(
address(_erc721) != address(0),
"Market: IERC721 contract address must be non-null"
);
erc20 = _erc20;
erc721 = _erc721;
}
function buy(uint256 _tokenId) external {
require(isListed(_tokenId), "Market: Token ID is not listed");
address seller = orderOfId[_tokenId].seller;
address buyer = msg.sender;
uint256 price = orderOfId[_tokenId].price;
require(
erc20.transferFrom(buyer, seller, price),
"Market: ERC20 transfer not successful"
);
erc721.safeTransferFrom(address(this), buyer, _tokenId);
removeListing(_tokenId);
emit Deal(buyer, seller, _tokenId, price);
}
function cancelOrder(uint256 _tokenId) external {
require(isListed(_tokenId), "Market: Token ID is not listed");
address seller = orderOfId[_tokenId].seller;
require(seller == msg.sender, "Market: Sender is not seller");
erc721.safeTransferFrom(address(this), seller, _tokenId);
removeListing(_tokenId);
emit CancelOrder(seller, _tokenId);
}
function changePrice(uint256 _tokenId, uint256 _price) external {
require(isListed(_tokenId), "Market: Token ID is not listed");
address seller = orderOfId[_tokenId].seller;
require(seller == msg.sender, "Market: Sender is not seller");
uint256 previousPrice = orderOfId[_tokenId].price;
orderOfId[_tokenId].price = _price;
Order storage order = orders[idToOrderIndex[_tokenId]];
order.price = _price;
emit ChangePrice(seller, _tokenId, previousPrice, _price);
}
function getAllNFTs() public view returns (Order[] memory) {
return orders;
}
function getMyNFTs() public view returns (Order[] memory) {
Order[] memory myOrders = new Order[](orders.length);
uint256 myOrdersCount = 0;
for (uint256 i = 0; i < orders.length; i++) {
if (orders[i].seller == msg.sender) {
myOrders[myOrdersCount] = orders[i];
myOrdersCount++;
}
}
Order[] memory myOrdersTrimmed = new Order[](myOrdersCount);
for (uint256 i = 0; i < myOrdersCount; i++) {
myOrdersTrimmed[i] = myOrders[i];
}
return myOrdersTrimmed;
}
function isListed(uint256 _tokenId) public view returns (bool) {
return orderOfId[_tokenId].seller != address(0);
}
function getOrderLength() public view returns (uint256) {
return orders.length;
}
/**
* @dev List a good using a ERC721 receiver hook
* @param _operator the caller of this function
* @param _seller the good seller
* @param _tokenId the good id to list
* @param _data contains the pricing data as the first 32 bytes
*/
function onERC721Received(
address _operator,
address _seller,
uint256 _tokenId,
bytes calldata _data
) public override returns (bytes4) {
require(_operator == _seller, "Market: Seller must be operator");
uint256 _price = toUint256(_data, 0);
placeOrder(_seller, _tokenId, _price);
return MAGIC_ON_ERC721_RECEIVED;
}
// https://stackoverflow.com/questions/63252057/how-to-use-bytestouint-function-in-solidity-the-one-with-assembly
function toUint256(
bytes memory _bytes,
uint256 _start
) public 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 placeOrder(
address _seller,
uint256 _tokenId,
uint256 _price
) internal {
require(_price > 0, "Market: Price must be greater than zero");
orderOfId[_tokenId].seller = _seller;
orderOfId[_tokenId].price = _price;
orderOfId[_tokenId].tokenId = _tokenId;
orders.push(orderOfId[_tokenId]);
idToOrderIndex[_tokenId] = orders.length - 1;
emit NewOrder(_seller, _tokenId, _price);
}
function removeListing(uint256 _tokenId) internal {
delete orderOfId[_tokenId];
uint256 orderToRemoveIndex = idToOrderIndex[_tokenId];
uint256 lastOrderIndex = orders.length - 1;
if (lastOrderIndex != orderToRemoveIndex) {
Order memory lastOrder = orders[lastOrderIndex];
orders[orderToRemoveIndex] = lastOrder;
idToOrderIndex[lastOrder.tokenId] = orderToRemoveIndex;
}
orders.pop();
}
}
连接remix
首先在命令行中输入:
在remix-ide中点击
连接成功
测试
在test文件夹中新建一个market.js测试代码
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Market", function () {
let usdt, market, myNft, account1, account2;
let baseURI = "https://sameple.com/";
beforeEach(async () => {
[account1, account2] = await ethers.getSigners();
// const MAX_ALLOWANCE = BigNumber.from(2).pow(256).sub(1);
const USDT = await ethers.getContractFactory("cUSDT");
usdt = await USDT.deploy();
const MyNFT = await ethers.getContractFactory("MyNFT");
myNft = await MyNFT.deploy();
const Market = await ethers.getContractFactory("Market");
market = await Market.deploy(usdt.target, myNft.target);
await myNft.safeMint(account1.address, baseURI + "0");
await myNft.safeMint(account1.address, baseURI + "1");
await myNft.approve(market.target, 0);
await myNft.approve(market.target, 1);
await usdt.transfer(account2.address, "10000000000000000000000");
await usdt
.connect(account2)
.approve(market.target, "1000000000000000000000000");
});
it("its erc20 address should be usdt", async function () {
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("account1 should have 2 nfts", async function () {
expect(await myNft.balanceOf(account1.address)).to.equal(2);
});
it("account2 should have 10000 USDT", async function () {
expect(await usdt.balanceOf(account2.address)).to.equal(
"10000000000000000000000"
);
});
it("account2 should have 0 nfts", async function () {
expect(await myNft.balanceOf(account2.address)).to.equal(0);
});
it("account1 can list two nfts to market", async function () {
const price =
"0x0000000000000000000000000000000000000000000000000001c6bf52634000";
// let price = "0x0100"
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
0,
price
)
).to.emit(market, "NewOrder");
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
1,
price
)
).to.emit(market, "NewOrder");
expect(await myNft.balanceOf(account1.address)).to.equal(0);
expect(await myNft.balanceOf(market.target)).to.equal(2);
expect(await market.isListed(0)).to.equal(true);
expect(await market.isListed(1)).to.equal(true);
expect((await market.getAllNFTs())[0][0]).to.equal(account1.address);
expect((await market.getAllNFTs())[0][1]).to.equal(0);
expect((await market.getAllNFTs())[0][2]).to.equal(price);
expect((await market.getAllNFTs())[1][0]).to.equal(account1.address);
expect((await market.getAllNFTs())[1][1]).to.equal(1);
expect((await market.getAllNFTs())[1][2]).to.equal(price);
expect(await market.getOrderLength()).to.equal(2);
expect((await market.getMyNFTs())[0][0]).to.equal(account1.address);
expect((await market.getMyNFTs())[0][1]).to.equal(0);
expect((await market.getMyNFTs())[0][2]).to.equal(price);
});
it("account1 can unlist one nft from market", async function () {
const price =
"0x0000000000000000000000000000000000000000000000000001c6bf52634000";
// let price = "0x0100"
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
0,
price
)
).to.emit(market, "NewOrder");
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
1,
price
)
).to.emit(market, "NewOrder");
expect(await market.cancelOrder(0)).to.emit(market, "CancelOrder");
expect(await market.getOrderLength()).to.equal(1);
expect(await market.isListed(0)).to.equal(false);
expect((await market.getMyNFTs()).length).to.equal(1);
});
it("account1 can change price of nft from market", async function () {
const price =
"0x0000000000000000000000000000000000000000000000000001c6bf52634000";
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
0,
price
)
).to.emit(market, "NewOrder");
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
1,
price
)
).to.emit(market, "NewOrder");
expect(await market.changePrice(1, "10000000000000000000000")).to.emit(
market,
"ChangePrice"
);
expect((await market.getMyNFTs()).length).to.equal(2);
expect((await market.getMyNFTs())[1][2]).to.equal(
"10000000000000000000000"
);
});
it("account2 can buy nft from market", async function () {
const price =
"0x0000000000000000000000000000000000000000000000000001c6bf52634000";
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
0,
price
)
).to.emit(market, "NewOrder");
expect(
await myNft["safeTransferFrom(address,address,uint256,bytes)"](
account1.address,
market.target,
1,
price
)
).to.emit(market, "NewOrder");
expect(await market.connect(account2).buy(1)).to.emit(market, "Deal");
expect(await market.getOrderLength()).to.equal(1);
expect(await usdt.balanceOf(account1.address)).to.equal(
"99990000000500000000000000"
);
expect(await usdt.balanceOf(account2.address)).to.equal(
"9999999500000000000000"
);
expect(await myNft.ownerOf(1)).to.equal(account2.address);
});
});