[Solidity Note] - 透過工程師的方式發布 基於 ERC721 的 NFT

Introduction & 前言

NFT

從去年開始漸漸越來越常聽到 NFT2021年3月12日,在佳士得拍賣會現場,有一件加密藝術品 Everydays: The First 5000 Days7000萬美元(約 19 億美元) 售出,讓該作品的作者 Beeple 成為了廣為人知的藝術品作家。

相信會看到 Solidity 智能合約文章的朋友都已經知道這是什麼了。如果你不知道,可以參考 NFT是什麼,可以賣到這麼貴?7分鐘帶你了解加密藝術投資熱

這篇文章不會探討 NFT 的優缺點,也不會提供任何投資建議,很單純只會教你如何透過智能合約上架自己的 NFT,並且可以有額外的延伸發揮空間。

工程師就該用工程師的方式上架 NFT


Summary & 摘要

接下來將會教你怎麼自己寫一份 Solidity 智能合約 上架自己的 NFT,最文章最後也會有一分鐘懶人包快速上架 NFT。

本篇文章預設學習前的基本條件需求:

  • 會基本的英文
  • 有不會侵權的圖片(最好是自己畫)
  • 知道終端機怎麼用,且環境已經安裝 Node.js - 不清楚的人可以參考這篇文章 從零開始: 使用NPM套件

這邊可以分為幾個小節:

  1. 認識 Solidity
  2. 認識 ERC系列
  3. 使用 Remix IDE
  4. 認識且使用 OpenZeppelin
  5. 申請 Pinata IPFS
  6. 申請 MataMask
  7. 領取測試幣
  8. 發布 NFT 且學會使用 Etherscan.io
  9. 使用 OpenSea Testnet
  10. 延伸應用

認識 Solidity

Solidity

想進入乙太坊做想做的事情,例如發行 NFT,就必須靠以太坊內部的 EVM(註1) 幫你處理;正因為要達到 去中心化不可竄改可信任 三件事情,所以我們一但把要做的事情(智能合約),部署到 EVM 去,就不能再做修改了。

*註1: **EVM(Ethereum Virtual Machine)*:中文翻譯為「以太坊虛擬機」,是智能合約的運行環境。如果有一點點程式底子,也可以把它想像成 VM(註2) ,詳情可查看 新手科普 | 以太坊虛擬機 EVM 是什麼,有何用處?

註2: 虛擬機器 (VM) ,這是用來建立虛擬化運算環境的技術,也是第一代雲端運算的基礎。簡單說就是一台主機,裡面可以安裝作業環境,詳情可參考 什麼是虛擬機?

直接藉由下面這張圖來講解會比較快,首先你要先確定要在什麼地方寫智能合約,並非所有的鏈都可以寫智能合約,由於我們要在 乙太鏈 上寫智能合約,編寫 乙太鏈 的智能合約其實不只有 Solidity, 但因為 Solidity 最成熟,所以我們選擇它。

*如果想選擇其他語言寫也可以,例如 Vyper(基於 Python)、MandalaObsidian(有漏掉請見諒),抱歉筆者只接觸過 Solidity,加上 Solidity 是基於 JavaScript 開發出來的,除了需要強型別,其他的寫法都差不多,所以這次會選擇這個語言寫智能合約*

智能合約運作流程

這邊我先簡單介紹一下整體運作流程:

  1. 首先透過 Solidity 或其他程式語言編寫智能合約
  2. 將智能合約上鍊,經過礦工的認證之後就有不可更改性,然後會在 EVM 上面執行
  3. 將智能合約打包產出 ABI(Application Binary Interface) 應用二進制接口,這個可以讓我們透過 ABI 上面提供的 function 去呼叫 EVM 執行 智能合約。

    *當然這邊省略了,呼叫前還是要有智能合約地址,這次教學不會寫怎麼呼叫,有興趣可以參考 在網頁上使用 web3 並操作區塊鏈,可以跳過 React 的部分,關鍵字為 Web3.jsether.js,之後會再寫一篇介紹怎麼呼叫 Web3 世界的東西*

  4. 前端或後端使用可以呼叫 Web3 的套件去做出 Dapp(Decentralized Application) 去中心化應用程式,例如寫一個可以 Mint NFT 的網站。
  5. 使用者對網站操作,觸發呼叫 ABI 的情況。
  6. Web3 的套件去呼叫 EVM 裡面的智能合約,然後操作智能合約。
  7. 智能合約回吐原本就已經寫好的程式碼(不可更改性)。

