如何用 JavaScript 玩转 NFT

4,554 阅读14分钟

NFT(非同质化 Token),作为一种新技术,它帮助我们以数字化的方式保护我们创作资产的所有权。相信有不少人已经听说过 NFT,但真正使用过或者开发过的人却很少,希望本文能通过一种浅显易懂的方式向您展示作为一个前端开发者是如何来玩转 NFT 的。我将介绍其重要背景知识、第三方服务、具体开发编码以及最终将我们自己铸造的 NFT 部署到 Ropsten 测试网络上去。

重要申明:本文不是炒币教程,不包含任何投资观点和建议,仅做文章研究 NFT 及其相关技术本身

背景知识

在我们真正开始之前,先来了解下有关 NFT 的一些技术背景知识和特点

同质化与非同质化

其实就是可互换性,本质上就是用一件物品去换一个同等价值的物品的能力,两张相同面额的纸币(如50元纸币)在任何地方都是等价的,你可以放心的与他人交换。换句话说,如果两者的价值不相等那就不可交换,比如任何《蒙娜丽莎》名画的赝品都不可能等价于真品,因为它是独一无二的。

当然有些物品既可以是同质化的也可以是非同质化的,比如:经济舱的座位一般来说都是等价的,有些人可能更倾向于靠窗的座位,因此这就使得座位的价值在这些人的眼中变低了。

区块链

区块链就是一个公共数据库,或者说是一个跟踪和保持交易记录的数字账本。它被复制到作为链的一部分的多个计算机系统上。本文将在以太坊区块链上来创造 NFT。

智能合约和 NFT

智能合约是部署到区块链并按原样运行的简单程序,这意味着它们不受用户控制。我们可以使用智能合约来创建和跟踪我们的 Token

NFT 是一个符合 ERC-721 标准的数字数据存储,它生存在一个公共区块链上。NFT 包含它们所代表的资产的信息或数据,它可以是一个数字项目,如 Tweet,也可以是一个物理项目,如连帽衫。

如果一个智能合约实现了ERC-721标准,它就可以被认为是一个NFT,而NFT是一个智能合约的实例。每次我们铸造一个新的NFT时,我们使用已经部署到区块链上的智能合约代码。

公共网络:Mainnet 和 Testnet

以太坊使用多个网络。生产中使用的网络通常被称为 Mainnet,其他用于测试的网络被称为 Testnet。我们将把我们创建的 NFT 部署到 Ropsten Testnet,以太坊的 PoW Testnet。

请注意,当我们最终将我们的 NFT 部署到生产或主网时,我们在 Ropsten Testnet 上的交易历史和余额将不会被继承。把 Testnet 看作是一个公共的测试/开发环境,把 Mainnet 看作是一个生产环境。

私有网络

如果一个网络中的所有节点都没有连接到公共区块链,则被认为是私有网络。你可以在私有网络上运行以太坊区块链,比如你的本地机器,或者在一组机器上运行,比如财团网络,这些机器在 Mainnet 或 Testnet 上都无法访问。

在像内网这样的机器群中运行以太坊区块链,需要用节点来验证交易,节点是运行在客户端的以太坊软件,用于验证区块和交易数据。

HardHatGanache 是以太坊区块链开发环境的两个例子,你可以在本地机器上运行,以编译、测试、部署和调试你的智能合约应用程序。

本次我们将在公共网络上运行我们的应用程序,这样它就可以被连接到网络的任何人访问。

水龙头 Faucets

为了测试我们的应用程序,我们需要从水龙头获得以太币(ETH),即以太坊加密货币。水龙头,如Ropsten 水龙头,它是一种网络应用,允许你指定并发送测试以太币到一个地址,然后你可以用它来完成在测试网的交易。

交易所的以太币价格是由任何时候在主网上发生的交易决定的。如果你选择在私有网络上运行你的以太坊应用程序,那么你就不需要测试以太币,也不需要水龙头了。

节点和客户端

