共计 22338 个字符,预计需要花费 56 分钟才能阅读完成。
简介
Web3 社区对于非同质化带币(NFT)充斥了期待。只管还没有杀手级利用的呈现,然而这项技术曾经重塑了数字资产所有权,身份体系,翻新范式和社区运作形式。
因为 NFT 是能够被交易交易的数字资产,而 NFT 交易所收集了 NFT 的信息并且撮合了买家和卖家,所以 NFT 交易所是生态中一个必不可少的局部。
这个教程解说了如何用 Solidity 来搭建 NFT 交易所的“后端”,如何开发承载交易所业务逻辑的智能合约。在代码中,咱们会创立一个 NftMarketplace.sol
智能合约和一个兼容 ERC-721(NFT)规范的代币合约,而后将这个 NFT 展现在咱们的交易所上。
你须要有一些编程教训,如果你理解一些根底的 Javascript,就能够实现整个我的项目开发。当然如果可能相熟以太坊的术语就更好了,以太坊的相干常识能够通过浏览以太坊官网来理解。
这个交易所将会有以下的根底性能:
- 上架 NFT
- 更新和下架 NFT
- 购买 NFT
- 获取所有的上架 NFT 的信息
- 获取卖家的以后状态
以上性能都会通过交易所智能合约实现。你能够先思考一下上述的性能是什么意思,因为这些性能的代码逻辑,就是它们业务逻辑的实现。比如说,在交易所中上架一个 NFT 的时须要什么数据?须要 Token ID。因为这个交易所能够上架很多不相干的 NFT,同时也须要可能给每一个 token 加上价格。
OK,那在写 NFT 相干合约之前,让咱们先设置好我的项目和开发环境。这个我的项目的 GitHub Repo 在这里。这个 Repo 会比这个教程的内容更深刻,所以你本人能够依据它去实现更多的性能。
我的项目环境搭建
在这个我的项目中,咱们会用到 yarn,运行 npm install -g yarn
来全局装置它。另外,你须要确定你的机器上有 Node.js,运行 node –version
来查看它有没有被装置。
除此以外,还会用到 Hardhat 来编译,部署,测试和交互咱们的智能合约,Hardhat 是一个以太坊开发环境,相干的常识所以浏览一下 Hardhat 的官网的老手教程。
我将用 <<root>>
来指代我的项目目录,关上命令行,进入到我的项目目录,关上 IDE(你能够应用任何反对 javascript 的 IDE,比方 VSCode)。
在我的项目目录中,创立一个 package.json
文件,复制这个文件的内容。这个文件中会蕴含 NPM 的依赖包,这些依赖很多都是 Hardhat 所需的。而后运行 yarn install
来装置所有的依赖。当装置实现当前,在我的项目目录下查看 node_modules
文件夹,这个文件夹会蕴含所有下载好的依赖文件。
这个教程会应用 Hardhat 的本地区块链网络,这意味着咱们并没有真正接触以太坊的主网和测试网。如果你想要在以太坊测试网比方 Rinkeby 中测试,请参考 Repo 的 README。
在你的我的项目根目录下,运行 /<root> yarn hardhat
来初始化 Hardhat 并且抉择第四个选项:“create an empty hardhat.config.js”
期待 Hardhat 的初始化实现当前,在我的项目根目录会有一个空白的 hardhat.config.js
文件。如果你想要部署在测试网上的话,那么就复制这个 配置信息 到 hardhat.config.js
中,对不同的测试网和主网的进行参数配置。另外还有一点须要留神,如果应用测试网或者主网,还须要用到以太坊节点运营商的接口,比方 Infura,Alchemy 或者 Moralis。
如果你当初不想部署在测试网或者主网上,那就复制上面的配置文件。我前面将会将这个文件称作“Hardhat Config”,别忘了 export 这个对象(module.exports = {defaultNetwork: {...} } )
。这个对象有我的项目的配置信息,可能定义默认,hardhat 和本地网络。
上面的最小配置就是你 export 的对象中的内容,你能够把链接中的文件替换为如下的配置:
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("hardhat-deploy");
require("solidity-coverage");
require("hardhat-contract-sizer");
require("dotenv").config();
module.exports = {
defaultNetwork: "hardhat",
networks: {
hardhat: {chainId: 31337,},
localhost: {chainId: 31337,},
},
namedAccounts: {
deployer: {
default: 0, // here this will by default take the first account as deployer
1: 0, // similarly on mainnet (network 1) it will take the first
//account as deployer. Note though that depending on how hardhat network are
//configured, the account 0 on one network can be different than on another
},
},
solidity: {
compilers: [
{version: "0.8.7",},
{version: "0.4.24",},
],
},
mocha: {timeout: 200000, // 200 seconds max for running tests},
};
当初,你的我的项目会有以下文件夹:
Contracts
文件夹,这里有咱们 NFT 交易所的逻辑和 NFT 样例合约。deploy
文件夹,这里有 hardhat-deploy plugin 和部署脚本,它们能够编译智能合约并且部署在 Hardhat 提供的本地区块链中。scripts
文件夹,这里有一些脚本文件,用来和部署在本地的 Hardhat 开发环境中的智能合约交互。
接下来,让咱们开始开发 NFT 交易所合约。
开发 NFT 交易所
在我的项目目录下,创立 contracts
文件夹。在文件夹中,而后创立 NftMarketplace.sol
文件(文件门路应该是 ../<< root >>/contracts/NftMarketplace.sol
)。
在 NftMarketplace
这个智能合约中,须要实现之前提到的不同的操作。这些办法如下所示:
function listItem(
address nftAddress,
uint256 tokenId,
uint256 price
) {}
function cancelListing(address nftAddress, uint256 tokenId){}
function buyItem(address nftAddress, uint256 tokenId){}
function updateListing(
address nftAddress,
uint256 tokenId,
uint256 newPrice
){}
function withdrawProceeds(){} // method caller should be withdrawer
function getListing(address nftAddress, uint256 tokenId){}
只管看起来很简略,但智能合约还有很多必要的查看,当初深入研究一下。咱们要保障智能合约不被重入攻打,重入攻打个别是对反复执行原本不该执行的代码来获利,通常是反复执行通证转账操作。
在实现这个交易所的逻辑时,咱们须要应用下列的属性和数据架构:
- 1 个构造体:
Listing
用来存储价格和卖房资产变量 - 3 个事件:
ItemListed
,ItemCanceled
和ItemBought
。 - 2 个 mapping:
s_listings
和s_proceeds
,它们存储在区块链上的状态变量。 - 3 个函数润饰器。
别着急,持续看上面的智能合约的时候,你就会明确下面的货色。
让咱们先申明智能合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract NftMarketplace is ReentrancyGuard {// TODO…}
能够看到,咱们从 OpenZeppelin 中引入了两个文件,OpenZeppelin 提供了开源且通过审计的,平安智能合约合约模版。咱们的智能合约继承了它的 ReentrancyGuard
智能合约(在 Github 上查看),这个智能合约中有咱们须要用到的修饰符和办法,用来避免重入攻打。
咱们还引入了 IERC721.sol 文件,这个接口咱们马上就会用到。然而,咱们的交易所智能合约不会继承 ERC-721 通证规范,因为交易所合约不是一个通证合约。
实现 listItem()
让咱们从 listItem()
函数开始,咱们须要把它定义为一个 external
函数,因为它会被内部合约或者终端用户调用(比如说从网页前端)。咱们须要 listItem()
做上面的操作:
- 保障这个正在被上架的物品还没有上架。咱们通过 Solidity 函数修饰符 来保障这点。
- 保障正在上架这个物品的人(正在调用这个办法)是它的的所有人。
- 保障这个通证的智能合约曾经“容许”咱们的 NFT 交易所来操作这个通证(比如说转账和其余操作)
- 查看它的价格是否高于 0 wei
- 发送一个 event 记录上架操作
- 在智能合约中存储上架的明细(比方交易所的状态)
函数代码如下:
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);
if (nft.getApproved(tokenId) != address(this)) {revert NotApprovedForMarketplace();
}
s_listings[nftAddress][tokenId] = Listing(price, msg.sender);
emit ItemListed(msg.sender, nftAddress, tokenId, price);
}
函数修饰符,实际和状态变量
当初实现函数修饰符,事件以及这个 App 的存储数据的状态变量。以下代码中的评论会阐明这些货色都应用在了哪里,或者你能够参考 GitHub 代码仓库。
contract NftMarketplace is ReentrancyGuard {
struct Listing {
uint256 price;
address seller;
}
event ItemListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
// State Variables
mapping(address => mapping(uint256 => Listing)) private s_listings;
mapping(address => uint256) private s_proceeds;
// Function modifiers
modifier notListed(
address nftAddress,
uint256 tokenId,
address owner
) {Listing memory listing = s_listings[nftAddress][tokenId];
if (listing.price > 0) {revert AlreadyListed(nftAddress, tokenId);
}
_;
}
modifier isOwner(
address nftAddress,
uint256 tokenId,
address spender
) {IERC721 nft = IERC721(nftAddress);
address owner = nft.ownerOf(tokenId);
if (spender != owner) {revert NotOwner();
}
_;
}
//….. Rest of smart contract …..
}
能够看到:
Listing
这个构造体存储了两个数据 – 卖家的以太坊地址和卖家这个 NFT 的价格。- 状态变量
s_listings
是一个mapping
,将 NFT 的智能合约地址和 token ID 对应起来,Token ID 会指向Listing
这个构造体,使得 Token ID 能够指向 NFT 的卖家地址和价格。 - 状态变量
s_proceeds
将卖家的地址与他们卖 NFT 所赚到的钱相绑定。 ItemListed
事件记录的信息包含 – 卖家的地址,token ID,token 合约地址和上架的物品的价格。
咱们还有两个函数修饰符。第一个是notListed
,用来确认这个 token ID 当初并没有被上架(咱们不想反复上架 s_listings
中曾经蕴含的物品)。如果这个 token 曾经上架,交易会被 revert,返回 AlreadyListed
谬误(等一会来写这些谬误)。notListed
还会取得 token 更多的细节以查看正在上架的 token 的价格是否大于零(如果你还记得,咱们的 listItem()
办法要要求价格比方大于零。如果这个条件没有被满足,交易会被 revert 并且返回 PriceMustBeAboveZero()
谬误)。
第二个修饰符是 isOwner()
,它是用来查看调用 listItem()
函数的地址是否领有这个物品。如果没有,对于 listItem()
的调用会被 revert 并且返回 notOwner()
谬误。
自定义错误信息
当初让看一下这些谬误。它们是 solidity 中的自定义谬误,咱们还没有实现它们。它们实际上是在智能合约主体之外申明的。当初来申明这些谬误,因为咱们马上就要在交易所函数中用到它们。留神咱们须要在援用申明当前,和智能合约申明之前来申明它们。
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
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 {…}
同时请留神,自定义谬误能够抉择传入或者不传入参数。如果参数被传入,能够蕴含这个谬误的无效信息。
你能够看到咱们的第一个函数,listItem()
函数应用了其中 3 个谬误:
NotOwner()
谬误(通过isOwner()
修饰符)。PriceMustBeAboveZero()
谬误。NotApprovedForMarketplace()
谬误。
这些谬误会被“抛出”,在 Solidity 意味着在执行函数的时候,如果遇到这些状态就会失败,并且被“revert”。
实现 cancelListing()
如果一个 token 的持有者想要下架他的的 token,不再在 Listing 中放弃这个 token 的信息。这意味着咱们必须在 s_listings
这个 mapping 中删除对应的条目。所以咱们在 listItem()
当前写这个函数。
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);
}
这是一个能够内部调用的函数,它的参数是 token 的合约地址和 token ID。它用了两个函数修饰符来查看函数调用者是否这个 token 的拥有者并且这个 token 是否曾经上架了(因为咱们要下架物品,所以还是有必要查看下)。而后删除这些上架的物品,在事件中记录。
咱们还没有实现修饰符 isOwner
– 咱们曾经实现反向查看,isNotOwner
!回到代码的修饰符局部,插入上面的代码:
modifier isListed(address nftAddress, uint256 tokenId) {Listing memory listing = s_listings[nftAddress][tokenId];
if (listing.price <= 0) {revert NotListed(nftAddress, tokenId);
}
_;
}
上述代码的逻辑和 isNotListed
是相同的。如果 item 还没有上架,这个修饰符会将交易 revert,然而对于 isNotListed
,当这个 item 是上架的,才会 revert 交易。如果不好了解,记住这个修饰符叫什么名字 – 名字指的是查看会通过的状态。
当初还须要写 ItemCanceled
这个事件,在 cancelListing()
这个函数中会被记录。然而咱们还须要在其余的函数中应用很多其余事件,就间接当初把它们全副实现吧。
其余的事件
看一下咱们的操作和函数列表,就晓得咱们接下来须要写 ItemCanceled
和 ItemBought
这两个事件。
所以,在 ItemListed
事件上面,咱们插入以下代码:
event ItemCanceled(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId
);
event ItemBought(
address indexed buyer,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
你会看到这两个事件惟一的区别就是一个用来记录卖方,而另一个用来记录买方,非常简单。
当初,让咱们实现交易所剩下的操作。
实现 BuyItem()
这个函数是交易所的最重要的。它间接解决领取 – 意味着真正去交易 NFT 和一些其余的电子资产。在这个例子中,咱们将会让交易所接管最小单位是 wei 的以太币。这也是咱们须要重入攻打爱护的中央,咱们之前探讨过这个爱护是为了避免歹意账户提空所有的代币。
基于所有要求,咱们须要保障以下:
- BuyItem() 函数能够被外币调用,承受领取,避免重入攻打。
- 收到的领取要被退出到卖家的状态中。
- 在交易当前,上架物要被删除。
- NFT 须要被转移给购买者。
- 事件要被正确地记录。
留神这里有一个重要的假如,卖家没有勾销交易所可能操作其 NFT 的受权。还记得咱们在 ListItem()
函数中进行了查看,然而在上架和销售之间的这段事件,卖家有可能会扭转勾销受权。
所以,通过了上述思考,函数代码如下:
function buyItem(address nftAddress, uint256 tokenId)
external
payable
isListed(nftAddress, tokenId)
nonReentrant
{Listing memory listedItem = s_listings[nftAddress][tokenId];
if (msg.value < listedItem.price) {revert PriceNotMet(nftAddress, tokenId, listedItem.price);
}
s_proceeds[listedItem.seller] += msg.value;
delete (s_listings[nftAddress][tokenId]);
IERC721(nftAddress).safeTransferFrom(listedItem.seller, msg.sender, tokenId);
emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price);
}
以上述的代码中,咱们在 s_proceeds
中更新了卖家的余额。这个余额记录了卖家通过销售 NFT 收到的以太币的总量。而后咱们调用了被上架的 NFT 的合约把转移所有权转移给买家(msg.sender
就是在调用这个函数的买家)。然而咱们没有把他们的支出发送给他们,这是因为咱们后续还会有一个 withdrawProceeds
函数。这个模式“被动提出”而不是“被动发送”这些支出,这篇文章记录了这个设计理念。实际上,让卖家被动取出资金比咱们的交易所合约将资金发给他要更加平安,因为被动发送会导致一些咱们的智能合约无法控制的执行谬误。更好的形式是让卖家本人有转移销售收入的权力,也承当它的责任,咱们的智能合约只负责更新销售收入的余额。
实现 updateListing()
这个函数容许卖家能够更新他们上架物的价格,只须要实现下列操作:
- 查看这个 item 是否曾经上架并且合约调用者是都领有这个 NFT。
- 查看这个新的价格是否是非零数值。
- 进攻重入攻打。
- 更新
s_listing mapping
的状态,Listing
数据指向更新后的价格。 - 记录正确的事件。
代码如下
function updateListing(
address nftAddress,
uint256 tokenId,
uint256 newPrice
)
external
isListed(nftAddress, tokenId)
nonReentrant
isOwner(nftAddress, tokenId, msg.sender)
{if (newPrice == 0) {revert PriceMustBeAboveZero();
}
s_listings[nftAddress][tokenId].price = newPrice;
emit ItemListed(msg.sender, nftAddress, tokenId, newPrice);
}
实现 withdrawProceeds()
正如之前探讨的,因为在实现 BuyItem() 的时候,应用“被动提出”的函数,所以提走销售收入这个操作做的是将合约调用者的余额发给他,不管他在 s_proceeds
中的状态变量是多少。如果这个调用者没有任何的销售收入,咱们就会 revert 交易并且返回自定义谬误 NoProceeds()
。在咱们简略的交易所合约中,如果状态变量正确更新,这个形式就是可行的。当然更重要的是,在发送实现后,咱们须要将卖家的销售收入余额清零。
function withdrawProceeds() external {uint256 proceeds = s_proceeds[msg.sender];
if (proceeds <= 0) {revert NoProceeds();
}
s_proceeds[msg.sender] = 0;
(bool success,) = payable(msg.sender).call{value: proceeds}("");
require(success, "Transfer failed");
}
在上述函数中,有一个比拟特地的申明:payable(msg.sender).call{value: proceeds}("");
。这个是新版本的 Solidity 给调用者发送代币的形式。这里的 value 指的是被发送的以太币的数量,奇怪的 (“”)
其实是在说这个 Solidity 的 call()
函数在调用的时候没有参数的,被传入的是一个空字符串。
这个 .call()
函数返回两个值,一个布尔型的返回值示意交易胜利与否,另一个返回值 – 咱们不须要应用因而也不把它赋值给任何变量。
实现 Utility Getter 函数
当初咱们简直曾经实现咱们的 NFT 交易所!咱们只须要再减少两个性能型函数。一个性能型函数会帮忙咱们通过一个 token ID 取得 Listing
对象,咱们能够卖家是谁,上架的时候价格是多少。第二个性能型函数帮忙咱们取得一个卖家的赚了多少(比方,他们的销售收入数据),这两个函数是对于咱们存储在状态变量中的特定数值的“getter”函数。
function getListing(address nftAddress, uint256 tokenId)
external
view
returns (Listing memory)
{return s_listings[nftAddress][tokenId];
}
function getProceeds(address seller) external view returns (uint256) {return s_proceeds[seller];
}
有了这两个办法,咱们的 NFT 交易所就实现了!当初咱们须要做的是写一些脚本部署到 Hardhat 本地区块链并且上架一些 NFT。
在咱们持续之前,让咱们疾速编译以下查看有没有出错。在你的命令行,在我的项目根目录下,运行 yarn hardhat compile
。如果运行胜利,那么祝贺你。如果没有运行胜利,请查看错误信息,从新看一遍你的流程,Debug 也是开发过程中重要的一部分!
NFT 样例合约
然而在咱们写脚本之前,咱们须要一个 NFT 样例合约,有了它咱们能力铸造 NFT 而后在咱们的交易所中上架。咱们会应用 ERC721 规范,所以咱们能够继承 OpenZeppelin 的 ERC721 库。在代码仓库,有两个 NFT 样例合约,门路是 <root>/contracts/test
。它们不须要在 test 文件夹下,而是应该在 contracts
文件夹下。
让咱们开发 BasicNft.sol
,这个合约会指向一个存在 IPFS 上狗的图片,这是咱们的 NFT 的艺术局部。这个 NFT 合约十分的简略。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract BasicNft is ERC721 {
string public constant TOKEN_URI =
"ipfs://bafybeig37ioir76s7mg5oobetncojcm3c3hxasyd4rvid4jqhy4gkaheg4/?filename=0-PUG.json";
uint256 private s_tokenCounter;
event DogMinted(uint256 indexed tokenId);
constructor() ERC721("Dogie", "DOG") {s_tokenCounter = 0;}
function mintNft() public {_safeMint(msg.sender, s_tokenCounter);
emit DogMinted(s_tokenCounter);
s_tokenCounter = s_tokenCounter + 1;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
return TOKEN_URI;
}
function getTokenCounter() public view returns (uint256) {return s_tokenCounter;}
}
tokenURI()
和 getTokenCounter()
是状态变量的 getter 函数。真正的逻辑在 mintNft()
这个函数中,这个函数是咱们从 OpenZeppelin 的 ERC721 根底合约继承的。mintNft()
铸造了 NFT 并且将 msg.sender
(函数的调用者)注册为 NFT 的拥有者,同时将 Token ID 作为第二个参数传入。咱们正在应用 s_tokenCounter
这个状态变量来追踪有多少 NFT 被铸造进去以及它们的 Token ID。所以咱们在每次记录 DogMinted
事件的之后都须要给 counter 减少 1。
如果想要在交易所中有超过一种的 NFT 的话(这部分是可选的),你能够从这里复制第二个 NFT 合约,名字叫 BasicNftTwo.sol
。你会看到这个合约和 BasicNft.sol
十分像,只是 TOKEN_URI 这个状态变量指向来不同的 IPFS 文件(另一个种类的狗)。留神这两个合约都在<<root>>/contracts/test
。
再次运行 yarn hardhat compile
,看看是否所有的货色都能够失常编译,另外再查看下你的 NFT 交易所合约和 NFT 合约在编译中没有报错。
部署脚本
当初,能够开始写部署脚本了。
首先了解什么是部署脚本。部署脚本将 Solidity 编译成 bytecode,bytcode 能够被部署在 EVM 兼容的区块链上被运行。智能合约就是存储在区块链上的一些能够被执行的数据,它们存储形式是 bytecode。编译中还会产生一个 JSON 文件,这个 JSON 文件蕴含了合约的 metadata,metadata 蕴含了很多有用的信息,比方 ABI(application binary interface),ABI 是咱们和智能合约交互的接口。
所以部署脚本编译了智能合约,并且将它们部署在区块链上。能够参考 Hardhat 文档中合约部署局部以理解原理。在这里咱们应用一个叫做 hardhat-deploy
的 NPM 包,它会帮忙咱们自动化生成这些步骤。如果咱们减少一个 deploy
的 Hardhat task 并且把它增加进 hardhat 的注册工作中,部署脚本还会主动运行咱们存储在一个叫 deploy 文件夹中的脚本,所以咱们须要做的就是通过 yarn hardhat deploy
运行咱们所有的脚本。
所以,先创立 deploy 文件夹,这个文件夹在文件构造中和 contracts
是同一级,比方:<<root>>/deploy
。
部署 NftMarketplace.sol
你能够看下述代码,然而请确保你的 Hardhat 配置文件(hardhat.config.js)曾经引入了这个代码仓库中有的合约,Hardhat 在部署脚本中退出了很多对象。在 deploy 文件夹中,创立一个 01-deploy-nft-marketplace.js
脚本。咱们应用 01-
前缀来给咱们的脚本编号,这样它们就能够被 hardhat-deploy 按程序执行。
const {network} = require("hardhat")
const {developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS} = require("../helper-hardhat-config")
const {verify} = require("../utils/verify")
module.exports = async ({getNamedAccounts, deployments}) => {const { deploy, log} = deployments
const {deployer} = await getNamedAccounts()
const waitBlockConfirmations = developmentChains.includes(network.name)
? 1
: VERIFICATION_BLOCK_CONFIRMATIONS
log("----------------------------------------------------")
const arguments = []
const nftMarketplace = await deploy("NftMarketplace", {
from: deployer,
args: arguments,
log: true,
waitConfirmations: waitBlockConfirmations,
})
// Verify the deployment
if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {log("Verifying...")
await verify(nftMarketplace.address, arguments)
}
log("----------------------------------------------------")
}
module.exports.tags = ["all", "nftmarketplace"]
所以,这个代码在做什么?首先,引入了咱们须要的相干的对象和 item,我会稍后解释 helper-hardhat-config 和 utils/verify 的援用。
而后咱们能够导出一个异步函数,这个函数的参数是一个对象。这个参数是 HRE(Hardhat runtime environment:Hardhat 运行时环境),HRE 是一个咱们所需的开发工具的汇合。HRE 还蕴含一些插件,包含咱们之前探讨的 hard-deploy 插件。
这个异步函数做了下述事件:
- 通过性能函数部署合约,而后在控制台打印出日志信息。
- 找到 hardhat.config.js 文件的 namedAccounts 属性,获取到合约部署人的 index。这个 index 默认是 0,代表着 deployer。这是一个用于部署合约的 Hardhat 钱包(即账户,这个测试账户是由 Hardhat 提供的)。请在这里和这里查看这些技术细节。
通过应用 HRE 中的 deploy()
函数,咱们能够在 Hardhat 的本地开发链上部署咱们的合约,而后传入配置信息,提供合约部署人的地址,合约构造函数的参数和其余的一些信息。
你还能够看到咱们引入并且应用了 developmentChains。它能够依据不同的配置来应用不同的环境,在这个脚本中的逻辑是判断咱们是否应用了开发链,体现在代码中就是 hardhat 还是 localhost。你能够在 hardhat.config.js
文件中看到这些网络。
在咱们的我的项目根目录下,咱们要创立一个叫做 helper-hardhat-config.js 的文件,而后从这里导出两个数据:
const developmentChains = ["hardhat", "localhost"]
const VERIFICATION_BLOCK_CONFIRMATIONS = 6
module.exports = {developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS}
VERIFICATION_BLOCK_CONFIRMATIONS
指的是在确认一个交易(也包含创立一个新的智能合约)上链之前,后面要有多少个区块。
在 01 脚本中的开发链配置中,只须要期待一个区块就能够确认交易胜利。如果不应用 Hardhat 提供的在本地链的话(比如说你在应用测试网或者主网),咱们只能通过 Etherscan 查看交易状况,这时须要用到 Etherscan API key(Infura 有收费版本,然而这个教程中不须要,因为用的是 Hardhat 在本地的链)。想要理解 verify()
的性能是怎么实现的话,请查看 Hardhat 文档。
只管咱们不须要在本地网络中验证,然而咱们还是会把这个参数留在脚本中所以咱们能够晓得有它。咱们引入了一个 verify() 性能函数,咱们须要在目录中创立它,让代码去编译。咱们先创立 <<root>>/utils
文件夹,而后把 verify.js
文件放在外面。你能够在这里复制这部分代码。
这段脚本最外围的局部是将 NftMarketplace.sol
智能合约部署到 Hardhat 本地开发网络上。如果你运行 yarn hardhat
,你会在其中看到 deploy 工作。如果没有,你须要查看你的 hardhat.config.js
文件和 deploy 文件夹,确保他们和这篇文章中和代码仓库中的统一。
图中是“deploy”工作在命令行中正确显示
而后通过命令 yarn hardhat deploy
运行这个工作。如果你遇到“Unrecognized task deploy”谬误,那就是 hardhat-deploy 的配置文件是不正确的 – 部署工作没有像上图一样,在可用工作列表中显示进去。
如果所有都失常的话,那就会有信息说你曾经编译和部署了交易所合约,同时有一个交易哈希和已部署智能合约的以太坊地址。
图中是 NftMarketplace.sol 胜利部署
有 14 个 Solidity 被编译的起因是咱们所依赖的 OpenZeppein 库也须要被编译。
部署 NFT 合约
接下来,咱们创立 02-deploy-basic-nft.js
脚本,这个脚本将 NFT 部署到咱们的本地开发链下面。你会留神到这个脚本很眼生,因为函数名和执行逻辑都和交易所合约的脚本一样。最大的不同正如脚本名字所示意,即咱们正在部署的是 NFT 样例合约而不是 NFT 交易所合约。
const {network} = require("hardhat")
const {developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS} = require("../helper-hardhat-config")
const {verify} = require("../utils/verify")
module.exports = async ({getNamedAccounts, deployments}) => {const { deploy, log} = deployments
const {deployer} = await getNamedAccounts()
const waitBlockConfirmations = developmentChains.includes(network.name)
? 1
: VERIFICATION_BLOCK_CONFIRMATIONS
log("----------------------------------------------------")
const args = []
const basicNft = await deploy("BasicNft", {
from: deployer,
args: args,
log: true,
waitConfirmations: waitBlockConfirmations,
})
const basicNftTwo = await deploy("BasicNftTwo", {
from: deployer,
args: args,
log: true,
waitConfirmations: waitBlockConfirmations,
})
// Verify the deployment
if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {log("Verifying...")
await verify(basicNft.address, args)
await verify(basicNftTwo.address, args)
}
log("----------------------------------------------------")
}
module.exports.tags = ["all", "basicnft"]
和之前一样,运行 yarn hardhat deploy
命令来部署所有的合约。
胜利运行“deploy”工作编译和部署所有智能合约
在 <<root>>/deploy
中还有第三个脚本。当初还用不到,这个脚本是用来将 ABI JSON 文件(在编译当前,这些 JSON 文件会被存储到 <<root>>/artifacts/contracts/…
)复制到另一个文件夹的,这个文件夹有前端网页 app 做 UI 展现让咱们能够与合约交互。这部分超出来这篇文章的范畴,所以咱们不须要创立这个脚本。你的 deploy 文件夹应该只蕴含咱们在这边文章中提到的两个脚本。
独自的 RPC 网络
在探讨交互脚本之前,先总结一下。咱们曾经写了两个 NFT 合约和一个 NFT 交易所合约,并且曾经用部署脚本在 Hardhat 本地开发链上部署来这些合约。然而当初咱们要怎么与这些合约交互,来查看交易所合约是有在文章开始所探讨的性能呢?
这里就要提到交互脚本来。交互脚本也是用 JavaScript 语言编写,与部署脚本很类似,然而他们的目标是可编程地与咱们曾经部署的智能合约交互。
为了可能和曾经部署的合约交互,咱们须要应用一个与 Hardhat 所提供的本地开发链略微不同的网络。这次咱们要应用的网络叫做 localhost,这个配置信息在 hardhat.config.js
文件中。咱们叫这个“独立的网络(standalone network)”(之前咱们运行的“过程中(in process)”的网络)。你能够在这个 Hardhat 文档中理解这些内容。这个 Hardhat 独立网络通过 localhost IP 地址 127.0.0.1 和端口 8545 来提供 JSON RPC 接口。
在你的控制台,输出 yarn hardhat
而后查看“AVAILABLE TASKS(可用的工作)”下的内容。你会看到有一个叫做 node 的工作,这个工作就是用来“在 Hardhat 的 EVM 之上启动一个 JSON-RPC 服务器”。这个 JSON-PRC 服务器就是独立的 localhost 开发链。
输出命令 yarn hardhat node
,你会失去多个控制台输入的后果,往下滑动你肯看到像下述上面的信息:
图中是启动独立的 JSON-RPC 本地开发链
node 工作会编译咱们的智能合约,并且把它们部署部署到独立的开发链。同时它也显示它“在 http://127.0.0.1:8545/ 启动来 HTTP 和 WebSocket JSON-RPC 服务器”。
在这个信息上面,它输入来很多钱包地址和它们的私钥。这些是以太坊开发账户,永远不要应用它们去发送实在的交易!
有一个独立的 RPC 接口的益处是,能够将前端的利用和开发网络以及下面的智能合约相连接。咱们甚至能够将 Metamask 这样的钱包利用连贯上来。
当初咱们了解了独立网络是做什么的,ctrl+c
敞开这个网络。每次更新智能合约,咱们都须要重新部署。如果智能合约出了问题,也须要敞开和重启本地的开发链以重置合约的数据。
交互脚本
为了和咱们的智能合约交互,咱们须要做以下事件:
- 通过部署脚本部署合约(index 为 0 的 Hardhat 测试账户)。
- 通过拥有者的账户,铸造并且上架 3 个 NFT(index 为 1 的 Hardhat 测试账户)。
- 通过卖家账户购买编号为 0 的 NFT(index 为 2 的 Hardhat 测试账户)。
- 更新编号为 1 的 NFT 的价格并且查看上架价格是否变动。
- 下架编号为 2 的 NFT 并且查看它是否下架。
- 在编号为 0 的 NFT 卖出当前,查看交易所是否正确地记录了拥有者 / 卖家的支出。
这些步骤笼罩了咱们交易所合约的次要操作。这篇文章中,这部分代码和代码仓库中有一些不同,然而在这个 gist 的对应文件中,你能够找到所有的脚本代码。
铸造并且上架 3 个
为了可能可编程地与已部署的合约交互,咱们须要应用 hardhat-ethers plugin 插件。这个插件蕴含了 ehter.js 库,能够提供 EVM 链的很多有用的 API。
在 NFT 铸造当前,所有权能够被拥有者或者非拥有者转移,你要分明这里的差别。在文章开始,NFT 的拥有者能够受权给一个以太坊地址,这个被受权的以太坊地址能够执行这个 NFT 相干的操作,比方转移。这就意味着在某个用户铸造一个 NFT 当前,它们须要“受权”给交易所合约,这样交易所合约能够在 buyItem()
函数被调用的时候转移 NFT 的所有权。你能够在这里理解对于 ERC721 的 approve()
函数。
留神在上面的代码中,所有者铸造了一个 NFT,而后受权给交易所合约能够代替所有者操作这个 NFT,而后这个拥有者将这个 NFT 上架。要在代码上实现这个逻辑,首先在我的项目目录中,创立一个新的文件夹 <<root>>/scripts
。而后创立一个新的文件 mint-and-list-item.js
,将下述代复制到这个新的文件中。
const {ethers} = require("hardhat")
const PRICE = ethers.utils.parseEther("0.1")
async function mintAndList() {const accounts = await ethers.getSigners()
const [deployer, owner, buyer1] = accounts
const IDENTITIES = {[deployer.address]: "DEPLOYER",
[owner.address]: "OWNER",
[buyer1.address]: "BUYER_1",
}
const nftMarketplaceContract = await ethers.getContract("NftMarketplace")
const basicNftContract = await ethers.getContract("BasicNft")
console.log(Minting NFT for ${owner.address})
const mintTx = await basicNftContract.connect(owner).mintNft()
const mintTxReceipt = await mintTx.wait(1)
const tokenId = mintTxReceipt.events[0].args.tokenId
console.log("Approving Marketplace as operator of NFT...")
const approvalTx = await basicNftContract
.connect(owner)
.approve(nftMarketplaceContract.address, tokenId)
await approvalTx.wait(1)
console.log("Listing NFT...")
const tx = await nftMarketplaceContract
.connect(owner)
.listItem(basicNftContract.address, tokenId, PRICE)
await tx.wait(1)
console.log("NFT Listed with token ID:", tokenId.toString())
const mintedBy = await basicNftContract.ownerOf(tokenId)
console.log(NFT with ID ${tokenId} minted and listed by owner ${mintedBy}
with identity ${IDENTITIES[mintedBy]}.
)
}
mintAndList()
.then(() => process.exit(0))
.catch((error) => {console.error(error)
process.exit(1)
})
让咱们合成学习这段代码,因为在剩下的脚本中也应用同样的模式。
首先,留神异步函数 mintAndList() 在底部被调用了,如果有谬误,咱们会把谬误在控制台打印进去而后退出,同时给出非正常退出的谬误。
在函数体,咱们能够通过函数 getSigners() 失去有 20 个 Hardhat 账户的列表,而后咱们应用 JS 解构赋值取得前三个地址对象而后给它们变量名 – deployer 是部署智能合约的地址,owner 铸造和领有 NFT,buyer1 会在下一个脚本中购买 NFT。
咱们还创立了一个 IDENTITIES 将地址映射到它们的角色,它可能让咱们在控制台 debug 的时候更不便。
而后咱们进入到合约中,应用铸造,受权和上架操作。咱们能够确认 mintedBy 是从交易所智能合约中读到的并且这个地址与 owner 统一。上架的时候,咱们传入 PRICE 这个常量,这个常量是以 wei 为单位计量的 0.1 个以太币。你会留神到咱们应用了 ehter.js 性能函数 parseEther() 来做这个转换(文档)。
在咱们运行这个脚本之前,咱们须要筹备好 Hardhat RPC 本地测试网。咱们须要两个关上的终端来做这件事。在第一个终端中,输出 yarn hardhat node,咱们后面所探讨的 standalone RPC 网络会启动。
在第二个终端窗口中,输出 yarn hardhat run scripts/mint-and-list-item.js --network localhost
。如果正确执行的话,你会看到像上面一样的后果:
胜利铸造和上架你的 NFT
如果你想晓得本地 RPC 服务器怎么记录你的交易,能够切换到第一个终端窗口那里查看输入。
有了编号为 0 的 NFT 当前,咱们须要再铸造 2 个。运行同样的 mint-and-list-item.js 脚本两次,token ID 的编号会到 2.
购买一个 NFT
从咱们下面的脚本中,你应该记得接下来咱们想要应用其中一个账号去购买 token ID 为 0 的 NFT。这里第二个脚本的构造和 mint-and-list 一样,然而咱们须要脚本中的逻辑。像上面代码一样,在 scripts 文件夹中,创立一个新的 buy-item.js
脚本:
const {ethers} = require("hardhat")
const TOKEN_ID = 0 // SET THIS BEFORE RUNNING SCRIPT
async function buyItem() {const accounts = await ethers.getSigners()
const [deployer, owner, buyer1] = accounts
const IDENTITIES = {[deployer.address]: "DEPLOYER",
[owner.address]: "OWNER",
[buyer1.address]: "BUYER_1",
}
const nftMarketplaceContract = await ethers.getContract("NftMarketplace")
const basicNftContract = await ethers.getContract("BasicNft")
const listing = await nftMarketplaceContract
.getListing(basicNftContract.address, TOKEN_ID)
const price = listing.price.toString()
const tx = await nftMarketplaceContract
.connect(buyer1)
.buyItem(basicNftContract.address, TOKEN_ID, {value: price,})
await tx.wait(1)
console.log("NFT Bought!")
const newOwner = await basicNftContract.ownerOf(TOKEN_ID)
console.log(New owner of Token ID ${TOKEN_ID} is ${newOwner} with identity of
${IDENTITIES[newOwner]}
)
}
buyItem()
.then(() => process.exit(0))
.catch((error) => {console.error(error)
process.exit(1)
})
这个脚本和之前的简直一样。最大的不同是在这个脚本中,咱们应用了 ehter.js 的 .connect(signer)
函数去给另一个账户发送交易。在这个场景下,咱们想要确定 buyer1 是交易所合约中的函数 buyItem() 的调用者。同时,咱们应用 getListing() 函数读取 Listing 的数据来取得价格,因为咱们在 buyItem() 中须要晓得要领取多少钱。最终,咱们间接读取 NFT 合约的状态变量,确定所有者是否变成了 buyer。
运行命令 yarn hardhat run scripts/buy-item --network localhost
,而后你应该看到以下后果:
图中胜利购买一个上架的 NFT
更新 NFT 的价格
当初咱们曾经卖出了 Token 0,它在交易所中曾经下架了。下一步就是更新 Token ID 1 的价格,token 1 在开始的时候就被铸造和上架了。模式在这里是反复的,所以创立 update-listing
脚本,而后从这个 gist 复制代码。如果你运行 yarn hardhat run scripts/update-listing --network localhost
,而后有设置了正确的 TOKEN_ID,那么你就会看到上面的日志信息,示意价格曾经被更新了:
图中是胜利更新了 NFT 的价格
最初两个操作
只剩下两个脚本了 — 一个是下架 NFT,另一个是让拥有者 / 卖家晓得他们卖掉的 NFT 的支出。和方才一样,在 gist 中复制代码,在运行命令 scripts/cancel-item.js
之后,你会在控制台看到如下的日志信息:
图中是胜利下架一个 NFT
运行 get-seller-proceeds.js
命令,查看 NFT 的所有者在卖出他们的 NFT 当前取得了多少支出,运行后果应该如下所示:
图中查看存储在交易所智能合约中的卖家支出
留神卖家的支出存储在交易所合约的 mapping s_proceeds
中,是以 wei 为单位的。咱们能够应用 ether.js 的性能函数 .formatEther()(文档)来将他转为可读的以太币数量。
总结
不论什么时候,让代码能够自动化测试都是个好习惯。你能够查看这个代码仓库,这里有一系列的测试,这些测试应用 Hardhat 工具,Hardhat 工具借助了 Mocha 和 Waffle 的性能。这些测试还查看了 revert(),函数修饰符和异样解决。这些测试十分有用,值得理解和学习,所以去查看一下它们吧。