認識 ERC系列

講到這邊讀者應該按耐不住性子了,開始前我們還是必須快速認識一下 ERC-20、ERC-721ERC-1155

這邊直接快速懶人包:

  1. ERC-20 是同質化代幣 - 例如 BTC,你的一顆等於我的一顆 BTC
  2. ERC-721 是非同值化代幣 - 例如 NFT,一個合約只會有一個幣,NFT 其實也可以想像成一個幣(後面會講解),只是這個合約裡面的你的一個幣跟我的一個幣不是相等的,白話說就是一個合約裡面每一個發行的號碼都有自己的 Data屬性圖片…等等
  3. ERC-1155 是使用多令牌(token)標準 - 因為 ERC-721 一個合約只能發一個幣,為了改進, ERC-1155 可以理解為 ERC-20 + ERC-721

這邊我們先選擇比較簡單的 ERC-721 來實作。

如果想要詳細閱讀可以參考 ERC-20 vs ERC-721 vs ERC-1155 Ethereum Token Smart Contract Red Pill — — 選擇哪一個?


使用 Remix IDE

接下來要開始實際操作了,開始前先說一下我們要做的目標:

  1. 上架五個 NFT 各自有各自的屬性
  2. 需要使用一定價格才可以 MInt 這個 NFT
  3. 最多提供 Mint 的數量
  4. 一次最多能 Mint 的數量
  5. 其他的一些查詢功能

Remix IDE

寫程式都需要 IDE(Integrated Development Environment) 整合開發環境,簡單說就是編輯器。這邊我們為了方便使用及上鍊,使用線上 IDERemix IDE

因為 Remix 裡面的檔案只是暫存在 LocalStorage,很有可能下次開啟就不見了,為了使我們寫好的合約可以下次開啟還能存在,我們需要使用 Remix 提供的教學 Remixd: Access your Local Filesystem,使用 connect to local host 可以讓我們本地的某個資料夾與 Remix 串連。

首先必須要先安裝串連的套件 npm install -g @remix-project/remixd ,如果不想要把套件安裝到全域的話,可以把 -g 拿掉,使用 npm install @remix-project/remixd ,但要記得在要串連的那個資料夾內安裝。

這邊筆者直接使用全域安裝 npm install -g @remix-project/remixd ,安裝結束可以輸入 remixd -v ,查看是否安裝成功。

接著我們先在電腦隨意一個地方開啟一個資料夾,最後就可以把 Remix 串連本地資料夾了。

1
$ remixd -s <absolute-path-to-the-shared-folder> --remix-ide <your-remix-ide-URL-instance>

<absolute-path-to-the-shared-folder> 是你資料夾的位子, <your-remix-ide-URL-instance>Remix IDE 的網址。

本地資料夾連接到 Remix

選項選到 connect to localhost

連上後左側下拉選項從 default_workspaces 改到 localhost,這時候會跳出彈窗詢問你是否連接。

同意連接


認識且使用 OpenZeppelin

OpenZeppelin

對於要在以太坊開發的人員來說,OpenZeppelin 是重要且必須熟知的,它具有 ERC Token 的標準,所有在上面的公開合約程式碼都會經過社區審核,當然裡面也有介紹如何透過其他套件去實作智能合約(不使用 Remix),有興趣可以查看 Developing smart contracts

就像大部分的套件一樣,有很多規範及工具我們可以從外部引入,透過人家已經做好的東西去利用,不用自己在造輪子,而這次我們需要使用 OpenZeppelin 裡面的 ERC721 合約。

