本文介绍了如何使用Hardhat在Polygon Mumbai测试网络上创建和部署NFT市场智能合约。内容涵盖了项目设置、智能合约的编写(包括创建Listing和购买Listing功能)、合约测试、部署到Mumbai测试网,以及使用Ethers.js与合约交互的步骤,最后还介绍了如何在Polygonscan上验证合约。
一个 Non-Fungible Token (NFT) 市场是一个用户购买和出售称为 NFT 的独特数字资产的平台。这些数字资产可以代表各种事物,例如收藏品、数字艺术、游戏内物品等等。本指南将教你如何使用 Hardhat 在 Polygon Mumbai 测试网络上创建和部署 NFT 市场智能合约。你还将学习如何使用 Ethers.js 测试和交互你部署的市场合约。让我们开始吧!
我们将做什么
使用 Hardhat 在 Polygon Mumbai 测试网上创建和部署一个 NFT 市场。
创建一个 NFT(使用 ERC-721 标准),我们将在 NFT 市场上使用它。
使用 Ethers.js 与 NFT 市场智能合约交互。
你需要什么
要为我们的智能合约设置 Hardhat 环境,请运行以下终端命令集:
mkdir marketplace-hardhat
cd marketplace-hardhat
npm install --save-dev hardhat
npx hardhat
系统将提示你在终端中选择项目类型。选择以下默认配置:
What do you want to do? · Create a JavaScript project
Hardhat project root: · /Users/User/*/marketplace-hardhat
Do you want to add a .gitignore? (Y/n) · y
Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
然后,运行以下命令来安装依赖项,例如 OpenZeppelin 库、一个用于在 Hardhat 上验证智能合约的插件和用于保护我们私有数据的 dotenv 库:
npm install @openzeppelin/contracts dotenv ethers@5.7
npm install --save-dev @nomiclabs/hardhat-etherscan
有关 Hardhat 的更多信息,请查看此 QuickNode 指南。
要部署我们的 NFT 市场合约并与之交互,我们需要一个连接到 Polygon Mumbai 测试网的完整节点。你可以通过查看 Polygon 文档上的 节点 选项卡来运行你自己的节点。但是,这有时可能难以管理,并且可能不如我们希望的那样优化。相反,你可以轻松地 在此处 设置一个免费的 QuickNode 帐户,并可以访问 20 多个区块链。QuickNode 的基础设施针对延迟和冗余进行了优化,使其比竞争对手快 8 倍。你可以使用 QuickNode 比较工具 来针对 QuickNodes 节点对不同的 RPC 进行基准测试。
单击 创建节点 按钮,然后选择 Polygon 链,Mumbai 测试网 网络。然后,一旦你的节点准备好,请记住 HTTP Provider URL,因为在设置环境变量时需要它。
你还需要 Mumbai 测试网上的一些 MATIC 代币来支付交易费用。你可以在 QuickNode 水龙头 或 Polygon Mumbai 水龙头 获取一些。
在进入实际代码之前,让我们首先了解我们的 NFT 市场合约应该包含哪些功能。它应该能够做到以下几点:
存储已上架 NFT 的详细信息,例如代币 ID、代币地址、NFT 类型(ERC-721 或 ERC-1155)、价格和卖家的地址。
允许用户在市场上出售 NFT(通过 createListing 函数)
允许用户购买市场上出售的 NFT(通过 buyNFT 函数)
促进买方和卖方之间的 NFT 转移(通过市场合约作为中介)
允许用户查看他们已上架和购买的 NFT(通过公共 getMyListedNFTs 和 getMarketItem 函数)
现在我们知道我们的 NFT 市场合约将如何工作,让我们开始创建市场合约。
导入依赖项并声明合约
转到你的 contracts 文件夹中的 marketplace-hardhat 文件夹,然后运行以下命令以创建所需的 solidity 文件:
echo > marketplace.sol
echo > NFT.sol
然后,打开 marketplace.sol 文件并输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Marketplace is ReentrancyGuard, Ownable {
我们将按部分查看代码以充分理解每个部分。如果你不想继续,请随时跳到本节末尾查看完整代码。
我们 solidity 文件的第一行是许可证标识符。然后,在第二行中,我们定义了我们想要编译的版本 pragma。^0.8.9 意味着我们可以在 solidity 版本 0.8.9 及更高版本上编译代码。
接下来,我们导入我们将继承和使用的所有合约。我们的合约名称将是 Marketplace,它将继承其他合约,例如 ReentrancyGuard 和 Ownable。
为 NFT 市场创建状态
现在,将以下代码粘贴到你之前复制到 marketplace.sol 中的代码下:
using Counters for Counters.Counter;
Counters.Counter private marketplaceIds;
Counters.Counter private totalMarketplaceItemsSold;
mapping(uint => Listing) private marketplaceIdToListingItem;
struct Listing {
uint marketplaceId;
address nftAddress;
uint tokenId;
address payable seller;
address payable owner;
uint listPrice;
}
event ListingCreated(
uint indexed marketplaceId,
address indexed nftAddress,
uint indexed tokenId,
address seller,
address owner,
uint listPrice
);
在上面的代码中,我们在 Counters.Counter 上调用 using 关键字,以将 Counters 库分配给该变量。我们还创建了两个私有函数 marketplaceIds 和 totalMarketplaceItemsSold,它们将跟踪 ID 和市场上出售的 NFT 总数。
该合约还声明了一个映射 marketplaceIdToListingItem,该映射将 uint 映射到一个名为 Listing 的结构。此结构将保存数据,例如 marketplaceId、nftAddress、tokenId、seller、owner 和 listPrice。
每当用户在市场上列出 NFT 时,都会发出事件 ListingCreated。此事件对于实时和历史列表可能很有用。
将 NFT 上架到市场
我们的市场还需要上架 NFT 的逻辑。将以下代码粘贴到你的 marketplace.sol 文件的末尾:
function createListing(
uint tokenId,
address nftAddress,
uint price
) public nonReentrant {
require(price > 0, "List price must be 1 wei >=");
marketplaceIds.increment();
uint marketplaceItemId = marketplaceIds.current();
marketplaceIdToListingItem[marketplaceItemId] = Listing(
marketplaceItemId,
nftAddress,
tokenId,
payable(msg.sender),
payable(address(0)),
price
);
IERC721(nftAddress).transferFrom(msg.sender, address(this), tokenId);
emit ListingCreated(
marketplaceItemId,
nftAddress,
tokenId,
msg.sender,
address(0),
price
);
}
让我们回顾一下代码。
createListing 公共函数采用三个参数:tokenId、nftAddress 和 price。我们使用 nonReentrant 修饰符来防止重入,并使用 require 语句来确保列表价格大于一 wei(即 ETH 的最小面额)。该函数的其余逻辑包括递增 marketplaceId,将列表详细信息添加到 Listing 结构,通过 transferFrom 将代币转账到市场,然后发出事件 ListingCreated。市场合约保护市场合约本身中的 NFT。这与将列出的 NFT 保存在你的钱包中不同,因为市场合约无权立即从你的钱包(即与你的私钥关联的帐户)中获取 NFT。
创建购买列表功能
function buyListing(uint marketplaceItemId, address nftAddress)
public
payable
nonReentrant
{
uint price = marketplaceIdToListingItem[marketplaceItemId].listPrice;
require(
msg.value == price,
"Value sent does not meet list price for NFT"
);
uint tokenId = marketplaceIdToListingItem[marketplaceItemId].tokenId;
marketplaceIdToListingItem[marketplaceItemId].seller.transfer(msg.value);
IERC721(nftAddress).transferFrom(address(this), msg.sender, tokenId);
marketplaceIdToListingItem[marketplaceItemId].owner = payable(msg.sender);
totalMarketplaceItemsSold.increment();
}
让我们回顾一下代码。
buyListing 函数是一个公共可支付函数,它接受 marketplaceItemId 和 nftAddress。它还利用 nonReentrant 修饰符来防止重入。该函数的逻辑包括检索价格列表并确保随函数调用一起发送的值符合价格列表。其余逻辑包括将 NFT 转移给买方,更改 marketplaceIdToListingItem 映射中的所有者值,以及递增 totalMarketplaceItemsSold 变量。
为 NFT 市场创建辅助函数
function getMarketItem(uint marketplaceItemId)
public
view
returns (Listing memory)
{
return marketplaceIdToListingItem[marketplaceItemId];
}
function getMyListedNFTs() public view returns (Listing[] memory) {
uint totalListingCount = marketplaceIds.current();
uint listingCount = 0;
uint index = 0;
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
listingCount += 1;
}
}
Listing[] memory items = new Listing[](listingCount);
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
uint currentId = marketplaceIdToListingItem[i + 1].marketplaceId;
Listing memory currentItem = marketplaceIdToListingItem[currentId];
items[index] = currentItem;
index += 1;
}
}
return items;
}
}
让我们回顾一下代码。
这两个函数是辅助函数,将返回市场项目并检索卖家的已上架 NFT。getMyListedNFTs 使用 for 循环 来迭代和返回市场项目。
你的完整市场代码应如下所示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Marketplace is ReentrancyGuard, Ownable {
using Counters for Counters.Counter;
Counters.Counter private marketplaceIds;
Counters.Counter private totalMarketplaceItemsSold;
mapping(uint => Listing) private marketplaceIdToListingItem;
struct Listing {
uint marketplaceId;
address nftAddress;
uint tokenId;
address payable seller;
address payable owner;
uint listPrice;
}
event ListingCreated(
uint indexed marketplaceId,
address indexed nftAddress,
uint indexed tokenId,
address seller,
address owner,
uint listPrice
);
function createListing(
uint tokenId,
address nftAddress,
uint price
) public nonReentrant {
require(price > 0, "List price must be 1 wei >=");
marketplaceIds.increment();
uint marketplaceItemId = marketplaceIds.current();
marketplaceIdToListingItem[marketplaceItemId] = Listing(
marketplaceItemId,
nftAddress,
tokenId,
payable(msg.sender),
payable(address(0)),
price
);
IERC721(nftAddress).transferFrom(msg.sender, address(this), tokenId);
emit ListingCreated(
marketplaceItemId,
nftAddress,
tokenId,
msg.sender,
address(0),
price
);
}
function buyListing(uint marketplaceItemId, address nftAddress)
public
payable
nonReentrant
{
uint price = marketplaceIdToListingItem[marketplaceItemId].listPrice;
require(
msg.value == price,
"Value sent does not meet list price for NFT"
);
uint tokenId = marketplaceIdToListingItem[marketplaceItemId].tokenId;
marketplaceIdToListingItem[marketplaceItemId].seller.transfer(msg.value);
IERC721(nftAddress).transferFrom(address(this), msg.sender, tokenId);
marketplaceIdToListingItem[marketplaceItemId].owner = payable(msg.sender);
totalMarketplaceItemsSold.increment();
}
function getMarketItem(uint marketplaceItemId)
public
view
returns (Listing memory)
{
return marketplaceIdToListingItem[marketplaceItemId];
}
function getMyListedNFTs() public view returns (Listing[] memory) {
uint totalListingCount = marketplaceIds.current();
uint listingCount = 0;
uint index = 0;
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
listingCount += 1;
}
}
Listing[] memory items = new Listing[](listingCount);
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
uint currentId = marketplaceIdToListingItem[i + 1].marketplaceId;
Listing memory currentItem = marketplaceIdToListingItem[currentId];
items[index] = currentItem;
index += 1;
}
}
return items;
}
}
接下来,我们需要创建测试 NFT,以便我们可以与我们的市场合约一起使用。对于我们的示例,我们将创建一个 ERC-721 进行测试。
打开 NFT.sol 文件并输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFT is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("YOUR_NFTS_NAME", "YOUR_NFTS_SYMBOL") {}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
}
让我们回顾一下代码。
上面的代码是从 OpenZeppelin 获得的样板代码。对于此 NFT,我们继承 ERC721URIStorage 和 Ownable 以进行元数据和访问控制。此外,我们还有一个 safeMint 函数、burn 函数和 tokenURI 函数,用于返回代币元数据。
在继续下一节之前,请花点时间通过将 YOUR_NFTS_NAME 和 YOUR_NFTS_SYMBOL 占位符替换为你的实际 NFT 名称和符号来重命名你的 NFT。记住要保存文件!
现在我们已经完成了创建我们的测试 NFT,现在是编译所有内容并确保它按预期工作的时候了。
要编译合约,请运行命令:**npx hardhat compile
**
编译后,你将注意到两个新文件夹 - artifacts 和 cache。Artifacts 是你可以在其中找到智能合约的 ABI 和字节码的地方。稍后在部署智能合约时,你将需要 ABI。
注意:如果你想清除缓存并删除编译的 artifacts,你可以运行 npx hardhat clean 命令。
现在,为了测试我们所有合约的功能,我们将在下一节中使用 Hardhat 的测试功能。
在我们将合约部署到 Mumbai 等测试区块链之前,我们应该在本地环境中测试我们的合约,以确保一切行为符合预期。
转到 marketplace-hardhat 目录中的 test 文件夹,并创建一个名为 **marketplace-test.js 的新文件
**
此测试文件将允许我们执行不同的函数,并查看市场合约是否按预期运行。例如,当有人上架 NFT 时,我们将检查 NFT 是否已从卖家转移到市场等。
将以下代码复制并粘贴到 marketplace-test.js 文件中:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Marketplace", function () {
let acc1, acc2;
let marketplaceAddress;
let nftAddress;
let nft;
let marketplace;
let listPrice = ethers.utils.parseEther("0.01", "ether");
beforeEach(async function () {
[acc1, acc2] = await ethers.getSigners();
const Marketplace = await ethers.getContractFactory("Marketplace");
nftMarketplace = await Marketplace.deploy();
await nftMarketplace.deployed();
marketplaceAddress = nftMarketplace.address;
const NFT = await ethers.getContractFactory("NFT");
nft = await NFT.deploy();
await nft.deployed();
nftAddress = nft.address.toString();
});
it("Should list an NFT onto the marketplace", async function () {
await nft.safeMint(acc1.address, "META_DATA_URI");
await nft.approve(marketplaceAddress, 0);
await nftMarketplace.createListing(0, nftAddress, listPrice); //0.01 MATIC
});
it("Should sell an active NFT listed on the marketplace ", async function () {
await nft.safeMint(acc1.address, "META_DATA_URI");
await nft.approve(marketplaceAddress, 0);
await nftMarketplace.createListing(0, nftAddress, listPrice); //0.01 MATIC
await expect(
await nftMarketplace
.connect(acc2)
.buyListing(1, nftAddress, { value: listPrice })
)
item = await nftMarketplace.getMarketItem(1);
expect(item.owner).to.equal(acc2.address);
});
it("Test a market sale that does not send sufficient funds", async function () {
await nft.safeMint(acc1.address, "META_DATA_URI");
await nft.approve(marketplaceAddress, 0);
await nftMarketplace.createListing(0, nftAddress, listPrice);
await expect(
nftMarketplace.connect(acc2).buyListing(1, nftAddress, { value: ethers.utils.parseEther("0.02", "ether")})
).to.be.revertedWith(
"Value sent does not meet list price for NFT"
);
item = await nftMarketplace.getMarketItem(1);
expect(item.owner).to.equal("0x0000000000000000000000000000000000000000");
});
});
上面的脚本显示了测试的大致轮廓。但是,请注意,此测试仅涵盖市场中的某些功能。
要运行上面的测试脚本,请运行此终端命令:npx hardhat test test/marketplace-test.js。
测试成功通过后,你应该会看到以下内容:
这意味着你的所有 expect 断言都为真,因此测试成功。
在下一节中,我们将把合约部署到 Polygon Mumbai 测试网区块链上。
到目前为止,你已经成功创建并测试了你的 NFT 市场。现在是时候将你的合约部署到 Polygon 的 Mumbai 测试网了。在我们执行此操作之前,我们需要设置一些依赖项并配置我们的环境变量。
首先,通过在你的 marketplace-hardhat 目录中运行以下终端命令来创建 .env 文件:
echo > .env
然后,打开 .env 文件并添加以下变量:
PRIVATE_KEY_ACCOUNT_1=
PRIVATE_KEY_ACCOUNT_2=
POLYGONSCAN_API_KEY=
RPC_URL=
花点时间从 QuickNode 填写你的 私钥 和 QuickNode HTTP 提供程序 URL。拥有两个帐户的目的是测试市场上的上架和购买功能。此外,在 Polygonscan 上创建一个帐户并检索 API 密钥。你可以通过在 Polygonscan 上单击“我的个人资料”,然后单击“API 密钥”选项卡,然后单击“添加”来生成 API 密钥。
在填充 .env 文件中的所有值后,保存该文件。
我们还需要配置 hardhat.config.js 文件。打开该文件并输入以下代码:
require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
module.exports = {
solidity: "0.8.9",
networks: {
mumbai: {
url: process.env.RPC_URL,
accounts: [process.env.PRIVATE_KEY_ACCOUNT_1],
gas: 2100000,
gasPrice: 8000000000,
},
},
etherscan: {
apiKey: {
polygonMumbai: process.env.POLYGONSCAN_API_KEY
}
},
};
注意:gas 和 gasPrice 被硬编码以防止在高网络活动期间出现无法预测的等待时间。
现在是时候部署智能合约了。我们将通过位于 scripts 文件夹中的脚本来执行此操作。将 scripts/deploy.js 文件中的内容替换为以下代码:
const hre = require("hardhat");
async function main() {
const Marketplace = await hre.ethers.getContractFactory("Marketplace");
const marketplace = await Marketplace.deploy()
await marketplace.deployed();
const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.deploy()
await nft.deployed();
console.log(
`NFT Marketplace deployed to ${marketplace.address} - Block explorer URL: https://mumbai.polygonscan.com/address/${marketplace.address}`);
console.log(
`NFT deployed to ${nft.address} - Block explorer URL: https://mumbai.polygonscan.com/address/${nft.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
然后,要部署合约,请运行以下终端命令:
npx hardhat run --network mumbai scripts/deploy.js
你应该会看到类似于以下输出的内容:
你刚刚将你的市场合约和 NFT 代币部署到 Mumbai 测试网!花点时间通过转到终端中提供的 URL 在 Polygonscan 上验证交易。在下一节中,我们将使用 Javascript 和 Ethers.js 与我们的市场智能合约交互。
随着我们的 NFT 市场合约的部署,我们现在将使用 Ethers.js 与其交互。在你的 scripts 文件夹中,创建一个名为 interact.js 的文件,然后输入以下代码:
const { ethers } = require("hardhat");
const hre = require("hardhat");
require("dotenv").config();
async function main() {
[acc1] = await ethers.getSigners(); //
const acc2 = await new ethers.Wallet(process.env.PRIVATE_KEY_ACCOUNT_2, acc1.provider)
const nConfirm = 10;
const marketplaceId = 1; //this value will need to be modified according to the NFT being listed/sold
//Create instances of the marketplace contract
const Marketplace = await hre.ethers.getContractFactory("Marketplace");
const marketplace = await Marketplace.attach(
"YOUR_MARKETPLACE_CONTRACT_ADDRESS" // The marketplace contract address
);
//Create instances of the NFT contract
const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.attach(
"YOUR_NFT_CONTRACT_ADDRESS" // The nft contract address
);
//Mint an NFT to list on the marketplace
const mintTxn = await nft.safeMint(acc1.address, "YOUR_META_DATA_URI");
console.log("safeMint function call Tx Hash:", mintTxn.hash);
const receipt = await mintTxn.wait([confirms = nConfirm])
let tokenId = parseInt(receipt["logs"][0].topics[3].toString())
//Approve the marketplace address as a spender
const approval = await nft.approve(marketplace.address, tokenId);
console.log("Approval function call Tx Hash:", approval.hash);
approval.wait([confirms = nConfirm]); //wait till the transaction mines
//List the NFT onto the marketplace
const createListing = await marketplace.createListing(tokenId, nft.address, ethers.utils.parseEther("0.01", "ether"));
console.log("createListing function call Tx Hash:", createListing.hash);
createListing.wait([confirms = nConfirm]); //wait till the transaction mines
//Buy the NFT from acc2
const buyNFT = await marketplace.connect(acc2).buyListing(marketplaceId, nft.address, { value: ethers.utils.parseEther("0.01", "ether")});
console.log("Buy NFT Tx Hash:", buyNFT.hash)
buyNFT.wait([confirms = nConfirm]); //wait till the transaction mines
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
重要提示:请记住用你自己的已部署智能合约地址替换上面的代码中的 YOUR_MARKETPLACE_CONTRACT_ADDRESS 和 YOUR_NFT_CONTRACT_ADDRESS 占位符。
此外,如果你想设置你的元数据 URL,请将 YOUR_META_DATA_URI 占位符替换为你的实际元数据 URL。要了解如何设置 NFT 元数据,请查看本 QuickNode 指南 中的“将文件添加到 IPFS”部分。
花几分钟时间查看上面 interact.js 代码中的代码注释。然后,运行以下终端命令以执行 interact.js 脚本:
npx hardhat run scripts/interact.js --network mumbai
你应该会看到类似于以下输出的内容:
**在 Polygonscan 上验证合约源代码
**
让我们花点时间在公共区块浏览器上验证我们合约的源代码。验证后,我们将分析市场和 NFT 合约活动以查看 NFT 销售。
在你的 marketplace-hardhat 文件夹中,为你要验证的每个已部署合约运行以下 hardhat 命令。请记住用你实际的已部署合约地址替换占位符值。
npx hardhat verify --network mumbai <contract_address>
成功执行命令后,你将看到指向你的合约的公开验证代码的链接。导航到 Polygonscan 上你的 NFT 合约的 URL,然后单击 合约 选项卡,然后单击 读取合约 选项卡。
如果你输入你的第二个帐户(购买 NFT 的那个)的地址,你将看到余额为 1。另请确认第一个帐户中的 NFT 余额应为零。
做得好!你已经学会了如何将 NFT 市场部署到 Polygon 的 Mumbai 测试网。尝试向市场合约添加你自己的逻辑(例如,奖励、佣金)以扩展其功能。你还可以编写你自己的测试,以查看合约在不同情况下的反应。
在 Twitter 或 Discord 上展示你的技能。我们很乐意知道你正在构建什么!
如果你对本指南有任何反馈,请 告诉我们!
- 原文链接: quicknode.com/guides/pol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!