如前所述,节点用于验证区块和交易数据。你可以使用 GethOpenEthereum 等客户端创建自己的节点,并通过验证区块链上的交易和区块为以太坊区块链作出贡献。

你也可以略过自己创建节点的过程,而使用像 Alchemy 这样的“节点即服务”平台在云上托管的节点。我们可以快速从开发到生产,并确保我们的应用程序获得重要的指标。

我们将使用 Alchemy API 将我们的应用程序部署到 Ropsten 区块链上。Alchemy 也被称为区块链的 AWS,并提供开发者工具,使我们能够查看我们的应用程序是如何运行的。

铸造 ERC721 Token

铸造是首次创造东西的过程,在这里我们将在区块链上发布一个 ERC721 Token 的唯一实例。ERC-721 是创建 NFT 的标准,ERC721 Token 是发布到以太坊区块链上的数字内容的唯一代表。没有两个 Token 是相同的,所以每次你用相同的代码区块铸造一个新的 Token 时,都会产生一个新的地址。

创建智能合约

前置条件

  • NodeJS 及 NPM
  • JavaScript 基础知识

如果我是一个摄影爱好者,还有什么方法比自己来铸造一个 NFT 来保护作品更好的方法呢?我可以把它转让给任何喜欢我作品的人,然后他们可以通过在 Mainnet 或 Testnet 上使用 NFT 数据来证明其拥有原始作品的权利。

pic.jpg

接下来我们将铸造以上这幅我在上海兴业太古汇拍摄的穹顶图片的 NFT

创建一个 Alchemy 账号

我们将使用 Alchemy 来编写我们的 NFT,这将让我们快速跳过搭建本地以太坊环境的工作。

首先,来到“创建应用“页面,使用我的英文名作为团队名(你也可以自己任意取名),然后我使用“Hkri Taikoo Hui NFT”作为应用名,并且使用 ETH 的 Ropsten 网络。然后你将在控制面板中找到刚才创建的应用。

image.png

创建一个以太坊账号

我们需要创建一个钱包来持有一个以太坊账户。为了将我们的应用程序部署到网络上,我们需要支付一笔以以太坊来计价的费用,即所谓的汽油费(gas fee)。在测试我们的应用程序时,我们可以使用测试以太坊网络来完成这个过程,后面我们将从水龙头里获取测试以太币。

我们将使用 MetaMask 创建一个以太坊账户,它是一个虚拟钱包,可以下载其 Chrome 扩展来获得。

当你安装完 MetaMask 并创建一个账号之后,从 Chrome 扩展应用中打开 MetaMask,并选择 Ropsten 网络。

image.png

MetaMask 将自动生成一个以以太币计价的钱包地址,你可以点击 Account 1 来复制钱包地址。

从水龙头获取测试以太币

让我们使用 Ropsten 水龙头来向我们的钱包发送测试以太币。

image.png

过几分钟就会到账 1 ETH,你也可以从其他渠道获得 Ropsten 测试以太币,比如我一共获得了 4.15 ETH。

image.png

编写 NFT Token

现在我们可以开始编写 NFT Token 了。首先,在本地创建一个开发目录,然后执行 NPM 初始化

$ mkdir hkri-taikoo-hui-nft && cd hkri-taikoo-hui-nft
$ npm init -y

我们需要安装 Hardhat 来帮助我们在本地环境编译应用程序,并在部署到 Ropsten 之前测试程序功能。

要在项目中安装 Hardhat 非常简单,只需要通过 npm 安装依赖包即可:

$ npm i -D hardhat

现在可以执行 npx hardhat 进行初始化 Hardhat

image.png

选择“Create an empty hardhat.config.js”,稍后我们将使用这个文件来配置我们的项目。

现在,我们在项目中创建两个文件夹,一个用来放智能合约代码,另一个用来放一些部署和智能合约交互的脚本:

$ mkdir contracts && mkdir scripts

创建智能合约

我们使用 Solidity 语言来编写智能合约,智能合约代码将基于 OpenZeppelin ERC721 的实现。ERC721是用于表现 NFT 所有权的标准,而 OpenZeppelin 合约提供了一些便捷使用 ERC72 的方法。