在我們開始使用之前必須要先安裝這個套件,所以在我們串連 Remix 的資料夾裡面先輸入 npm init -y ,這個指令可以幫我們快速產出 package.json,跟著指令一問一答,這邊筆者皆直接一路狂按 Enter 最後順利產生出 package.json,接著再輸入 npm i @openzeppelin/contracts,然後就會在 node_modules 裡面看見 @openzeppelin/contracts 這個套件。

OpenZeppelin 的 Contracts 文件.png

在開始使用這個套件之前,當我們點選 OpenZeppelin 官網裡的 Contracts 可以看到左側有 ERC20ERC1155 的合約介紹,以及 API;如上面提到的這次會用 ERC721 來開發,所以我們先點選到 Tokens 底下的 ERC721 可以看到基本用法。

ERC721 基本用法

接著點選到左側 API 底下的 ERC721,這時候會看到很多基於 ERC721 的合約內容,我們這邊先忽略 IERC721,點選右側的 ERC721

ERC721 API

點選之後就會看到怎麼引用。

各種 Function

從最基本的智能合約 constructor 應用,一直到其他的 function

因為我們要使用查詢功能,例如查詢這個 NFTMetaDate 或 持有人所持有的 NFT 有哪些,其中一個 function 會用到 tokenOfOwnerByIndex() 這個 API,所以我們不使用 ERC721,改為使用 ERC721Enumerable,當然這邊可以依照你的習慣或者目的不同,引入不同的 API

在引入之前我們要先在剛剛串連本地的 Remix 創建一個新的合約,筆者習慣在串連的資料夾內再開一個 contracts,所以筆者會在 contracts 裡面創建一個 myFirstNFT.sol 的合約。

第一個合約

這時候如果碰到新增之後會無法點開該合約,回去終端機查看,如果出現 permission denied 的問題,記得先中斷這個連接,在試著連接一次,只是這次前面加上 sudo

權限問題

1
$ sudo remixd -s <absolute-path-to-the-shared-folder> --remix-ide <your-remix-ide-URL-instance>

可以成功新增合約之後,先點選左邊第二個圖示,然後把 Auto compile 勾起來。

自動編譯

複製下面的合約內容,貼到剛剛的 myFirstNFT.sol 裡面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract MyFirstNFT is ERC721Enumerable, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;

string public baseURI = "ipfs://你的baseURI"; // 這一行是 NFT 該去哪裡找你的 MetaData
bool public paused = false; // 可以拿來暫停或者開啟 Mint
uint256 public cost = 0.0001 ether; // Mint 價格
uint256 public maxSupply = 5; // 只有五個 NFT
uint256 public maxMintAmount = 1; // 一次最多只能 Mint 一個

constructor() ERC721("WoodElf", "Wood") payable {
// 剛開始我們先自己 Mint 一個
safeMint(msg.sender, 5);
}

// 不用經過我們自己設定的判斷直接 Mint(設定為 private 且只有 onlyOwner 可以使用)
function safeMint(address to, uint256 tokenId) private onlyOwner {
_safeMint(to, tokenId);
}

// 提供給大家 Mint 的 Function(另外加上 payable 代表這個 function 可以接收乙太幣)
function mint(address _to, uint256 _mintAmount) public payable {
require(paused != true, "Sale must be active"); // 合約必須不是暫停
require(_mintAmount > 0); // 每次必須鑄造超過 0 個
require(_mintAmount <= maxMintAmount, "You can only adopt 1 BigBenFun at a time"); // 鑄造的數量不可以大於每次最大鑄造數量
require(cost * _mintAmount <= msg.value, "Ether value sent is not correct"); // Mint 的價格不可以少於我們訂定的價格

for(uint256 i = 0; i < _mintAmount; i++) {
uint256 mintIndex = _tokenIdCounter.current();
// 查看接下來要 Mint 的 NFT 是否有人持有了,如果已經持有就跳過
while(_exists(mintIndex)){
i++;
}

// 接下來如果 Mint NFT 的編號如果小於初始提供的數量就讓使用者 Mint
if (mintIndex <= maxSupply) {
_safeMint(_to, mintIndex);
_tokenIdCounter.increment();
}
}
}

