如何在以太坊或乐观上构建 NFT 市场 DApp¶
概述¶
今天,我们将构建 Bored Pets Marketplace,一个使用Truffle、Infura、MetaMask和Web3.js的简单 NFT 市场!我们将从基础开始,首先向您展示如何将您的以太坊合约部署到 Truffle 的本地区块链、Ganache 和 Goerli 测试网。然后,我们将向您展示如何将这些合约转换为 Optimism 并部署在 Optimistic Goerli 测试网上。请注意,本教程不会教您 Javascript,只会简要介绍一些 Solidity 原则。
我们的市场具有一组核心功能: 1. 铸造和列出 NFT 2. 买卖 NFT 3. 查看列出的 NFT、您拥有的 NFT 以及您正在出售的 NFT
本教程的灵感来自 Nader Dabit 的市场教程。他的教程使用ethers.js并部署到 Polygon,您都可以使用 Truffle!
本教程的完整代码位于此处。
先决条件¶
系统要求¶
我们将从 Truffle 的Optimism 框开始,搭建代码以帮助您配置 Truffle 项目以尽快开始使用 Optimism!自述文件更详细地解释了项目结构。
至少,您需要安装:
如果你想在本地运行Optimism,你也需要这些先决条件。
创建 Infura 帐户和项目¶
要将您的 DApp 连接到 Ethereum、Optimism 和其他网络,您需要一个 Infura 帐户。在这里注册一个帐户。
登录后,创建一个项目!让我们称之为nft-marketplace。由于我们正在部署到 Optimism,请继续添加 Optimistic Goerli 端点。它会要求您注册免费试用。

注册 MetaMask 钱包¶
要在浏览器中与您的 DApp 交互,您需要一个 MetaMask 钱包。在这里注册一个帐户。
将 Optimistic Goerli 添加到您的钱包¶
让我们将 Optimistic Goerli 添加到您的 MetaMask 钱包的可用网络列表中!为此,请打开 MetaMask 扩展,单击网络,然后单击添加网络。然后,填写网络属性(您可以从您的 Infura 项目中复制您的 Infura idf):
- 网络名称:乐观歌尔里
- 新的 RPC URL:https://optimism-goerli.infura.io/v3/INFURA_PROJECT_ID
- 链号:420
- 货币符号:ETH
- 区块浏览器网址:https://blockscout.com/optimism/goerli/

获取乐观的 Goerli Eth¶
要使用 Optimistic Goerli 测试网,您需要一些测试 eth。为此,您需要:
1) 获取一些 Goerli Eth。你可以使用这个水龙头2) Optimism 的网关仍然不支持 Goerli,但是如果你将 Goerli ETH 转移到0x636Af16bf2f682dD3109e60102b8E1A089FedAa8,你会在 Optimistic Goerli 上得到它。

