---
# System prepended metadata

title: 2022-03-28 自己發 NFT—Part 1
tags: [' Cryptocurrency', Notes]

---

---
lang: zh-tw
tags: Notes, Cryptocurrency
date: 20220324
robots: noindex, nofollow
license: GPL-3.0
---

2022-03-28 自己發 NFT—Part 1
===
歡迎加入 Discord 聊天室「清交區塊鏈DAO」：https://discord.com/invite/rpQy37a8tk

這次讀書會延續上週 Tommy 的分享，把主題轉向另一種熱門代幣種類：ERC721（NFT）。

我們會先使用 Remix 和 Pinata 快速發行最基本的 NFT。接著，透過閱讀 EIP721 提案內容，來看看 EIP721 和 EIP20 之間的差別在哪邊。

這份教學只是發行 NFT 的入門，對於實際掌握所有 NFT 使用技巧仍有相當差距；鼓勵各位讀者閱讀文末的連結，並在 Discord 聊天室與大家討論，相信會有更多收穫。


Remix＋Pinata/NFT.Storage
---
1. 開啟 [Remix](https://remix.ethereum.org/)
2. 創立一個新檔案 `MyNFT.sol` 並貼上這份 solidity code
```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract MyNFT is ERC721URIStorage {

    address private immutable _owner;
    uint256 private _highest_id;

    constructor() ERC721("MyNFT", "MNFT") {
        _owner = msg.sender;
    }
    
    function mintNFT(string calldata token_URI) public {
        require(msg.sender == _owner, "You are not owner.");
        
        uint256 new_token_ID = _highest_id;
        _highest_id += 1;
        
        _safeMint(msg.sender, new_token_ID);
        _setTokenURI(new_token_ID, token_URI);
    }
}
```
3. 編譯合約、部屬合約到 **Rinkeby** testnet
4. 登入 IPFS SaaS 服務
    - 登入 [Pinata](https://www.pinata.cloud/)
    - 登入 [NFT.Storage](https://nft.storage/)
5. 上傳圖片到 IPFS SaaS 服務，並取得 `<Pic_URL>`
    - Pinata：點擊檔名旁邊的👁圖示
    - NFT.Storage：Actions → Copy IPFS URL
6. 把 `<Pic_URL>` 填入以下 JSON 格式當中
```json
{
  "description": "Hello, this is my first NFT.",
  "image": "<Pic_URL>",
  "name": "MyNFT No.0"
}
```
7. 把上述 JSON 存成檔案並上傳到 IPFS SaaS，然後取得 `<Metadata_URL>`
    - Pinata：點擊檔名旁邊的👁圖示
    - NFT.Storage：Actions → Copy IPFS URL
8. 回到 Remix，把 `<Metadata_URL>` 塞給 `mintNFT()` 函數的 `token_URI` 欄位
9. 按下按鈕送出 txn，等待 txn 被成功 mined
10. 前往 [OpenSea](https://testnets.opensea.io) **testnet** 搜尋合約地址就能看到稍早鑄造的 NFT

以下提供一個操作完成之後的例子；不需要完全相同，實際情形依個人操作而有所不同
- https://rinkeby.etherscan.io/address/0xa52705cbc4931f11161b45212df417c54b345c91
- https://testnets.opensea.io/collection/mynft-y7rsblfbfy
    - 出現「y7rsBlFbfy」字串是因為 OpenSea testnet 上面，已經存在其他 NFT project 是以相同名稱 MyNFT 命名，所以由系統自動加上以免搜尋出衝突的結果，不是合約本身的問題


閱讀 EIP-721
---
- https://eips.ethereum.org/EIPS/eip-721

```solidity
interface ERC721 {
    // Events
    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);
    
    // Functions (Read-Only Methods)
    function balanceOf(address _owner) external view returns (uint256);
    function ownerOf(uint256 _tokenId) external view returns (address);
    function getApproved(uint256 _tokenId) external view returns (address);
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
    
    // Functions (Write Methods)
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function approve(address _approved, uint256 _tokenId) external payable;
    function setApprovalForAll(address _operator, bool _approved) external;
}
```
重點解析
- 一個地址可以擁有多顆 token
    - 由 token 持有人（owner）一顆一顆授予權限給某地址實在太沒效率，因此設計有 `approve()` 和 `setApprovalForAll()` 兩種函數
    - 由 owner 呼叫 `approve(address(0), token_id)` 代表撤銷權限
    - 由 owner 呼叫 `setApprovalForAll(operator, false)` 代表撤銷權限
- `approve()` 負責設定「**地址**與單獨**一顆 token** 之間關聯性」
    - 換句話說：Owner `approve()` the authorized address for managing `token_id` token.
    - 例如：OpenZeppelin 的實作細節就是在維護 state variable `_tokenApprovals`
    - 呼叫這個函數的 EOA，一定要是 token owner
    - 轉移 token 所有權時，一定要呼叫 `approve(address(0), token_id)`
    - 一個 token 只能有一個 approved address（以 OpenZeppelin 的實作來說）
    - approved address 可與 operator address 不同
- `setApprovalForAll()` 負責設定「**地址**與**地址**之間關聯性」
    - 換句話說：Owner `setApprovalForAll()` of his/her owned tokens to the operator address.
    - 例如：OpenZeppelin 的實作細節就是在維護 state variable `_operatorApprovals`
    - 呼叫這個函數的 EOA，一定要是 token owner
    - 轉移 token 所有權時，不干這個函數的事情（token owner 可以換人，operator 不一定要換人）
    - 一個 address 只能有一個 operator address（以 OpenZeppelin 的實作來說）
    - operator address 可與 approved address 不同
- `safeTransferFrom()` 和 `transferFrom()` 的差別
    - 函數是否幫忙檢查：如果 token 轉移過去的地址是合約，那麼該合約具備 `IERC721Receiver` 嗎？
- `_isApprovedOrOwner()`
    - 這個函數的實做細節舉例三種「能夠花用特定某一顆 token」的條件
    - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/76eee35971c2541585e05cbf258510dda7b2fbc6/contracts/token/ERC721/ERC721.sol#L232-L236


Further reading
---
- EIP-721 & (OpenZeppelin's) Implementation
    - https://eips.ethereum.org/EIPS/eip-721
    - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
    - https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721/extensions
- Remix
    - https://remix.ethereum.org
    - https://remix-ide.readthedocs.io
- Pinata
    - https://www.pinata.cloud
- NFT.Storage（Made by Protocol Labs）
    - https://nft.storage/docs/
- OpenSea
    - https://docs.opensea.io/docs/metadata-standards
- Chainlink 水龍頭
    - https://faucets.chain.link/rinkeby