目录

1、ERC721的基础知识

1.1、什么是不可替代代币?

NFT 是独一无二的,每个令牌都有独特的特征和价值。可以成为 NFT 的东西类型有收藏卡、艺术品、机票等,它们之间都有明显的区别,不可互换。将不可替代代币 (NFT) 视为稀有收藏品;而且大多数时候,还有它的元数据属性。

1.2、什么是 ERC-721?

ERC-721(Ethereum Request for Comments 721)由 William Entriken、Dieter Shirley、Jacob Evans 和 Nastassia Sachs 于 2018 年 1 月提出,是一种不可替代的代币标准。描述了如何在 EVM(以太坊虚拟机)兼容的区块链上构建不可替代的代币;它是不可替代代币的标准接口;它有一套规则,可以很容易地使用 NFTNFT 不仅是 ERC-721 类型;它们也可以是ERC-1155 令牌。

ERC-721 引入了 NFT 标准,换句话说,这种类型的 Token 是独一无二的,并且可能具有与来自同一智能合约的另一个 Token 不同的价值,可能是由于它的年龄、稀有性甚至是其他类似自定义属性等等。

所有 NFT 都有一个 uint256 类型的变量 tokenId,因此对于任何 ERC-721 合约,该对 contract addressuint256 tokenId 必须是全局唯一的。也就是说,一个 dApp 可以有一个“转换器”,它使用 tokenId 作为输入并输出一些很酷的东西的图像,比如僵尸、武器、技能或猫、狗一类的!

1.3、什么是元数据

所有 NFT 都有元数据。您可以在最初的ERC/EIP 721 提案中了解这一点 。 基本上,社区发现在以太坊上存储图像真的很费力而且很昂贵。如果你想存储一张 8 x 8 的图片,存储这么多数据是相当便宜的,但如果你想要一张分辨率不错的图片,你就需要花更多的 GAS 费用。

虽然 以太坊 2.0 将解决很多这些扩展难题,但目前,社区需要一个标准来帮助解决这个问题,这也就是元数据的存在原因。

{
"name": "mshk-logo-black",
"description": "mshk.top logo black",
"image": "https://bafybeihodzhbtntgml7t72maxill576ssax6md5kfu72aq4gd4p53oipn4.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 100
}
]
}

name,NFT的代币名称

description,NFT的代币描述

image,NFT图像的URL

attributes,NFT代币的属性,可以定义多个

一旦我们将 tokenId 分配给他们的 tokenURINFT 市场将能够展示你的代币,您可以在 Rinkeby 测试网上的 OpenSea 市场上看到我使用元数据更新后的效果。类似展示 NFT 的市场 还有如 MintableRarible

1.4、如何在链上保存NFT的图像

您会在上面的元数据代码示例中注意到,图像使用指向 IPFSURL,这是一种流行的图像存储方式。

IPFS 代表星际文件系统,是一种点对点超媒体协议,旨在使网络更快、更安全、更开放。它允许任何人上传文件,并且该文件被散列,因此如果它发生变化,它的散列也会发生变化。这是存储图像的理想选择,因为这意味着每次更新图像时,链上的 hash/tokenURI 也必须更改,这意味着我们可以记录元数据的历史记录。将图像添加到 IPFS 上也非常容易,并且不需要运行服务器!

推荐使用 CoinTool 中的 IPFS 工具

2、HardHat

关于 HardHat 的介绍以及安装,可以参考文章 如何使用ERC20代币实现买、卖功能并完成Dapp部署

3、创建项目

3.1、创建 NFT 市场

进入 hardhat 项目目录,创建 contracts/ERC721/NftMarketplace.sol 文件,内容如下:

$ cat contracts/ERC721/NftMarketplace.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14; import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; // Check out https://github.com/Fantom-foundation/Artion-Contracts/blob/5c90d2bc0401af6fb5abf35b860b762b31dfee02/contracts/FantomMarketplace.sol
// For a full decentralized nft marketplace // 从Solidity v0.8.4开始,有一种方便且省 gas 的方式可以通过使用自定义错误向用户解释操作失败的原因。
// 错误的语法类似于 事件的语法。它们必须与revert 语句一起使用,这会导致当前调用中的所有更改都被还原并将错误数据传递回调用者
error PriceNotMet(address nftAddress, uint256 tokenId, uint256 price);
error ItemNotForSale(address nftAddress, uint256 tokenId);
error NotListed(address nftAddress, uint256 tokenId);
error AlreadyListed(address nftAddress, uint256 tokenId);
error NoProceeds();
error NotOwner();
error NotApprovedForMarketplace();
error PriceMustBeAboveZero(); contract NftMarketplace is ReentrancyGuard {
// 保存卖家地址和价格
struct Listing {
uint256 price;
address seller;
} // 加入市场列表事件
event ItemListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
); // 更新事件
event UpdateListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
); // 取消市场列表事件
event ItemCanceled(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId
); // 买入事件
event ItemBuy(
address indexed buyer,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
); // 保存NFT列表和卖家的对应状态
mapping(address => mapping(uint256 => Listing)) private s_listings; // 卖家地址和卖出的总金额
mapping(address => uint256) private s_proceeds; modifier notListed(
address nftAddress,
uint256 tokenId,
address owner
) {
Listing memory listing = s_listings[nftAddress][tokenId];
if (listing.price > 0) {
revert AlreadyListed(nftAddress, tokenId);
}
_;
} // 检查卖家是否在列表中
modifier isListed(address nftAddress, uint256 tokenId) {
Listing memory listing = s_listings[nftAddress][tokenId];
if (listing.price <= 0) {
revert NotListed(nftAddress, tokenId);
}
_;
} // 检查 NFT 地址的 tokenId owner 是否为 spender
modifier isOwner(
address nftAddress,
uint256 tokenId,
address spender
) {
IERC721 nft = IERC721(nftAddress); // 查找NFT的所有者,分配给零地址的 NFT 被认为是无效的,返回NFT持有者地址
address owner = nft.ownerOf(tokenId);
if (spender != owner) {
revert NotOwner();
}
_;
} /*
* @notice 将 NFT 加入到市场列表中,external 表示这是一个外部函数
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
* @param price sale price for each item
*/
function listItem(
address nftAddress,
uint256 tokenId,
uint256 price
)
external
notListed(nftAddress, tokenId, msg.sender)
isOwner(nftAddress, tokenId, msg.sender)
{
if (price <= 0) {
// 终止运行并撤销状态更改
revert PriceMustBeAboveZero();
}
IERC721 nft = IERC721(nftAddress);
// 获取单个NFT的批准地址,如果tokenId不是有效地址,抛出异常,
if (nft.getApproved(tokenId) != address(this)) {
revert NotApprovedForMarketplace();
} // 存储智能合约状态
s_listings[nftAddress][tokenId] = Listing(price, msg.sender); // 注册事件
emit ItemListed(msg.sender, nftAddress, tokenId, price);
} /*
* @notice 从NFT列表中删除 卖家信息
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
*/
function cancelListing(address nftAddress, uint256 tokenId)
external
isOwner(nftAddress, tokenId, msg.sender)
isListed(nftAddress, tokenId)
{
delete (s_listings[nftAddress][tokenId]); // 注册 事件
emit ItemCanceled(msg.sender, nftAddress, tokenId);
} /*
* @notice 允许买家使用ETH,从卖家列表中买入 NFT
* nonReentrant 方法 防止合约被重复调用
* @param nftAddress NFT 合约地址
* @param tokenId NFT 的通证 ID
*/
function buyItem(address nftAddress, uint256 tokenId)
external
payable
isListed(nftAddress, tokenId)
nonReentrant
{
// 获取卖家列表,并判断支付的ETH是否小于卖家的价格
Listing memory listedItem = s_listings[nftAddress][tokenId];
if (msg.value < listedItem.price) {
revert PriceNotMet(nftAddress, tokenId, listedItem.price);
} // 更新卖家卖出的金额
s_proceeds[listedItem.seller] += msg.value;
// Could just send the money...
// https://fravoll.github.io/solidity-patterns/pull_over_push.html // 从卖家列表中删除
delete (s_listings[nftAddress][tokenId]); // 将 NFT(tokenId) 所有权从 listedItem.seller 转移到 msg.sender
IERC721(nftAddress).safeTransferFrom(
listedItem.seller,
msg.sender,
tokenId
); //注册买家事件
emit ItemBuy(msg.sender, nftAddress, tokenId, listedItem.price);
} /*
* @notice 卖家更新NFT在市场上的价格
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
* @param newPrice Price in Wei of the item
*/
function updateListing(
address nftAddress,
uint256 tokenId,
uint256 newPrice
)
external
isListed(nftAddress, tokenId)
nonReentrant
isOwner(nftAddress, tokenId, msg.sender)
{
s_listings[nftAddress][tokenId].price = newPrice;
emit UpdateListed(msg.sender, nftAddress, tokenId, newPrice);
} /*
* @notice 将ETH转移到其他帐号,同时设置收益余额为0
*/
function withdrawProceeds() external {
uint256 proceeds = s_proceeds[msg.sender];
if (proceeds <= 0) {
revert NoProceeds();
}
s_proceeds[msg.sender] = 0; // 将 ETH 发送到地址的方法,关于此语法更多介绍可以参考下面链接
// https://ethereum.stackexchange.com/questions/96685/how-to-use-address-call-in-solidity
(bool success, ) = payable(msg.sender).call{value: proceeds}("");
require(success, "Transfer failed");
} /*
* @notice 获取NFT卖家列表
*/
function getListing(address nftAddress, uint256 tokenId)
external
view
returns (Listing memory)
{
return s_listings[nftAddress][tokenId];
} // 获取 seller 卖出的总金额
function getProceeds(address seller) external view returns (uint256) {
return s_proceeds[seller];
}
}

