Nic Lin's Blog

喜歡在地上滾的工程師

NFT 開發實戰!基礎智能合約入門 (2)

這個章節主要會圍繞在由 Solidity 語言所開發的智能合約

ERC-721 其實就是定義了一系列的接口,讓你能夠透過制訂的接口,去實作所謂的 NFT

再撰寫 NFT 合約之前,可以先稍微看過 ERC721 的定義接口,我把解釋寫在下方程式的註解,可以搭配著看

interface ERC721 /* is ERC165 */ {
    // 轉帳、權限的基礎 function
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    // 用來判斷一個地址下有多少的 NFT
    function balanceOf(address _owner) external view returns (uint256);

    // 用來判斷一個 NFT 是不是屬於這個地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    // NFT 要可以被交易,這些就是關於轉帳的 function
    // safe 開頭的差別在於避免轉到黑洞
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    // Approve 系列是把自己的 NFT 交出託管權限
    function approve(address _approved, uint256 _tokenId) external payable;
    function setApprovalForAll(address _operator, bool _approved) external;
    function getApproved(uint256 _tokenId) external view returns (address);
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);

    // ERC721 一定要有 ERC165,用來檢測合約是否實現接口}
    interface ERC165 {
        function supportsInterface(bytes4 interfaceID) external view returns (bool);
    }

    // 使用 transfer 方法後的 callback,如果傳的值有錯誤,會導致 transfer 失敗
    interface ERC721TokenReceiver {
        function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
    }

    // ERC721 的元資料,讓每個 NFT 能夠有自己的屬性設計
    interface ERC721Metadata /* is ERC721 */ {
        function name() external view returns (string _name);
        function symbol() external view returns (string _symbol);
        function tokenURI(uint256 _tokenId) external view returns (string);
    }

    // 實現 NFT 中不會被改變的屬性,例如總供應量
    interface ERC721Enumerable /* is ERC721 */ {
        function totalSupply() external view returns (uint256);
        function tokenByIndex(uint256 _index) external view returns (uint256);
        function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
    }

瞭解了大致上 ERC721 的 function 之後,我們就可以動手來實作這次發行 NFT 的合約

以下是建立資料夾跟安裝基本環境的過程

mkdir nic_meta
cd nic_meta
mkdir contracts
cd contracts
cd ..
yarn init
touch nic_meta_nft.sol
yarn add @openzeppelin/contracts
remixd -s . --remix-ide https://remix.ethereum.org

OpenZeppelin

為什麼裡面要安裝 OpenZeppelin?

因為這個套件基本上已經實作完目前 ERC 標準,可以直接拿來使用,就直接依照標準來製作合約比較快

只要照著這些樣本,就不用花太多時間去實作標準,針對你想調整的部分進行調整跟覆寫就可以了

Remix

基本上初次開發合約的我都建議直接使用 ETH 官方的 Remix 線上編輯器,比較不用花時間自己搞一些節點跟環境

https://remix.ethereum.org/

這個編輯器很方便,可以很快速的跟你的 Metamask 做整合

對於開發來說,非常方便能夠跟合約作互動,不用自己裝一些工具去做 RPC

然後他可以連動你本地的專案進行開發

複製以下的 Code,或是到我的 github 查看

// Contract based on https://docs.openzeppelin.com/contracts/3.x/erc721
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract NicMeta is ERC721Enumerable, Ownable {
    using Strings for uint256;

    bool public _isSaleActive = false;
    bool public _revealed = false;

    // Constants
    uint256 public constant MAX_SUPPLY = 10;
    uint256 public mintPrice = 0.3 ether;
    uint256 public maxBalance = 1;
    uint256 public maxMint = 1;

    string baseURI;
    string public notRevealedUri;
    string public baseExtension = ".json";

    mapping(uint256 => string) private _tokenURIs;

    constructor(string memory initBaseURI, string memory initNotRevealedUri)
        ERC721("Nic Meta", "NM")
    {
        setBaseURI(initBaseURI);
        setNotRevealedURI(initNotRevealedUri);
    }

    function mintNicMeta(uint256 tokenQuantity) public payable {
        require(
            totalSupply() + tokenQuantity <= MAX_SUPPLY,
            "Sale would exceed max supply"
        );
        require(_isSaleActive, "Sale must be active to mint NicMetas");
        require(
            balanceOf(msg.sender) + tokenQuantity <= maxBalance,
            "Sale would exceed max balance"
        );
        require(
            tokenQuantity * mintPrice < msg.value,
            "Not enough ether sent"
        );
        require(tokenQuantity <= maxMint, "Can only mint 1 tokens at a time");

        _mintNicMeta(tokenQuantity);
    }

    function _mintNicMeta(uint256 tokenQuantity) internal {
        for (uint256 i = 0; i < tokenQuantity; i++) {
            uint256 mintIndex = totalSupply();
            if (totalSupply() < MAX_SUPPLY) {
                _safeMint(msg.sender, mintIndex);
            }
        }
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        if (_revealed == false) {
            return notRevealedUri;
        }

        string memory _tokenURI = _tokenURIs[tokenId];
        string memory base = _baseURI();

        // If there is no base URI, return the token URI.
        if (bytes(base).length == 0) {
            return _tokenURI;
        }
        // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
        if (bytes(_tokenURI).length > 0) {
            return string(abi.encodePacked(base, _tokenURI));
        }
        // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
        return
            string(abi.encodePacked(base, tokenId.toString(), baseExtension));
    }

    // internal
    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }

    //only owner
    function flipSaleActive() public onlyOwner {
        _isSaleActive = !_isSaleActive;
    }

    function flipReveal() public onlyOwner {
        _revealed = !_revealed;
    }

    function setMintPrice(uint256 _mintPrice) public onlyOwner {
        mintPrice = _mintPrice;
    }

    function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
        notRevealedUri = _notRevealedURI;
    }

    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }

    function setBaseExtension(string memory _newBaseExtension)
        public
        onlyOwner
    {
        baseExtension = _newBaseExtension;
    }

    function setMaxBalance(uint256 _maxBalance) public onlyOwner {
        maxBalance = _maxBalance;
    }

    function setMaxMint(uint256 _maxMint) public onlyOwner {
        maxMint = _maxMint;
    }

    function withdraw(address to) public onlyOwner {
        uint256 balance = address(this).balance;
        payable(to).transfer(balance);
    }
}