// 重新設定 baseURI
function setBaseURI(string memory _newBaseURI) public onlyOwner {
baseURI = _newBaseURI;
}

// 取得這個持有者有多少 NFT
function walletOfOwner(address _owner) public view returns (uint256[] memory) {
uint256 ownerTokenCount = balanceOf(_owner);
uint256[] memory tokenIds = new uint256[](ownerTokenCount);
for (uint256 i; i < ownerTokenCount; i++) {
tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokenIds;
}

// 查看使用者持有的 NFT MetaDate
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);

string memory currentBaseURI = baseURI;
return bytes(currentBaseURI).length > 0 ? string(abi.encodePacked(currentBaseURI, "/", Strings.toString(tokenId), ".json")) : "";
}

// 查看總提供數量
function totalSupply() public view override returns (uint256) {
return maxSupply;
}

// 重新設定 Mint 價格
function setCost(uint256 _newCost) public onlyOwner() {
cost = _newCost;
}

// 重新設定總提供量
function setMaxSupply(uint256 _newMaxSupply) public onlyOwner() {
maxSupply = _newMaxSupply;
}

// 重新設定一次能 Mint 的數量
function setmaxMintAmount(uint256 _newmaxMintAmount) public onlyOwner() {
maxMintAmount = _newmaxMintAmount;
}

// 開關販賣開關
function pause() public onlyOwner {
paused = !paused;
}

}

2022/03/30 後更:後來筆者研究發現,因為 Solidity 的特性,原本 import 進來的 function 有可覆蓋特性,像是 **totalSupply()**,由於筆者也是邊研究別人的合約邊學習,當初以為這個是總發行數量,所以直接寫了一個 functionreturn maxSupply,其實這是錯的,因為 totalSupply() 返回的其實是已經被 Mint 出去的 NFT 數量,但因為這邊使用了 Counters.sol 這個套件,所以在 Mint 的時候才不會出錯,但目前範例不會出問題,還是可以使用這個範例程式碼。
另外有的合約範例會在建構器 constructor() 那邊帶變數進去,其實也是可以的。

這邊簡單快速帶過,因為內容較多,基本都註解在合約內了,剩下的還是必須要由你的親自理解才能成功地寫出下一個合約。

首先必須宣告你的合約 License,想看詳細列表的話可以參考 SPDX License List,裡面有各種不同的 License,不宣告的話一定會噴錯。

pragma solidity 這是宣告你合約的版本,請記得不同版本一些宣告等等的語法會不同。

接著會先引入我們需要使用的一些合約,像是基本的 ERC721EnumerableCountersOwnable(拿來確定是發布合約的人,方便上鍊後如果有一些設定要更改,可以確保只有合約發起人可以更改)、Strings(拿來可以讓數字轉字串用的)。

再下來任何一個合約都會由 contract 開頭,但一個 .sol 裡面可以有多個 contract。而我們有些引入進來的合約要使用就會透過 is 去繼承,有些像 Strings 這種 utils 就不需要去繼承即可使用。

再往下除了一些基本設定之外,像是 _safeMint() 這個 function 就是 ERC721 - _safeMint() 提供的 API;在我們自己寫的 mint() 裡面我們特地加上 require() 來做一些判斷,像是可以開啟或關閉 Mint

最重要的就是 baseURI,因為我們使用的這個合約不需要去使用 _setTokenURI() ,我們使用 ERC721Enumerable 所提供的 tokenURI(),當我們訪問這個 NFT 的時候,會自動返回我們定義好的一串網址,而返回的網址就是上面 tokenURI() 這個 function 的內容,等等下面會繼續介紹這串網址怎麼來,還有 MeataData 是什麼。

到這邊你的合約應該會出現紅色的錯誤警告,這時候就可以繼續進入下個章節啦。

合約錯誤


申請 Pinata IPFS

Pinata

