本篇預計閱讀時間 15 分鐘。

 

在 Solidity 中,當一個 Event 被呼叫之後它的參數就會被置於區塊鏈之中。

「事件」讓我們在 DApp 或瀏覽器的環境下可以呼叫 Javascript 的監聽,藉由監聽得到一些我們想知道的訊息。

區塊鏈上的交易會有所謂的日誌(logs),我們沒有辦法以一合約直接訪問該合約中的事件和 logs,所以它的可視性是 external 的。

事件是合約的可繼承成員,發出一個事件可以使用合約的地址訪問事件,生成的事件不能從合約內部訪問。

以下是我們使用事件的用途:

  • 事件是一個合約跟外部前端 app(或說使用者 interface)溝通的方法,可以在前端聆聽(listening)合約裡的特定事件或者動作,當他們被啟動或者改變的時候。

  • 同步於資料啟動某些動作

  • 相同資料量的情況下,比儲存在 storage 更便宜

  • 如果我們今天要 debug 或者測試的時候,也可以利用 event 來模擬一個 Javascript console.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() returns boolean: 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: 執行每個從區塊鏈被移除的 loglog 會有額外的屬性 "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: 執行每個從區塊鏈被移除的 loglog 會有額外的屬性 "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!');});

官方文件:web3.eth.subscribe

Logs with Data & Topics

  • 當一個參數沒有 indexed 屬性時, 他們會被 ABI-encodedlogdata 部分。

  • 如果一個參數有 indexed 屬性,他們會被包含在 topics 裡面。

Topics

Topics 是一個包含 event 裡的 indexed 參數們的物件。

topic[0] 代表的是一個事件它自己的 hash 的 hash。可以擁有最多三個的 indexed 參數,每一個會有相對應的 topics 元素。

以下的範例是使用 web3.js subscribe("logs") 這個方法去過濾 logs 中符合特定 addresstopics:


 

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;
}