Solidity v0.8.4开始,有一种方便且省 GAS 的方式可以通过使用自定义错误向用户解释操作失败的原因。错误的语法类似于事件的语法。它们必须与 revert 语句一起使用,这会导致当前调用中的所有更改都被还原并将错误数据传递回调用者。

  自定义错误是在智能合约主体之外声明的。当错误被抛出时,在 Solidity 中意味着当某些检查和条件失败,周围函数的执行被“还原”。

代码中主要内容介绍:

  • notListed、isListed、isOwner是函数修饰符的应用。
  • listItem方法,将 NFT 加入到列表,会做一些权限验证。其中用到了函数修饰符事件
  • cancelListing方法,从列表中删除 NFT,将 NFT 下架。
  • buyItem方法,购买 NFT ,项目中主要用 ETH 来交换 NFT 资产,也可以用其他数字资产进行交换。同时会更新卖家余额。从listItem中下架 NFT
  • updateListing方法,更新 NFT 的价格。
  • withdrawProceeds方法,将卖出的收益从合约中转移给卖家。
  • getListing方法,根据 NFT 地址和 tokenId,返回卖家和价格信息。
  • getProceeds方法,查看卖家卖出后的收益。

3.2、创建 NFT 智能合约

在编写测试脚本前,我们需要一个 NFT的智能合约示例,以便我们铸造的 NFT可以在市场上展示、销售。我们将遵守 ERC721 令牌规范,我们将从 OpenZeppelinERC721URIStorage 库继承。

进入 hardhat 项目目录,创建 contracts/ERC721/MSHK721NFT.sol 文件,内容如下:

$ cat contracts/ERC721/MSHK721NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14; import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "hardhat/console.sol"; contract MSHK721NFT is ERC721URIStorage, Ownable {
// 递增递减计数器
using Counters for Counters.Counter;
Counters.Counter private _tokenIds; // 声明事件
event NFTMinted(uint256 indexed tokenId); constructor() ERC721("MSHKNFT", "MyNFT") {} /**
* 制作NFT,返回铸造的 NFT ID
* @param recipient 接收新铸造NFT的地址.
* @param tokenURI 描述 NFT 元数据的 JSON 文档
*/
function mintNFT(address recipient, string memory tokenURI)
external
onlyOwner
returns (uint256)
{
// 递增
_tokenIds.increment(); // 获取当前新的 TokenId
uint256 newTokenId = _tokenIds.current(); // 铸造NFT
_safeMint(recipient, newTokenId); // 保存NFT URL
_setTokenURI(newTokenId, tokenURI); // 注册事件
emit NFTMinted(newTokenId); return newTokenId;
} function getTokenCounter() public view returns (uint256) {
return _tokenIds.current();
}
}

