# 如何构建全栈 NFT dApp

在本教程中，您将构建一个 NFT 铸币机，并学习如何通过使用 Metamask 和 Web3 工具将智能合约连接到 React 前端来创建全栈 dApp。

来自 Web2 背景的开发人员面临的最大挑战之一是弄清楚如何将您的智能联系人连接到前端项目并与之交互。

通过构建 NFT 铸币机——一个简单的用户界面，您可以在其中输入数字资产的链接、标题和描述——您将学习如何：

* 通过您的前端项目连接到 Metamask
* 从您的前端调用智能合约方法
* 使用 Metamask 签署交易

在本教程中，我们将使用[React](https://reactjs.org/)作为我们的前端框架。因为本教程主要关注 Web3 开发，所以我们不会花太多时间分解 React 基础知识。相反，我们将专注于为我们的项目带来功能

> 作为先决条件，您应该对 React 有初学者水平的了解——了解组件、道具、useState/useEffect 和基本函数调用的工作原理。如果您以前从未听说过这些术语中的任何一个，您可能需要查看[React 简介教程](https://reactjs.org/tutorial/tutorial.html)。对于更多的视觉学习者，我们强烈推荐 Net Ninja 的这个优秀的[完整现代 React 教程](https://www.youtube.com/playlist?list=PL4cUxeGkcC9gZD-Tvwfod2gaISzfRiP9d)视频系列。

事不宜迟，让我们开始吧！

### 第 0 步：制作 NFT 101

在我们开始查看任何代码之前，了解 NFT 的工作原理非常重要。它包括两个步骤：

1\) 你在以太坊区块链上发布了一个 NFT 智能合约。通常这是[ERC-721](https://eips.ethereum.org/EIPS/eip-721)或[ERC-1155](https://eips.ethereum.org/EIPS/eip-1155)智能合约。

> 两种NFT智能合约标准最大的区别在于，ERC-1155是多代币标准，包含批量功能，而ERC-721是单代币标准，因此一次只支持转移一个代币。

2）您调用该 NFT 智能合约上的铸币函数来铸币 NFT。*铸造*只是在区块链上发布不可替代令牌的唯一实例的行为。

通常，这个铸造函数需要你传入两个变量作为参数，第一个是接收者，它指定将接收你新铸造的 NFT 的地址，第二个是 NFT 的 tokenURI ，一个解析为描述 NFT 元数据的 JSON 文档的字符串。

> NFT 的元数据实际上是赋予它生命的东西，允许它具有属性，例如名称、描述、图像（或不同的数字资产）和其他属性。这[是 tokenURI 的示例](https://gateway.pinata.cloud/ipfs/QmSvBcb4tjdFpajGJhbFAWeK3JAxCdNQLQtr6ZdiSi42V2)，其中包含 NFT 的元数据。

在本教程中，我们将重点关注第 2 部分，使用我们的 React UI 调用现有 NFT 的智能合约铸造功能。

这[是](https://ropsten.etherscan.io/address/0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE)我们将在本教程中调用的 ERC-721 NFT 智能合约的链接。如果您想了解我们是如何制作的，我们强烈建议您查看我们的其他教程[“如何创建 NFT ](https://docs.alchemyapi.io/alchemy/tutorials/how-to-create-an-nft)”<br>

很酷，现在我们了解了 NFT 的工作原理，让我们克隆我们的入门文件！

### 第 1 步：克隆启动文件

首先，转到[nft-minter-tutorial](https://github.com/alchemyplatform/nft-minter-tutorial) github 存储库以获取该项目的启动文件。将此存储库克隆到您的本地环境中。

> 不知道如何克隆存储库？查看来自 Github 的[指南。](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository)

当您打开这个克隆**的 nft-minter-tutorial**存储库时，您会注意到它包含两个文件夹：**minter-starter-files**和**nft-minter**。

* **minter-starter-files**包含该项目的启动文件（本质上是 React UI）。在本教程中，**我们将在此目录中工作**，您将学习如何通过将此 UI 连接到您的以太坊钱包和 NFT 智能合约来使它栩栩如生。
* **nft-minter包含完整的完整教程，如果您遇到困难**，可以作为**参考。**

接下来在您最喜欢的代码编辑器中打开您的**minter-starter-files**副本（在 Alchemy，我们是[VSCode](https://code.visualstudio.com/download)的忠实粉丝），然后导航到您的**src**文件夹：

<figure><img src="/files/ELn21seMElJRpImHVLVu" alt=""><figcaption></figcaption></figure>

我们将要编写的所有代码都将位于**src**文件夹下。我们将编辑**Minter.js**组件并编写额外的 javascript 文件来为我们的项目提供 Web3 功能。

### 第 2 步：检查我们的入门文件

在我们开始编码之前，重要的是检查启动文件中已经为我们提供的内容。

**让你的反应项目运行**

让我们从在浏览器中运行 React 项目开始。React 的美妙之处在于，一旦我们的项目在浏览器中运行，我们保存的任何更改都会在浏览器中实时更新。

要运行项目，请导航到**minter-starter-files 文件**夹的根目录，然后在终端中运行**npm install**以安装项目的依赖项：

```js
cd minter-starter-files
npm install
```

安装完成后，在终端中运行**npm start ：**

```js
npm start
```

这样做应该会在您的浏览器中打开<http://localhost:3000/>，您会在其中看到我们项目的前端。它应该由 3 个字段组成：输入 NFT 资产链接的地方，输入 NFT 的名称，并提供描述。

<figure><img src="/files/d07BLzdkXXNgvZLBSu94" alt=""><figcaption></figcaption></figure>

如果您尝试单击“连接钱包”或“Mint NFT”按钮，您会发现它们不起作用——那是因为我们仍然需要对它们的功能进行编程！:)

**Minter.js 组件**

> **注意：**&#x786E;保你在**minter-starter-files 文件**夹中而不是**nft-minter**文件夹中！

让我们回到编辑器中的**src**文件夹并打开**Minter.js**文件。我们理解这个文件中的所有内容非常重要，因为它是我们将要处理的主要 React 组件。

在我们这个文件的顶部，我们有我们将在特定事件发生后更新的状态变量。

```js
//State variables
const [walletAddress, setWallet] = useState("");
const [status, setStatus] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [url, setURL] = useState("");
```

> 从未听说过 React 状态变量或状态挂钩？查看[这些](https://reactjs.org/docs/hooks-state.html)文档。

以下是每个变量代表的内容：

* **walletAddress** - 存储用户钱包地址的字符串
* **status** - 包含要显示在 UI 底部的消息的字符串
* **name** - 存储 NFT 名称的字符串
* **description** - 存储 NFT 描述的字符串
* **url** - 一个字符串，它是指向 NFT 数字资产的链接

在状态变量之后，您将看到三个未实现的函数：**useEffect**、**connectWalletPressed**和**onMintPressed**。您会注意到所有这些函数都是**异步**的，那是因为我们将在其中进行异步 API 调用！它们的名称与其功能同名：

```js
useEffect(async () => { //TODO: implement

}, []);

const connectWalletPressed = async () => { //TODO: implement

};

const onMintPressed = async () => { //TODO: implement

};
```

* [useEffect](https://reactjs.org/docs/hooks-effect.html) - 这是一个 React 挂钩，在您的组件呈现后调用。因为它有一个空数&#x7EC4;**\[]**&#x4F20;递给它（见第 3 行），它只会在组件的*第一次*渲染时被调用。在这里，我们将调用我们的钱包侦听器和另一个钱包功能来更新我们的 UI 以反映钱包是否已连接。
* **connectWalletPressed** - 将调用此函数以将用户的 Metamask 钱包连接到我们的 dApp。
* **onMintPressed** - 将调用此函数来铸造用户的 NFT。

在这个文件的末尾，我们有我们组件的用户界面。如果您仔细扫描这段代码，您会注意到当相应文本字段中的输入发生变化时，我们会更新我们的**url**、**name**和**description状态变量。**

您还会看到，当分别单击ID 为 mintButton 和 walletButton 的按钮&#x65F6;**，**&#x4F1A;**调用connectWalletPressed**和**onMintPressed**。

```js
//the UI of our component
  return (
    <div className="Minter">
      <button id="walletButton" onClick={connectWalletPressed}>
        {walletAddress.length > 0 ? (
          "Connected: " +
          String(walletAddress).substring(0, 6) +
          "..." +
          String(walletAddress).substring(38)
        ) : (
          <span>Connect Wallet</span>
        )}
      </button>

      <br></br>
      <h1 id="title">🧙‍♂️ Alchemy NFT Minter</h1>
      <p>
        Simply add your asset's link, name, and description, then press "Mint."
      </p>
      <form>
        <h2>🖼 Link to asset: </h2>
        <input
          type="text"
          placeholder="e.g. https://gateway.pinata.cloud/ipfs/<hash>"
          onChange={(event) => setURL(event.target.value)}
        />
        <h2>🤔 Name: </h2>
        <input
          type="text"
          placeholder="e.g. My first NFT!"
          onChange={(event) => setName(event.target.value)}
        />
        <h2>✍️ Description: </h2>
        <input
          type="text"
          placeholder="e.g. Even cooler than cryptokitties ;)"
          onChange={(event) => setDescription(event.target.value)}
        />
      </form>
      <button id="mintButton" onClick={onMintPressed}>
        Mint NFT
      </button>
      <p id="status">
        {status}
      </p>
    </div>
  );
```

‍<br>

最后，让我们解决这个 Minter 组件添加到哪里。

如果你转到**App.js**文件，它是 React 中的主要组件，充当所有其他组件的容器，你会看到我们的 Minter 组件在第 7 行被注入。

**在本教程中，我们只会编辑 Minter.js 文件并在我们的 src 文件夹中添加文件。**

现在我们了解了我们正在使用的是什么，让我们设置我们的以太坊钱包吧！

### 第 3 步：设置您的以太坊钱包

为了让用户能够与您的智能合约进行交互，他们需要将他们的以太坊钱包连接到您的 dApp。 &#x20;

**下载Metamask**

在本教程中，我们将使用 Metamask，这是浏览器中的一个虚拟钱包，用于管理您的以太坊帐户地址。如果您想了解更多有关以太坊交易如何运作的信息，请查看以太坊基金会的[此页面](https://ethereum.org/en/developers/docs/transactions/)。

[您可以在此处](https://metamask.io/download.html)免费下载并创建 Metamask 帐户。当您创建一个帐户时，或者如果您已经有一个帐户，请确保切换到右上角的“Ropsten 测试网络”（这样我们就不会处理真钱）。

<figure><img src="/files/aZSJYbKiMIT7qdM9fRLF" alt=""><figcaption></figcaption></figure>

**从Faucet添加ETH**

为了铸造我们的 NFT（或在以太坊区块链上签署任何交易），我们需要一些假 Eth。要获得 Eth，您可以转到Ropsten**Faucet**并输入您的 Ropsten 帐户地址，然后单击“发送 Ropsten Eth”。不久之后，您应该会在您的 Metamask 帐户中看到 Eth！

**检查您的余额**

为了仔细检查我们的余额，让我们使用[Alchemy 的 composer 工具发出](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D)[eth\_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance)请求。这将返回我们钱包中的 ETH 数量。输入您的 Metamask 帐户地址并单击“发送请求”后，您应该会看到如下响应：

```js
{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}
```

> **注意：**&#x8FD9;个结果是在 wei 而不是 eth 中。魏被用作以太的最小面额。从 wei 到 eth 的转换是：1 eth = 10¹⁸ wei。因此，如果我们将 0xde0b6b3a7640000 转换为十进制，我们将得到 1\*10¹⁸，等于 1 eth。

我们的假钱都在那里！🤑

### 第 4 步：将 Metamask 连接到您的 UI

现在我们的 Metamask 钱包已经设置好了，让我们将我们的 dApp 连接到它吧！

因为我们想要规定[MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)范例，我们将创建一个单独的文件，其中包含我们的函数来管理我们的 dApp 的逻辑、数据和规则，然后将这些函数传递给我们的前端（我们的 Minter.js 组件).

**连接钱包功能**

为此，让我们在**src目录中创建一个名为utils**的新文件夹，并在其中添加一个名为**interact.js**的文件，该文件将包含我们所有的钱包和智能合约交互功能。

在我们的**interact.js**文件中，我们将编写一个**connectWallet**函数，然后我们将导入该函数并在我们的**Minter.js**组件中调用。

在您的**interact.js**文件中，添加以下内容

```js
export const connectWallet = async () => {
  if (window.ethereum) {
    try {
      const addressArray = await window.ethereum.request({
        method: "eth_requestAccounts",
      });
      const obj = {
        status: "👆🏽 Write a message in the text-field above.",
        address: addressArray[0],
      };
      return obj;
    } catch (err) {
      return {
        address: "",
        status: "😥 " + err.message,
      };
    }
  } else {
    return {
      address: "",
      status: (
        <span>
          <p>
            {" "}
            🦊{" "}
            <a target="_blank" href={`https://metamask.io/download.html`}>
              You must install Metamask, a virtual Ethereum wallet, in your
              browser.
            </a>
          </p>
        </span>
      ),
    };
  }
};
```

让我们分解这段代码的作用：

首先，我们的函数检查您的浏览器是否启用了**window\.ethereum 。**

> window\.ethereum 是由 Metamask 和其他钱包提供商注入的全球 API，允许网站请求用户的以太坊帐户。如果获得批准，它可以从用户连接的区块链中读取数据，并建议用户签署消息和交易。查看[Metamask 文档](https://docs.metamask.io/guide/ethereum-provider.html#table-of-contents)以获取更多信息！

如果**window\.ethereum** *不*存在，则表示未安装 Metamask。这导致返回一个 JSON 对象，其中返回的**地址**是一个空字符串，**状态**JSX 对象中继用户必须安装 Metamask。

> **我们编写的大多数函数将返回 JSON 对象，我们可以使用这些对象来更新我们的状态变量和 UI。**

现在，如果**window\.ethereum***存在*，那就是事情变得有趣的时候。

使用 try/catch 循环，我们将尝试通过调用[window.ethereum.request({ method: "eth\_requestAccounts" });连接到 Metamask。](https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts)调用此函数将在浏览器中打开 Metamask，系统将提示用户将他们的钱包连接到您的 dApp。 &#x20;

* 如果用户选择连接，**方法：“eth\_requestAccounts”** 将返回一个数组，其中包含连接到 dApp 的所有用户帐户地址。总之，我们的**connectWallet**函数将返回一个 JSON 对象，其中包含此数组中的*第一个* **地址（请参阅第 9 行）和一条状态**消息，提示用户向智能合约写入消息。
* 如果用户拒绝连接，则 JSON 对象将包含返回**地址**的空字符串和反映用户拒绝连接的**状态消息。**

**将 connectWallet 功能添加到您的 Minter.js UI 组件**

现在我们已经编写了这个**connectWallet**函数，让我们将它连接到我们的**Minter.js。**&#x96F6;件。

首先，我们必须通过添加将函数导入到**Minter.js**文件中

**从“./utils/interact.js”导入{connectWallet}；**&#x5230; Minter.js 文件的顶部。**Minter.js**的前 11 行现在应该如下所示：

```js
import { useEffect, useState } from "react";
import { connectWallet } from "./utils/interact.js";

const Minter = (props) => {

  //State variables
  const [walletAddress, setWallet] = useState("");
  const [status, setStatus] = useState("");
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [url, setURL] = useState("");
```

然后，在我们的**connectWalletPressed**函数中，我们将调用导入的**connectWallet**函数，如下所示：

```js
const connectWalletPressed = async () => {
  const walletResponse = await connectWallet();
  setStatus(walletResponse.status);
  setWallet(walletResponse.address);
};
```

请注意我们的大部分功能是如何从**interact.js**文件中的**Minter.js**组件中抽象出来的？这是为了让我们遵守 MVC 范式！

在**connectWalletPressed**中，我们只需对导入的**connectWallet**函数进行 await 调用，并使用其响应，通过状态挂钩更新**状态**和**walletAddress变量。**

现在，让我们保存这两个文件（**Minter.js**和**interact.js**）并测试我们目前的 UI。

[在http://localhost:3000/](http://localhost:3000/)页面打开浏览器，然后点击页面右上角的“连接钱包”按钮。

如果您安装了 Metamask，系统会提示您将钱包连接到 dApp。接受连接邀请。

您应该看到钱包按钮现在反映您的地址已连接！哟哟哟🔥

接下来，尝试刷新页面……这很奇怪。我们的钱包按钮提示我们连接 Metamask，即使它已经连接......🔥

<figure><img src="/files/bTh8vNlfFMXvzCpWSM8G" alt=""><figcaption></figcaption></figure>

不过别担心！**我们可以通过实现一个名为getCurrentWalletConnected**的函数轻松解决这个问题，该函数将检查地址是否已经连接到我们的 dApp 并相应地更新我们的 UI！ &#x20;

**getCurrentWalletConnected 函数**

在您的**interact.js**文件中，添加以下**getCurrentWalletConnected**函数：

```js
export const getCurrentWalletConnected = async () => {
  if (window.ethereum) {
    try {
      const addressArray = await window.ethereum.request({
        method: "eth_accounts",
      });
      if (addressArray.length > 0) {
        return {
          address: addressArray[0],
          status: "👆🏽 Write a message in the text-field above.",
        };
      } else {
        return {
          address: "",
          status: "🦊 Connect to Metamask using the top right button.",
        };
      }
    } catch (err) {
      return {
        address: "",
        status: "😥 " + err.message,
      };
    }
  } else {
    return {
      address: "",
      status: (
        <span>
          <p>
            {" "}
            🦊{" "}
            <a target="_blank" href={`https://metamask.io/download.html`}>
              You must install Metamask, a virtual Ethereum wallet, in your
              browser.
            </a>
          </p>
        </span>
      ),
    };
  }
};
```

这段代码与我们之前编写的**connectWallet**函数*非常*相似。

主要区别在于，这里我们调用方法 **eth\_accounts ，而不是调用方法eth\_requestAccounts**，它打开 Metamask 供用户连接他们的钱包，它只返回一个数组，其中包含当前连接到我们的 dApp 的 Metamask 地址。

要查看此函数的运行情况，让我们在**Minter.js**组件的**useEffect**函数中调用它。

就像我们对**connectWallet**所做的那样，我们必须将这个函数从我们的**interact.js**文件导入到我们的**Minter.js**文件中，如下所示：

```js
import { useEffect, useState } from "react";
import {
  connectWallet,
  getCurrentWalletConnected //import here
} from "./utils/interact.js";
```

现在，我们只需在**useEffect**函数中调用它：

```js
useEffect(async () => {
    const {address, status} = await getCurrentWalletConnected();
    setWallet(address)
    setStatus(status);
}, []);
```

注意，我们使用调用**getCurrentWalletConnected**的响应来更新我们的**walletAddress**和**status**状态变量。

添加此代码后，请尝试刷新我们的浏览器窗口。该按钮应该表明您已连接，并显示已连接钱包地址的预览 - 即使在您刷新后也是如此！🤗

**实现 addWalletListener**

我们 dApp 钱包设置的最后一步是实现钱包监听器，以便我们的 UI 在我们的钱包状态发生变化时更新，例如当用户断开连接或切换帐户时。

在您的**Minter.js**文件中，添加如下所示的函数**addWalletListener**：

```js
function addWalletListener() {
  if (window.ethereum) {
    window.ethereum.on("accountsChanged", (accounts) => {
      if (accounts.length > 0) {
        setWallet(accounts[0]);
        setStatus("👆🏽 Write a message in the text-field above.");
      } else {
        setWallet("");
        setStatus("🦊 Connect to Metamask using the top right button.");
      }
    });
  } else {
    setStatus(
      <p>
        {" "}
        🦊{" "}
        <a target="_blank" href={`https://metamask.io/download.html`}>
          You must install Metamask, a virtual Ethereum wallet, in your
          browser.
        </a>
      </p>
    );
  }
}
```

让我们快速分解这里发生的事情：

首先，我们的函数检查是否启用了**window\.ethereum**（即安装了 Metamask）。

* 如果不是，我们只需将**status**状态变量设置为提示用户安装 Metamask 的 JSX 字符串。
* 如果启用，我们在第 3 行设置侦听器**window\.ethereum.on("accountsChanged")**&#x4EE5;侦听 Metamask 钱包中的状态更改，其中包括用户何时将其他帐户连接到 dApp、切换帐户或断开帐户。如果至少有一个帐户已连接，则**walletAddress**状态变量将更新为侦听器返回的**帐户**数组中的第一个帐户。否则，**walletAddress**被设置为空字符串。

最后，我们必须在我们的**useEffect**函数中调用它：

```js
useEffect(async () => {
    const {address, status} = await getCurrentWalletConnected();
    setWallet(address)
    setStatus(status);

    addWalletListener();
}, []);
```

瞧！我们已经完成了所有钱包功能的编程！现在我们的钱包已经设置好了，让我们来看看如何铸造我们的 NFT！

### 步骤 5：NFT 元数据 101

所以请记住我们刚刚在本教程的第 0 步中谈到的 NFT 元数据——它使 NFT 栩栩如生，使其具有属性，例如数字资产、名称、描述和其他属性。

我们需要将此元数据配置为 JSON 对象并存储它，这样我们就可以在调用智能合约的**mintNFT函数时将其作为tokenURI**参数传入。

“资产链接”、“名称”、“描述”字段中的文本将包含我们 NFT 元数据的不同属性。我们会将此元数据格式化为 JSON 对象，但对于存储此 JSON 对象的位置有几个选项：

* 我们可以将其存储在以太坊区块链上；然而，由于以太坊的性质，这样做会非常昂贵（我们说的是数百美元以上）。❌
* 我们可以将其存储在中央服务器上，例如 AWS 或 Firebase。但这会破坏我们的权力下放精神。❌
* 我们可以使用 IPFS，一种去中心化协议和对等网络，用于在分布式文件系统中存储和共享数据。由于该协议去中心化且免费，是我们最好的选择！✅

为了将我们的元数据存储在 IPFS 上，我们将使用[Pinata](https://pinata.cloud/)，一个方便的 IPFS API 和工具包。在下一步中，我们将详细说明如何执行此操作！

### 第 6 步：使用[Pinata](https://pinata.cloud/)将您的元数据固定到 IPFS

如果您没有 Pinata 帐户，请[在此处](https://pinata.cloud/signup)注册一个免费帐户并完成验证您的电子邮件和帐户的步骤。

**创建您的 Pinata API 密钥**

导航到<https://pinata.cloud/keys>页面，然后选择顶部的“New Key”按钮，将 Admin 小部件设置为已启用，并为您的密钥命名。

<figure><img src="/files/CjAzzw1pNvTOVaoYPQFG" alt=""><figcaption></figcaption></figure>

然后您将看到一个弹出窗口，其中包含您的 API 信息。确保将它放在安全的地方。

<figure><img src="/files/DKwfTmKU5eKmfn4M7QaS" alt=""><figcaption></figcaption></figure>

现在我们的密钥已经设置好了，让我们把它添加到我们的项目中，这样我们就可以使用它了。

**创建一个 .env 文件**

我们可以安全地将我们的 Pinata 密钥和秘密存储在环境文件中。让我们在您的项目目录中安装[dotenv 包。](https://www.npmjs.com/package/dotenv)

在您的终端中打开一个新选项卡（与正在运行的本地主机分开）并确保您位于**minter-starter-files 文件** 夹中，然后在您的终端中运行以下命令：

```js
npm install dotenv --save
```

接下来，通过在命令行中输入以下内容，在**minter-starter-files**的根目录中创建一&#x4E2A;**.env文件：**

```js
vim .env
```

这将在 vim（一个文本编辑器）中打开你&#x7684;**.env**文件。要保存它，请按顺序在键盘上点击“esc”+“：”+“q”。

接下来，在 VSCode 中，导航到您的 .env 文件并向其中添加您的 Pinata API 密钥和 API 密码，如下所示：

```js
REACT_APP_PINATA_KEY =
REACT_APP_PINATA_SECRET =
```

保存文件，然后您就可以开始编写将 JSON 元数据上传到 IPFS 的函数了！

**实施 pinJSONToIPFS**

对我们来说幸运的是，Pinata 有一个[专门用于将 JSON 数据上传到 IPFS 的 API](https://pinata.cloud/documentation#PinJSONToIPFS)和一个方便的 JavaScript with axios 示例，我们可以使用它，只需稍作修改。

在您的 utils 文件夹中，让我们创建另一个名为 pinata.js 的文件，然后从 .env 文件中导入我们的 Pinata 秘密和密钥，如下所示：

```js
require('dotenv').config();
const key = process.env.REACT_APP_PINATA_KEY;
const secret = process.env.REACT_APP_PINATA_SECRET;
```

接下来，将下面的附加代码粘贴到您的 pinata.js 文件中。别担心，我们会分解一切的含义！

```js
require('dotenv').config();
const key = process.env.REACT_APP_PINATA_KEY;
const secret = process.env.REACT_APP_PINATA_SECRET;

const axios = require('axios');

export const pinJSONToIPFS = async(JSONBody) => {
    const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`;
    //making axios POST request to Pinata ⬇️
    return axios
        .post(url, JSONBody, {
            headers: {
                pinata_api_key: key,
                pinata_secret_api_key: secret,
            }
        })
        .then(function (response) {
           return {
               success: true,
               pinataUrl: "https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash
           };
        })
        .catch(function (error) {
            console.log(error)
            return {
                success: false,
                message: error.message,
            }

    });
};
```

那么这段代码到底做了什么？

首先，它导入[axios](https://www.npmjs.com/package/axios)，一个基于 promise 的浏览器 HTTP 客户端和 node.js，我们将使用它向 Pinata 发出请求。

然后我们有我们的异步函数**pinJSONToIPFS**，它将**JSONBody**作为其输入，并在其标头中使用 Pinata api 密钥和秘密，所有这些都向其**pinJSONToIPFS** API 发出 POST 请求。

* 如果此 POST 请求成功，那么我们的函数将返回一个 JSON 对象，其中**成功**布尔值为 true 以及固定元数据的**pinataUrl**。我们将使用返回的这个**pinataUrl**作为**tokenURI**输入到我们的智能合约的 mint 函数。
* 如果此 post 请求失败，那么我们的函数将返回一个 JSON 对象，**成功**布尔值为 false，并返回一个**消息**字符串来传递我们的错误。

与我们的**connectWallet**函数返回类型一样，我们返回 JSON 对象，因此我们可以使用它们的参数来更新我们的状态变量和 UI。

### 第 7 步：加载您的智能合约

现在我们已经有了通过 pinJSONToIPFS 函数将 NFT 元数据上传到 IPFS 的方法，我们将需要一种方法来加载智能合约的实例，以便我们可以调用其 mintNFT 函数。

> 正如我们之前提到的，在本教程中我们将使用[现有的 NFT 智能合约](https://ropsten.etherscan.io/address/0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE)；但是，如果您想了解我们是如何制作的，或者想自己制作一个，我们强烈建议您查看我们的其他教程[“如何创建 NFT”。](https://docs.alchemyapi.io/alchemy/tutorials/how-to-create-an-nft)

**合约ABI**

如果您仔细检查我们的文件，您会注意到在我们的**src**目录中，有一个**contract-abi.json**文件。ABI 是指定合约将调用哪个函数以及确保该函数将以您期望的格式返回数据所必需的。

我们还需要一个 Alchemy API 密钥和 Alchemy Web3 API 来连接到以太坊区块链并加载我们的智能合约。但首先，您需要一个 Alchemy 帐户。

[**如果您还没有 Alchemy 帐户，请在此处免费注册。**](https://dashboard.alchemyapi.io/signup/?a=web3u-nft-minter-2)

创建 Alchemy 帐户后，您可以通过创建应用程序来生成 API 密钥。这将允许我们向 Ropsten 测试网络发出请求。

通过将鼠标悬停在导航栏中的“应用程序”上并单击“创建应用程序”，导航到 Alchemy 仪表板中的“创建应用程序”页面

<figure><img src="/files/fNbny7FWe62H0f85chqc" alt=""><figcaption></figcaption></figure>

为您的应用程序命名（我们选择“我的第一个 NFT！”），提供简短描述，为环境选择“Staging”（用于您的应用程序簿记），并为您的网络选择“Ropsten”。

<figure><img src="/files/wyvzkL7tigSFWenoSzUN" alt=""><figcaption></figcaption></figure>

单击“创建应用程序”即可！您的应用程序应出现在下表中。

太棒了，现在我们已经创建了我们的 HTTP Alchemy API URL，像这样将它复制到您的剪贴板......

…然后让我们将它添加到我们&#x7684;**.env**文件中。总之，您的 .env 文件应该如下所示：

```js
REACT_APP_PINATA_KEY =
REACT_APP_PINATA_SECRET =
REACT_APP_ALCHEMY_KEY = https://eth-ropsten.alchemyapi.io/v2/
```

现在我们有了我们的合约 ABI 和我们的 Alchemy API 密钥，我们准备好使用[Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3)加载我们的智能合约。

**设置您的 Alchemy Web3 端点和合约**

首先，如果您还没有安装Alchemy Web3 ，则需要导航到主目录来安装[Alchemy Web3 ：终端中](https://github.com/alchemyplatform/alchemy-web3)**的 nft-minter-tutorial**：

```js
cd ..
npm install @alch/alchemy-web3
```

接下来让我们回到我们的**interact.js**文件。在文件的顶部，添加以下代码以从 .env 文件导入您的 Alchemy 密钥并设置您的 Alchemy Web3 端点：

```js
require('dotenv').config();
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY;
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);
```

> [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3)是[Web3.js](https://web3js.readthedocs.io/en/v1.2.9/)的包装器，提供增强的 API 方法和其他重要优势，让您作为 web3 开发人员的生活更轻松。它旨在要求最少的配置，因此您可以立即开始在您的应用程序中使用它！

接下来，让我们将合约 ABI 和合约地址添加到我们的文件中。

```js
require('dotenv').config();
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY;
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);

const contractABI = require('../contract-abi.json')
const contractAddress = "0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE";
```

一旦我们拥有了这两个，我们就可以开始编写我们的 mint 函数了！

### 第八步：实现mintNFT功能

在您的**interact.js**文件中，让我们定义我们的函数**mintNFT**，它将以同名方式铸造我们的 NFT。

因为我们将进行大量异步调用（调用 Pinata 以将我们的元数据固定到 IPFS，调用 Alchemy Web3 来加载我们的智能合约，调用 Metamask 来签署我们的交易），我们的功能也将是异步的。

我们函数的三个输入将是我们的数字资产的**url 、名称**和**描述**。**在connectWallet**函数下方添加以下函数签名：

```js
export const mintNFT = async(url, name, description) => {
}
```

**输入错误处理**

自然地，在函数的开头进行某种输入错误处理是有意义的，因此如果我们的输入参数不正确，我们将退出该函数。在我们的函数中，让我们添加以下代码：

```js
export const mintNFT = async(url, name, description) => {
 //error handling
 if (url.trim() == "" || (name.trim() == "" || description.trim() == "")) {
   return {
    success: false,
    status: "❗Please make sure all fields are completed before minting.",
   }
  }
}
```

本质上，如果任何输入参数是空字符串，那么我们将返回一个 JSON 对象，其中**成功**布尔值是 false，**状态**字符串表示我们 UI 中的所有字段都必须是完整的。

**将元数据上传到 IPFS**

一旦我们知道我们的元数据格式正确，下一步就是将它包装成一个 JSON 对象并通过我们编写的**pinJSONToIPFS**将其上传到 IPFS ！

为此，我们首先需要将**pinJSONToIPFS**函数导入到我们的**interact.js**文件中。在**interact.js**的最顶部，让我们添加：

```js
import {pinJSONToIPFS} from './pinata.js'
```

回想一下，**pinJSONToIPFS**接收一个 JSON 正文。因此，在调用它之前，我们需要将**url**、**name**和**description**参数格式化为 JSON 对象。

让我们更新我们的代码以创建一个名为**元数据**的 JSON 对象，然后使用此**元数据参数调用pinJSONToIPFS**：

```js
export const mintNFT = async(url, name, description) => {
 //error handling
 if (url.trim() == "" || (name.trim() == "" || description.trim() == "")) {
        return {
            success: false,
            status: "❗Please make sure all fields are completed before minting.",
        }
  }

  //make metadata
  const metadata = new Object();
  metadata.name = name;
  metadata.image = url;
  metadata.description = description;

  //make pinata call
  const pinataResponse = await pinJSONToIPFS(metadata);
  if (!pinataResponse.success) {
      return {
          success: false,
          status: "😢 Something went wrong while uploading your tokenURI.",
      }
  }
  const tokenURI = pinataResponse.pinataUrl;
}
```

请注意，我们将调用**pinJSONToIPFS(metadata)**&#x7684;响应存储在**pinataResponse**对象中。然后，我们解析这个对象是否有任何错误。

如果出现错误，我们将返回一个 JSON 对象，其中**成功**布尔值是 false 并且我们的**状态**字符串会传达我们的调用失败。否则，我们从 pinataResponse 中提取pinataURL**并将其**存储为我们的**tokenURIvariable**。

现在是时候使用我们在文件顶部初始化的 Alchemy Web3 API 加载我们的智能合约了。将以下代码行添加到**mintNFT函数的底部，以在window\.contract**全局变量中设置合约：

```js
window.contract = await new web3.eth.Contract(contractABI, contractAddress);
```

**在我们的mintNFT**函数中添加的最后一件事是我们的以太坊交易：

```js
//set up your Ethereum transaction
 const transactionParameters = {
        to: contractAddress, // Required except during contract publications.
        from: window.ethereum.selectedAddress, // must match user's active address.
        'data': window.contract.methods.mintNFT(window.ethereum.selectedAddress, tokenURI).encodeABI()//make call to NFT smart contract
 };

//sign the transaction via Metamask
 try {
    const txHash = await window.ethereum
        .request({
            method: 'eth_sendTransaction',
            params: [transactionParameters],
        });
    return {
        success: true,
        status: "✅ Check out your transaction on Etherscan: https://ropsten.etherscan.io/tx/" + txHash
    }
 } catch (error) {
    return {
        success: false,
        status: "😥 Something went wrong: " + error.message
    }

 }
```

如果您已经熟悉以太坊交易，您会注意到其结构与您所见非常相似。

首先，我们设置交易参数。

* **指定**收件人地址（我们的智能合约）
* **from**指定交易的签名者（用户连接到 Metamask 的地址：**window\.ethereum.selectedAddress**）
* **数据**包含对我们的智能合约**mintNFT**方法的调用，该方法接收我们的**tokenURI**和用户的钱包地址**window\.ethereum.selectedAddress**作为输入

然后，我们进行 await 调用**window\.ethereum.request**，我们要求 Metamask 签署交易。请注意，在此请求中，我们指定了我们的 eth 方法 (eth\_SentTransaction) 并传入了我们的**transactionParameters**。此时，Metamask 将在浏览器中打开，并提示用户签署或拒绝交易。

* 如果交易成功，该函数将返回一个 JSON 对象，其中布尔值**成功**设置为 true，**状态**字符串提示用户查看 Etherscan 以获取有关其交易的更多信息。
* 如果交易失败，该函数将返回一个 JSON 对象，其中**成功**布尔值设置为 false，**状态**字符串中继错误消息。

总之，我们的**mintNFT**函数应该是这样的：

```js
export const mintNFT = async(url, name, description) => {

    //error handling
    if (url.trim() == "" || (name.trim() == "" || description.trim() == "")) {
        return {
            success: false,
            status: "❗Please make sure all fields are completed before minting.",
        }
    }

    //make metadata
    const metadata = new Object();
    metadata.name = name;
    metadata.image = url;
    metadata.description = description;

    //pinata pin request
    const pinataResponse = await pinJSONToIPFS(metadata);
    if (!pinataResponse.success) {
        return {
            success: false,
            status: "😢 Something went wrong while uploading your tokenURI.",
        }
    }
    const tokenURI = pinataResponse.pinataUrl;

    //load smart contract
    window.contract = await new web3.eth.Contract(contractABI, contractAddress);//loadContract();

    //set up your Ethereum transaction
    const transactionParameters = {
        to: contractAddress, // Required except during contract publications.
        from: window.ethereum.selectedAddress, // must match user's active address.
        'data': window.contract.methods.mintNFT(window.ethereum.selectedAddress, tokenURI).encodeABI() //make call to NFT smart contract
    };

    //sign transaction via Metamask
    try {
        const txHash = await window.ethereum
            .request({
                method: 'eth_sendTransaction',
                params: [transactionParameters],
            });
        return {
            success: true,
            status: "✅ Check out your transaction on Etherscan: https://ropsten.etherscan.io/tx/" + txHash
        }
    } catch (error) {
        return {
            success: false,
            status: "😥 Something went wrong: " + error.message
        }
    }
}
```

这是一个巨大的功能！现在，我们只需要将我们的**mintNFT**函数连接到我们的**Minter.js**组件......

### 第 9 步：将 mintNFT 连接到我们的 Minter.js 前端

打开你的**Minter.js**文件并更新**import { connectWallet } from "./utils/interact.js"**；顶部的行是：

```js
import { connectWallet, mintNFT } from "./utils/interact.js";
```

最后，实现**onMintPressed**函数以等待调用您导入的**mintNFT**函数并更新**status**状态变量以反映我们的交易是成功还是失败：

```js
const onMintPressed = async () => {
    const { status } = await mintNFT(url, name, description);
    setStatus(status);
};
```

### 第 10 步：将您的 NFT 部署到实时网站

‌准备好让您的项目上线供用户互动了吗？查看本[教程](https://app.gitbook.com/@alchemyapi/s/alchemy/tutorials/nft-minter/how-do-i-deploy-nfts-online)以将您的 Minter 部署到实时网站：

### 第 11 步：席卷区块链世界 🚀

JK，你已经完成了教程！回顾一下，通过构建 NFT 铸币机，您成功学会了如何：

* 通过您的前端项目连接到 Metamask
* 从您的前端调用智能合约方法
* 使用 Metamask 签署交易

而且，一如既往，如果您有任何疑问，我们会在Discord中为您提供帮助。我们迫不及待地想看看您如何将本教程中的概念应用到您未来的项目中！🧙‍​​♂️


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.chengwf.com/build-your-first-nft/building-a-full-stack-nft-dapp.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