在 Remix 編輯器裡面,先 Compile 之後就可以嘗試 deploy 來部屬合約了

照設定做

  • ENVIROMMENT 選 Injected Web3
  • CONTRACT 選你剛在本地寫的 .sol 專案(我的是 contracts/nic_meta_nft.sol)

接下來我們嘗試先 Deploy 一次

這時候有個 BaseURI 設定要填,可以先忽略,我們先做合約互動

完成後應該會出現 Deployed Contracts

然後我們試試看要 Mint 自己的 NFT 會發生什麼事情

我們會看到錯誤訊息說,目前暫時沒有公開發售的訊息

Sale must be active to mint NicMetas

接著我們把販售打開

戳合約的功能 flipSaleActive 並同意 metamask

這邊可以嘗試 call _isSaleActive 去確認是不是已經搞定

然後再次按下 Mint 你會發現

Not enough ether sent

沒有夾帶足夠的 ETH 是沒辦法買的

這時候要設定跟合約一樣的轉帳費用 0.3 ether

可以透過這個網站去轉換 Wei 單位,因為編輯器上面 ETH 沒辦法打小數點

https://eth-converter.com/

好的選了 Gwei 並從網站中獲得轉換的數據之後填入 300000000

再次按下 Mint 會發生什麼?

Metamask 確實出現要支付 0.3 ETH 的訊息,請確定使用測試網路後按下確認

然後我們到 Opensea 上面看一下,大概稍微等個三分鐘,應該可以看到自己擁有了一個 NFT?

https://testnets.opensea.io/account

恭喜你此時擁有一個什麼都沒有的 NFT

既然已經練習過一次合約的完整流程,我們就來做圖片的處理

首先先下載一個拼圖的工具

https://github.com/HashLips/hashlips_art_engine

yarn install && yarn run build

下載下來先改 code,不然等等生成的數字會錯

把原本依照 network 判斷的直接改成 0

我們進去資料夾可以看到五張照片,先把這照片上傳到 IFPS

【Pinata】

首先我們要到這個 Pinata 註冊一個帳號,我們擁有免費的 1GB 空間可以上傳我們的檔案!

Pinata | Your Home for NFT Media

Upload → Folder → 選取五張圖片

命名為 META_IMAGES

接下來你會看到有 CID,把他複製下來之後來到我們拼圖的工具進行修改

然後更新我們的 meta data

yarn run update_info

這樣一來我們的 json 系列的 image URL 都會被更新囉

然後再到 IFPS 上傳我們的 JSON files

命名為 META_JSONS

確認這兩個資料夾都上傳成功

複製 META_JSONS 的 CID

上傳盲盒圖片

上傳盲盒 metadata

// unpack.json

{
  "name": "Nic Meta 盲盒",
  "description": "Remember to replace this description",
  "image": "ipfs://xxx"
}

這次我們總共上傳四個部分

需要用到的 CID

setBaseURI 請使用 META_JSONS 的 CID(ipfs://QmPXXLCFE2QLNXMnxdB6DVJMLX8k4uyQBErKg248gNyBR2/)

setNotRevealedURI 請使用 unpack.json 的 CID (ipfs://QmPCj5DNWVdQNzpx8Vi1UHbhCSkJavFEPstDDoddCLBZHq)

上傳好可以按下 Refresh Metadata

盲盒就是一個關閉的圖片

接下來 Call flipReveal 就可以打開盲盒

是不是就可以看到自己抽到什麼了,而且也有屬性啦

全部完成就到這邊啦,基本上能 Mint & 開關盲盒就是這樣做的

想給朋友玩,做到這裡就可以了,而且你不爽還可以把他打開的再關起來

本系列其他文章

comments powered by Disqus