上面的代码中,通过 mintNFT 方法铸造 NFT,主要有2个参数,第1个参数是接收NFT 的地址,第2个参数是 NFTURL 地址,也就是上文中提到的元数据地址。

3.3、编写测试脚本

在编写测试脚本前,我们先通过 IPFS工具,上传我们的图片和元数据文件,下面是我们已经上传好的2个元数据文件:

文件1,内容如下:

{
"name": "mshk-logo-black",
"description": "mshk.top logo black",
"image": "https://bafybeihodzhbtntgml7t72maxill576ssax6md5kfu72aq4gd4p53oipn4.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 100
}
]
}

文件2,内容如下:

{
"name": "mshk-logo-blue",
"description": "mshk.top logo blue",
"image": "https://bafybeifxkvzedhwclmibidf5hjoodwqkk2vlbbrlhd3bxbl3wzmkmyrvpq.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 200
}
]
}

进入 hardhat 项目目录,创建 test/ERC721/01_NFT.js 测试文件,内容如下:

const { expect } = require("chai");
const { ethers } = require("hardhat"); /**
* 运行测试方法:
* npx hardhat test test/ERC721/01_NFT.js
*/
describe("NFT MarketPlace Test", () => { // NFT 元数据1
const TOKEN_URI1 = "https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io";
// NFT 元数据2
const TOKEN_URI2 = "https://bafybeibyb2rdn6raav4ozyxub2r5w4vh3wmw46s6bi54eq7syjzfkmbjn4.ipfs.infura-ipfs.io"; let owner;
let addr1;
let addr2;
let addrs; let nftMarketplaceContractFactory;
let nftContractFactory;
let nftMarketplaceContract;
let nftContract; let IDENTITIES; beforeEach(async () => {
[owner, addr1, addr2, ...addrs] = await ethers.getSigners(); IDENTITIES = {
[owner.address]: "OWNER",
[addr1.address]: "DEPLOYER",
[addr2.address]: "BUYER_1",
} var NFTMarketplaceContractName = "NftMarketplace";
var NFTContractName = "MSHK721NFT" // 获取 NFTMarketplace 实例
nftMarketplaceContractFactory = await ethers.getContractFactory(NFTMarketplaceContractName);
// 部署 NFTMarketplace 合约
nftMarketplaceContract = await nftMarketplaceContractFactory.deploy() // 获取 nftContract 实例
nftContractFactory = await ethers.getContractFactory(NFTContractName);
// 部署 nftContract 合约
nftContract = await nftContractFactory.deploy() console.log(`owner:${owner.address}`)
console.log(`addr1:${addr1.address}`)
console.log(`addr2:${addr2.address}`) //
console.log(`${NFTMarketplaceContractName} Token Contract deployed address -> ${nftMarketplaceContract.address}`); //
console.log(`${NFTContractName} Token Contract deployed address -> ${nftContract.address} owner:${await nftContract.owner()}`); }); it("mint and list and buy item", async () => { console.log(`Minting NFT for ${addr1.address}`)
// 为 addr1 铸造一个 NFT
let mintTx = await nftContract.connect(owner).mintNFT(addr1.address, TOKEN_URI1)
let mintTxReceipt = await mintTx.wait(1) // 非常量(既不pure也不view)函数的返回值仅在函数被链上调用时才可用(即,从这个合约或从另一个合约)
// 当从链下(例如,从 ethers.js 脚本)调用此类函数时,需要在交易中执行它,并且返回值是该交易的哈希值,因为不知道交易何时会被挖掘并添加到区块链中
// 为了在从链下调用非常量函数时获得它的返回值,可以发出一个包含将要返回的值的事件
let tokenId = mintTxReceipt.events[0].args.tokenId expect(tokenId).to.equal(1); // 授权 市场合约 可以操作这个NFT
console.log("Approving Marketplace as operator of NFT...")
let approvalTx = await nftContract
.connect(addr1)
.approve(nftMarketplaceContract.address, tokenId)
await approvalTx.wait(1) // NFT交易价格 10 ETH
let PRICE = ethers.utils.parseEther("10") // 将 NFT 加入到列表
console.log("Listing NFT...")
let listItemTX = await nftMarketplaceContract
.connect(addr1)
.listItem(nftContract.address, tokenId, PRICE)
await listItemTX.wait(1)
console.log("NFT Listed with token ID: ", tokenId.toString()) const mintedBy = await nftContract.ownerOf(tokenId) // 检查 nft 的 owner 是否为 addr1
expect(mintedBy).to.equal(addr1.address) console.log(`NFT with ID ${tokenId} minted and listed by owner ${mintedBy} with identity ${IDENTITIES[mintedBy]}. `) //---- Buy // 根据 tokenId 获取 NFT
let listing = await nftMarketplaceContract.getListing(nftContract.address, tokenId)
let price = listing.price.toString() // 使用 addr2 从 nftMarketplaceContract 买入 TOKEN_ID 为 0 的NFT
const buyItemTX = await nftMarketplaceContract
.connect(addr2)
.buyItem(nftContract.address, tokenId, {
value: price,
})
await buyItemTX.wait(1)
console.log("NFT Bought!") const newOwner = await nftContract.ownerOf(tokenId)
console.log(`New owner of Token ID ${tokenId} is ${newOwner} with identity of ${IDENTITIES[newOwner]} `) //---- proceeds
const proceeds = await nftMarketplaceContract.getProceeds(addr1.address) const proceedsValue = ethers.utils.formatEther(proceeds.toString())
console.log(`Seller ${owner.address} has ${proceedsValue} eth!`) //---- withdrawProceeds
const addr1OldBalance = await ethers.provider.getBalance(addr1.address);
await nftMarketplaceContract.connect(addr1).withdrawProceeds()
const addr1NewBalance = await ethers.provider.getBalance(addr1.address);
console.log(`${addr1.address} old:${ethers.utils.formatEther(addr1OldBalance)} eth,withdrawProceeds After:${ethers.utils.formatEther(addr1NewBalance)} eth!`) }); it("update and cancel nft item", async () => {
// 为 addr2 铸造一个 NFT
let mintTx = await nftContract.connect(owner).mintNFT(addr2.address, TOKEN_URI2)
let mintTxReceipt = await mintTx.wait(1) // 非常量(既不pure也不view)函数的返回值仅在函数被链上调用时才可用(即,从这个合约或从另一个合约)
// 当从链下(例如,从 ethers.js 脚本)调用此类函数时,需要在交易中执行它,并且返回值是该交易的哈希值,因为不知道交易何时会被挖掘并添加到区块链中
// 为了在从链下调用非常量函数时获得它的返回值,可以发出一个包含将要返回的值的事件
let tokenId = mintTxReceipt.events[0].args.tokenId // 授权 市场合约 可以操作这个NFT
console.log("Approving Marketplace as operator of NFT...")
approvalTx = await nftContract.connect(addr2).approve(nftMarketplaceContract.address, tokenId)
await approvalTx.wait(1) // NFT交易价格 0.1 ETH
PRICE = ethers.utils.parseEther("0.1") // 将 NFT 加入到列表
console.log("Listing NFT...")
listItemTX = await nftMarketplaceContract.connect(addr2).listItem(nftContract.address, tokenId, PRICE)
await listItemTX.wait(1)
console.log("NFT Listed with token ID: ", tokenId.toString()) console.log(`Updating listing for token ID ${tokenId} with a new price`) listing = await nftMarketplaceContract.getListing(nftContract.address, tokenId)
let oldPrice = listing.price.toString()
console.log(`oldPrice: ${ethers.utils.formatEther(oldPrice.toString())}`) // 更新价格
const updateTx = await nftMarketplaceContract.connect(addr2).updateListing(nftContract.address, tokenId, ethers.utils.parseEther("0.5")) // 等待链上处理
const updateTxReceipt = await updateTx.wait(1) // 从事件中获取更新的价格
const updatedPrice = updateTxReceipt.events[0].args.price
console.log(`updated price: ${ethers.utils.formatEther(updatedPrice.toString())}`) // 获取信息,确认价格是否有变更.
const updatedListing = await nftMarketplaceContract.getListing(
nftContract.address,
tokenId
)
console.log(`Updated listing has price of ${ethers.utils.formatEther(updatedListing.price.toString())}`) //----------cancel
let tx = await nftMarketplaceContract.connect(addr2).cancelListing(nftContract.address, tokenId)
await tx.wait(1)
console.log(`NFT with ID ${tokenId} Canceled...`) // Check cancellation.
const canceledListing = await nftMarketplaceContract.getListing(nftContract.address, tokenId)
console.log("Seller is Zero Address (i.e no one!)", canceledListing.seller)
}); });

