2021年8月11日 星期三

Ethereum Smart Contract and ERC Spec working with IPFS, Project Base with Hardhat and Ethers.js

 乙太坊的智能合約逐漸引入了各式標準,第三方應用透過制式標準的介面 (Interface) 打造通用的代幣使用體驗,像是只要符合 ERC 標準的智能合約,Metamask 就可以引入該代幣合約的錢包。


本文章用於介紹 ERC Solidity 實作自己的代幣合約,在不同種的代幣合約應用的方式,以及將 IPFS 檔案 Token 與 ERC 合約綁定; 最後在終端使用者的 Web 專案中,可以使用 Remix 外的 Hardhat 做合約專案及編譯流,以及 Ethers.js 在前端讀取合約,呼叫 Metamask 進行交易。


大量的內容事前準備


這個文章會涉及大量的去脈絡化概念,可能會直接迷失在資訊海中,因此在此先做好各項解釋。


智能合約, Solidity, Fe, Vyper...


智能合約,簡單的說就是 Ethereum 的 Blockchain 本身的特性可以允許在 block 上寫入更多的資料,進而衍生出智能合約的協議,合約本身可以用任何語言寫 (目前熱門的舉例是: Solidity, Fe, Vyper ...etc),合約會編譯成 Bytecode,而可以跟合約本身互動做應用的是 ABI (Application Binary Interface), ABI 就是合約編譯成 Bytecode 的形式,通常要做合約執行、讀取都會吃 abi 這個格式。


合約寫完之後要寫到鏈上,此時需要付一點點的 ETH Gas 作為手續費,寫到鏈上之後,你會獲得這個合約的 address 地址,之後就是要跟這個地址的合約進行互動。


使用者本身可以對這個合約上含有 payable 的 function 進行付款,付款之後可以決定合約要做什麼事,以及誰可以提款 (佈署合約時可以在 constructor 指定佈署合約的人可以提款)。


延伸閱讀: EVM: From Solidity to byte code, memory and storage


代幣合約 / 自行發幣 / 山寨幣, ERC, OpenZeppelin


當我們說自己發幣、發代幣、山寨幣,其實都是同一個東西,總之就是在 ETH 或相關的幣上,直接發行自己的代幣,這個代幣可以是有價無價,例如用 ETH 發行數張電影票,電影票就是代幣; 或是用 ETH 發行【母湯 Bear】代幣,讓玩家可以用這個代幣去樂園玩實體遊樂設施。


在乙太方中,你可以自己寫一個完全脫離公制標準的代幣合約,可以完全不按照標準進行,只是會缺少很多第三方應用支援,而且你在做的事情基本上就是在重新造輪子。


因此,代幣公制合約出現了,最典型的有幾個合約: ERC20, ERC721, ERC1155,以下會介紹這幾個合約的簡單說。


ERC 20

簡單銀行,對於單幣種發行適用,比方說單一發行美金,單一發行日幣,單一發行歐元...etc。

最重要的概念是,ERC 20 適用任何量級對所有人來說價值是一致的,比方說我的 1 美元的價值完全等於你手上的 1 美元的價值。


如果要無中生有一堆幣給別人,可以自行鑄幣 (Mint, Creating Supply)。


ERC 721

獨一無二形式的契約,適用於房地契、世界僅有一個的契約證明等,想發行一個獨特合約,就必須先鑄幣,這裡鑄幣的意思是發行一個獨一無二的契約。


發行時可以帶一些 URL 資訊或額外資訊在上面。


ERC 1155

混和形式的 Token 發幣標準,適用於遊戲中的倉庫物件、物品、也可以包含獨一無二的物品。

可以一開始就把需要的數量鑄幣好,做成有限個數的量然後發行,獨一無二的物品只需要發行一個即可達到這樣的概念。


共同概念

這些幣的共同概念,就是轉帳,鑄幣 (Mint),如果要預先製造好所有的物品,那可以把所有物品一開始的擁有者歸在發行合約者身上,最後要取用、轉移則是透過合約發行者去轉帳,這個角色也可以是自動化程式執行者。