安装 OpenZeppelin 合约包:

$ npm i @openzeppelin/contracts@4.0.0

contracts 目录下创建 HkriTaikooHuiNFT.sol 文件,并拷贝如下内容:

// https://wizard.openzeppelin.com/ 可以通过这个向导页面创建代码模板
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract HkriTaikooHui is ERC721, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("HkriTaikooHui", "Tor") {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId)
        internal
        override(ERC721, ERC721URIStorage)
    {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
}

由于合约开发和标准的错综复杂,对于新手很难一下子掌握其中的变化,因此这里推荐使用 OpenZeppilin 的合约生成向导来生成代码

连接 MetaMask 钱包

现在,我们将连接 MetaMask 钱包到项目。在虚拟钱包上每个交易都需要通过私钥来完成,因此我们需要获得我们 MetaMask 钱包的私钥。

在 Chrome 上打开 MetaMask 扩展应用,点击右上角的三点符号选择“Account Details”。然后点击“Export Private Key”按钮导出私钥。

image.png

重要‼️ 确保私钥的安全尤为重要,我们需要保护私钥以防在使用 Github 时意外暴露私钥。为此我们可以安装 dotenv 包。

$ npm i dotenv

在项目根目录创建一个 .env 文件,然后在其中添加 MetaMask 私钥。同时,你也需要在 Alchemy 中添加 API_URL 属性,可以在 Alchemy 的控制面板中找到 Apps 选择我们的应用,然后点击 VIEW KEY 获得 HTTP 属性。

image.png

# .env
METAMASK_PRIVATE_KEY="yourprivatekey"
API_URL="yourhttpurl"

设置 Ether.js

Ether.js 是一个用来简化与以太坊区块链交互的工具,我们将使用支持 Hardhat 的 Ether 插件。

$ npm i -D @nomiclabs/hardhat-ethers 'ethers@^5.0.0'

回到此前添加的 hardhat.config.js 文件头部,我们需要添加一些依赖项目:

/**
* @type import('hardhat/config').HardhatUserConfig
*/
require('dotenv').config(); // 使我们允许访问环境变量(此前设置的私钥、API_URL)
require("@nomiclabs/hardhat-ethers"); // 在部署脚本上运行一些由 Ehter 提供的用户友好的方法
const { API_URL, METAMASK_PRIVATE_KEY } = process.env;

module.exports = {
   solidity: "0.8.2",
   defaultNetwork: "ropsten", // 指定 Hardhat 应该将应用部署到哪个网络中
   networks: {
      hardhat: {},
      ropsten: {
         url: API_URL, // 指定在 Alchemy 上的后端 NodeJS 接口地址
         accounts: [`0x${METAMASK_PRIVATE_KEY}`] // MetaMask 私钥,用于完成交易
      }
   },
}

我们将通过 URL 连接到 Ropsten 测试网络,更详细的配置项可以参考这里

现在,我们运行 Hardhat 提供的编译命令,来检查一切是否正常:

$ npx hardhat compile

如果一切正常,你将看到以下成功的消息。

image.png

创建部署脚本

至此,我们可以打包我们的智能合约代码了,需要编写以下脚本来将我们的智能合约部署到以太坊区块链上去(Ropsten 测试网):