VSCode¶
随意使用您想要的任何 IDE,但我们强烈建议使用 VSCode!您可以使用 Truffle 扩展来完成本教程的大部分内容,以创建、构建和部署您的智能合约,而无需使用 CLI!你可以在这里阅读更多关于它的信息。
设置您的项目¶
拆箱乐观¶
首先,我们将打开 Truffle 的 Optimism 盒子。您可以通过调用truffle unbox optimism [FOLDER_NAME]或通过 VSCode 扩展命令面板来做到这一点!继续安装必要的软件包。
truffle unbox optimism nft-marketplace
cd nft-marketplace
npm install
创建您的客户文件夹¶
我们还将为我们的客户端使用Next.js和Tailwind CSS。因此,为了进行设置,我们将使用 Next 的create-next-app实用程序。
npx create-next-app@latest client
然后,我们将下载 Tailwind 依赖项并填充其配置。
cd client
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
编辑tailwind.config.js
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
并替换代码styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
编辑 Truffle 配置文件¶
由于我们将在客户端引用您编译的合约,让我们更改contracts_build_directoryTruffle 配置文件:
- 在
truffle-config.js中,将值更改为'./client/contracts/ethereum-contracts'。 - 在
truffle-config.ovm.js中,将值更改为'./client/contracts/optimism-contracts'。
您的顶级文件夹结构应如下所示!
nft-marketplace
├── LICENSE
├── README.md
├── box-img-lg.png
├── box-img-sm.png
├── client
├── contracts
├── migrations
├── node_modules
├── package-lock.json
├── package.json
├── test
├── truffle-config.js
└── truffle-config.ovm.js
构建 NFT 智能合约¶
由于我们首先向您展示如何在以太坊上进行部署,因此我们将SimpleStorage.sol在contracts/ethereum. 将文件名和合同名称更改为BoredPetsNFT.sol.
我们需要安装OpenZeppelin。首先,切换回您的nft-marketplace目录
cd ..
npm install @openzeppelin/contracts
智能合约如下所示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract BoredPetsNFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address marketplaceContract;
event NFTMinted(uint256);
constructor(address _marketplaceContract) ERC721("Bored Pets Yacht Club", "BPYC") {
marketplaceContract = _marketplaceContract;
}
function mint(string memory _tokenURI) public {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, _tokenURI);
setApprovalForAll(marketplaceContract, true);
emit NFTMinted(newTokenId);
}
}
创建 NFT 智能合约既简短又甜蜜!我们先来看看导入:
- @openzeppelin/contracts/token/ERC721/ERC721.sol
要成为有效的 NFT,通过继承实现BoredPetNFTERC721标准ERC721URIStorage.sol
- @openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol
使用此实现,ERC721以便我们将链上的 tokenURI 存储在 中storage,这使我们能够将我们上传到链下 IPFS 的元数据存储起来。
- @openzeppelin/contracts/utils/Counters.sol
我们使用计数器来跟踪 NFT 的总数,并为每个 NFT 分配一个唯一的令牌 ID。
在顶部,我们定义了一些变量:
address marketplaceContract是我们将在下一节中编写的 Marketplace 合约的地址。event NFTMinted每次铸造 NFT 时都会发出。当事件以solidity 发出时,参数将存储在事务的日志中。稍后在构建 Web 应用程序时,我们将需要 tokenId。
最后,我们只需要定义mint函数!它只有一个参数: –string memory _tokenURI指向 IPFS 上存储 NFT 元数据(即图像、名称、描述)的 JSON 元数据
mint相对简单——它铸造了一个具有不断增加的唯一令牌 ID 的 NFT。然而,值得注意的是setApprovalForAll。这很重要,因为我们的市场合约需要批准者访问才能在不同地址之间转移 NFT 的所有权。
建立市场合约¶
Marketplace.sol现在,让我们在下添加一个新合约contracts/ethereum,它将存储所有市场功能。这将是它最终的样子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Marketplace is ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _nftsSold;
Counters.Counter private _nftCount;
uint256 public LISTING_FEE = 0.0001 ether;
address payable private _marketOwner;
mapping(uint256 => NFT) private _idToNFT;
struct NFT {
address nftContract;
uint256 tokenId;
address payable seller;
address payable owner;
uint256 price;
bool listed;
}
event NFTListed(
address nftContract,
uint256 tokenId,
address seller,
address owner,
uint256 price
);
event NFTSold(
address nftContract,
uint256 tokenId,
address seller,
address owner,
uint256 price
);
constructor() {
_marketOwner = payable(msg.sender);
}
// List the NFT on the marketplace
function listNft(address _nftContract, uint256 _tokenId, uint256 _price) public payable nonReentrant {
require(_price > 0, "Price must be at least 1 wei");
require(msg.value == LISTING_FEE, "Not enough ether for listing fee");
IERC721(_nftContract).transferFrom(msg.sender, address(this), _tokenId);
_nftCount.increment();
_idToNFT[_tokenId] = NFT(
_nftContract,
_tokenId,
payable(msg.sender),
payable(address(this)),
_price,
true
);
emit NFTListed(_nftContract, _tokenId, msg.sender, address(this), _price);
}
// Buy an NFT
function buyNft(address _nftContract, uint256 _tokenId) public payable nonReentrant {
NFT storage nft = _idToNFT[_tokenId];
require(msg.value >= nft.price, "Not enough ether to cover asking price");
address payable buyer = payable(msg.sender);
payable(nft.seller).transfer(msg.value);
IERC721(_nftContract).transferFrom(address(this), buyer, nft.tokenId);
_marketOwner.transfer(LISTING_FEE);
nft.owner = buyer;
nft.listed = false;
_nftsSold.increment();
emit NFTSold(_nftContract, nft.tokenId, nft.seller, buyer, msg.value);
}
// Resell an NFT purchased from the marketplace
function resellNft(address _nftContract, uint256 _tokenId, uint256 _price) public payable nonReentrant {
require(_price > 0, "Price must be at least 1 wei");
require(msg.value == LISTING_FEE, "Not enough ether for listing fee");
IERC721(_nftContract).transferFrom(msg.sender, address(this), _tokenId);
NFT storage nft = _idToNFT[_tokenId];
nft.seller = payable(msg.sender);
nft.owner = payable(address(this));
nft.listed = true;
nft.price = _price;
_nftsSold.decrement();
emit NFTListed(_nftContract, _tokenId, msg.sender, address(this), _price);
}
function getListingFee() public view returns (uint256) {
return LISTING_FEE;
}
function getListedNfts() public view returns (NFT[] memory) {
uint256 nftCount = _nftCount.current();
uint256 unsoldNftsCount = nftCount - _nftsSold.current();
NFT[] memory nfts = new NFT[](unsoldNftsCount);
uint nftsIndex = 0;
for (uint i = 0; i < nftCount; i++) {
if (_idToNFT[i + 1].listed) {
nfts[nftsIndex] = _idToNFT[i + 1];
nftsIndex++;
}
}
return nfts;
}
function getMyNfts() public view returns (NFT[] memory) {
uint nftCount = _nftCount.current();
uint myNftCount = 0;
for (uint i = 0; i < nftCount; i++) {
if (_idToNFT[i + 1].owner == msg.sender) {
myNftCount++;
}
}
NFT[] memory nfts = new NFT[](myNftCount);
uint nftsIndex = 0;
for (uint i = 0; i < nftCount; i++) {
if (_idToNFT[i + 1].owner == msg.sender) {
nfts[nftsIndex] = _idToNFT[i + 1];
nftsIndex++;
}
}
return nfts;
}
function getMyListedNfts() public view returns (NFT[] memory) {
uint nftCount = _nftCount.current();
uint myListedNftCount = 0;
for (uint i = 0; i < nftCount; i++) {
if (_idToNFT[i + 1].seller == msg.sender && _idToNFT[i + 1].listed) {
myListedNftCount++;
}
}
NFT[] memory nfts = new NFT[](myListedNftCount);
uint nftsIndex = 0;
for (uint i = 0; i < nftCount; i++) {
if (_idToNFT[i + 1].seller == msg.sender && _idToNFT[i + 1].listed) {
nfts[nftsIndex] = _idToNFT[i + 1];
nftsIndex++;
}
}
return nfts;
}
}
这里有很多东西要解压!您可能会注意到 Marketplace 合约继承了ReentrancyGuard. 我们这样做是为了防御重入攻击。你可以在这里阅读更多关于它们的信息。
首先,让我们深入了解合约变量: –Counters.Counter private _nftsSold当 NFT 出售时增加,当 NFT 重新上市时减少。-Counters.Counter private _nftCount跟踪列出了多少 NFT。-uint256 public LISTING_FEE每当出售 NFT 时,从卖家那里取出并转移给市场合约所有者。-address payable private _marketOwner存储 Marketplace 合同所有者,以便我们知道向谁支付上市费用。-mapping(uint256 => NFT) private _idToNFT将唯一的 tokenId 关联到 NFT 结构。-struct NFT存储市场上列出的 NFT 的相关信息。-event NFTListed每次列出 NFT 时都会发出。-event NFTSold每次出售 NFT 时都会发出。
接下来,我们将讨论会改变状态的函数: –listNft当用户首次铸造并列出他们的 NFT 时调用。它将所有权从用户转移到市场合约。-resellNft允许用户出售他们在市场上购买的 NFT。-buyNft当用户购买 NFT 时调用。买方成为 NFT 的新所有者,代币从买方转移到卖方,上币费交给市场所有者。
最后,查询功能相对简单:
getListingFee返还挂牌费。这完全是可选的。当您部署智能合约时,实际上会为您创建一个 LISTING_FEE() 函数,但我们创建了一个 getter 函数来清洁代码。getListedNfts检索当前挂牌出售的 NFT。getMyNfts检索用户购买的 NFT。getMyListedNfts检索用户列出出售的 NFT。
在本地部署智能合约¶
为了部署我们的智能合约,我们需要修改migrations/1_deploy_contracts.js. 因为BoredPetsNFT需要Marketplace合约地址,所以这里下单很重要!Truffle 允许您使用Promise或await/来按顺序部署合约async。你可以在这里阅读更多关于它的信息。(请注意,以前,Truffle 也需要单独的Migrations.sol合同,但现在不再是这样了!)
您的部署文件应如下所示:
var BoredPetsNFT = artifacts.require("BoredPetsNFT");
var Marketplace = artifacts.require("Marketplace");
module.exports = async function(deployer) {
await deployer.deploy(Marketplace);
const marketplace = await Marketplace.deployed();
await deployer.deploy(BoredPetsNFT, marketplace.address);
}
有多种方法可以启动本地 Ganache 实例:通过 VS Code 扩展、Ganache CLI 和 Ganche 图形用户界面。每个都有自己的优势,您可以在此处查看 v7 最酷的功能。
在本例中,我们将使用 GUI。打开它,创建一个工作区,然后点击保存!

