본문 바로가기
Blockchain

Ethers.js를 이용해서 Event logging하기

2023. 3. 29.

Solidity를 이용해서 스마트 컨트랙트를 작성할 때에 중요한 개념 중 하나는 바로 event입니다. 스마트 컨트랙트에서 event는 어떠한 특정 행동이 일어났음을 알려주는 지표인데요. 외부에서는 이 event를 구독(listen)하면서 대기하다가 해당 event가 발생하면(emit) 이에 알맞은 액션을 취하거나, event에 포함되어 있는 정보들을 분석하여 필요한 값을 추출할 수도 있습니다. 즉, event를 통해서 블록체인 상에 존재하는 스마트 컨트랙트와 외부 환경(ex. 유저 인터페이스)이 서로 상호작용할 수 있는 것이죠. 혹은 스마트 컨트랙트에서 발생한 event들은 모두 블록체인 상에 저장되므로 일종의 기록 용도로도 사용할 수 있습니다. 
 
이번 포스팅에서는 ethers.js를 이용하여 블록체인에 기록된 이벤드들 중 특정 스마트 컨트랙트의 특정 이벤트를 찾고, 해당 이벤트에 포함된 정보를 분석하여 필요한 값을 추출하는 것까지의 과정을 정리해 보았습니다. 
 
 


Event와 Log

스마트 컨트랙트 상에서는 event를 정의하고, 함수 안에서 해당 event를 발생시킬 수 있습니다. 
 

contract Foo {
    event Hello(address indexed sender, string name);
    event World();

    function emitEvents() public {
        emit Hello(msg.sender, "Seo Mingyun");
        emit World();
    }
}

 
이때, <Hello> event에서 볼 수 있는 것처럼 파라미터를 넘겨줄 수 있고, 해당 매개변수에 선택적으로 indexed라는 키워드를 추가할 수 있습니다. 이 indexed 키워드가 붙은 매개변수는 나중에 event가 log가 되어 블록체인 상에 저장되었을 때 event를 필터링할 수 있는 조건으로 사용할 수 있습니다. 
 
위 컨트랙트에서 emitEvnets 함수를 호출시키면 Hello event와 World event가 함께 일어납니다. 이렇게 event가 발생하면 이 event들이 발생했다는 기록이 log가 되어 블록체인 상에 저장됩니다. 이 log는 emitEvents 함수를 호출하면서 생성된 Tx의 receipt안에 포함됩니다.
 
또한, hex값으로 인코딩 되어 나타나므로 눈으로 분간하기는 어렵긴 하지만 etherscan에서도 해당 Tx에서 발생한 이벤트들을 확인할 수 있습니다. 
 

 
Filter 생성

위에서 말했던 것처럼, 이벤트 로그들은 Tx에 포함됩니다. 지금 이 순간에도 Tx들은 계속 생성되고 있고 블록 역시 채굴되고 있기 때문에 수많은 Tx를 직접 까서 원하는 이벤트가 들어있는 Tx만 필터링하기엔 어렵습니다. 이를 위해서 ethers.js에서는 이벤트 로그 filter 기능을 제공합니다. 이벤트 중에서 필터링하고 싶은 요소들을 골라 object로 만들어 filter를 생성하고, 이를 통해 Tx에 포함된 이벤트들을 필터링할 수 있습니다. 

 

개인적으로 생각했을 때, filter에서 자주 쓰일 수 있는 요소들은 다음과 같습니다. 

  • address : 어떤 contract에서 나온 event인지 contract의 주소를 지정
  • topics : array type. filtering을 진행할 event의 topics
  • fromBlock : 어느 height의 블록부터 searching할 것인지
  • toBLock : 어느 height의 블록까지 searching할 것인지
  • blockHash : searching하고자 하는 특정한 block의 hash값

 

저는 특정 블록 시점부터 최근 블록까지, product contract에서 발생한 Deposit 이벤트를 필터링하고 싶었기 때문에 다음과 같은 filter를 생성하였습니다.

 

let myFilter = {
      fromBlock: Number(process.env.BLOCKNUMBER),
      toBlock: 'latest',
      address: productAddress,
      topics: [
        utils.id("Deposit(address,address,uint256,uint256,uint256,uint256)"),
        null,
        hexZeroPad(currentAddress, 32) // receiver
      ]
    };

 