首先要先說說 MetaData,這個裡面就是一個 Json,由於這次我們是使用 OpenSea Testnet 上架 NFT,基本組成可以參考 Opensea Developer Platform 提供的文件 Metadata Standards

MetaData

因為我們要一次上架多個 NFT,如果是使用 ERC721 去寫智能合約,基本上設定完 baseURI,在我們查看 NFT 的時候它就會去讀取你設定的 tokenURI,而我們這邊使用 tokenURI() 會返回我們設定好的 URIbaseURI + / + tokenId + .json

而這個 MetadataJSON 可以讀取的網址不止可以使用 IPFS,還可以使用自己架設的 Server,依樣可以參考 Opensea Developer Platform 提供的文件 Metadata Standards

關於 TokenURI 的文件解釋

*所以其實你要購買的 NFT 如果是使用自己架設的 HTTP,就有可能在某一天被換掉圖片及內容的可能性,這個這邊就不多做討論了*

由於要有公平性,我們這邊使用 IPFS

IPFS

星際檔案系統(InterPlanetary File System,縮寫為IPFS) 是一個旨在實現檔案的分散式儲存、共享和持久化的網路傳輸協定。簡單一點基本概念可以把它當成是以前我們很常用的 迅雷 或者 foxy(如果沒聽過沒關係,有點年代了),大家都會提供自己的節點,然後你的檔案會去這些提供節點的人下載

關於 IPFS 可以使用的方式不止一種,這邊我們使用 **Pinata**,它提供我們免費 1GB 空間可以上傳我們的檔案,所以我們要用來上傳我們的 MetaData

Pinata 可以接受一般檔案或者是文件夾,這邊我們先在桌面建立一個 NFTImg(名稱隨意) 的資料夾,然後把準備好的五個圖檔放到資料夾內,把整個資料夾上傳上去,之後會拿到一串 CID

拿到剛剛上傳的 CID

這時候使用 https://ipfs.io/ipfs/${你剛剛上傳資料夾的 CID}/0.jpg 就可以看到圖檔了

然後在桌面建立 NFTJson(名稱隨意) 的資料夾,然後建立 0.json5.json 共五個檔案,裡面打上下面的內容(記得每一個 Json 都要有點差異):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "WoodElf Blue",
"description": "藍色的木頭精靈,有一雙紅眼,似乎是血輪眼.",
"image": "https://ipfs.io/ipfs/${你剛剛上傳資料夾的 CID}/0.jpg",
"external_url": "https://ipfs.io/ipfs/${你剛剛上傳資料夾的 CID}/0.jpg",
"background_color": "341f47",
"attributes": [
{
"trait_type": "Level",
"value": 1
},
{
"trait_type": "Eyes",
"value": "normal"
},
{
"trait_type": "Feature",
"value": "grass"
}
],
"properties": {
"name": {
"type": "string",
"description": "WoodElf Blue"
},
"description": {
"type": "string",
"description": "藍色的木頭精靈,有一雙紅眼,似乎是血輪眼."
},
"image": {
"type": "string",
"description": "https://ipfs.io/ipfs/${你剛剛上傳資料夾的 CID}/0.jpg"
}
}
}

每一個 Json 記得圖片的 image 一定要填寫正確,不然到時候到 OpenSea Testnet 會看不到圖片,裡面的屬性 attributes 可以參考 Metadata StandardsAttributes 介紹去填寫

最後我們使用 https://ipfs.io/ipfs/${你剛剛上傳JSON資料夾的 CID}/0.json 就可以看到原本填寫的檔案內容。

MetaData Json

關於 JSON 美化排版可以考慮安裝 Google Chrome 擴充套件 JSON Formatter

接下來只要讓 NFTbaseURI 可以訪問到這串網址即可,所以我們會透過 tokenURI() 這個 function 的內容去返回這串網址。因為 baseURI 訪問的網址開頭可以是 HTTP 或者 IPFS,這邊我們把剛剛的 string public baseURI = "ipfs://你的baseURI"; 改為 string public baseURI = "ipfs://QmU....Xa5"; ,後面改成你的 CID,之後應該就可以成功 Compile


