Optimism NFT市场开发教程
我们将使用 Truffle、Infura、MetaMask 和 Web3.js 构建 Bored Pets Marketplace,一个简单的 可以运行在Optimism上的NFT 市场
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK
今天,我们将使用 Truffle、Infura、MetaMask 和 Web3.js 构建 Bored Pets Marketplace,一个简单的 NFT 市场! 我们将从基础知识开始,首先向您展示如何将以太坊合约部署到 Truffle 的本地区块链、Ganache 和 Goerli 测试网。 然后,我们将向你展示如何将这些合约转换为 Optimism 并部署在 Optimistic Goerli 测试网上。 请注意,本教程不会教你 Javascript,而只会简要介绍一些 Solidity 原则。
我们的市场具有一组核心功能: 1. 铸造并列出 NFT 2. 购买和出售 NFT 3. 查看列出的 NFT、你拥有的 NFT 以及正在出售的 NFT
本教程的灵感来自 Nader Dabit 的市场教程。 他的教程使用 ethers.js 并部署到 Polygon,您都可以使用 Truffle!
本教程的完整代码位于此处。
1、开发环境搭建
我们将从 Truffle 的 Optimism 框、脚手架代码开始,帮助你配置 Truffle 项目以尽快开始使用 Optimism! 自述文件更详细地解释了项目结构。
至少,你需要安装:
- Node.js,v12 或更高版本
- Truffle
- Ganache
如果想在本地运行 Optimism,你还需要这些先决条件。
1.1 创建 Infura 帐户和项目
要将你的 DApp 连接到以太坊、Optimism 和其他网络,你需要一个 Infura 帐户。 在这里注册一个帐户。
登录后,创建一个项目! 我们称之为 nft-marketplace。 由于我们要部署到 Optimism,因此请继续添加 Optimistic Goerli 端点。 它会要求您注册免费试用。
1.2 注册 MetaMask 钱包
要在浏览器中与 DApp 交互,您需要一个 MetaMask 钱包。 在这里注册一个帐户。
1.3 将 Optimistic Goerli 添加到你的钱包
让我们将 Optimistic Goerli 添加到 MetaMask 钱包的可用网络列表中! 为此,请打开 MetaMask 扩展,单击网络,然后单击添加网络。 然后,填写网络属性(您可以从 Infura 项目复制 Infura idf):
- 网络名称:Optimism Goerli
- 新的 RPC URL:https://optimism-goerli.infura.io/v3/INFURA_PROJECT_ID
- 链ID:420
- 货币符号:ETH
- 区块浏览器网址:https://blockscout.com/optimism/goerli/
1.4 获取Optimism的Goerli Eth
要使用 Optimistic Goerli 测试网,你需要一些测试 eth。 为此,你需要:
1.5 VSCode
你可以随意使用任何你想要的 IDE,但我们强烈建议使用 VSCode! 你可以使用 Truffle 扩展来运行本教程的大部分内容,以创建、构建和部署你的智能合约,而无需使用 CLI! 你可以在这里读更多关于它的内容。
2、项目设置
首先,我们首先拆开 Truffle 的Optimism Box。 你可以通过调用 truffle unbox optimism [FOLDER_NAME]
或通过 VSCode 扩展命令面板来完成此操作! 继续并安装必需的软件包。
truffle unbox optimism nft-marketplace
cd nft-marketplace
2.1 创建客户端文件夹
我们还将为客户端使用 Next.js 和 Tailwind CSS。 因此,为了进行设置,我们将使用 Next 的 create-next-app 实用程序。
npx create-next-app client
然后,我们将下载 Tailwind 依赖项并填充其配置。
cd client
npm install -D tailwindcss postcss autoprefixer
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;
2.2 编辑 Truffle 配置文件
由于我们将在客户端中引用你编译的合约,因此让我们更改 Truffle 配置文件中的 contracts_build_directory
:
- 在
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
3、构建NFT智能合约
由于我们首先向您展示如何在以太坊上进行部署,因此我们将在 contracts/ethereum
下编辑 SimpleStorage.sol
合约。 将文件名和合约名称更改为 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,BoredPetNFT 通过继承 ERC721URIStorage.sol
的实现来实现 ERC721 标准
@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol
使用 ERC721 的这种实现,以便我们将链上的 tokenURI 存储在存储中,这使我们能够将上传到 IPFS 链下的元数据存储起来。
@openzeppelin/contracts/utils/Counters.sol
我们使用计数器来跟踪 NFT 的总数,并为每个 NFT 分配一个唯一的代币 ID。
在顶部我们定义了一些变量:
address MarketplaceContract
是我们将在下一节中编写的 Marketplace 合约的地址。- 每次铸造 NFT 时都会发出
NFTMinted
事件。 当以 Solidity 方式发出事件时,参数将存储在事务日志中。 稍后当我们构建 Web 应用程序时,我们将需要 tokenId。
最后,我们只需要定义 mint
函数! 它只有一个参数: - string memory _tokenURI
指向 IPFS 上存储 NFT 元数据(即图像、名称、描述)的 JSON 元数据
mint
相对简单——它铸造一个具有递增的、唯一的代币 ID 的 NFT。 但值得注意的是 setApprovalForAll
。 这很重要,因为我们的 Marketplace 合约需要批准者访问才能在不同地址之间转移 NFT 的所有权。
4、构建市场合约
现在,让我们在 contracts/ethereum
下添加一个新的合约 Marketplace.sol
,它将存储所有市场功能。 这将是它最终的样子:
// 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);
_marketOwner.transfer(LISTING_FEE);
_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);
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 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 的新所有者,代币从买方转移给卖方,上市费用则交给市场所有者。
最后,查询功能相对简单:
getListedNfts
检索当前列出出售的 NFT。getMyNfts
检索用户已购买的 NFT。getMyListedNfts
检索用户列出待售的 NFT。
5、本地部署智能合约
为了部署我们的智能合约,我们需要修改 migrations/1_deploy_contracts.js
。 因为 BoredPetsNFT
需要 Marketplace
合约地址,所以这里的订单很重要! Truffle 允许您使用 Promise 或等待/异步按顺序部署合约。 你可以在这里读更多关于它的内容。 请注意,以前,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-config.js
中编辑开发网络以匹配端口号。 现在,只需在 CLI 中从 nft-marketplace 文件夹运行 truffle migrate
,该文件夹将默认为开发网络。 这将编译并部署你的合约。 你应该看到与此类似的输出:
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
下找到编译的合约
6、编写脚本
为了执行常见操作,我们将利用 truffle exec
运行脚本来自动执行常见任务。 让我们编写一个脚本来执行所有不同的函数。 首先,在名为 run.js
的新脚本文件夹下创建一个新文件。
var BoredPetsNFT = artifacts.require("BoredPetsNFT");
var Marketplace = artifacts.require("Marketplace");
async function logNftLists(marketplace) {
let listedNfts = await marketplace.getListedNfts()
const accounts = await web3.currentProvider.request({
method: 'eth_accounts',
params: [],
});
const accountAddress = accounts[0];
let myNfts = await marketplace.getMyNfts({from: accountAddress})
let myListedNfts = await marketplace.getMyListedNfts({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.LISTING_FEE()
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
的更多信息。
运行 truffle exec script/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 以零配置分叉主网。
7、测试你的智能合约
让我们测试一下我们的 Marketplace 合约! 为此,请运行:
truffle create test Marketplace
然后,添加以下代码:
require("@openzeppelin/test-helpers/configure")({
provider: web3.currentProvider,
singletons: {
abstraction: "truffle",
},
});
const { balance, ether, expectRevert, expectEvent } = require('@openzeppelin/test-helpers');
const Marketplace = artifacts.require("Marketplace");
const BoredPetsNFT = artifacts.require("BoredPetsNFT");
function assertListing(actual, expected) {
assert.equal(actual.nftContract, expected.nftContract, "NFT contract is not correct");
assert.equal(actual.tokenId, expected.tokenId, "TokenId is not correct");
assert.equal(actual.owner, expected.owner, "Owner is not correct");
assert.equal(actual.seller, expected.seller, "Seller is not correct");
assert.equal(actual.price, expected.price, "Price is not correct");
assert.equal(actual.listed, expected.listed, "Listed is not correct")
}
function getListing(listings, tokenId) {
let listing = {};
listings.every((_listing) => {
if (_listing.tokenId == tokenId) {
listing = _listing;
return false;
} else {
return true;
}
});
return listing
}
function listingToString(listing) {
let listingCopy = {...listing};
listingCopy.tokenId = listing.tokenId.toString();
listingCopy.price = listing.price.toString();
if (listing.listed) {
listingCopy.listed = listing.listed.toString();
}
return listingCopy;
}
async function mintNft(nftContract, tokenOwner) {
return (await nftContract.mint("fakeURI", {from: tokenOwner})).logs[0].args.tokenId.toNumber()
}
contract("Marketplace", function (accounts) {
const MARKETPLACE_OWNER = accounts[0];
const TOKEN_OWNER = accounts[1];
const BUYER = accounts[2];
let marketplace;
let boredPetsNFT;
let nftContract;
let listingFee;
before('should reuse variables', async () => {
marketplace = await Marketplace.deployed();
boredPetsNFT = await BoredPetsNFT.deployed();
nftContract = boredPetsNFT.address;
listingFee = (await marketplace.LISTING_FEE()).toString();
console.log("marketplace %s", marketplace.address)
console.log("token_owner %s", TOKEN_OWNER)
console.log("buyer %s", BUYER)
});
it("should validate before listing", async function () {
await expectRevert(
marketplace.listNft(nftContract, 1, ether(".005"), {from: TOKEN_OWNER}),
"Not enough ether for listing fee"
);
await expectRevert(
marketplace.listNft(nftContract, 1, 0, {from: TOKEN_OWNER, value: listingFee}),
"Price must be at least 1 wei"
);
});
it("should list nft", async function () {
let tokenID = await mintNft(boredPetsNFT, TOKEN_OWNER);
let tracker = await balance.tracker(MARKETPLACE_OWNER);
await tracker.get();
let txn = await marketplace.listNft(nftContract, tokenID, ether(".005"), {from: TOKEN_OWNER, value: listingFee});
assert.equal(await tracker.delta(), listingFee, "Listing fee not transferred");
let expectedListing = {
nftContract: nftContract,
tokenId: tokenID,
seller: TOKEN_OWNER,
owner: marketplace.address,
price: ether(".005"),
listed: true
};
assertListing(getListing(await marketplace.getListedNfts(), tokenID), expectedListing);
assertListing(getListing(await marketplace.getMyListedNfts({from: TOKEN_OWNER}), tokenID), expectedListing);
delete expectedListing.listed;
expectEvent(txn, "NFTListed", listingToString(expectedListing));
});
it("should validate before buying", async function () {
await expectRevert(
marketplace.buyNft(nftContract, 1, {from: BUYER}),
"Not enough ether to cover asking price"
);
});
it("should modify listings when nft is bought", async function () {
let tokenID = await mintNft(boredPetsNFT, TOKEN_OWNER);
await marketplace.listNft(nftContract, tokenID, ether(".005"), {from: TOKEN_OWNER, value: listingFee});
let expectedListing = {
nftContract: nftContract,
tokenId: tokenID,
seller: TOKEN_OWNER,
owner: marketplace.address,
price: ether(".005"),
listed: true
};
assertListing(getListing(await marketplace.getListedNfts(), tokenID), expectedListing);
let tracker = await balance.tracker(TOKEN_OWNER);
let txn = await marketplace.buyNft(nftContract, tokenID, {from: BUYER, value: ether(".005")});
expectedListing.owner = BUYER;
expectedListing.listed = false;
assert.equal((await tracker.delta()).toString(), ether(".005").toString(), "Price not paid to seller");
assertListing(getListing(await marketplace.getMyNfts({from: BUYER}), tokenID), expectedListing);
delete expectedListing.listed;
expectEvent(txn, "NFTSold", listingToString(expectedListing));
});
it("should validate reselling", async function () {
await expectRevert(
marketplace.resellNft(nftContract, 1, 0, {from: BUYER, value: listingFee}),
"Price must be at least 1 wei"
);
await expectRevert(
marketplace.resellNft(nftContract, 1, ether(".005"), {from: BUYER}),
"Not enough ether for listing fee"
);
});
it("should resell nft", async function () {
let tokenID = await mintNft(boredPetsNFT, TOKEN_OWNER);
await marketplace.listNft(nftContract, tokenID, ether(".005"), {from: TOKEN_OWNER, value: listingFee});
await marketplace.buyNft(nftContract, tokenID, {from: BUYER, value: ether(".005")});
let expectedListing = {
nftContract: nftContract,
tokenId: tokenID,
seller: TOKEN_OWNER,
owner: BUYER,
price: ether(".005"),
listed: false
};
assertListing(getListing(await marketplace.getMyNfts({from: BUYER}), tokenID), expectedListing);
await boredPetsNFT.approve(marketplace.address, tokenID, {from: BUYER});
let txn = await marketplace.resellNft(nftContract, tokenID, ether(".005"), {from: BUYER, value: listingFee});
expectedListing.seller = BUYER;
expectedListing.owner = marketplace.address;
expectedListing.listed = true;
assertListing(getListing(await marketplace.getListedNfts(), tokenID), expectedListing);
assertListing(getListing(await marketplace.getMyListedNfts({from: BUYER}), tokenID), expectedListing);
delete expectedListing.listed;
expectEvent(txn, "NFTListed", listingToString(expectedListing));
});
});
8、其他部署方式
本部分介绍其他几种部署智能合约的方法。
8.1 使用Truffle Dashboard部署
我们将引导你了解如何使用 Truffle 仪表板部署到测试网,该仪表板允许你使用 MetaMask 签署交易。 这使你可以保证私钥的安全,因为你不必将其保存在本地。 为此,你需要从水龙头获取一些测试 eth。
首先,在单独的终端窗口中运行 truffle 仪表板。 它应该在 http://127.0.0.1:24012/ 处打开 truffle 仪表板。 然后,你可以使用 MetaMask 钱包连接到选择的网络。 在本例中,我们将选择 Goerli 网络。
接下来,使用 truffle migrate --network Dashboard
将合约迁移到仪表板。 如果你返回仪表板选项卡,将看到使用 MetaMask 进行签名的请求。 点击接受,瞧! 返回终端,你应该看到合约已部署。
更改账号后,可以再次执行脚本进行测试:truffle exec scripts/run.js
8.2 使用 .env 文件部署
如果你不想使用仪表板,可以修改 Truffle 配置文件以使用你设置的环境变量。 使用以下代码创建 .env 文件:
INFURA_KEY="<Your Infura project key>"
GANACHE_MNEMONIC="<Your Ganache mnemonic>"
GOERLI_MNEMONIC="<Your Metamask mnemonic>"
.gitignore
已经忽略了 .env
,但由于你在此处填充助记符/密钥,请不要在任何地方公开提交。
然后,只需运行 truffle migrate --network [NETWORK NAME]
或 npm run migrate:ovm --network=[NETWORK NAME]
8.3 部署到 Optimistic Goerli
由于 Optimism 与 EVM 等价,但有一些小例外,我们可以将合约从 contracts/ethereum
复制到 contracts/optimism
。
要为 Optimistic Goerli 获取 Eth,可以使用这个水龙头。 你需要使用 1 个月以上的 Github 帐户进行注册,并关注至少 5 个存储库: trufflesuite 、truffle-box 、 Infura 、MetaMask 和Optimism 。
我们的盒子已经在 truffle-confg.ovm.js
中包含了 Optimistic Goerli 配置。 运行 npm run migrate:ovm --network=optimistic_goerli
进行部署,然后 npm run exec:ovm script/run.js --network=optimistic_goerli
进行测试!
由于我们使用的是测试网,因此连接可能会有点不稳定。 你可以通过运行带有 --reset
的 migrate
命令来重试。 或者,您可能偶尔会看到类似这样的内容:
TypeError: Cannot read properties of null (reading '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"});
}
8.4 本地部署Optimism
你还可以部署到本地运行的 Optimism 实例。 你需要满足非常具体的系统要求。 确保端口 9545 和 8545 空闲,然后运行:
npm run installLocalOptimism
npm run startLocalOptimism
9、创建 Infura IPFS 项目
你需要 Infura IPFS 帐户和专用网关来上传你的 NFT 元数据。 要创建 IPFS 项目,请选择创建 IPFS 项目。
然后,你需要创建一个唯一的网关名称。 在这个项目中,我们将其称为 optimism-demo
。 你需要为自己的专用网关提供自己独特的名称。
10、创建你的前端
首先,我们需要安装一些软件包来启动并运行我们的客户端:
cd client
npm install axios
npm install web3modal
npm install web3
npm install ipfs-http-client
然后,我们需要创建或编辑位于 client/pages
下的 6 个文件:
_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="/" className="mr-4 text-teal-400">
Home
</Link>
<Link href="/create-and-list-nft" className="mr-6 text-teal-400">
Sell a new NFT
</Link>
<Link href="/my-nfts" className="mr-6 text-teal-400">
My NFTs
</Link>
<Link href="/my-listed-nfts" className="mr-6 text-teal-400">
My Listed NFTs
</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
:这是“销售”选项卡,用户可以在其中创建并列出 NFT。 确保替换为你在 Infura 上的 IPFS 项目中创建的专用网关名称。 你还需要添加 IPFS API 和 Secret 来创建我们的 IPFS 客户端。 为此,请在客户端文件夹中创建.env.local
。 然后,用这些值填充它:
NEXT_PUBLIC_IPFS_SECRET=
NEXT_PUBLIC_IPFS_PROJECT_ID=
然后,复制粘贴此代码:
import { useState } from 'react'
import Web3 from 'web3'
import Web3Modal from 'web3modal'
import { useRouter } from 'next/router'
import { create as ipfsHttpClient } from 'ipfs-http-client'
import Marketplace from '../contracts/optimism-contracts/Marketplace.json'
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json'
const projectId = process.env["NEXT_PUBLIC_IPFS_KEY"];
const projectSecret = process.env["NEXT_PUBLIC_IPFS_PROJECT_ID"];
const auth =
'Basic ' + Buffer.from(projectId + ':' + projectSecret).toString('base64');
const client = ipfsHttpClient({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: {
authorization: auth,
},
});
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://<DEDICATED_GATEWAY>.infura-ipfs.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)
console.log('added: ', added)
const url = `https://<DEDICATED_GATEWAY>.infura-ipfs.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.LISTING_FEE().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.LISTING_FEE().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>
)
}
}
11、Web3 客户端概述
让我们回顾一下用于连接前端的 Web3 概念和实用程序。
- web3Modal 是我们用来检索用户的网络提供商的库
- ipfs-http-client 是我们用来将 NFT 元数据上传到 IPFS 的库
- web3 是一个允许我们使用智能合约抽象的库
根据你想要使用 Optimism 合约还是以太坊合约,需要更改合约导入路径:
import Marketplace from '../contracts/optimism-contracts/Marketplace.json'
import BoredPetsNFT from '../contracts/optimism-contracts/BoredPetsNFT.json'
当我们使用 Web3.js 调用合约方法时,我们在不改变合约状态时使用 call,在改变合约状态时使用 send。 你可以在这里读更多关于它的内容。
12、部署你的前端
要查看前端的运行情况,只需使用 package.json 中的节点脚本。 从客户端文件夹运行 npm run dev
,你的网站应该在 http://localhost:3000/
上启动!
请注意,前端将使用 MetaMask 扩展上设置的任何网络和帐户。
12.1 将 Ganache 添加到 MetaMask
如果想使用 Ganache 作为您的网络,可以将该网络添加到具有以下属性的 MetaMask 钱包中:
- 网络名称:Ganache
- 新的 RPC URL:http://127.0.0.1:7545
- 链号:1337
- 货币符号:ETH
然后,导入一个新帐户。 如果使用的是 Ganache UI,则可以通过单击密钥图标来获取私钥以显示帐户密钥。
12.2 将Optimism添加到 MetaMask
如果您想使用本地 Optimism 实例,可以将网络添加到具有以下属性的 MetaMask 钱包中:
- 网络名称:Optimism Ethereum
- 新的 RPC URL:http://127.0.0.1:8545
- 链号:17
- 货币符号:ETH
然后,导入一个新帐户。 可以在此处获取帐户和私钥列表。
原文链接:How to Build a NFT Marketplace DApp on Ethereum or Optimism
DefiPlot翻译整理,转载请标明出处