Web3-Web3.js核心操作:Metamask、合約調用、事件訂閱全指南
我們做了Solidity的合約代碼,但是合約僅僅是一個后端邏輯;我們想要讓用戶來操作你的邏輯還需要做一個基本的網頁。如果要做一個基本的網頁,我們就要使用到以太坊基金發布的JavaScript庫–Web3.js
什么事Web3.js
以太坊網絡是由節點組成的,每一個節點都包含了區塊鏈的一份拷貝。當你想要調用一份智能合約的一個方法,你需要從其中一個節點中查找并告訴它:
- 智能合約的地址
- 你想調用的方法,以及
- 你想傳入那個方法的參數
以太坊節點只能識別一種叫做 *JSON-RPC* 的語言。這種語言直接讀起來并不好懂。當你你想調用一個合約的方法的時候,需要發送的查詢語句將會是這樣的:
{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
幸運的是 Web3.js 把這些令人討厭的查詢語句都隱藏起來了, 所以你只需要與方便易懂的 JavaScript 界面進行交互即可。
你不需要構建上面的查詢語句,在你的代碼中調用一個函數看起來將是這樣:
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔").send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
準備工作
// 用 NPM
npm install web3// 用 Yarn
yarn add web3// 用 Bower
bower install web3// ...或者其他。
你可以從 github 直接下載壓縮后的 .js
文件 然后包含到你的項目文件中:
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
Web3提供者
現在項目中有了Web3.js, 我們需要初始化它然后和區塊鏈對話。首先我們需要 *Web3 Provider*.
要記住,以太坊是由共享同一份數據的相同拷貝的 節點 構成的。 在 Web3.js 里設置 Web3 的 Provider
(提供者) 告訴我們的代碼應該和 哪個節點 交互來處理我們的讀寫。這就好像在傳統的 Web 應用程序中為你的 API 調用設置遠程 Web 服務器的網址。
你可以運行你自己的以太坊節點來作為 Provider。 不過,有一個第三方的服務,可以讓你的生活變得輕松點,讓你不必為了給你的用戶提供DApp而維護一個以太坊節點— *Infura*.
Infura
Infura 是一個服務,它維護了很多以太坊節點并提供了一個緩存層來實現高速讀取。你可以用他們的 API 來免費訪問這個服務。 用 Infura 作為節點提供者,你可以不用自己運營節點就能很可靠地向以太坊發送、接收信息。
你可以通過這樣把 Infura 作為你的 Web3 節點提供者:
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
不過,因為我們的 DApp 將被很多人使用,這些用戶不單會從區塊鏈讀取信息,還會向區塊鏈 寫 入信息,我們需要用一個方法讓用戶可以用他們的私鑰給事務簽名。
注意: 以太坊 (以及通常意義上的 blockchains )使用一個公鑰/私鑰對來對給事務做數字簽名。把它想成一個數字簽名的異常安全的密碼。這樣當我修改區塊鏈上的數據的時候,我可以用我的公鑰來 證明 我就是簽名的那個。但是因為沒人知道我的私鑰,所以沒人能偽造我的事務。
加密學非常復雜,所以除非你是個專家并且的確知道自己在做什么,你最好不要在你應用的前端中管理你用戶的私鑰。
不過幸運的是,你并不需要,已經有可以幫你處理這件事的服務了: *Metamask*.
Metamask
Metamask 是 Chrome 和 Firefox 的瀏覽器擴展, 它能讓用戶安全地維護他們的以太坊賬戶和私鑰, 并用他們的賬戶和使用 Web3.js 的網站互動(如果你還沒用過它,你肯定會想去安裝的——這樣你的瀏覽器就能使用 Web3.js 了,然后你就可以和任何與以太坊區塊鏈通信的網站交互了)
作為開發者,如果你想讓用戶從他們的瀏覽器里通過網站和你的DApp交互(就像我們在 CryptoZombies 游戲里一樣),你肯定會想要兼容 Metamask 的。
注意: Metamask 默認使用 Infura 的服務器做為 web3 提供者。 就像我們上面做的那樣。不過它還為用戶提供了選擇他們自己 Web3 提供者的選項。所以使用 Metamask 的 web3 提供者,你就給了用戶選擇權,而自己無需操心這一塊。
使用Metamask的Web3的提供者
Metamask 把它的 web3 提供者注入到瀏覽器的全局 JavaScript對象web3
中。所以你的應用可以檢查 web3
是否存在。若存在就使用 web3.currentProvider
作為它的提供者。
這里是一些 Metamask 提供的示例代碼,用來檢查用戶是否安裝了MetaMask,如果沒有安裝就告訴用戶需要安裝MetaMask來使用我們的應用。
window.addEventListener('load', function() {// 檢查web3是否已經注入到(Mist/MetaMask)if (typeof web3 !== 'undefined') {// 使用 Mist/MetaMask 的提供者web3js = new Web3(web3.currentProvider);} else {// 處理用戶沒安裝的情況, 比如顯示一個消息// 告訴他們要安裝 MetaMask 來使用我們的應用}// 現在你可以啟動你的應用并自由訪問 Web3.js:startApp()})
你可以在你所有的應用中使用這段樣板代碼,好檢查用戶是否安裝以及告訴用戶安裝 MetaMask。
注意: 除了MetaMask,你的用戶也可能在使用其他他的私鑰管理應用,比如 Mist 瀏覽器。不過,它們都實現了相同的模式來注入
web3
變量。所以我這里描述的方法對兩者是通用的。
和合約對話
我們已經用 MetaMask 的 Web3 提供者初始化了 Web3.js。接下來就讓它和我們的智能合約對話。
Web3.js 需要兩個東西來和你的合約對話: 它的 地址 和它的 *ABI*。
合約地址
在寫完智能合約后,我們需要去編譯合約然后把合約部署到以太坊。部署后的合約會生成一個合約地址,就是以太坊上的永久地址。
合約 ABI
另一個 Web3.js 為了要和你的智能合約對話而需要的東西是 *ABI*。
ABI 意為應用二進制接口(Application Binary Interface)。 基本上,它是以 JSON 格式表示合約的方法,告訴 Web3.js 如何以合同理解的方式格式化函數調用。
當你編譯你的合約向以太坊部署時, Solidity 編譯器會給你 ABI,所以除了合約地址,你還需要把這個也復制下來。
因為我們這一課不會講述部署,所以現在我們已經幫你編譯了 ABI 并放在了名為cryptozombies_abi.js
,文件中,保存在一個名為 cryptoZombiesABI
的變量中。
如果我們將cryptozombies_abi.js
包含進我們的項目,我們就能通過那個變量訪問 CryptoZombies ABI 。
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
調用合約函數
Web3.js 有兩個方法來調用我們合約的函數: call
and send
.
Call
call
用來調用 view
和 pure
函數。它只運行在本地節點,不會在區塊鏈上創建事務。
復習:
view
和pure
函數是只讀的并不會改變區塊鏈的狀態。它們也不會消耗任何gas。用戶也不會被要求用MetaMask對事務簽名。
使用 Web3.js,你可以如下 call
一個名為myMethod
的方法并傳入一個 123
作為參數:
myContract.methods.myMethod(123).call()
Send
send
將創建一個事務并改變區塊鏈上的數據。你需要用 send
來調用任何非 view
或者 pure
的函數。
注意:
send
一個事務將要求用戶支付gas,并會要求彈出對話框請求用戶使用 Metamask 對事務簽名。在我們使用 Metamask 作為我們的 web3 提供者的時候,所有這一切都會在我們調用send()
的時候自動發生。而我們自己無需在代碼中操心這一切,挺爽的吧。
使用 Web3.js, 你可以像這樣 send
一個事務調用myMethod
并傳入 123
作為參數:
myContract.methods.myMethod(123).send()
語法幾乎 call()
一模一樣。
MetaMask賬戶
MetaMask 允許用戶在擴展中管理多個賬戶。
我們可以通過這樣來獲取 web3
變量中激活的當前賬戶:
var userAccount = web3.eth.accounts[0]
因為用戶可以隨時在 MetaMask 中切換賬戶,我們的應用需要監控這個變量,一旦改變就要相應更新界面。
我們可以通過 setInterval
方法來做:
var accountInterval = setInterval(function() {// 檢查賬戶是否切換if (web3.eth.accounts[0] !== userAccount) {userAccount = web3.eth.accounts[0];// 調用一些方法來更新界面updateInterface();}
}, 100);
這段代碼做的是,每100毫秒檢查一次 userAccount
是否還等于 web3.eth.accounts[0]
(比如:用戶是否還激活了那個賬戶)。若不等,則將 當前激活用戶賦值給 userAccount
,然后調用一個函數來更新界面。
發送事務
現在我們來看看用 send
函數來修改我們智能合約里面的數據。
相對 call
函數,send
函數有如下主要區別:
-
send
一個事務需要一個from
地址來表明誰在調用這個函數(也就是你 Solidity 代碼里的msg.sender
)。 我們需要這是我們 DApp 的用戶,這樣一來 MetaMask 才會彈出提示讓他們對事務簽名。 -
send
一個事務將花費 gas -
在用戶
send
一個事務到該事務對區塊鏈產生實際影響之間有一個不可忽略的延遲。這是因為我們必須等待事務被包含進一個區塊里,以太坊上一個區塊的時間平均下來是15秒左右。如果當前在以太坊上有大量掛起事務或者用戶發送了過低的 gas 價格,我們的事務可能需要等待數個區塊才能被包含進去,往往可能花費數分鐘。所以在我們的代碼中我們需要編寫邏輯來處理這部分異步特性。
我們的函數 send
一個事務到我們的 Web3 提供者,然后鏈式添加一些事件監聽:
receipt
將在合約被包含進以太坊區塊上以后被觸發,這意味著僵尸被創建并保存進我們的合約了。error
將在事務未被成功包含進區塊后觸發,比如用戶未支付足夠的 gas。我們需要在界面中通知用戶事務失敗以便他們可以再次嘗試。
注意:你可以在調用
send
時選擇指定gas
和gasPrice
, 例如:.send({ from: userAccount, gas: 3000000 })
。如果你不指定,MetaMask 將讓用戶自己選擇數值。
調用 Payable 函數
attack
, changeName
, 以及 changeDna
的邏輯將非常雷同,所以本課將不會花時間在上面。
實際上,在調用這些函數的時候已經有了非常多的重復邏輯。所以最好是重構代碼把相同的代碼寫成一個函數。(并對
txStatus
使用模板系統——我們已經看到用類似 Vue.js 類的框架是多么整潔)
我們來看看另外一種 Web3.js 中需要特殊對待的函數 — payable
函數。
function levelUp(uint _zombieId) external payable {require(msg.value == levelUpFee);zombies[_zombieId].level++;
}
和函數一起發送以太非常簡單,只有一點需要注意: 我們需要指定發送多少 wei
,而不是以太。
啥是 Wei?
一個 wei
是以太的最小單位 — 1 ether
等于 10^18 wei
太多0要數了,不過幸運的是 Web3.js 有一個轉換工具來幫我們做這件事:
// 把 1 ETH 轉換成 Wei
web3js.utils.toWei("1", "ether");
在我們的 DApp 里, 我們設置了 levelUpFee = 0.001 ether
,所以調用 levelUp
方法的時候,我們可以讓用戶用以下的代碼同時發送 0.001
以太:
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001","ether") })
訂閱事件
通過 Web3.js 和合約交互非常簡單直接——一旦你的環境建立起來, call
函數和 send
事務和普通的網絡API并沒有多少不同。
還有一點東西我們想要講到——訂閱合約事件
在 Web3.js里, 你可以 訂閱 一個事件,這樣你的 Web3 提供者可以在每次事件發生后觸發你的一些代碼邏輯:
cryptoZombies.events.NewZombie()
.on("data", function(event) {let zombie = event.returnValues;console.log("一個新僵尸誕生了!", zombie.zombieId, zombie.name, zombie.dna);
}).on('error', console.error);
使用 indexed
為了篩選僅和當前用戶相關的事件,我們的 Solidity 合約將必須使用 indexed
關鍵字,就像我們在 ERC721 實現中的Transfer
事件中那樣:
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
在這種情況下, 因為_from
和 _to
都是 indexed
,這就意味著我們可以在前端事件監聽中過濾事件
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {let data = event.returnValues;// 當前用戶更新了一個僵尸!更新界面來顯示
}).on('error', console.error);
看到了吧, 使用 event
和 indexed
字段對于監聽合約中的更改并將其反映到 DApp 的前端界面中是非常有用的做法。
查詢過去的事件
我們甚至可以用 getPastEvents
查詢過去的事件,并用過濾器 fromBlock
和 toBlock
給 Solidity 一個事件日志的時間范圍(“block” 在這里代表以太坊區塊編號):
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: 'latest' })
.then(function(events) {// events 是可以用來遍歷的 `event` 對象 // 這段代碼將返回給我們從開始以來創建的僵尸列表
});
因為你可以用這個方法來查詢從最開始起的事件日志,這就有了一個非常有趣的用例: 用事件來作為一種更便宜的存儲。
若你還能記得,在區塊鏈上保存數據是 Solidity 中最貴的操作之一。但是用事件就便宜太多太多了。
這里的短板是,事件不能從智能合約本身讀取。但是,如果你有一些數據需要永久性地記錄在區塊鏈中以便可以在應用的前端中讀取,這將是一個很好的用例。這些數據不會影響智能合約向前的狀態。
舉個栗子,我們可以用事件來作為僵尸戰斗的歷史紀錄——我們可以在每次僵尸攻擊別人以及有一方勝出的時候產生一個事件。智能合約不需要這些數據來計算任何接下來的事情,但是這對我們在前端向用戶展示來說是非常有用的東西。
Web3.js 事件 和 MetaMask
上面的示例代碼是針對 Web3.js 最新版1.0的,此版本使用了 *WebSockets* 來訂閱事件。
但是,MetaMask 尚且不支持最新的事件 API (盡管如此,他們已經在實現這部分功能了, 點擊這里 查看進度)
所以現在我們必須使用一個單獨 Web3 提供者,它針對事件提供了WebSockets支持。 我們可以用 Infura 來像實例化第二份拷貝:
var web3Infura = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
var czEvents = new web3Infura.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
然后我們將使用 czEvents.events.Transfer
來監聽事件,而不再使用 cryptoZombies.events.Transfer
。我們將繼續在課程的其他部分使用 cryptoZombies.methods
。
將來,在 MetaMask 升級了 API 支持 Web3.js 后,我們就不用這么做了。但是現在我們還是要這么做,以使用 Web3.js 更好的最新語法來監聽事件。
總結
Web3.js是以太坊官方JavaScript庫,簡化了與智能合約的交互過程。本文介紹了Web3.js的核心功能與使用方法:1) 通過Web3提供者(如Infura或Metamask)連接以太坊節點,2) 使用合約地址和ABI與智能合約對話,3) 區分call和send方法調用合約函數。重點講解了如何集成Metamask進行用戶賬戶管理,包括檢測賬戶變更和事務簽名。該指南為開發者提供了Web3.js的基本操作框架,包括初始化、合約交互和事件處理等關鍵環節。