这会在 HTTP://127.0.0.1:7545 创建一个正在运行的 Ganache 实例。现在,只需从文件夹truffle migrate --network development中运行 CLI 。nft-marketplace这将编译和部署你的合约。您应该会看到与此类似的输出:
Compiling your contracts...
===========================
> Compiling ./contracts/ethereum/BoredPetsNFT.sol.sol
> Compiling ./contracts/ethereum/Marketplace.sol
> Compiling @openzeppelin/contracts/security/ReentrancyGuard.sol
> Compiling @openzeppelin/contracts/token/ERC721/ERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol
> Compiling @openzeppelin/contracts/utils/Address.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Compiling @openzeppelin/contracts/utils/Counters.sol
> Compiling @openzeppelin/contracts/utils/Strings.sol
> Compiling @openzeppelin/contracts/utils/introspection/ERC165.sol
> Compiling @openzeppelin/contracts/utils/introspection/IERC165.sol
> Artifacts written to /Users/emilylin/truffle/nft-marketplace/client/contracts/ethereum-contracts
> Compiled successfully using:
- solc: 0.8.13+commit.abaa5c0e.Emscripten.clang
Starting migrations...
======================
> Network name: 'development'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_deploy_contracts.js
=====================
Deploying 'Marketplace'
-----------------------
> transaction hash: 0xca3f5fcc301c700bdfd7bfb58c853e39085335ed0ef249bf57334ad856848383
> Blocks: 0 Seconds: 0
> contract address: 0x6F1A6D8a5414d1B1E67b69D69D5206498447aceE
> block number: 1
> block timestamp: 1652487728
> account: 0x24D126143330E7f76753e13FAE39a844cbcEe829
> balance: 99.96632092
> gas used: 1683954 (0x19b1f2)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.03367908 ETH
Deploying 'BoredPetsNFT'
---------------------------
> transaction hash: 0x077ab3a709addd41229de58f3bb6ad913a5c4646cd5df39272a9db6ad3a04f05
> Blocks: 0 Seconds: 0
> contract address: 0x2935aBf19126137D47bCa1612Dc4900815A15E92
> block number: 2
> block timestamp: 1652487729
> account: 0x24D126143330E7f76753e13FAE39a844cbcEe829
> balance: 99.91495866
> gas used: 2568113 (0x272fb1)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.05136226 ETH
> Saving artifacts
-------------------------------------
> Total cost: 0.08504134 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.08504134 ETH
您应该能够在下面找到您编译的合同./client/contracts/ethereum-contracts
测试你的智能合约¶
为了快速测试我们的智能合约,我们将利用truffle exec运行脚本来自动执行常见任务。让我们编写一个脚本来执行我们所有的不同功能。首先,在一个scripts名为run.js.
var BoredPetsNFT = artifacts.require("BoredPetsNFT");
var Marketplace = artifacts.require("Marketplace");
async function logNftLists(marketplace) {
let listedNfts = await marketplace.getListedNfts.call()
const accountAddress = 'FIRST_ACCOUNT_ADDRESS'
let myNfts = await marketplace.getMyNfts.call({from: accountAddress})
let myListedNfts = await marketplace.getMyListedNfts.call({from: accountAddress})
console.log(`listedNfts: ${listedNfts.length}`)
console.log(`myNfts: ${myNfts.length}`)
console.log(`myListedNfts ${myListedNfts.length}n`)
}
const main = async (cb) => {
try {
const boredPets = await BoredPetsNFT.deployed()
const marketplace = await Marketplace.deployed()
console.log('MINT AND LIST 3 NFTs')
let listingFee = await marketplace.getListingFee()
listingFee = listingFee.toString()
let txn1 = await boredPets.mint("URI1")
let tokenId1 = txn1.logs[2].args[0].toNumber()
await marketplace.listNft(boredPets.address, tokenId1, 1, {value: listingFee})
console.log(`Minted and listed ${tokenId1}`)
let txn2 = await boredPets.mint("URI1")
let tokenId2 = txn2.logs[2].args[0].toNumber()
await marketplace.listNft(boredPets.address, tokenId2, 1, {value: listingFee})
console.log(`Minted and listed ${tokenId2}`)
let txn3 = await boredPets.mint("URI1")
let tokenId3 = txn3.logs[2].args[0].toNumber()
await marketplace.listNft(boredPets.address, tokenId3, 1, {value: listingFee})
console.log(`Minted and listed ${tokenId3}`)
await logNftLists(marketplace)
console.log('BUY 2 NFTs')
await marketplace.buyNft(boredPets.address, tokenId1, {value: 1})
await marketplace.buyNft(boredPets.address, tokenId2, {value: 1})
await logNftLists(marketplace)
console.log('RESELL 1 NFT')
await marketplace.resellNft(boredPets.address, tokenId2, 1, {value: listingFee})
await logNftLists(marketplace)
} catch(err) {
console.log('Doh! ', err);
}
cb();
}
module.exports = main;
在此脚本中,您可以使用artifacts.require来访问合同抽象。@truffle/contracts然后,我们使用便利库与合约进行交互。您可以使用此功能在 Truffle 中用 javasacript 或 typescript编写单元测试。请注意,如果您使用 typescript,则需要创建一个tsconfig.json文件并用于tsc编译为 javascript。tsc 您可以在此处阅读更多信息。
要运行此脚本,您需要填写FIRST_ACCOUNT_ADDRESS以查询列出的 NFT。默认情况下,Truffle 使用第一个可用的帐户来执行函数。您可以通过前往 Ganache GUI 并复制/粘贴第一个帐户地址来获取此地址。

