Optimism NFT市场开发教程

我们将使用 Truffle、Infura、MetaMask 和 Web3.js 构建 Bored Pets Marketplace,一个简单的 可以运行在Optimism上的NFT 市场

Optimism NFT市场开发教程
一键发币: SOLANA | BNB | ETH | POLYGON | HECO | OKEX

今天,我们将使用 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):

1.4 获取Optimism的Goerli Eth

要使用 Optimistic Goerli 测试网,你需要一些测试 eth。 为此,你需要:

  • 获取一些 Goerli Eth。 你可以使用这个水龙头,这也会给一些 Optimistic Goerli Eth
  • 通过桥接程序获得一些 Optimstic 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 个存储库: trufflesuitetruffle-boxInfuraMetaMaskOptimism

我们的盒子已经在 truffle-confg.ovm.js 中包含了 Optimistic Goerli 配置。 运行 npm run migrate:ovm --network=optimistic_goerli 进行部署,然后 npm run exec:ovm script/run.js --network=optimistic_goerli进行测试!

由于我们使用的是测试网,因此连接可能会有点不稳定。 你可以通过运行带有 --resetmigrate 命令来重试。 或者,您可能偶尔会看到类似这样的内容:

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 UI,则可以通过单击密钥图标来获取私钥以显示帐户密钥。

12.2 将Optimism添加到 MetaMask

如果您想使用本地 Optimism 实例,可以将网络添加到具有以下属性的 MetaMask 钱包中:

然后,导入一个新帐户。 可以在此处获取帐户和私钥列表。


原文链接:How to Build a NFT Marketplace DApp on Ethereum or Optimism

DefiPlot翻译整理,转载请标明出处