上面的测试脚本中,我们分成两部分,注释比较详细,下面是简要介绍这两部分测试的功能。

  第1部分:

  • addr1 用户铸1个NFT
  • 授权 NFT市场 可以操作这个 addr1 的 NFT。
  • NFT 加入到 NFT市场,设置价格为 10 ETH。
  • 使用 addr2 用户购买 addr1 的NFT。
  • 查看addr1NFT市场 的余额
  • NFT市场中的余额取出到 addr1 的余额,对比前后余额数据。

第2部分:

  • addr2 用户铸1个NFT
  • 授权 NFT市场 可以操作这个 addr2 的 NFT。
  • NFT 加入到 NFT市场,设置价格为 0.1 ETH。
  • addr2 的NFT价格从 0.1 ETH 更新为 0.5 ETH。进行数据对比输出。
  • NFT市场 中下架 addr2 的 NFT。

下面是我们运行测试脚本的效果:

到目前为止,我们已经完成了 NFT 的创建,并将 NFT 加入到市场完成了买、卖、查看销售后的余额,转帐给卖家等功能。

项目的源码都保存在 Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace

克隆项目到本地后,进入 hardhat 项目目录,先执行 yarn install 下载依赖包。

$ yarn install
yarn install v1.22.19
warning package.json: No license field
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
warning hardhat-project: No license field
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
warning " > @nomiclabs/hardhat-waffle@2.0.3" has incorrect peer dependency "@nomiclabs/hardhat-ethers@^2.0.0".
warning " > @openzeppelin/hardhat-upgrades@1.19.0" has incorrect peer dependency "@nomiclabs/hardhat-ethers@^2.0.0".
warning "hardhat-deploy > zksync-web3@0.4.0" has incorrect peer dependency "ethers@~5.5.0".
[4/4] Building fresh packages...
Done in 15.42s.