运行truffle exec scripts/run.js,您的输出应如下所示:
MINT AND LIST 3 NFTs
listedNfts: 3
myNfts: 0
myListedNfts 3
BUY 2 NFTs
listedNfts: 1
myNfts: 2
myListedNfts 1
RESELL 1 NFT
listedNfts: 2
myNfts: 1
myListedNfts 1
成功!如果您想在已填充的区块链上部署您的合约,您可以使用 Ganache 以零配置分叉主网。
其他部署方式¶
部署到 Truffle 仪表板¶
在本教程中,我们将带您了解如何使用Truffle 仪表板部署到测试网,它允许您使用 MetaMask 签署您的交易。这使您可以保护您的私钥安全,因为您不必将其保存在本地。为此,您需要从faucet获取一些测试 eth 。
首先,truffle dashboard在单独的终端窗口中运行。它应该在 http://127.0.0.1:24012/ 打开松露仪表板。然后,您可以使用您的 MetaMask 钱包连接到您选择的网络。在这种情况下,我们将选择 Goerli 网络。
接下来,使用 . 将您的合同迁移到仪表板truffle migrate --network dashboard。如果您返回仪表板选项卡,您将看到使用 MetaMask 进行签名的请求。点击接受,瞧!回到终端,您应该会看到您的合约已部署。