鑄幣、轉帳都可以決定先轉給誰,也可以先轉給自己,之後再轉給別人,不管怎樣,執行鑄幣都要收手續費,你可以一次鑄好 (Batch),甚至 ERC1155 也可以 Batch 發送轉帳給別人。


合約與公版

這些合約事實上就已經是 .sol 檔案了,基礎函式、操作都已經有了,甚至不需要做任何事就可以直接發行代幣,唯一要做的就是去下載合約,然後上鏈。


Open Zeppelin 甚至提供了合約建立精靈,用 UI 就可以加上想要的功能:

https://docs.openzeppelin.com/contracts/4.x/



下載後直接上鏈或是可以到 Remix 做測試。


Open Zeppelin 提供了 ERC 系列的解釋、用法,這是一份值得詳細讀完的文件: 

https://docs.openzeppelin.com/contracts/4.x/

(記得要切到最新版看, Google 都會搜尋到舊版的)


對價標準

這是涉及通貨、經濟學的概念,你需要考慮的是,你所有發行的貨幣是有限個數,還是無限個數的,數量量級都會影響代幣與有價貨幣的對價關係,引發通貨膨脹、通貨緊縮的問題。


IPFS, Ethereum, Token

IPFS 是一個區塊鍊的分散式檔案架構的服務,記帳權是持有量證明,用的幣是檔案幣,檔案上傳後,你會得到一組 token,取得的方式是去找 IPFS Gateway 列表 [2],隨便找一個還活著的 Gateway 使用 token 作為 url 參數下載:

https://ipfs.github.io/public-gateway-checker/


選定一個 Gateway 之後,網址後面加上 /ipfs/:token_hash 就可以下載了,範例:

https://xxxxxxxxxxxxxxxxxxxx.com/ipfs/3NK21N3N3K10NBS90


這串網址就可以作為 token,把資訊強加在 ERC 合約上綁著一起賣,反正這就表示該張合約賣的東西跟 IPFS 所述相同。


Mainnet, Testnet 與 Infura 代理速度變快

所有的鏈互動,都可以用預設 Official 提供的節點伺服器位置,甚至有些服務為了要加快你的應用程式的存取速度,提供你他們的節點伺服器位置讓你填寫,然後使用者每個月付費就可以享有這些好處。

像是 Infura 就提供 IPFS, Ethereum ...etc 的 net 可以串,註冊帳號後,可以拿到 Infura 提共的 Mainnet, Testnet 地址, Mainnet 是正式網路,需要付給真實的錢,Testnet 是測試網路,可以透過水龍頭服務給你帳號發點錢來測試。



Hardhat, Remix

Remix 是一個線上 IDE,你可以自己寫一寫在線上測試,或是真的佈署到鏈上,可是本地專案也會需要一個這樣的佈署流程,雖然沒 Remix IDE 來的方便,但是卻是專案結構變大時會需要的流程建立,而本文章使用的工具是 Hardhat ,Hardhat 可以幫你編譯 .sol 檔案,還可以提共你本地測試用的 RPC Server,預設給你 10 組 100ETH 的錢包可以用,但是要記得每次重新開啟 Hardhat 時,綁定錢包服務的 address 要重新匯入新的。


Hardhat 還可以幫你佈署合約到目的網路,可以是 Mainnet 也可以是 localhost 也可以是 Testnet。


Ethers.js, Web3.js

Web3.js, Ethers.js 都是在網頁上操作合約互動的工具,終端的互動就是要呼叫 metamask 這個瀏覽器外掛起來交易,所以你必須先安裝好 Metamask 在 chrome 之類的瀏覽器中,還要設定好你的錢包 (錢包可以用 localhost 或 Mainnet 或 Testnet)。


合約執行 Function 時,就會用 Ethers.js 觸發,甚至佈署合約時也要透過 Ethers.js 給 constructor 丟資料,佈署等等。


Etherscan, Indexing APIs

所有對 Blockchain 的交易,都可以在 Etherscan 上找到,包含合約佈署、合約 ABI (可轉為程式碼) ,甚至不只 Etherscan,也有其他服務會提供 Indexing API 去處理,像是 The Graph 就提供 GraphQL 查詢的方式給終端。