이때, event에서 topics는 contract를 작성할 때에 만든 해당 event의 interface과 indexed 되어있는 parameter들이 들어갑니다.
이때, 32byte를 맞춰주어야 하므로 address 같은 값들은 32 바이트가 될 수 있도록 0으로 패딩해주어야 합니다.

topics: [
        utils.id("Deposit(address,address,uint256,uint256,uint256,uint256)"),
        null, // 첫 번째 indexed parameter는 넘어간다
        hexZeroPad(currentAddress, 32) // 두 번째 indexed parameter는 필터링 진행
	      ]​

 

  

getLogs를 통해 이벤트 로그 가져오기

provider.getLogs(filter) => Promise<Array<Log>>

 

getLogs는 filter와 매칭되는 Log들을 array에 담아 반환해 주는 메서드입니다.

 

이벤트 로그 객체는 다음과 같은 요소들을 지니고 있습니다.

  • blockNumber: 해당 log가 발생한 transaction을 포함하고 있는 block의 height
  • blockHash: 해당 log가 발생한 transaction을 포함하고 있는 block의 hash
  • removed: network가 re-org 되면서 고아 트랜잭션이 되었다면, 해당 log는 삭제되며, removed 값이 true로 세팅됨.
  • transactionLogIndex: 해당 트랜잭션 안에서 이 log의 index
  • address: 이 log를 생성한 컨트랙트의 address
  • data: 이 log에 포함되어 있는 data
  • topics: 이 log에 포함되어 있는 topics(=indexed properties)
  • transactionHash: 이 log의 트랜잭션의 hash
  • transactionIndex: 이 log의 트랜잭션의 블록에서의 index
  • logIndex: 해당 block에서 전체 log들에 대한 이 log의 index

Ex.

{
    "blockNumber": 39649763,
    "blockHash": "0x11bc21f367e73aede602d658bb16d173cb3c31e428cabceaebed0cfc508e4082",
    "transactionIndex": 47,
    "removed": false,
    "address": "0xbcf9e1c3fb0CeB5a8735DC4d64190E93f5f89368",
    "data": "0x000000000000000000000000000000000000000000000000000000000c380d4000000000000000000000000000000000000000000000000b1cf24ddd0b1400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000063f872e4",
    "topics": [
        "0x5f971bd00bf3ffbca8a6d72cdd4fd92cfd4f62636161921d1e5a64f0b64ccb6d",
        "0x00000000000000000000000093d702002f1232247ad2349e3a98110c8ce4190a",
        "0x00000000000000000000000093d702002f1232247ad2349e3a98110c8ce4190a"
    ],
    "transactionHash": "0xef598172906e1536f5ad83d91fc35b6fa5340f55299820fd4ba5dd59ab90c7ee",
    "logIndex": 240
}

 

Decoding

컨트랙트에서 이벤트를 정의할 때에는 위에서 말한 것처럼 indexed 키워드를 사용할 수 있습니다. 매개변수에 indexed 키워드를 붙이면 해당 변수는 로그 객체 중 topics array에 포함되고, 따라서 filter를 생성할 때에 해당 값을 이용할 수 있습니다. 

이와 달리 indexed 키워드가 붙지 않은 값들은 data 필드에 포함되며, array가 아니라 모두 하나로 합쳐져 인코딩 된 값으로 나옵니다. 

 

Ex. Polygon scanner에서 Event 확인

 

따라서 data 값이 각각 어떤 변수들로 이루어져 있는지 확인하기 위해서는 디코딩이 필요합니다. 

디코딩은 다음과 같이 진행하면 됩니다. 

 

utils.defaultAbiCoder.decode(['uint256', 'uint256', 'uint256', 'uint256'], log.data);

 

decode 메서드의 첫 번째 값은 indexed가 붙지 않은 파라미터들의 type들을 순서대로 넣어 만든 array를 넣으면 되고, 두 번째에는 로그의 data 필드 값을 넣으면 됩니다. 그럼 각각의 값이 해당 타입에 맞춰 디코딩되어 array 형태로 반환됩니다.

 


refs

https://blog.chain.link/events-and-logging-in-solidity/
https://medium.com/@kaishinaw/ethereum-logs-hands-on-with-ethers-js-a28dde44cbb6

https://consensys.net/blog/developers/guide-to-events-and-logs-in-ethereum-smart-contracts/
 
 

댓글