---
title: "輕鬆上手 NFT"
date: 2021-09-21T00:00:00+08:00
publishDate: 2021-09-21T16:55:51+08:00
lastmod: 2022-01-21T16:16:57+08:00
tags: ["NFT","區塊鏈","筆記"]
toc: true
permalink: "https://blog.aotoki.me/posts/2021/09/21/getting-started-nft/"
language: "zh-tw"
---


年初的時候跟朋友聊到 NFT 覺得有趣想嘗試看看，不過當時搜集了一些資料還是搞不太懂該怎麼在 NFT 平台上面上架作品。

這次利用中秋節連假做了不少嘗試，其中一個就是上架一個 NFT 看看。

<!--more-->

## 開始之前{#before-start}

這篇文章適合有一點程式撰寫經驗的人，如果有使用過 Node.js 的話會更好，我們會用到一些 JavaScript 和 Yarn 來建構環境。

如果你是創作者，目前一些交易平台（像是 [OpenSea](https://opensea.io)）都可以直接上架作品，這篇文章主要是設計給想了解 NFT 的一些技術細節的人作為參考。

## NFT

因為我自己不是在區塊鏈產業的從業人員，對區塊鏈了解也只限於一些興趣上的知識。因此解釋上可能有一些問題，不過這不影響我們使用區塊鏈技術。

前幾年很熱門的 ICO 和這幾年熱門的 NFT 都是屬於[智慧型合約（Smart Contract）](https://zh.wikipedia.org/zh-tw/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6)的一種，我們可以想像原本的合約只是一張紙需要有人去判讀，而智慧型合約則是把約定的事項用程式設定，讓程式自動執行約定的內容。

基於這樣的概念，就有了 ECR20 和 ECR721 兩種標準的合約規格，前者就是 ICO 主要使用的合約方式，後者就是我們所討論的 NFT。

NFT 中文叫做[非同質化代幣](https://zh.wikipedia.org/wiki/%E9%9D%9E%E5%90%8C%E8%B3%AA%E5%8C%96%E4%BB%A3%E5%B9%A3)簡單來說有點類似客製化商品的概念，像是「紀念幣」這種感覺，每個 NFT 都會有自己的編號而且是唯一的。

## ECR721

也因此，如果想要實現一個 NFT 我們需要符合 ECR721 所規定的[合約介面](https://eips.ethereum.org/EIPS/eip-721)

我們可以快速的來看一下 ETH（以太坊）所定義的 ECR721 需要實現哪些介面。

> 這邊的「實現介面」有點類似撰寫合約的條文，類似於「轉移 NFT 需要所有人同意」這樣的意思。

因為 ECR721 的介面定義相當的多，這邊我們挑選幾個 ECR721 特有的介面來看一下，詳細的用意在文件上已經有相當仔細的說明。

### ownerOf

在 ECR20 的情況中 Token（代幣）比較類似於一個「數值」但是在 ECR721 中因為是「非同質化代幣」因此每一個代幣都是獨一無二的，所以我們可以利用 `ownerOf(1)` 來查詢某個代幣的所有者是誰。

### tokenURI

這個屬於 ECR721 Metadata 介面，沒有定義不一定會影響 NFT 的發行，不過大多數的 NFT 都是圖片、影片等等，因此勢必要有一個方法去定義「NFT 的位置」

基本上只要符合 [URI](https://zh.wikipedia.org/wiki/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E6%A0%87%E5%BF%97%E7%AC%A6) 的規範都可以被當作 `tokenURI` 來回傳，一般來說我們可以用網址 `https://example.com/nft.png` 回傳，或者使用跟區塊鏈相同技術的 IPFS（星際檔案系統）來儲存，可能就會變成像是 `ipfs://example-nft-xxxx` 這樣的格式。

有些 NFT 可能是某個裝備之類的，因此也不一定會回傳。

除此之外我們也可以使用 JSON 格式來詳細定義資料。像是 OpenSea 平台建議的 [Metadata 標準格式](https://docs.opensea.io/docs/metadata-standards) 就包含了名稱、圖片、屬性等等設定。

透過這樣的方式，我們就可以將遊戲道具跟 NFT 對應起來。

假設有一款遊戲叫做 `NFTCard` 好了，會將道具資訊透過網址 `https://nft-card.io/metadata/1.json` 這樣的方式回傳以下內容

```json
{
  "name": "NFT Card#1",
	"image_url": "https://nft-card.io/assets/card1.png",
	"attributes": [
		"trait_type": "Attack",
		"value": 100
	]
}
```

如此一來我們就可以透過 `tokenURI(1)` 查詢到這是一張卡片，同時在遊戲中具備 `Attack` 為 `100` 的數值。

> 實際上我們也可以將卡片數值紀錄在 NFT 之中，不過這會讓遊戲很難調整卡片數值，因此利用 `tokenURI` 的方式將實際數值指向遊戲伺服器是一個相對不錯的作法。

## OpenZeppelin

直到這幾年還是會聽到有駭客盜走大量加密貨幣的新聞，以 ECR20 的情況來說就是合約設計有漏洞被利用。

OpenZeppelin 提供了一套開源的合約範本，我們基本上只需要擴充 OpenZeppelin 的 ECR721 範本就可以快速的製作出 NFT 同時有一定程度的安全性。

> 這個工具很大的解決了我在撰寫智慧型合約的疑慮，以下操作大多參考 [# How To Build Layer 2 NFTs With Polygon and IPFS](https://medium.com/pinata/how-to-create-layer-2-nfts-with-polygon-and-ipfs-aef998ff8ef2) 這篇文章。

## Testnet

在開發的時候為了驗證合約的設計正常，我們可以直接使用 Testnet 來處理，這邊選用的是 Polygon 區塊鏈，他會將資料彙整回 ETH 上面，同時因為價格相對低因此很適合練習或者做比較小的專案。

> 實際上也是看到朋友分享的[蛋黃酥專案](https://www.facebook.com/eggyolkpastry/)也使用 Polygon 才評估適合體驗或者嘗試。

因為從以前實驗就是透過 [MetaMask](https://metamask.io/) 來測試，這次我們在新增一個自訂的網路把 Polygon 的測試網路加進來。

| 欄位    | 數值                               |
| ------- | ---------------------------------- |
| RPC URL | https://rpc-mumbai.maticvigil.com/ |
| Key ID  | 80001                              |
| Symbol  | MATIC                              |

 官方教學說的不太清楚，如果不知道怎麼自訂網路可以參考 MetaMask 的 [說明文件](https://metamask.zendesk.com/hc/en-us/articles/360043227612-How-to-add-a-custom-network-RPC)

 接下來我們到 Polygon 的 [測試環境](https://faucet.polygon.technology/) 請求轉帳到錢包就會有可以使用的加密貨幣來測試，錢包的位置跟 ETH 錢包的相同從 MetaMask 複製即可。

 ## Hardhat

 經過幾年的發展區塊鏈相關的開發工具越來越成熟，我們可以直接透過 Hardhat 這個套件撰寫測試、部署智慧型合約。

 ```bash
 mkdir MyNFT
 yarn add -D hardhat && npx hardhat
```

中途 Hardhat 會問一些問題，基本上選有範例的那個選項就可以使用。

完成之後會長出一堆檔案，我們可以先不管他。

## Dotenv

因為我們很可能會利用 Git 來管理智慧型合約的原始碼，也就表示在上傳到 GitHub 或者 GitLab 時可能會把 PrivateKey（密鑰）流出，我們透過 Dotenv 可以利用 `.env` 設定檔的方式來管理。

> 在目前容器化成熟的角度來看 Dotenv 也在很多網站專案被使用，同時使用環境變數設定還有在 CI/CD 環境方便設定的好處。

我們先加入 Dotenv 套件

```bash
yarn add -D dotenv
```

接著新增 `.env` 檔案加入設定，如果有使用版本管理工具記得排除這個檔案。

```
PRIVATE_KEY=xxx
```

Private Key 可以直接從 MetaMask 匯出，如果要求安全性的話可以直接產生一個新錢包來使用避免主要的錢包遇到問題。

接著我們修改一下 `hardhat.config.js` 讓 Dotenv 可以被使用。

```js
// ...
require("dotenv").config();

const PRIVATE_KEY = process.env.PRIVATE_KEY;

// ...
module.exports = {
  defaultNetwork: "hardhat",
  networks: {
    hardhat: {},
    matic: {
      url: "https://rpc-mumbai.maticvigil.com",
      accounts: [PRIVATE_KEY],
    },
  },
};
```

這邊的設定主要有兩個，預設的是 `hardhat` 讓我們用來跑測試時模擬，另一個則是 `matic` 上面對應到測試的 Mumbai 網路上，這樣我們就可以在後面的操作中部署到測試環境。

## 實作{#implement}

基於 OpenZeppelin 實作 ECR721 非常的簡單，我們先將基礎合約加入專案。

```bash
yarn add @openzeppelin/contracts
```

接著編輯 `contracts/MyNFT.sol` 撰寫我們自己的智慧型合約。

```sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, Ownable {
  using Counters for Counters.Counter;
  using Strings for uint256;
  Counters.Counter private _tokenIds;
  mapping (uint256 => string) private _tokenURIs;

  constructor() ERC721("MyNFT", "MNFT") {}
  function _setTokenURI(uint256 tokenId, string memory _tokenURI)
    internal
    virtual
  {
    _tokenURIs[tokenId] = _tokenURI;
  }

  function tokenURI(uint256 tokenId)
    public
    view
    virtual
    override
    returns (string memory)
  {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
    string memory _tokenURI = _tokenURIs[tokenId];
    return _tokenURI;
  }

  function mint(address recipient, string memory uri)
    public
    returns (uint256)
  {
    _tokenIds.increment();
    uint256 newItemId = _tokenIds.current();
    _mint(recipient, newItemId);
    _setTokenURI(newItemId, uri);
    return newItemId;
  }
}
```

因為實作概念大同小異，因此就直接使用文章提到的教學範例。

基本上即使我們什麼都不做，只把 `mint` 實現就可以正常運作，而這邊教學則多了 `tokenURI` 和 `_setTokenURI` 兩個實作。

### tokenURI{#implement-tokenURI}

在這份範例合約中，定義了一組 `mapping` 資料，將 Token ID 和一段文字對應起來，而這段文字就是 TokenURI。如果是前面提到的卡牌遊戲範例，我們可以直接回傳像是 `/metadata/1.json` 的數值搭配 OpenZeppelin 的 `baseURL` 設定就可以了。

### setTokenURI{#implement-setTokenURI}

在原本的 ECR721 中並沒有 `TokenID -> URI` 這樣的 `mapping` 資訊，因此需要在 `mint` 之後額外多一個 `_setTokenURI` 步驟將網址儲存進去。

> 前面提到的卡牌資訊也可以透過像是 `_setCardAttribute` 之類的方式設定，不一定要儲存一段文字。

## 部署{#deploy}

當我們完成合約後，就可以直接部署。如果想要寫測試的話可以參考 Hardhat 的文件。

我們新增一個 `scripts/deploy.js` 檔案來定義部署的行為

```js
const hre = require("hardhat");

async function main() {
  const NFT = await hre.ethers.getContractFactory("MyNFT");
  const nft = await NFT.deploy();
	await nft.deployed();

	console.log("NFT deployed to:", nft.address);
}

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

上面這段程式基本上就是依靠 Hardhat 提供的 API 取的我們剛剛定義的 `MyNFT` 然後呼叫部署操作，最後回傳部署的合約位址。

接下來利用 `hardhat` 提供的 `run` 指令來部署。

```bash
npx hardhat run scripts/deploy.js --network matic
```

到這一步，我們就算是把合約部署到 Polygon 的測試網路上。

## 生成 NFT{#mint}

既然我們知道如何使用 Hardhat 自動的做一些處理，接下來就是加入 `scripts/mint.js` 來產生新的 NFT 物件。

```js
const hre = require("hardhat");
require("dotenv").config();

const WALLET_ADDRESS = process.env.WALLET_ADDRESS;
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS

async function main() {
  const NFT = await hre.ethers.getContractFactory("MyNFT");

  const URI = "ipfs://YOUR_METADATA_CID"

	const contract = NFT.attach(CONTRACT_ADDRESS);
	await contract.mint(WALLET_ADDRESS, URI);

  console.log("NFT minted:", contract);
}

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

為了方便調整錢包跟合約的位址我們一樣可以把它放到 `.env` 裡面，完成腳本之後同樣呼叫 `hardhat run` 指令來生成 NFT。

```bash
npx hardhat run scripts/mint.js --network matic
```

到此為止，我們在幾乎沒有寫太多程式的狀況下就完成了 NFT 的發行。

可以到 OpenSea 在 Testnet 上面的[測試環境](https://testnets.opensea.io/)會發現我們測試的 NFT 出現在自己的帳號中，並且可以交易。

> 實際部署到 Mainnet 的話上面的步驟就會成為真正的 NFT 了，因為 Polygon 的交易費用相對的便宜，很適合製作一些小專案嘗試看看。

## 未來{#future}

相比前幾年智慧型合約的撰寫大家還在摸索的階段，現在有非常多資源可以快速實現想法。也因此有出現像是 [12 歲小朋友製作 NFT](https://www.bnext.com.tw/article/64844/nft-wired-whales-benyamin-ahmed) 這類新聞，技術一直都是一種手段，先不論是否高明的狀況下還是創意比較重要。

當然，文章提到的 ECR721 實作只是非常基本的處理。以 OpenZeppelin 提供的基礎，還有權限控管、合約修訂（可升級）等等處理，再加上利用 Hardhat 撰寫完善的合約測試，這些條件都具備之後才算是一個完整的 NFT 應用，希望大家可以玩得開心。