申請 MataMask

在我們把合約寫好之後,就是要將合約上鏈,這邊我們需要使用 MataMask 去幫我們發布合約,不清楚狐狸錢包的可以參考 1分鐘教你安裝MetaMask,一個你值得擁有的虛擬貨幣錢包,這個錢包應該是目前多數人使用的錢包,它不止可以讓你在大多個網頁得以使用 Web3 的功能,也可以快速切換不同的鏈,因為這次我們的合約要發布在測試網上(發布在主網需要花費真的錢先去兌換乙太幣,而測試網可以免費領取測試網的乙太幣),所以我們需要使用它來切換且發布。

申請好且登入之後,請在 Remix 裡面點選網頁右上方的狐狸錢包,然後點擊右上角頭像,在點擊設定。

設定打開可見測試網

接著點擊進階然後往下滑把 Show test networks 打開,不然你會看不到測試網路。

點選進階

打開 show test networks

接著我們點擊上面的 以太坊 主網路,然後會彈出下拉選項,選擇 Rinkeby 測試網路

選擇 Rinkeby Network

請記得接下來一定要使用 Rinkeby 測試網路,筆者在這之前的練習卡了好久好久,結果就是犯了文件沒看清楚的錯誤,根據 OpenSea TestnetAPI 文件 Testnets NFT API Overview,只能接受顯示在 Rinkeby 測試網建立的 NFT

關於 OpenSea 測試網的文件要好好看


領取測試幣

MataMask 錢包

*這邊要先知道我們要在乙太鏈上做任何事情都必須要花費 ETH,詳細可以參考 【區塊鏈入門】到底什麼是Gas、Gas Price、Gas Limit?,至於等等要領的測試幣就是拿來支付這個 ETH*

每一個測試網路都有自己的測試幣可以領取,剛切換過來現在我們錢包裡面應該是 0 ETH,每一個測試網路領幣的方法都可以透過點擊 ,然後往下滑到 測試水管 點擊 取得乙太幣 去領取,一般領取測試幣的水管必須要發文再拿網址來換測試幣,但我們這次為了方便要透過 ChainLinkRequest testnet LINK ****去拿測試乙太幣。

到 ChainLink 去領測試幣

成功領到測試幣

將網路選到 Ethereum Rinkeby,然後通過機器人驗證後點選 Send request,然窗顯示後等待一陣子就能看到成功的畫面,可以點選下面的 0x4f1db2ba2e55ffd9016e0bc99ce6cae8d3b6476d6fd7108604e946ef62d18ea8 會跳到 Rinkeby 的 Etherscan 去,Etherscan 是一個可以查看任何在鏈上活動詳細資料的網站,任何資料都是公開透明。

成功領到測試幣且 Etherscan 有記錄

等一段時間後再開啟我們的錢包,會發現裡面多了 0.1ETH


發布 NFT 且學會使用 Etherscan.io

在我們領取好測試幣之後就可以準備上架我們的 NFT

準備上鏈

  1. 點選左側第三個圖示
  2. ENVIRONMENT 切換到 Injected Web3,如果看到 Account 有出現你的錢包地址及餘額就是成功了
  3. 記得要確定 CONTRACT 是切換到你要發布的智能合約,這邊切到剛剛創建的 myFirstNFT.sol
  4. 點擊 Deploy 發布

查看需花費的 ETH 並且點選確認

然後會跳出一個視窗問你要不要確認,這邊會顯示目前需要的費用,我們直接點擊確認。

上鏈的詳細過程在下方