安装完依赖包后,运行npx hardhat test test/ERC721/01_NFT.js 命令,可以看到和上图一样的效果。

$ npx hardhat test test/ERC721/01_NFT.js
Compiled 16 Solidity files successfully NFT MarketPlace Test
owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
addr1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8
addr2:0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
NftMarketplace Token Contract deployed address -> 0x5FbDB2315678afecb367f032d93F642f64180aa3
MSHK721NFT Token Contract deployed address -> 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Minting NFT for 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Approving Marketplace as operator of NFT...
Listing NFT...
NFT Listed with token ID: 1
NFT with ID 1 minted and listed by owner 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 with identity DEPLOYER.
NFT Bought!
New owner of Token ID 1 is 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC with identity of BUYER_1
Seller 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 10.0 eth!
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 old:9999.999797616067546951 eth,withdrawProceeds After:10009.9997570794102017 eth!
mint and list and buy item (232ms)
owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
addr1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8
addr2:0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
NftMarketplace Token Contract deployed address -> 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
MSHK721NFT Token Contract deployed address -> 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Approving Marketplace as operator of NFT...
Listing NFT...
NFT Listed with token ID: 1
Updating listing for token ID 1 with a new price
oldPrice: 0.1
updated price: 0.5
Updated listing has price of 0.5
NFT with ID 1 Canceled...
Seller is Zero Address (i.e no one!) 0x0000000000000000000000000000000000000000
update and cancel nft item (156ms) 2 passing (2s)

4、将 NFT 部署到 Rinkeby 网络,在 OpenSea 上查看

打开 hardhat.config.js 文件,编辑内容如下并保存:

  • 修改里面的 RINKEBY_RPC_URL 为你的地址,如果没有帐号,可以去 alchemy.com 注册一个,以后开发区块链时会经常使用到。
  • 修改 PRIVATE_KEY 为你要部署的帐号私钥。

4.1、部署 NFT市场

运行下面的命令,将 NFT市场 部署到 Rinkeby 网络:

$ npx hardhat run script/ERC721/01-deploy-NftMarketplace.js --network rinkeby
----------------------------------------------------
deployer address -> 0xbB0a92d634D7b9Ac69079ed0e521CC2e0a97c420
NftMarketplace Contract deployed address -> 0x48aD115EE899Cc01d6Fd2Ea9BC3fE5bd7d3E1B1C