最基礎的 Ether API 大概會提供你查詢這個 block 的狀態、block 交易、block 證明或被 mining 的量、手續費等等。


如果你要發的代幣要通用很多自己商業生態的服務,那麼你需要自己再寫一層 API Wrapper ,提供廠商服務,不過在這塊需要考量的大概就是分頁問題 (pagnition)。


進行合約開發


上面針對了許多去脈絡化的概念做了一些精簡的解釋,現在要開始寫合約來做發幣了。


Wizard 與寫一個 ERC20 代幣合約


其實你什麼都不用做,只要去 Wizard 勾好選項去下載就好,在這邊對 Wizard 一些項目做精簡的解釋:

ERC20

  • Settings
    • Name: 合約名稱 (代幣完整名稱)
    • Symbol: 代幣符號 (通常是縮寫: 如 ETH, BTC)
    • Premint: ERC20 初始需要製幣數量
  • Features
    • Mintable: 可再鑄幣
    • Burnable: 可燒毀指定數量的幣以維持平衡
    • Pausable: 可停止交易
    • Permit: 終端使用者不須要支付手續費(gas),取代之就是合約佈署者要代為支付
    • Votes: 終端使用者可以擁有投票權 (公司、事務決策,類似董事會持股高的人可以有部分投票決策權)
    • Flash Minting: 借貸、貸款功能
  • Access Control: 存取控制
    • Ownable: 只有建立合約的人才有權限操作合約內容 (根據限制而定)
    • Roles: 可以建立浮動機制,讓一張合約有很多人可以擁有權限
  • Upgradeability: 合約升級的方法
    • UUPS
    • Transparent

// contracts/GLDToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract GLDToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("Gold", "GLD") {
        _mint(msg.sender, initialSupply);
    }
}


在 Remix 上操作代幣合約

選好之後,按下 Open in Remix,對檔案儲存一下,在 Compile 區域,可以選擇你的合約,然後做 Compile:



然後,在 Deploy 欄位直接選擇剛才的合約, Deploy 上去。


佈署上去後,可以在任意下方執行 Function:


只要輸入參數後,按下按鈕就可以執行 Function。


對於自製任何需要付費的 payable function,要在上方的 value 輸入你要付的錢數量,才去點紅色的按鈕開始交易:



如果想判斷到使用者做了某些事情之後,要取消交易怎麼辦?

可以在 solidity 使用 assert, require 之類斷言的方法判斷,如果判斷錯誤,就會終止交易。


寫一個 ERC721 合約


ERC721
  • Settings
    • Name
    • Symbol
    • Base URI: 綁定此合約而外的資訊參數,這裡就可以放已經上 IPFS 鏈的網址檔案
  • Features
    • Mintable 有沒有鑄幣功能
      • Auto Increment Ids (每個 ID 都是獨特的,每次鑄幣之後要幫您增加 ID 嗎?)
    • URI Storage 每個 Ids 合約底下的獨特物件,都可以綁一個 URL 帶有額外參數,這裡也可以放已經上 IPFS 鏈的網址檔案
  • Access Control
  • Upgradeability


在 Remix 上操作代幣合約

可先參考 721 文件: https://docs.openzeppelin.com/contracts/4.x/erc721

我複製了文件中的 awardItem 出來改,可以看到合約 function 吃了兩個參數,第一個是 player,第二個是 tokenURI,這個 tokenURI 就是專門放網址,或是 IPFS Token 專用的,第一個放 player,是希望在鑄幣之後,直接把這個 721 Token 賦予給這個人 (轉帳)。



在這些合約中,要善用 balanceOf 的方式檢查餘額 (對 1155 來說,甚至該物品餘額),或是在 721 情境下檢查此人是否擁有此 id token 物件。

並可以善用 transferFrom 相關函數來做轉帳,使用 mint 相關關鍵字函數來進行鑄幣。


建立一個 Hardhat 專案


相關流程請直接跑: https://hardhat.org/tutorial/  這份教學,這裡只閘述要做什麼事,首先是 hardhat 專案的結構:

  • contracts: 所有 .sol 放在這裡
  • artifacts: 編譯完的 .sol 會放在這裡
  • scripts: 執行用腳本
    • deploy.js: 佈署腳本