*這邊會顯示 WoodElfNFT 是正常的,因為筆者在創建的時候忘記把 contract WoodElfNFT is ERC721Enumerable, Ownable { 改成 contract MyFirstNFT is ERC721Enumerable, Ownable { *

然後我們看 Remix 底下的小視窗,會看到正在跑建立合約,過一段時間建立成功會有綠色勾勾出來。

所有部署過的合約都在這

除了透過小視窗看有沒有部署成功,看左邊 Deployed Contracts 也可以看看有沒有部署成功的合約,成功上鏈的後面會有 BLOCKCHAIN,我們可以把 WOODELFNFT AT 0XF13...F9869 (BLOCKCHAIN) 這個打開,裡面會有各式各樣剛剛寫好可以用的合約 function,但是因為視窗一關閉我們的這個紀錄就會不見,所以我們點擊剛剛小視窗顯示的 view on etherscan 去查看建立的合約內容。

合約詳細內容

Status 是 Suucess 就代表成功了

我們點擊 Interacted With (To): 那行的 0xf1394843578942bad8ef798e073a0b43c62f9869 會到 RinkebyEtherscan.io 去。

公開合約

接著我們要公開這個合約,之後才能方便在 Etherscan.io 操作一些我們寫好的 function,像上面提到的, Remix 一但關閉,Deployed Contracts 就會空了,點擊 Verify and Publish

公開第一步

把該選的選一選,然後送出

接下來先讓我們回去 Remix,點選左側的插頭,這裡面提供很多可以裝在 Remix 上的插件,然後再輸入匡輸入 flattener,接著點選 activate 啟動這個套件,這個套件可以幫助我們壓縮所有任何 import 進來的合約以及我們寫的合約在同一份檔案內。

安裝壓縮插件 Flattener

然後點選左側的 flattener,再點擊 Flatten contract/myFirstNFT.sol ,最後點擊 save

開始壓縮

這時候會產生出一個新的 .sol,這邊用意是把剛剛的合約也合併進同一個檔案,把全部內容複製起來,貼到剛剛的 Etherscan.io 去。

所有壓縮到同一份合約內的程式碼

這邊我們為了方便,記得要把下面的 Constructor Arguments ABI-encoded (for contracts that were created with constructor parameters) 內容全部刪掉,最後點擊最下面的 Verify and Publish,這邊 Etherscan.io 會幫你做簡單的程式碼檢查。

刪除 ABI

成功後再回去剛剛合約查看,可以看到合約裡面會出現源碼,這邊就達到 不可竄改可信任,而 Read Contract 可以使用智能合約裡面的 Call FunctionWrite Contract 可以使用智能合約裡面的 Send Function,基本上 Call Function 不會改到區塊鏈的資料,不需要礦工來驗證,所以不需要花任何費用。

合約源碼

如果之後你關閉了 Remix 的視窗,剛剛上面的 Deployed Contracts 部署上去的合約列表會被清空,這時候只要記得原本的合約地址即可,如果忘記也可以用你部署合約的帳號地址去 Etherscan 裡面搜尋,這邊我們可以試著開啟另一個 Remix 視窗,然後輸入合約地址在 At Address 及可。

輸入合約地址到 At Address

但是這邊實際測試有發現一個問題,你必須把 CONTRACT 切換你開發的那份合約,如果本地的源碼合約也消失的話,就無法把合約加進來。


使用 OpenSea Testnet

OpenSea 也有提供測試網的 NFT 網站

接下來讓我們到 OpenSea Testnet Account 去查看我們的錢包,這邊會自動抓取你 MetaMask 錢包,因為合約上我們一開始就有先 Mint 一個 NFT,所以我們可以看到現在裡面已經有一個 NFT 了,圖片也是我們自己定義好的。

我在 OpenSea Testnet 的 NFT 錢包

這邊也可以看到一些我們定義的 attributes

剛剛發佈合約 Mint 的 NFT

會是 WoodElf V3 是因為如果發布了已經存在的 NFT 名稱,會自動變成 V2,因為我發布了第三次,所以是 V2

除了可以在 OpenSea Testnet Account 看到我們的 NFT,還記得每一個 NFT 都是可以代表一個 Token 對吧,讓我們打開錢包,然後把剛剛的 Ether 合約地址加進錢包內。

引入 Tokens

填進剛剛的合約地址,會自動帶出代幣代號,小數點精度我們填寫 0.1

填寫合約地址

之後回去錢包看會發現我們有一開始先 MintNFT

NFT Token

這邊要說一下為什麼要一開始先 Mint 一個 NFT 呢?因為有些 NFT 會預留白單名額,有一種做法是我們可以在發布合約的時候預先 Mint 預定的數量,最後再透過我們轉發出去,當然這麼做有風險,這邊不多做討論


延伸應用

到這邊我們應該已經成功發布 NFT 了,接下來的延伸應用滿多的,最基本的就是我們可以使用剛剛 Remix 裡面合約已經 Compile 完成後的 ABI,拿來串接前端網頁做一個 Dapp,這邊之後會再另外做介紹。

而合約內容可以自己寫,像是上面我們有使用 require() 加上 paused 來判斷現在是不是能 Mint,但合約真的要小心寫,如果有接觸過 NFT 一段時間一定都有聽過 Yolo Cat 偷跑事件,詳細可以參考 如何避免 YOLO-Cat-Club NFT專案合約偷跑問題? 的事件的關鍵重點部分。


Conclusion & 結論

在去年初接觸了幣圈後,一直對幣圈產生濃厚的興趣,到了年底開始接觸 NBA Topshot,那是我第一次接觸 NFT,但當時還不知道智能合約這個東西,一直到今年開始接觸 OpenSea 之後,才開始知道 Solidity 這個東西。

從想要自己發布一個 NFT 到 我既然是工程師,就要用工程師的方法發布 NFT,所以開始找資源找文章,然後透過寫 Solidity 去發布 NFT,過程雖然滿多地方碰壁,但真的很有趣。

開始慢慢研究後發現每一個 ERC721NFT 其實就是一個 Token(基於 ERC20),只是裡面的每一個 Token 都會有相對應的 MetaData(ERC721),所以才會有 你的一塊不是我的一塊 這種很難理解的話出現。

我想除了 Front-end Developer 之外,又有新的地方可以前往探索了,且 Dapp 跟前端離不開,加上從以前從前端工作開始就常常體驗到工作上 80% 的時間討論跟前端都沒有八竿子關係,不論參與度,技術方面也都是感覺有一個天花板在那邊,後來一聽到區塊鏈的部分幾乎是盡量能抽離後端就抽離,因為要去中心化,所以前端比例佔比較重,讓我下定決心好好研究一番。

我知道發布 NFT 只是很簡易的入門智能合約的一個方式,還有很多需要學習,但這是一個很好的入門方式。

最後如果有興趣玩看看我發布的 NFT 可以到 Etherscan 去,合約地址是 0xf1394843578942bad8ef798e073A0b43c62f9869

這份合約其實沒有寫得很好,之後會再找機會優化,其中可以優化的地方包含,盲盒機制、把錢從合約領出來的機制、合約自毀機制、合約升級、白單、新增管理員(不然只有 Ownable 可以控制合約,雖然要去中心化就要減少人為控制,但一般商業邏輯老闆肯定會希望也能控制,也希望底下的主管也可以控制)…還有很多可以優化,就當作是之後新文章的內容囉。

一分鐘 NFT 合約 懶人包

最後如果覺得上面合約太多太麻煩,只想發布一個 NFT,可以使用下面這個智能合約(參考至 2021 快速建立 ERC721 標準智能合約並且 Mint NFT):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721PresetMinterPauserAutoId.sol";

contract UUP is ERC721PresetMinterPauserAutoId {

constructor() ERC721PresetMinterPauserAutoId("WoodElf", "Wood", "ipfs.io/ipfs/Qm...wrG/")
{}

function setTokenURI(uint256 tokenId, string memory tokenURI) public {
require(hasRole(MINTER_ROLE, _msgSender()), "web3 CLI: must have minter role to update tokenURI");

setTokenURI(tokenId, tokenURI);
}
}

然後一樣把一個 NFTJson 上傳到 Pinata 上面,然後用那個 CID,剩下就是發布,跟上面流程一樣。

不用寫程式碼的懶人包

如果真的覺得寫程式很麻煩,可以直接參考 OpenSea 的文件 如何創建 NFT?如何出售NFT?


參考網站