scripts 文件夹中,创建文件 deploy.js

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying contracts with the account:", deployer.address);

  console.log("Account balance:", (await deployer.getBalance()).toString());
  const NFT = await ethers.getContractFactory("HkriTaikooHui");

  // Start deployment, returning a promise that resolves to a contract object
  const nft = await NFT.deploy();
  console.log("Contract deployed to address:", nft.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

现在,我们来执行部署命令:

$ npx hardhat run scripts/deploy.js --network ropsten

几秒钟后,我们将看到我们的智能合约已经被成功部署到 Ropsten 测试网络上了。控制台也打印出了新建的智能合约的地址。

image.png

大约1分钟后等待区块链出块,让我们再次确认我们的智能合约是否被部署到了区块链上。我们可以访问 Ropsten Etherscan 来粘贴我们的合约地址到搜索栏,你可以立即看到合约的详情内容。

image.png

此时,如果你再次检查你的 MetaMask 钱包,你会发现你的余额减少了,因为扣除了部署合约需要的汽油费。现在,我们已经成功的将智能合约部署到了以太坊上。

铸造 NFT

我们的智能合约有两个入参:touri。其中 to 代表 NFT 接收者的钱包地址,uri 是此 NFT 的数据。

在区块链上存储数据需要在多个网络之间被处理、验证、复制,所以区块链上的数据存储代价很高。上传整个图片到区块链显然是不明智的,因此,你可以该 NFT 的元数据取代图片进行存储。

尽管一个 NFT 的 URL 可以被存储到区块链上,但这个链接并不稳定,它可能随时被下线。此外,那些能访问到该 URL 内容的人也可能会篡改它。因此,我们需要一种代价更低、更持久、去中心化且不可篡改的方式来存储数据。

使用 IPFS

IPFS 是一个分布式系统用于存储和访问文件,它使用 content addressing 来解决以上问题。

任何上传到 IPFS 的数据都会被赋予一个唯一内容识别码(CID)。一旦 CID 生成之后,它将永远代表所上传的数据本身,且该数据不可篡改。

这里有一个 IPFS URI 的例子:

ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi

部分浏览器(Chrome 90及以上版本)支持直接解析 ipfs 协议到 https://ipfs.io/ipfs/{CID} 的形式来展示图片。

有关 IPFS 的最佳实践可以阅读这篇文章

安装 Web3.js

让我们继续来添加 NFT 的元数据。我们将安装 Alchemy Web3 套件

$ npm i @alch/alchemy-web3

根据文档可见,Web3.js 是一个库,用于让你和本地或远程以太坊代码使用 HTTP、IPC 或 WebSocket 协议交互。

Alchemy 封装了 Web3.js 库,扩展了它在自动重试和更稳定的 Websocket 支持方面的功能特性

创建用于铸造 NFT 的脚本

scripts 文件夹中创建 mint-nft.js。然后添加如下代码:

require("dotenv").config();
const API_URL = process.env.API_URL;
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const alchemyWeb3 = createAlchemyWeb3(API_URL);
const contract = require("../artifacts/contracts/HkriTaikooHuiNFT.sol/HkriTaikooHui.json");

上传 NFT 元数据到 Pinata

Pinata 是一个使用 IPFS 协议来存储 NFT 元数据的平台。我们可以先创建一个账号。然后上传本次需要铸造的 NFT 图片。

image.png

一旦图片上传成功后,可以在控制面板中找到它,留意其中的 CID 字段,我们将稍后使用这个字符串。

先在应用程序根目录下创建一个 JSON 文件,nft-metadata.json 并添加以下信息,注意这的 image 需要使用刚才图片文件的 CID:

{
  "description": "一张在兴业太古汇拍摄的穹顶照片",
  "image": "https://ipfs.io/ipfs/QmWEsQXpHLJ5V92tR7VhPhbYBtBMZwAiN2DtTAm6D8vGxj",
  "photographer": "块马 @martintsan"
}

然后我们还需要上传这个 JSON 文件到 Pinata,之后你可以在控制面板看到两者

image.png

创建合约的实例

为了铸造 NFT,首先先找到我们刚才部署的合约地址:0x026FBBB73feF93a2b27258b1f7b8cd13D2dd8007,然后回到 mint-nft.js 文件,添加如下代码:

const contractAddress = "0x026FBBB73feF93a2b27258b1f7b8cd13D2dd8007";
const nftContract = new alchemyWeb3.eth.Contract(contract.abi, contractAddress);

然后更新 .env 文件,加入我们自己的 MetaMask 钱包地址:

METAMASK_PUBLIC_KEY="0x572BB0dd7A400D223E2E83ECfE468842277154D3"

接下来,我们需要创建一个交易,添加如下代码到 mint-nft.js

const METAMASK_PUBLIC_KEY = process.env.METAMASK_PUBLIC_KEY;
const METAMASK_PRIVATE_KEY = process.env.METAMASK_PRIVATE_KEY;

async function mintNFT(uri) {
  // 获取 nonce - nonce 是出于安全考虑,它用于记录交易序号以防止重放攻击
  const nonce = await alchemyWeb3.eth.getTransactionCount(
    METAMASK_PUBLIC_KEY,
    "latest"
  );
  const tx = {
    from: METAMASK_PUBLIC_KEY, // 我们 MetaMask 的公钥
    to: contractAddress, // 智能合约地址
    nonce: nonce, // nonce
    gas: 1000000, // 完成交易的预估汽油费
    data: nftContract.methods
      .safeMint("0xEB0EC48d8D5aD7745726B627f4297f5023086f60", uri) // 新建了一个钱包来接收 NFT
      .encodeABI(),
  };
}

我又新建了一个 MetaMask 钱包来接收 NFT,你也可以直接使用现在的钱包地址 METAMASK_PUBLIC_KEY。在生产环境这里的地址就是实际需要接收该 NFT 的钱包地址。

现在交易已经被创建,我们需要使用私钥 METAMASK_PRIVATE_KEY 来签收,继续在 mint-nft.js 文件的 mintNFT 方法中添加如下代码:

  const signPromise = alchemyWeb3.eth.accounts.signTransaction(
    tx,
    METAMASK_PRIVATE_KEY
  );
  signPromise
    .then((signedTx) => {
      alchemyWeb3.eth.sendSignedTransaction(
        signedTx.rawTransaction,
        function (err, hash) {
          if (!err) {
            console.log(
              "The hash of our transaction is: ",
              hash,
              "\nCheck Alchemy's Mempool to view the status of our transaction!"
            );
          } else {
            console.log(
              "Something went wrong when submitting our transaction:",
              err
            );
          }
        }
      );
    })
    .catch((err) => {
      console.log(" Promise failed:", err);
    });

最后,我们从刚才上传到 Pinata 的 nft-metadata.json 文件,复制其 CID 并传入 mintNFT 方法:

mintNFT("https://ipfs.io/ipfs/Qmb17cSuFzJ1bNZo8puCHpeWAEX5zTaAsqZcNL1C4spjhZ");

现在,我们可以在终端运行 node scripts/mint-nft.js 来铸造我们的 NFT,大约几秒钟之后,应该就能收到如下图所示的消息了

image.png

然后我们可以通过访问 Alchemy Mempool 来查看所有的交易信息及状态。

image.png

我们也可以在 Etherscan 上查到本次交易的信息,以及最重要的在区块链上的 nft-metadata.json 文件

image.png

向下滚动找到 input data,点击 decode input data 按钮。你可以看到我们传入的 NFT 元数据 JSON 文件 URL。

image.png

如果你在 Etherscan 上搜索该合约地址,可以查到所有由该合约铸造的 NFT 记录列表以及所有通过该合约发生的交易记录。

image.png

添加 NFT 到 MetaMask 钱包

  1. 复制合约地址
  2. 在 Chrome 中打开 MetaMask 钱包扩展应用
  3. 选择 Ropsten Test Network
  4. 点击 import token
  5. 粘贴合约地址到其中,MetaMask 会自动生成该 NFT 的符号。
  6. 点击 next 添加 NFT 到钱包

image.png

结论

至此,我们学习了以下内容:

  • 在 Ropsten Testnet 上创建和部署智能合约
  • 通过部署智能合约到区块链来铸造 NFT
  • 使用基于 IPFS 的内容地址来为我们的 NFT 创建元数据
  • 在 MetaMask 钱包中浏览 NFT

在生产环境,所有的步骤都和本文提到的一致,唯一的区别就是所交互网络是 Mainnet。NFT 是一个非常迷人的技术,通过它可以真正保护我们自己的创作资产。希望它能够得到更好更顺利的发展。

以上所有内容的源代码可以去我的 GitHub 获取。