本篇預計閱讀時間 15 分鐘。
在 Solidity 中,當一個 Event 被呼叫之後它的參數就會被置於區塊鏈之中。
「事件」讓我們在 DApp 或瀏覽器的環境下可以呼叫 Javascript 的監聽,藉由監聽得到一些我們想知道的訊息。
區塊鏈上的交易會有所謂的日誌(logs),我們沒有辦法以一合約直接訪問該合約中的事件和 logs,所以它的可視性是 external 的。
事件是合約的可繼承成員,發出一個事件可以使用合約的地址訪問事件,生成的事件不能從合約內部訪問。
以下是我們使用事件的用途:
-
事件是一個合約跟外部前端 app(或說使用者 interface)溝通的方法,可以在前端聆聽(listening)合約裡的特定事件或者動作,當他們被啟動或者改變的時候。
-
同步於資料啟動某些動作
-
相同資料量的情況下,比儲存在
storage
更便宜 -
如果我們今天要 debug 或者測試的時候,也可以利用
event
來模擬一個 Javascriptconsole.log
的功能。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract EventExample {
mapping(address => uint) public tokenBalance;
constructor() {tokenBalance[msg.sender] = 100;
}
function sendToken(address _to, uint _amount) public returns(bool) {
require(tokenBalance[msg.sender] >= _amount, "Not enough tokens");
assert(tokenBalance[_to] + _amount >= tokenBalance[_to]);
assert(tokenBalance[msg.sender] - _amount <= tokenBalance[msg.sender]);
tokenBalance[msg.sender] -= _amount;
tokenBalance[_to] += _amount;return true;
}
}
當我們呼叫了 sendToken()
這個函式,在不同的環境它會回傳不同的結果:
-
使用 Javascript VM(Virtual Machine) 時:
-
sendToken()
returnsboolean: true
-
因為其是模擬一個「瀏覽器」環境
-
-
使用 Injected Web3 時:
-
sendToken()
不會 returns 任何東西 -
因為其是在真實的區塊鏈上運作;
-
這也就是說,如果我們希望在真實的區塊鏈上聆聽回傳結果,必須透過 Event,並將其 emit
置於函數之中(記得要在函數 return
之前)。而Event也可以被像是 MetaMask 之類的 Provider 聆聽。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract EventExample {
mapping(address => uint) public tokenBalance;
event TokenSent(address _from, address _to, uint _amount);
constructor() {tokenBalance[msg.sender] = 100;
}
function sendToken(address _to, uint _amount) public returns(bool) {
require(tokenBalance[msg.sender] >= _amount, "Not enough tokens");
assert(tokenBalance[_to] + _amount >= tokenBalance[_to]);
assert(tokenBalance[msg.sender] - _amount <= tokenBalance[msg.sender]);
tokenBalance[msg.sender] -= _amount;tokenBalance[_to] += _amount;
emit TokenSent(msg.sender, _to, _amount);
return true;
}
}
在以上的例子之中當我們呼叫了具有 emit
的 sendToken()
,我們可以在 Transaction Information 中看見 logs
,而在 logs
中會看見我們 emit
的資訊。
Event 具有以下的性質:
-
應用程式可以透過 Ethereum 客戶端的 RPC interface,以
subscribe
來聆聽這些事件 -
Events 是作為合約的一部分是可繼承的
-
合約不可以存取自己 Event 被啟動之後的
logs
和任何相關 data。 -
被標示
indexed
的 Event 參數可以在未來被檢索
emit
我們可以寫一個函數裡面加上 emit
這個動詞來觸發事件。
舉例來說:如果我們先建立事件並且於函數裡觸發它。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract Test {
event Deposit(address indexed _from, bytes32 indexed _id, uint _value);
function deposit(bytes32 _id) public payable {
emit Deposit(msg.sender, _id, msg.value);
}
}
在 Javascript 使用 web3.js 的套件呼叫合約事件
var abi = /* abi as generated using compiler */;
// 要使用合約內容必須要得到合約在 compiler 之後輸出的 abi
var ClientReceipt = web3.eth.contract(abi);
var clientReceiptContract = ClientReceipt.at("0x1234...ab67" /* address */);
// 呼叫事件 Deposit:
var event = clientReceiptContract.Deposit(function(error, result) {if (!error)console.log(result);
});
在前端(DApp)回傳的結果如下:
{
"returnValues": {
"_from": "0x1111...FFFFCCCC",
"_id": "0x50...sd5adb20",
"_value": "0x420042"
},
"raw": {
"data": "0x7f...91385",
"topics": ["0xfd4...b4ead7", "0x7f...1a91385"]
}
}
如果我們今天輸入 0x05416460deb76d57af601be17e777b93592d8d4d4a4096c57876a91c84f4a712
也可以直接在 REMIX 的下方互動環境看見一些資訊,包含 logs
等:
logs =
[
{
"from": "0x9DD41ECd6e1701CE34523ed98423c1eFb0805aBD",
"topic": "0x19dacbf83c5de6658e14cbf7bcae5c15eca2eedecf1c66fbca928e4d351bea0f",
"event": "Deposit",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "0x05416460deb76d57af601be17e777b93592d8d4d4a4096c57876a91c84f4a712",
"2": "0",
"_from": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","_id": "0x05416460deb76d57af601be17e777b93592d8d4d4a4096c57876a91c84f4a712",
"_value": "0"
}
}
]
接下來我們會一一介紹 indexed
, data
, topics
等專有名詞。
Indexed
indexed 作為 event 的參數綴詞是用於在呼叫 event 時我們可以用 indexed 來作為過濾,找到我們要找的參數。
需要注意的是最多只有三個 event 參數可以被宣告 indexed。
舉例來說有一個事件為:
event Transfer(address indexed _from, address indexed _to, uint256 _value)
這代表前端可以有效率的查出 token 的交易資訊:
-
從何地址送出:tokenContract.Transfer({_from: senderAddress})
-
被何地址接收:tokenContract.Transfer({_to: receiverAddress})
-
從一個地址送往另一個特定地址:tokenContract.Transfer({_from: senderAddress, _to: receiverAddress})
只有當我們想要這個 event 的參數可以被搜尋的到時才使用 indexed。
使用或不使用 indexed 的 gas 花費比較:
event NewEvent(address addr)
event NewEvent(address indexed addr)
web3.eth.subscribe()
這邊為了更了解 event
的運作,首先我們要來到 web3.js 來看 web3.eth.subscribe()
的運作。web3.eth.subscribe()
讓我們可以聆聽區塊鏈上合約的特定事件。
web3.eth.subscribe(type [, options] [, callback]);
-
type
: 輸入型態為String
-
我們想要聽的類型。
-
-
options
: 輸入型態為Mixed
-
(optional) 根據想要聆聽的類型(type)來決定這邊選填的內容是什麼。
-
-
callback
: 輸入型態為Function
-
(optional) 回傳的第一個參數為 error 物件,第二個參數為 result 物件,第三個參數為聆聽類型(subscription)自己
-
呼叫完 web3.eth.subscribe()
後回傳的物件為 EventEmitter
,內容如下:
-
subscription.id
: subscription 的id
,可以用來辨識或者取消聆聽此 subscription -
subscription.subscribe([callback])
: 可以使用同樣的參數來重新聆聽(re-subscribe) -
subscription.unsubscribe([callback])
: 取消聆聽這個(Unsubscribes)subscription 並且在成功時回傳TRUE
-
subscription.arguments
: subscription 的參數(arguments
),可以被用來re-subscribing
. -
on("data") returns Object
: 以log
物件當作參數執行每個即將發生的log
-
on("changed") returns Object
: 執行每個從區塊鏈被移除的log
。log
會有額外的屬性"removed: true"
-
on("error") returns Object
: 聆聽過程中如果出現錯誤就會執行 -
on("connected") returns String
: 聆聽成功後就會回傳subscription id
接下來我們講解 web3.eth.subscribe()
裡面 type 參數的重點聆聽類型 'logs'
,當然 type 參數還有很多其他的選擇,介紹 web3.js 的章節有專門講解。
web3.eth.subscribe('logs', options [, callback]);
使用給定的 options
聆聽 logs
。
參數 Parameters:
-
"logs"
-String
,聆聽的類型 -
Object
- 聆聽類型的options
-
fromBlock
-Number
: 更早的區塊數量,預設為null
-
address
-String|Array
: 一個儲存address
的陣列或一個address
來得到某個特定帳號的logs
-
topics
-Array
: 一個在 log 裡面出現的數值陣列,其順序是有意義的,如果我們希望不要使用某個元素可以使用null
,e.g.[null, '0x00...']
。
-
-
callback
-Function
: (optional) 回傳的第一個參數為 error 物件,第二個參數為 result 物件
回傳值 Returns:
-
EventEmitter:
-
"data" returns Object
: 以log
物件當作參數執行每個即將發生的log
-
"changed" returns Object
: 執行每個從區塊鏈被移除的log
。log
會有額外的屬性"removed: true"
-
"error" returns Object
: 聆聽過程中如果出現錯誤就會執行 -
"connected" returns Number
: 聆聽成功後就會回傳subscription id
-
範例:
var subscription = web3.eth.subscribe('logs', {
address: '0x123456..',
topics: ['0x12345...']
}, function(error, result){
if (!error)
console.log(result);})
.on("connected", function(subscriptionId){console.log(subscriptionId);
})
.on("data", function(log){console.log(log)
;})
.on("changed", function(log){
});
// unsubscribes the subscription
subscription.unsubscribe(function(error, success){
if(success)
console.log('Successfully unsubscribed!');});
Logs with Data & Topics
-
當一個參數沒有
indexed
屬性時, 他們會被ABI-encoded
成log
的data
部分。 -
如果一個參數有
indexed
屬性,他們會被包含在topics
裡面。
Topics
Topics 是一個包含 event
裡的 indexed
參數們的物件。
topic[0]
代表的是一個事件它自己的 hash 的 hash。可以擁有最多三個的 indexed
參數,每一個會有相對應的 topics
元素。
以下的範例是使用 web3.js
subscribe("logs")
這個方法去過濾 logs 中符合特定 address
的 topics
:
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});
EVM 使用低階原始語言去呼叫 logs 並且 map
他們成高階的 Solidity 結構,也就是 Event
。Logs 裡面可能儲存 indexed arguments
於不同的 topics
。
舉例來說:
//SPDX-License-Identifier: MIT
contract SimpleAuction {
event PersonCreated(uint indexed age, uint height);
}
function foobar() {
emit PersonCreated(26, 176);
}
當我們觸發了這個事件,將會製造一個擁有 topics
的低階的以太坊虛擬機日誌接口(low-level EVM log entry):
-
0x6be15e8568869b1e100750dd5079151b32637268ec08d199b318b793181b8a7d
-
Keccak-256 hash of
PersonCreated(uint256,uint256)
-
-
0x36383cc9cfbf1dc87c78c2529ae2fcd4e3fc4e575e154b357ae3a8b2739113cf
-
Keccak-256 hash of
age
, 反得到value
為 26
-
- 我們會發現
height
並不存在於topics
裡面,因為它屬於event
裡面data
的部分。 - 如果我們在 web3 的客戶端想要聆聽所有
persons
的事件,其中每個人都是age == 26
,我們可以做以下動作:
var createdEvent = myContract.PersonCreated({age: 26});
createdEvent.watch(function(err, result) {
if (err) {
console.log(err)
return;
}
console.log("Found ", result);
})
要計算這個長 16 進位的事件簽名,計算方式為:keccak256("myEventName(address, hash256, uint256)");
anonymous
anonymous: 不會儲存任何的事件的 signature 在 topics 內,在 Solidity 中使用匿名 event 也會耗費較少的成本。
不同於一般的 events,匿名事件不會包含任何 indexed signature 的 keccak加密結果。因為他們不可以被簡單地搜尋或被特別地解密,除非我們擁有合約 ABI。變相來說普通的事件,其簽名會被保存在 topic 中,而 anonymous 則不會,所以無法透過名字來過濾。
要使一個合約為匿名(anonymous)可以在每個事件後面加上 anonymous 作為修飾詞:
//SPDX-License-Identifier: MIT
contract TestContract {
event Start(uint start, uint middle, uint end) anonymous;
event End(uint start, uint middle, uint end) anonymous;
}