本系列文將分享如何使用 Python 的 Brownie 框架開發 ERC-721,也就是 NFT。並且 deploy contract 到 Ethereum 的測試網路 Rinkeby,圖檔和 Metadata 則上傳到 IPFS,再透過測試版本的 OpenSea 來看看成果!
在上一篇 如何以 Python Brownie 開發 DeFi 應用?- 環境準備 我們討論到如何用 Brownie 準備好 Solidity 的開發環境。這次我們將延續上篇,簡單的建立一個 ERC 721 的 contract ,快速走過一輪開發過程。
目錄
設定 OpenZeppelin dependency
現在寫 smart contract 已經有很多現成的樣本可以直接採用,他們通常也經過社群的檢視,出問題的機率也比較小。
其中一個較知名的為 OpenZeppelin,可以直接去他們官方的 github 看各種 contract 樣板,以及把這些樣板當作 library 的方式引入。
引入的方式也很簡單,首先在 brownie-config.yaml 中宣告 dependency:
dependencies:
# - <organization/repo>@<version>
- OpenZeppelin/[email protected]
這樣就可以了。為了讓後面寫 import 的時候可以少寫一點字,我們可以在 brownie-config.yaml 中做個 remapping:
compiler:
solc:
remappings:
- '@openzeppelin=OpenZeppelin/[email protected]'
之後到 contract 中只要用 @openzepplin 就能直接引入。未來如果 library 升版也不用一一去修改,統一調整 config 即可
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
什麼是 NFT ERC-721?
最常聽到的說法是 ERC-721 相對於 ERC-20 (也就是常常聽到的「幣」)的差異在於每一個 NFT 都是獨一無二不可分割的。這樣說是很白話,但感覺沒說到原理。
如果 ERC-721 從技術上來看,他其實是定義 smart contrace 的 interface,透過實現這些 interface 來完成概念上的 NFT。
從資料上來看,我們把 ERC-721 儲存資料的方法用關聯式資料庫來譬喻,他其實就是一張表,記錄著 token ID 的擁有地址是誰
NFT token ID | owner address |
---|---|
1 | 0x6973c95F239A833193e1D3692e7D1753936Ae4Da |
2 | 0x447698d2b749dc0acd37ae821f25fb1ef9e5bea4 |
就這樣!有沒有很訝異?其實也沒什麼神奇的地方。既然是用 id 對擁有者地址的方式儲存,當然就會是「獨一無二」且「不可分割」。
當然除此之外,還會有其他的表來存一些資訊,其中有一個比較重要的是 tokenURI 的表。
NFT token ID | tokenURI |
---|---|
1 | ipfs://0xB4531a14fd2d92C5026B8EdCA8fAEB3104Bd0a85 |
2 | ipfs://QmQrUs9JTM3ZdjYxqmk12dFKWB4su9pAJMNsTRMmRhuT1s |
如果沒有給定好 tokenURI 的指向,或是指向的內容設定錯誤,你的 NFT 掛到 opensea 上就會是空白一片,雖然合約存在於區塊鏈上,但肯定不會有價值的! 這部分我們後續會再描述如何設定。
寫第一份 NFT ERC-721 合約吧!
由於我們使用 OpenZeppelin 的樣板來撰寫,ERC-721 制定的介面基本上都已經處理好,我們只需要:
- 增加 contract owner
- 增加產生 tokenId 的方法
現在以發一個 IKEA 可愛恐龍的 NFT 為例,我們在專案中的 contracts 目錄新增一個檔案「KongLongNFG.sol」,然後將整份合約貼入:
// contracts/KongLongNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract KongLongNFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address public owner;
constructor() ERC721("KongLongNFT", "KLG") {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can do this");
_;
}
function mintToken(address newOwner, string memory tokenURI)
public
onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(newOwner, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
比想像中簡單很多!這邊講解一下程式內容
- 一開始宣告 code 的 license,沒有什麼特別的。
- 緊接著宣告 Solidify 使用的版號,因為不同版本的語法會有差異。這邊我們以 0.8.0 下去寫。
- 接下來就是 import 寫好的 lib 啦!我們引入了基本的 ERC721.sol
- 另外引入的 ERC721URIStorage.sol,實現前面提到的 tokenURI 表,讓開發者可以為每個 NFT token 設定完全不一樣的 tokenURI。細節我們後續再討論
- 引入 Counters.sol,實現如資料庫中能遞增的 Integer unique id
// contracts/KongLongNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
- 基本的起手式,用 contract 關鍵字宣告你的 smart contract,就像 OOP 的語言用 class 一樣
- 繼承 ERC721URIStorage,關鍵字是 is
- 宣告 _tokenIds 和 owner 變數。_tokenIds 的類型是在引入的 Counters.sol 中定義。owner 用來儲存 account 地址,因此使用 address 類型
- solidity 中如果要呼叫 parent 的建構式,是直接接在合約建構子宣告後面。這邊呼叫 ERC721(),並傳入寫死的 name 和 symbol
- 在建構式中,將 msg.sender,也就是 deploy contract 的 account 地址,寫入 owner 變數,作為後續實作權限控管使用
contract KongLongNFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
address public owner;
constructor() ERC721("KongLongNFT", "KLG") {
owner = msg.sender;
}
- solidity 中的 modifier 蠻像 python 中的 decorator。這邊宣告一個 onlyOwner 的 modifier,讓被修飾的函數在執行前會先檢查呼叫者是否為 owner,若非則拋出錯誤,退出 contract
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can do this");
_;
}
- 最後是 mintToken 函數,實作鑄造 NFT 方法
- 由 public 修飾,可以透過外部直接呼叫
- 同時由 onlyOwner 的 modifier 修飾,具有呼叫者權限檢查
- 傳入新鑄造 NFT 的 owner address,以及 NFT 的 tokenURI
- tokenURI 變數宣告前使用 memory 修飾,memory / storage 的差異可以詳閱這篇文
- 回傳型態為 uint256
- 進入方法第一步,先遞增 tokenIds
- 利用新的 tokenId 和 newOwner 地址來呼叫內部方法 _mint 儲存 NFT record
- 為此 NFT 存入 tokenURI
- 回傳最新 NFT 的 ID
function mintToken(address newOwner, string memory tokenURI)
public
onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(newOwner, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
Compile Smart Contract 檢查語法
我們先 compile 看看語法有沒有問題
$ brownie compile
Brownie v1.17.2 - Python development framework for Ethereum
Project has been compiled. Build artifacts saved at /xxxxxxxx/build/contracts
如果有問題的話,會在 console 列出來有問題的行數
CompilerError: solc returned the following errors:
ParserError: Expected '(' but got identifier
--> contracts/KongLongNFT.sol:33:14:
|
33 | function getCurrentTokenId() public view returns (uint256) {
| ^^^^^^^^^^^^^^^^^
到這整個 NFT 的 contract 大致上都差不多了,下一篇我們會分享如何用 python 寫 deploy 的 script,實際把這份合約 deploy 到 local Ganache 測試看看!
延伸閱讀:
如何以 Python Brownie 開發 DeFi 應用?- 環境準備
自己寫 NFT 吧!- Deploy 到 Ganache – 以 Python Brownie 開發
自己寫 NFT 吧!- 準備 Metadata – 以 Python Brownie 開發
自己寫 NFT 吧!- Deploy 到 Rinkeby testnet – 以 Python Brownie 開發