更改帐号后,可以再次执行脚本进行测试:truffle exec scripts/run.js
使用.env文件部署¶
如果您不想使用仪表板,可以修改 Truffle 配置文件以使用您设置的环境变量。.env使用以下代码创建一个文件:
MNEMONIC="candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"
INFURA_KEY=""
GANACHE_MNEMONIC=""
GOERLI_MNEMONIC=""
.gitignore已经忽略了,但是由于您在.env此处填充助记键/密钥,因此请不要在任何地方公开。
然后,只需运行truffle migrate --network [NETWORK NAME]或npm run migrate:ovm --network=[NETWORK NAME]
部署到 Optimistic Goerli¶
由于 Optimism 与 EVM 等价,但有少量例外,我们只需将合约从contracts/ethereumover 复制到contracts/optimism.
要获得 Eth for Optimistic Goerli,你可以使用这个faucet。你需要注册一个超过 1 个月的 Github 帐户并至少关注 5 个 repos: – trufflesuite – truffle -box – Infura – MetaMask – Optimism
我们的盒子已经包含了 Optimistic Goerli 配置truffle-confg.ovm.js。运行npm run migrate:ovm --network=optimistic_goerli部署和npm run exec:ovm scripts/run.js --network=optimistic_goerli测试!
由于我们使用的是测试网,因此连接可能会有点不稳定。您可以通过使用 运行migrate命令重试--reset。或者,您可能会偶尔看到如下内容:
TypeError: Cannot read properties of null (reading 'from')
在这种情况下,您可以from在部署脚本中指定地址,如下所示(ACCOUNT_ADDRESS将签署交易的地址):
module.exports = async function(deployer) {
await deployer.deploy(Marketplace);
const marketplace = await Marketplace.deployed();
await deployer.deploy(BoredPetsNFT, marketplace.address, {from: "ACCOUNT_ADDRESS"});
}
在本地部署到 Optimism¶
您还可以部署到本地运行的 Optimism 实例。您需要有非常具体的系统要求。确保端口 9545 和 8545 空闲,然后运行:
npm run installLocalOptimism
npm run startLocalOptimism
创建你的前端¶
首先,我们需要安装一些包来让我们的客户端启动并运行:
cd client
npm install axios
npm install web3modal
npm install web3
npm install ipfs-http-client
然后,我们需要创建或编辑以下 6 个文件client/pages:
_app.js¶
该文件组织链接路由
import '../styles/globals.css'
import Link from 'next/link'
function MyApp({ Component, pageProps }) {
return (
<div>
<nav className="border-b p-6">
<p className="text-4xl font-bold">Bored Pet Marketplace</p>
<div className="flex mt-4">
<Link href="/">
<a className="mr-4 text-teal-400">
Home
</a>
</Link>
<Link href="/create-and-list-nft">
<a className="mr-6 text-teal-400">
Sell a new NFT
</a>
</Link>
<Link href="/my-nfts">
<a className="mr-6 text-teal-400">
My NFTs
</a>
</Link>
<Link href="/my-listed-nfts">
<a className="mr-6 text-teal-400">
My Listed NFTs
</a>
</Link>
</div>
</nav>
<Component {...pageProps} />
</div>
)
}
export default MyApp
index.js¶
该文件是“主页”选项卡,用户可以在其中查看和购买所有列出的 NFT。
import Web3 from 'web3';
import Web3Modal from 'web3modal';
import { useEffect, useState } from 'react';
import axios from 'axios';
import Marketplace from '../contracts/optimism-contracts/Marketplace.json'
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json'
export default function Home() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => { loadNFTs() }, [])
async function loadNFTs() {
const web3Modal = new Web3Modal()
const provider = await web3Modal.connect()
const web3 = new Web3(provider)
const networkId = await web3.eth.net.getId()
// Get all listed NFTs
const marketPlaceContract = new web3.eth.Contract(Marketplace.abi, Marketplace.networks[networkId].address)
const listings = await marketPlaceContract.methods.getListedNfts().call()
// Iterate over the listed NFTs and retrieve their metadata
const nfts = await Promise.all(listings.map(async (i) => {
try {
const boredPetsContract = new web3.eth.Contract(BoredPetsNFT.abi, BoredPetsNFT.networks[networkId].address)
const tokenURI = await boredPetsContract.methods.tokenURI(i.tokenId).call()
const meta = await axios.get(tokenURI)
const nft = {
price: i.price,
tokenId: i.tokenId,
seller: i.seller,
owner: i.buyer,
image: meta.data.image,
name: meta.data.name,
description: meta.data.description,
}
return nft
} catch(err) {
console.log(err)
return null
}
}))
setNfts(nfts.filter(nft => nft !== null))
setLoadingState('loaded')
}
async function buyNft(nft) {
const web3Modal = new Web3Modal()
const provider = await web3Modal.connect()
const web3 = new Web3(provider)
const networkId = await web3.eth.net.getId();
const marketPlaceContract = new web3.eth.Contract(Marketplace.abi, Marketplace.networks[networkId].address);
const accounts = await web3.eth.getAccounts();
await marketPlaceContract.methods.buyNft(BoredPetsNFT.networks[networkId].address, nft.tokenId).send({ from: accounts[0], value: nft.price });
loadNFTs()
}
if (loadingState === 'loaded' && !nfts.length) {
return (<h1 className="px-20 py-10 text-3xl">No pets available!</h1>)
} else {
return (
<div className="flex justify-center">
<div className="px-4" style={ { maxWidth: '1600px' } }>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} />
<div className="p-4">
<p style={ { height: '64px' } } className="text-2xl font-semibold">{nft.name}</p>
<div style={ { height: '70px', overflow: 'hidden' } }>
<p className="text-gray-400">{nft.description}</p>
</div>
</div>
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">{Web3.utils.fromWei(nft.price, "ether")} ETH</p>
<button className="mt-4 w-full bg-teal-400 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy</button>
</div>
</div>
))
}
</div>
</div>
</div>
)
}
}
create-and-list-nft.js¶
这是 Sell 选项卡,用户可以在其中创建和列出 NFT。
import { useState } from 'react'
import Web3 from 'web3'
import Web3Modal from 'web3modal'
import { create as ipfsHttpClient } from 'ipfs-http-client'
import { useRouter } from 'next/router'
const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')
import Marketplace from '../contracts/optimism-contracts/Marketplace.json'
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json'
export default function CreateItem() {
const [fileUrl, setFileUrl] = useState(null)
const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })
const router = useRouter()
async function onChange(e) {
// upload image to IPFS
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 uploadToIPFS() {
const { name, description, price } = formInput
if (!name || !description || !price || !fileUrl) {
return
} else {
// first, upload metadata 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}`
// after metadata is uploaded to IPFS, return the URL to use it in the transaction
return url
} catch (error) {
console.log('Error uploading file: ', error)
}
}
}
async function listNFTForSale() {
const web3Modal = new Web3Modal()
const provider = await web3Modal.connect()
const web3 = new Web3(provider)
const url = await uploadToIPFS()
const networkId = await web3.eth.net.getId()
// Mint the NFT
const boredPetsContractAddress = BoredPetsNFT.networks[networkId].address
const boredPetsContract = new web3.eth.Contract(BoredPetsNFT.abi, boredPetsContractAddress)
const accounts = await web3.eth.getAccounts()
const marketPlaceContract = new web3.eth.Contract(Marketplace.abi, Marketplace.networks[networkId].address)
let listingFee = await marketPlaceContract.methods.getListingFee().call()
listingFee = listingFee.toString()
boredPetsContract.methods.mint(url).send({ from: accounts[0] }).on('receipt', function (receipt) {
console.log('minted');
// List the NFT
const tokenId = receipt.events.NFTMinted.returnValues[0];
marketPlaceContract.methods.listNft(boredPetsContractAddress, tokenId, Web3.utils.toWei(formInput.price, "ether"))
.send({ from: accounts[0], value: listingFee }).on('receipt', function () {
console.log('listed')
router.push('/')
});
});
}
return (
<div className="flex justify-center">
<div className="w-1/2 flex flex-col pb-12">
<input
placeholder="Asset Name"
className="mt-8 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, name: e.target.value })}
/>
<textarea
placeholder="Asset Description"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, description: e.target.value })}
/>
<input
placeholder="Asset Price in Eth"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
/>
<input
type="file"
name="Asset"
className="my-4"
onChange={onChange}
/>
{
fileUrl && (
<img className="rounded mt-4" width="350" src={fileUrl} />
)
}
<button onClick={listNFTForSale} className="font-bold mt-4 bg-teal-400 text-white rounded p-4 shadow-lg">
Mint and list NFT
</button>
</div>
</div>
)
}
my-nfts.js¶
这是我的 NFT 选项卡,用户可以在其中查看他们拥有的 NFT 并选择转售。
import Web3 from 'web3';
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from 'web3modal'
import { useRouter } from 'next/router'
import Marketplace from '../contracts/optimism-contracts/Marketplace.json';
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json';
export default function MyAssets() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
const router = useRouter()
useEffect(() => { loadNFTs() }, [])
async function loadNFTs() {
const web3Modal = new Web3Modal()
const provider = await web3Modal.connect()
const web3 = new Web3(provider)
const networkId = await web3.eth.net.getId()
const marketPlaceContract = new web3.eth.Contract(Marketplace.abi, Marketplace.networks[networkId].address)
const boredPetsContractAddress = BoredPetsNFT.networks[networkId].address
const boredPetsContract = new web3.eth.Contract(BoredPetsNFT.abi, boredPetsContractAddress)
const accounts = await web3.eth.getAccounts()
const data = await marketPlaceContract.methods.getMyNfts().call({from: accounts[0]})
const nfts = await Promise.all(data.map(async i => {
try {
const tokenURI = await boredPetsContract.methods.tokenURI(i.tokenId).call()
const meta = await axios.get(tokenURI)
let nft = {
price: i.price,
tokenId: i.tokenId,
seller: i.seller,
owner: i.buyer,
image: meta.data.image,
name: meta.data.name,
description: meta.data.description,
tokenURI: tokenURI
}
return nft
} catch(err) {
console.log(err)
return null
}
}))
setNfts(nfts.filter(nft => nft !== null))
setLoadingState('loaded')
}
function listNFT(nft) {
router.push(`/resell-nft?id=${nft.tokenId}&tokenURI=${nft.tokenURI}`)
}
if (loadingState === 'loaded' && !nfts.length) {
return (<h1 className="py-10 px-20 text-3xl">No NFTs owned</h1>);
} else {
return (
<div className="flex justify-center">
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4">
<p style={ { height: '64px' } } className="text-2xl font-semibold">{nft.name}</p>
<div style={ { height: '70px', overflow: 'hidden' } }>
<p className="text-gray-400">{nft.description}</p>
</div>
</div>
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {Web3.utils.fromWei(nft.price, "ether")} Eth</p>
<button className="mt-4 w-full bg-teal-400 text-white font-bold py-2 px-12 rounded" onClick={() => listNFT(nft)}>List</button>
</div>
</div>
))
}
</div>
</div>
</div>
);
}
}
resell-nft.js¶
这是用户被引导到转售其 NFT 的页面。
import { useEffect, useState } from 'react'
import Web3 from 'web3'
import { useRouter } from 'next/router'
import axios from 'axios'
import Web3Modal from 'web3modal'
import Marketplace from '../contracts/optimism-contracts/Marketplace.json'
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json'
export default function ResellNFT() {
const [formInput, updateFormInput] = useState({ price: '', image: '' })
const router = useRouter()
const { id, tokenURI } = router.query
const { image, price } = formInput
useEffect(() => { fetchNFT() }, [id])
async function fetchNFT() {
if (!tokenURI) {
return
} else {
const meta = await axios.get(tokenURI)
updateFormInput(state => ({ ...state, image: meta.data.image }))
}
}
async function listNFTForSale() {
if (!price) {
return
} else {
const web3Modal = new Web3Modal()
const provider = await web3Modal.connect()
const web3 = new Web3(provider)
const networkId = await web3.eth.net.getId()
const marketPlaceContract = new web3.eth.Contract(Marketplace.abi, Marketplace.networks[networkId].address)
let listingFee = await marketPlaceContract.methods.getListingFee().call()
listingFee = listingFee.toString()
const accounts = await web3.eth.getAccounts()
marketPlaceContract.methods.resellNft(BoredPetsNFT.networks[networkId].address, id, Web3.utils.toWei(formInput.price, "ether"))
.send({ from: accounts[0], value: listingFee }).on('receipt', function () {
router.push('/')
});
}
}
return (
<div className="flex justify-center">
<div className="w-1/2 flex flex-col pb-12">
<input
placeholder="Asset Price in Eth"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
/>
{
image && (
<img className="rounded mt-4" width="350" src={image} />
)
}
<button onClick={listNFTForSale} className="font-bold mt-4 bg-teal-400 text-white rounded p-4 shadow-lg">
List NFT
</button>
</div>
</div>
)
}
my-listed-nfts.js¶
这是我列出的 NFT 选项卡,用户可以在其中查看他们列出的待售 NFT。
import Web3 from 'web3';
import { useEffect, useState } from 'react';
import axios from 'axios';
import Web3Modal from 'web3modal';
import Marketplace from '../contracts/optimism-contracts/Marketplace.json';
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json';
export default function CreatorDashboard() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => { loadNFTs() }, [])
async function loadNFTs() {
const web3Modal = new Web3Modal()
const provider = await web3Modal.connect()
const web3 = new Web3(provider)
const networkId = await web3.eth.net.getId()
// Get listed NFTs
const marketPlaceContract = new web3.eth.Contract(Marketplace.abi, Marketplace.networks[networkId].address)
const accounts = await web3.eth.getAccounts()
const listings = await marketPlaceContract.methods.getMyListedNfts().call({from: accounts[0]})
// Iterate over my listed NFTs and retrieve their metadata
const nfts = await Promise.all(listings.map(async i => {
try {
const boredPetsContract = new web3.eth.Contract(BoredPetsNFT.abi, BoredPetsNFT.networks[networkId].address)
const tokenURI = await boredPetsContract.methods.tokenURI(i.tokenId).call();
const meta = await axios.get(tokenURI);
let item = {
price: i.price,
tokenId: i.tokenId,
seller: i.seller,
owner: i.owner,
image: meta.data.image,
}
return item
} catch(err) {
console.log(err)
return null
}
}))
setNfts(nfts.filter(nft => nft !== null))
setLoadingState('loaded')
}
if (loadingState === 'loaded' && !nfts.length) {
return (<h1 className="py-10 px-20 text-3xl">No NFTs listed</h1>)
} else {
return (
<div>
<div className="p-4">
<h2 className="text-2xl py-2">Items Listed</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {Web3.utils.fromWei(nft.price, "ether")} Eth</p>
</div>
</div>
))
}
</div>
</div>
</div>
)
}
}
Web3 客户端概述¶
让我们回顾一下我们用来连接前端的 Web3 概念和实用程序。
web3Modal是我们用来检索用户网络提供商的库ipfs-http-client是我们用来将 NFT 元数据上传到 IPFS 的库web3是一个允许我们使用智能合约抽象的库
根据您是要使用 Optimism 合约还是 Ethereum 合约,您将更改合约导入路径:
import Marketplace from '../contracts/optimism-contracts/Marketplace.json'
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json'
当我们用 调用我们的合约方法时Web3.js,我们call会在不更改合约状态以及更改合约状态send时使用。你可以在这里阅读更多关于它的信息。
部署您的前端¶
要查看运行中的前端,只需使用package.json. npm run dev从文件夹中运行client,您的网站应该会启动http://localhost:3000/!
请注意,前端将使用您的 MetaMask 扩展上设置的任何网络和帐户。
将 Ganache 添加到 MetaMask¶
如果您想使用 Ganache 作为您的网络,您可以使用以下属性将该网络添加到您的 MetaMask 钱包:
- 网络名称:Ganache
- 新的 RPC 网址:http://127.0.0.1:7545
- 链号:1337
- 货币符号:ETH
然后,导入一个新帐户。如果您使用的是 Ganache UI,您可以通过单击钥匙图标来获取私钥以显示帐户密钥。


将乐观以太坊添加到 MetaMask¶
如果您想使用 Optimism 的本地实例,您可以使用以下属性将网络添加到您的 MetaMask 钱包:
- 网络名称:乐观以太坊
- 新的 RPC 网址:http://127.0.0.1:8545
- 链号:17
- 货币符号:ETH
然后,导入一个新帐户。您可以在此处获取帐户和私钥列表。
补充说明¶
你有它 – 一个非常基本的市场!您可以通过多种方式在此项目的基础上进行构建: – 允许用户对 NFT 进行投标 – 允许用户将其 NFT 下架 – 将 Marketplace 合约和 NFT 合约合二为一 – 桥接 Optimism 合约(Truffle L2 桥接盒即将推出!)
如果您有兴趣在以太坊和 Optimism 之间建立真正的桥梁,请查看我们的 Optimism Bridge Box!
加入Github Discussions加入 Truffle 社区讨论和提问!
