在本次实战所使用的技术是React, Next.js, Tailwind CSS, RemixIDE, Solidity, Ethers.
编写智能合约
市场将由两个主要的智能合约组成:
用于铸造 NFT的NFT合约 和 促进 NFT销售的市场合约
为了编写 NFT,我们可以使用OpenZeppelin获得的ERC721标准。
参考:https://docs.openzeppelin.com/contracts/4.x/erc721
contract NFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address contractAddress;
constructor(address marketplaceAddress) ERC721("Xia Blocks NFTs", "XBNFT") {
contractAddress = marketplaceAddress;
}//合约可以允许市场合约批准将代币从所有者转移到卖家
function createToken(string memory tokenURI) public returns (uint) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(msg.sender, newItemId);
_setTokenURI(newItemId, tokenURI);
setApprovalForAll(contractAddress, true);
return newItemId;
}
}
编写市场合约
继承ReentrancyGuard:有助于防止对函数的可重入调用的合同模块
// SPDX-License-Identifier: XXL
pragma solidity ^0.8.3;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract NFTMarket is ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _itemIds;
Counters.Counter private _itemsSold;
uint[] marketItems;
//存储记录
struct MarketItem {
uint itemId;
address nftContract;
uint256 tokenId;
address payable seller;
address payable owner;
uint256 price;
}
mapping(uint256 => MarketItem) private idToMarketItem;
event MarketItemCreated (
uint indexed itemId,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address owner,
uint256 price
);
function getMarketItem(uint256 marketItemId) public view returns (MarketItem memory) {
return idToMarketItem[marketItemId];
}
function createMarketItem(
address nftContract,
uint256 tokenId,
uint256 price
) public payable nonReentrant {
require(price > 0, "Price must be at least 1 wei");
_itemIds.increment();
uint256 itemId = _itemIds.current();
marketItems.push(itemId);
idToMarketItem[itemId] = MarketItem(
itemId,
nftContract,
tokenId,
payable(msg.sender),
payable(address(0)),
price
);
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
emit MarketItemCreated(
itemId,
nftContract,
tokenId,
msg.sender,
address(0),
price
);
}
function createMarketSale(
address nftContract,
uint256 itemId
) payable public {
uint price = idToMarketItem[itemId].price;
uint tokenId = idToMarketItem[itemId].tokenId;
require(msg.value == price, "Please submit the asking price in order to complete the purchase");
idToMarketItem[itemId].seller.transfer(msg.value);
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
idToMarketItem[itemId].owner = payable(msg.sender);
_itemsSold.increment();
}
function fetchMarketItem(uint itemId) public view returns (MarketItem memory) {
MarketItem memory item = idToMarketItem[itemId];
return item;
}
//返回所有仍在销售的市场商品
function fetchMarketItems() public view returns (MarketItem[] memory) {
uint itemCount = _itemIds.current();
uint unsoldItemCount = _itemIds.current() - _itemsSold.current();
uint currentIndex = 0;
MarketItem[] memory items = new MarketItem[](unsoldItemCount);
for (uint i = 0; i < itemCount; i++) {
if (idToMarketItem[i + 1].owner == address(0)) {
uint currentId = idToMarketItem[i + 1].itemId;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
//返回用户已购买的NFT
function fetchMyNFTs() public view returns (MarketItem[] memory) {
uint totalItemCount = _itemIds.current();
uint itemCount = 0;
uint currentIndex = 0;
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
itemCount += 1;
}
}
MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
uint currentId = idToMarketItem[i + 1].itemId;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
}
Home页面:使用web3Modal加载钱包连接以太坊网络
调用合约函数fetchMarketItems加载页面
const [nfts, setNfts] = useState([])
const [loaded, setLoaded] = useState('not-loaded')
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
const providerOptions = {
fortmatic: {
package: Fortmatic,
options: {
// Mikko's TESTNET api key
key: "pk_test_391E26A3B43A3350"
}
},
walletconnect: {
package: WalletConnectProvider, // required
options: {
infuraId: "INFURA_ID" // required
}
}
};
const web3Modal = new Web3Modal({
network: "mainnet",
cacheProvider: true,
disableInjectedProvider: false,
providerOptions: providerOptions, // required
});
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const tokenContract = new ethers.Contract(nftaddress, nftabi, provider)
const marketContract = new ethers.Contract(nftmarketaddress, nftmarketabi, provider)
const data = await marketContract.fetchMarketItems()
const items = await Promise.all(data.map(async i => {
const tokenUri = await tokenContract.tokenURI(i.tokenId)
const meta = await axios.get(tokenUri)
let price = web3.utils.fromWei(i.price.toString(), 'ether');
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.data.image,
name: meta.data.name,
description: meta.data.description,
}
return item
}))
console.log('items: ', items)
setNfts(items)
setLoaded('loaded')
}
Create NFT 铸币页面(信息上传ipfs返回链接作为tokenURI)
const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')
const [fileUrl, setFileUrl] = useState(null)
const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })
const router = useRouter()
async function createSale(url) {
const web3Modal = new Web3Modal({
network: "mainnet",
cacheProvider: true,
});
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()
let contract = new ethers.Contract(nftaddress,nftabi, signer)
let transaction = await contract.createToken(url)
let tx = await transaction.wait()
let event = tx.events[0]
let value = event.args[2]
let tokenId = value.toNumber()
const price = web3.utils.toWei(formInput.price, 'ether')
const listingPrice = web3.utils.toWei('0.1', 'ether')
contract = new ethers.Contract(nftmarketaddress, nftmarketabi, signer)
transaction = await contract.createMarketItem(nftaddress, tokenId, price, { value: listingPrice })
await transaction.wait()
router.push('/')
}
async function onChange(e) {
const file = e.target.files[0];
try {
const added = await client.add(
file,
{
progress: (prog) => console.log(`received: ${prog}`)
}
)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
setFileUrl(url)
} catch (error) {
console.log('Error uploading file: ', error);
}
}
async function createMarket() {
const { name, description, price } = formInput
if (!name || !description || !price || !fileUrl) return
// first, upload to IPFS
const data = JSON.stringify({
name, description, image: fileUrl
})
try {
const added = await client.add(data)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
createSale(url)
} catch (error) {
console.log('Error uploading file: ', error);
}
}
MyNFT 同上loadNFTS一样,不过调用合约fetchMyNFTs
完整代码已放置Gitee https://gitee.com/xiaxuliang/nftmakret