i007.cc

i007.cc

优先队列-降维打击

05.价值资料

如何在以太坊或乐观上构建 NFT 市场 DApp¶

原文地址

 

由艾米丽·林( Emily Lin)撰写

概述

今天,我们将构建 Bored Pets Marketplace,一个使用TruffleInfuraMetaMaskWeb3.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 端点。它会要求您注册免费试用。

添加乐观歌尔礼的GIF

注册 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/

添加乐观歌尔礼的GIF

获取乐观的 Goerli Eth

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

1) 获取一些 Goerli Eth。你可以使用这个水龙头2) Optimism 的网关仍然不支持 Goerli,但是如果你将 Goerli ETH 转移到0x636Af16bf2f682dD3109e60102b8E1A089FedAa8,你会在 Optimistic Goerli 上得到它。

获得乐观 Goerli Eth 的 GIF

VSCode

随意使用您想要的任何 IDE,但我们强烈建议使用 VSCode!您可以使用 Truffle 扩展来完成本教程的大部分内容,以创建、构建和部署您的智能合约,而无需使用 CLI!你可以在这里阅读更多关于它的信息。

设置您的项目

拆箱乐观

首先,我们将打开 Truffle 的 Optimism 盒子。您可以通过调用truffle unbox optimism [FOLDER_NAME]或通过 VSCode 扩展命令面板来做到这一点!继续安装必要的软件包。

truffle unbox optimism nft-marketplace
cd nft-marketplace
npm install

创建您的客户文件夹

我们还将为我们的客户端使用Next.jsTailwind 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.solcontracts/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 允许您使用Promiseawait/来按顺序部署合约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。打开它,创建一个工作区,然后点击保存!

创建 Ganache 工作区的 GIF

这会在 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 进行签名的请求。点击接受,瞧!回到终端,您应该会看到您的合约已部署。

使用仪表板的 GIF

更改帐号后,可以再次执行脚本进行测试: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.jsonnpm run dev从文件夹中运行client,您的网站应该会启动http://localhost:3000/

请注意,前端将使用您的 MetaMask 扩展上设置的任何网络和帐户。

将 Ganache 添加到 MetaMask

如果您想使用 Ganache 作为您的网络,您可以使用以下属性将该网络添加到您的 MetaMask 钱包:

  • 网络名称:Ganache
  • 新的 RPC 网址:http://127.0.0.1:7545
  • 链号:1337
  • 货币符号:ETH

然后,导入一个新帐户。如果您使用的是 Ganache UI,您可以通过单击钥匙图标来获取私钥以显示帐户密钥。

获取私钥的 GIF

导入帐号的 GIF

将乐观以太坊添加到 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 社区讨论和提问!

发表回复