Rinkeby 网络,查看我们创建的NFT交易市场合约,效果如下图:

4.2、部署 NFT 721示例

运行下面的命令,将 NFT示例 部署到 Rinkeby 网络:

$ npx hardhat run script/ERC721/02-deploy-MSHKNFT.js --network rinkeby
----------------------------------------------------
deployer address -> 0xbB0a92d634D7b9Ac69079ed0e521CC2e0a97c420
MSHK721NFT Contract deployed address -> 0x4b241b36D445E46dAE1916f5A0e76dfE470df115
----------------------------------------------------

记住我们创建的合约地址0x4b241b36D445E46dAE1916f5A0e76dfE470df115,后面我们会对合约进行线上验证。

Rinkeby 网络,查看我们创建的NFT721合约,效果如下图:

4.3、对 NFT 721示例 合约在 Rinkeby 网络进行验证

验证 NFT示例 合约:

$ npx hardhat verify --contract contracts/ERC721/MSHK721NFT.sol:MSHK721NFT 0x4b241b36D445E46dAE1916f5A0e76dfE470df115 --network rinkeby
Nothing to compile
Successfully submitted source code for contract
contracts/ERC721/MSHK721NFT.sol:MSHK721NFT at 0x4b241b36D445E46dAE1916f5A0e76dfE470df115
for verification on the block explorer. Waiting for verification result... Successfully verified contract MSHK721NFT on Etherscan.
https://rinkeby.etherscan.io/address/0x4b241b36D445E46dAE1916f5A0e76dfE470df115#code

4.4、在 Rinkeby 网络铸造 NFT

我们打开 Rinkeby 网络,浏览刚刚创建的 NFT 721示例 合约,为地址 0x0BFd206c851729590DDAdfCa9439b30aD2AAbf9F 创建一个 NFTNFT 的元数据,使用 IPFS工具创建好的元数据地址 https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io

操作步骤如下图:

创建 NFT 后我们可以通过 交易哈希 看到,NFT合约 0x4b241b36d445e46dae1916f5a0e76dfe470df115,刚刚创建的 Token ID1的 Token。

4.5、在 opensea 查看刚刚铸造的NFT

浏览以下地址 https://testnets.opensea.io/assets/rinkeby/0x4b241b36d445e46dae1916f5a0e76dfe470df115/1 可以看到我们刚刚铸的NFT 图片。

在URL部分,rinkeby 表示网络名称,0x4b241b36d445e46dae1916f5a0e76dfe470df115NFT721 的合约地址,1Token ID

至此,我们完成了如何铸造NFT,以及完善一个可以买、卖交易的 NFT市场,包括发布到 rinkeby 网络后,在 opensea 测试网络查看。

如果发布到主网,将 rinkeby 更改为 ethmainnet 即可。

5、项目源码

Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace

6、推荐阅读

常用词汇表

  Solidity v0.8.4 Custom Error


转载声明:可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明,谢谢合作!