首先,將寫完的 .sol 放在 contracts 目錄,就可以執行 hardhat compile 進行編譯,編譯後檔案就會出現在 artifacts。

接下來,要針對編譯的 artifacts 做佈署腳本,開一個檔案叫做 deploy.js 放在 scripts 目錄,寫道:

const hardhat = require("hardhat");
const fs = require('fs');

async function main() {
  // getContractFactory 會去 artifacts 找出你剛才編譯完的合約,名稱要跟 Solidity 一致才會被找到
  const MyTokenContract = await hardhat.ethers.getContractFactory("MyToken");
  
  // deploy() 等同於 .sol 中的 constructor,可以放建構子需要的參數,比方說某人的 address
  // 例如 const mytoken = await MyTokenContract.deploy(XXXContract.address); // 放入其他已上鏈合約的 address
  // 也可以是多個參數,取決於 solidity constructor 如何定
  const mytoken = await MyTokenContract.deploy(); 
  
  // 將此合約進行佈署
  await mytoken.deployed();
  console.log('deployed address', mytoken.address);

}

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

寫完之後,我們要佈署這個合約,如果有 testnet 可以直接跳過這關,如果想用 localhost,則需要開啟本地端 ETH RPC, Hardhat 有提供這項功能,直接執行指令:


npx hardhat node


就會開啟了。


此時,要佈署合約,只需要使用指令:


npx hardhat run ./scripts/deploy.js --network localhost


他就會把合約佈署在第一個錢包上。


一個簡單的 js function, 上傳圖片到 IPFS


我們會直接使用 IPFS 測試鏈,infura 有提供這樣的測試鏈:


import { create as ipfsHttpClient } from "ipfs-http-client";
const client = ipfsHttpClient("https://ipfs.infura.io:5001/api/v0");

async function uploadFileToIPFS(e) {
    const file = e.target.files[0];
    const ipfs_response = await client.add(file, {});
    const url_with_token = `https://ipfs.infura.io/ipfs/${ipfs_response.path}`;
    // 現在 IPFS 測試檔案已經在 IPFS 中,可以把它當作 TokenURI 送出了
}

執行已上鏈合約的 Function,然後綁定 IPFS Token 到 ERC721


import { ethers } from "ethers";
// 從編譯完的 artifacts 中引用合約資料
import MyToken from "../artifacts/contracts/MyToken.sol/MyToken.json";


const web3Modal = new Web3Modal();

const connection = await web3Modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();

// 使用 ethers.js, 第一個要填已上鏈的合約地址,然後從 artifacts 取得合約的 ABI
let contract = new ethers.Contract("已上鏈的合約地址", MyToken.abi, signer);

// contract.createToken 是合約中的 function, 可以自行呼叫合約的 function
// playerAddress 是玩家的 address, url_with_token 是剛才的 IPFS 檔案位置,可以直接夾帶寫入執行 function
let transaction = await contract.awardItem(playerAddress, url_with_token);

// 此時,網頁外掛 metamask 會開始執行交易
let tx = await transaction.wait();

// 交易完成



References:

https://www.quicknode.com/guides/solidity/an-overview-of-how-smart-contracts-work-on-ethereum

https://docs.ipfs.io/concepts/ipfs-gateway/#gateway-providers

https://docs.openzeppelin.com/contracts/4.x/erc721

https://eips.ethereum.org/EIPS/eip-2612

https://github.com/dapphub/ds-dach

https://github.com/graphprotocol/graph-node

https://medium.com/@austin_48503/tl-dr-scaffold-eth-ipfs-20fa35b11c35

https://etherscan.io/apis

https://github.com/ethereum/eips/issues/1155

https://eips.ethereum.org/EIPS/eip-3386

https://www.abmedia.io/what-is-erc-1155

https://nftschool.dev/tutorial/end-to-end-experience/#how-minting-works

https://ethereum.org/zh-tw/developers/docs/standards/tokens/erc-721/

https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb



沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014