如何使用Solidity和Hardhat构建你自己的NFT以及NFT交易市场的更多相关文章

  1. solidity语言介绍以及开发环境准备

    solidity语言介绍以及开发环境准备   Solidity 是一门面向合约的.为实现智能合约而创建的高级编程语言.这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的 ...

  2. 初遇NFT-IPFS

    初遇NFT-IPFS 本次学习如何使用Hardhat框架制作可预售的NFT并利用IPFS存储元数据. NFT简介 NFT全称Non-fungible Token(即非同质化通证).不可分割性(目前有碎 ...

  3. 用Solidity在Truffle上构建一个HelloWorld智能合约

    原文地址:石匠的blog Truffle的环境安装在前文已经整理,这次用Solidity在Truffle写一个HelloWorld以太坊智能合约,并在testrpc上进行测试.当前的软件版本信息如下: ...

  4. 【阿菜用工具】Slither:Solidity静态分析框架

    工具简介 Slither 是一个 python3 开发,用于检测智能合约(solidity)漏洞的静态分析框架. Slither 的 Github 地址:https://github.com/cryt ...

  5. 以太坊开发DApp实战教程——用区块链、星际文件系统(IPFS)、Node.js和MongoDB来构建电商平台(一)

    第一节 简介 欢迎和我们一起来用以太坊开发构建一个去中心化电商DApp!我们将用区块链.星际文件系统(IPFS).Node.js和MongoDB来构建电商平台类似淘宝的在线电商应用,卖家可以自由地出售 ...

  6. 智能合约开发solidity编程语言开发一个以太坊应用区块链投票实例

    智能合约开发用solidity编程语言部署在以太坊这个区块链平台,本文提供一个官方实战示例快速入门,用例子深入浅出智能合约开发,体会以太坊构建去中心化可信交易技术魅力.智能合约其实是"执行合 ...

  7. 智能合约 solidity 开发的环境基本搭建

    以太坊Dapp开发快速入门 以太坊为开源社区,虽然设计东西都很优秀,但是组件十分的杂乱,因此下面首先简单介绍下以太坊的一些常用组件以及各种工具介绍 Geth Geth是由以太坊基金会提供的官方客户端软 ...

  8. Solidity 官方文档中文版 3_安装Solidity

    基于浏览器的Solidity 如果你只是想尝试一个使用Solidity的小合约,你不需要安装任何东西,只要访问 基于浏览器的Solidity http://remix.ethereum.org/. 如 ...

  9. Solidity 文档--第二章:安装 Solidity

    安装Solidity 基于浏览器的Solidity 如果你只是想尝试一个使用Solidity的小合约,你不需要安装任何东西,只要访问基于浏览器的Solidity. 如果你想离线使用,你可以保存页面到本 ...

随机推荐

  1. 【工具-Nginx】从入门安装到高可用集群搭建

    文章已收录至https://lichong.work,转载请注明原文链接. ps:欢迎关注公众号"Fun肆编程"或添加我的私人微信交流经验 一.Nginx安装配置及常用命令 1.环 ...

  2. 每日一题20180401-Linux

    一.题目 1.1 在mysql命令行临时开启自动补全 1.2 通过shell脚本打印乘法口诀表 二.答案 2.1 # auto-rehash:读取表信息和列信息,可以在连上终端后开启tab补齐功能 # ...

  3. iTextSharp 提取签名图像

    原文 本文使用 iTextSharp 5.5.13.2,记录使用 iTextSharp 提取图片时,获得的知识点. pdf 中的签名并不是单纯的一张图片,它是由一张基础的底色图和一张蒙版图片组成.需要 ...

  4. 人脸识别库 face_recognition

    face_recognition Windows系统环境下安装 默认环境:anaconda的python3.7版本,win10环境 第一步:安装dlib 从网络上下载: http://dlib.net ...

  5. ROS基本程序实现

    0.前言 现在介绍ROS基本程序实现的教程有很多,步骤无非就是建工作空间,编译,创建功能包,创建文件和修改CMakeList,之后再编译.运行程序这几步.但是这些教程中很多在文件夹切换的很混乱,导致会 ...

  6. Linux系统sed命令常用参数实战

    Linux系统sed命令常用参数实战 常用参数 -n 输出某行的文本内容,通常与p联合使用, -e 命令行模式下进行sed的动作编辑,输出编辑后的内容,源文件不会发生变化 -f 以命令中指定的scri ...

  7. springboot引入mybatis遇到的坑

      前边分享了springboot项目的创建及springboot项目的默认配置文件等,想温习的小伙伴可移步至文章末尾阅读,感谢.今天来分享下springboot引入mybatis框架的步骤,有小伙伴 ...

  8. 【Redis】skiplist跳跃表

    有序集合Sorted Set zadd zadd用于向集合中添加元素并且可以设置分值,比如添加三门编程语言,分值分别为1.2.3: 127.0.0.1:6379> zadd language 1 ...

  9. java类的学习

    什么是类: 类=属性+方法 属性来源于状态(以变量的形式存在):方法来源于动作: *属性对应的是数据,而数据只能存在变量中. 方法内的变量为局部变量:类体中的变量称为成员变量(也称为属性) java中 ...

  10. 5. `sklearn`下的线性回归

    本文以线性回归为例,介绍使用sklearn进行机器学习的一般过程. 首先生成模拟数据 import numpy as np def get_data(theta_true,N): X=np.rando ...