[Ethers Js 겉핥기] #4. event 조회하기

toto9602·2024년 4월 21일
0

Ethers js

목록 보기
5/5

💡 본 포스팅에서는 ethers js를 사용하여 체인에서 발생한 event(Solidity)를 조회하는 기본적인 내용을 담으려 합니다.

본 시리즈의 모든 내용은 ethers js v5를 기준으로 작성하였습니다.

작성일(2024.04.19) 기준, ethers js는 6.12.0 버전까지 공개되었고,
ethers js를 설치하면 기본으로 6 버전이 설치됩니다!
본문의 내용은 해당 버전과 상이할 수 있습니다.

잘못된 내용에 대한 지적은 항상 감사드립니다!! 🙇‍

참고 자료

ethers js 공식 문서
Solidity-events-Explained
Chainlist

0. 들어가기 전에 : Solidity의 Event

Solidity의 Event에 대한 개념 및 자세한 설명은 아래 글을 중점적으로 참고하였습니다!
보다 자세한 내용을 위해 참고하시면 좋을 것 같습니다!

[Medium] Solidity-events-Explained

Solidity의 Event란 무엇일까?

  • 스마트 컨트랙트상의 특정한 동작을 기록하고, 외부(다른 컨트랙트, 혹은 HTTP API 서버 등 외부)에 알리기 위한 방법 중 하나!
  • 블록체인 상에 데이터를 기록하여, 이에 쉽게 접근할 수 있도록 하는 것

블록체인 상에서 어떤 동작이 발생했음을 기록하는 한 형태로, 외부에서 해당 형태(Event)로 된 데이터에 쉽게 접근할 수 있음!

Solidity상 Event의 정의 (선언)

  • Event는 스마트 컨트랙트 상에 정의됨
  • Event의 이름, 포함될 파라미터들과 각 파라미터의 데이터 타입을 포함!
contract SampleContract {
	  event SampleEvent(uint indexed sampleId, address sender, uint sampleAmount);
}
  • 위 예시에서는 sampleId, sender, sampleAmount 3개의 파라미터를 갖는 SampleEvent이라는 이름의 event를 정의했습니다!

Solidity상 Event의 발생

  • 컨트랙트 상에서 특정 event를 발생시킬 때는, emit 키워드와 Event 이름을 사용합니다!
function sampleTransaction(address sampleAddress, uint sampleAmount) {
	...
    emit SampleEvent(1, sampleAddress, sampleAmount);
}
  • 위 예시에서는, sampleId 파라미터로 1, sender 파라미터로 sampleAddress, sampleAmount 파라미터로 sampleAmount에 할당된 변수를 넣어 event를 발생시키고 있습니다!

Event가 포함된 ABI

본 시리즈의 이전 글에서 살펴 본 바와 같이, 스마트 컨트랙트가 배포될 때는 해당 컨트랙트의 ABI 파일이 반환됩니다.
그리고, Event가 정의된 컨트랙트의 경우, 이 Event의 인터페이스 정보 또한 ABI에 포함되게 됩니다!

[ ABI의 Event 예시 ]

[
  {
    "type":"event", // 유형은 event
    "anonymous":false,
    "name":"Transfer", // event 이름은 Transfer
    "inputs": [ // 파라미터 유형, 이름, indexed 설정 여부
      {
        	"type":"address",
        	"name":"src",
        	"indexed":true
      },
      {
        	"type":"address",
        	"name":"dst",
        	"indexed":true
      },
      {
        	"type":"uint256",
        	"name":"val"
      }
    ]
  }
]

1. 이벤트 조회하기

이하에서는,

  • Polygon 네트워크의
  • WMATIC(Wrapped MATIC) 토큰 컨트랙트에서 발생한
  • Transfer 이벤트를 조회한다고 가정하고 작성하려 합니다!
    (블록체인 상에서 특정 토큰을 다른 EOA로 전송(transfer)할 때 발생)

ethers - getLogs

스마트 컨트랙트의 Event를 조회하기 위해, ethers js에서 제공하는 getLogs 메서드를 사용합니다!

getLogs - ethers 공식 문서 설명

getLogs 메서드는 ethers 문서상 Filter 인터페이스 를 파라미터로 받아, 해당 파라미터(즉, Event들에 대한 Filter)에 해당하는 event를 반환합니다!

getLogs의 주요 파라미터

[ fromBlock ]

[ toBlock ]

[ address ]

  • 이벤트를 조회할 스마트 컨트랙트의 주소

[ topics ]

  • 조회하고자 하는 Event를 필터링하기 위한 값입니다.
  • 통상 첫 번째 topic으로는 해당 Event의 id값이, 두 번째 이후부터의 topic은 해당 Event의 indexed 속성을 가진 파라미터가 위치합니다!

getLogs 호출

호출을 위해 필요한 값들

Provider 클래스에서 메서드 호출을 위해 필요한 값들(ex. RPC URL)등은 이전 글에서 일부 다루었지만,
한 번 더 보고 지나가려 합니다! :)

1. 조회할 네트워크의 RPC URL

https://rpc-mainnet.matic.quiknode.pro	

→ 본 포스팅에서는 Polygon 네트워크의 이벤트를 조회하고 하기에, Polygon 네트워크의 RPC url을 사용합니다!

cf. Chainlist - 네트워크별 public rpc url 정보 등

2. fromBlock, toBlock

  • fromBlock : 최신 Block - 1번부터
  • toBlock : 최신 Block까지!

P.S : fromBlocktoBlock 의 차이가 10,000 이상이면,
ethers의 getLogs 메서드 차원에서 범위가 너무 넓다는 에러가 발생합니다!

넓은 범위의 Event를 조회하셔야 하는 경우, 관련 방어 로직이 필요할 수 있을 것 같습니다 :D

3. Event 조회의 대상이 될 컨트랙트 주소

0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270

→ Polygon 네트워크의 WMATIC 토큰 컨트랙트 주소입니다!

4. Event Topic

import ethers from "ethers";

// Event 인터페이스를 포함한 ethers Interface 클래스를 만들어 줍니다. 
const iface = new ethers.utils.Interface([
  {
    "type":"event",
    "anonymous":false,
    "name":"Transfer",
    "inputs": [
      {
        	"type":"address",
        	"name":"src",
        	"indexed":true
      },
      {
        	"type":"address",
        	"name":"dst",
        	"indexed":true
      },
      {
        	"type":"uint256",
        	"name":"val"
      }
    ]
  }
])
// getEventTopic을 호출하여, Filter에 첫 번째 topic으로 사용할 event id를 조회합니다.
const eventTopic = iface.getEventTopic("Transfer");
console.log(eventTopic); // 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

호출 예시


// RPC URL을 넣어 JsonRpcProvider 클래스를 초기화합니다. 
const provider = new ethers.providers.JsonRpcProvider("https://rpc-mainnet.matic.quiknode.pro");

// latest 태그로, 해당 네트워크의 최신 block 정보를 조회합니다. 
const latestBlock = await provider.getBlock("latest");

const logs = await provider.getLogs({
  fromBlock: latestBlock.number - 1,
  toBlock: latestBlock.number,
  topics: [eventTopic],
  address:"0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270",
});

console.log(logs);

호출 결과

호출 시점 기준으로, 아래와 같은 7개의 Log가 조회되었습니다!

[
  {
    blockNumber: 56077243,
    blockHash: '0x4782480525daff333bbc860ba25d0f6db9dbd4498034037edf16e364c0e9ed18',
    transactionIndex: 44,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x0000000000000000000000000000000000000000000000007c607447cbe26e32',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x000000000000000000000000479e1b71a702a595e19b6d5932cd5c863ab57ee0',
      '0x000000000000000000000000def171fe48cf0115b1d80b88dc8eab59176fee57'
    ],
    transactionHash: '0x9ca38dcf1c28eb9aaa42f5090052c9fe103139cf5e1412f3ac1ddb476b349817',
    logIndex: 99
  },
  {
    blockNumber: 56077243,
    blockHash: '0x4782480525daff333bbc860ba25d0f6db9dbd4498034037edf16e364c0e9ed18',
    transactionIndex: 47,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x0000000000000000000000000000000000000000000000038a44a341d9b8c000',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x00000000000000000000000042cbe08532ac33dfa1ff25f0a214b1f004eb8206',
      '0x00000000000000000000000086efb351b092a32d833a1ad7374d9bf0fc164aab'
    ],
    transactionHash: '0x58165577af47c4413a4c33752f704565a136f56cdaeefe1edc01f2f8ad2d5369',
    logIndex: 178
  },
  {
    blockNumber: 56077244,
    blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
    transactionIndex: 25,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x000000000000000000000000000000000000000000000005fc7401f64a3d77f3',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x0000000000000000000000009b08288c3be4f62bbf8d1c20ac9c5e6f9467d8b7',
      '0x00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45'
    ],
    transactionHash: '0x57da7a20a3eadd4b25bb792a92969b2b91ff335f4910362c0d13b59dede284eb',
    logIndex: 90
  },
  {
    blockNumber: 56077244,
    blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
    transactionIndex: 26,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x000000000000000000000000000000000000000000000007975db5b06e16f9dd',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x000000000000000000000000ae81fac689a1b4b1e06e7ef4a2ab4cd8ac0a087d',
      '0x00000000000000000000000033c6e799dd65a204534842a8a32760867d504696'
    ],
    transactionHash: '0x44b2b44ac083a34d46a7be1f3ff81068815cd02cd6b3bcad62c36697a54a2110',
    logIndex: 110
  },
  {
    blockNumber: 56077244,
    blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
    transactionIndex: 102,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x000000000000000000000000000000000000000000000000001606ddfd9b8000',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x0000000000000000000000001b02da8cb0d097eb8d57a175b88c7d8b47997506',
      '0x00000000000000000000000054c3bb4cb56ff00c980aa4f742e400c08b0d46eb'
    ],
    transactionHash: '0x783cc84fb7ed7bcd48910da3145e263d00364d0389dec1336dbe5c21ebd22479',
    logIndex: 365
  },
  {
    blockNumber: 56077244,
    blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
    transactionIndex: 108,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x000000000000000000000000000000000000000000000001141eb3e5945f4018',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x00000000000000000000000044027452acade12f55858666aaeb3523027ff55f',
      '0x000000000000000000000000b4bdd233ef8111a99b92a4dc42aa5e73ab72abbc'
    ],
    transactionHash: '0xc75e76a03bd72b901f50be21a72ef576938b1407969647968e1774d3f340535b',
    logIndex: 386
  },
  {
    blockNumber: 56077244,
    blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
    transactionIndex: 114,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
    data: '0x00000000000000000000000000000000000000000000000012fac38bc254a392',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x000000000000000000000000b3866eb993e1aef93f219c3da0a71c3f11becbf2',
      '0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564'
    ],
    transactionHash: '0x4cbe1cc05ae564d9b3da0f5db162bd9266e0a22b5b3d1770597218c552ed8cc8',
    logIndex: 424
  }
]

2. 조회한 Log 파싱하기

예제 편의상, 위에서 조회된 7개의 Log 중 하나만을 대상으로
파싱하는 부분까지 진행해 보도록 하겠습니다! :)

Log 객체 간단히 살펴 보기

ethers 공식 문서 - Log

{
    blockNumber: 56077244,
    blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
    transactionIndex: 114,
    removed: false,
    address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', 
    data: '0x00000000000000000000000000000000000000000000000012fac38bc254a392',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x000000000000000000000000b3866eb993e1aef93f219c3da0a71c3f11becbf2',
      '0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564'
    ],
    transactionHash: '0x4cbe1cc05ae564d9b3da0f5db162bd9266e0a22b5b3d1770597218c552ed8cc8',
    logIndex: 424
  }
  • blockNumber : 이 Log의 트랜잭션을 포함하고 있는 block 번호
  • address : 이 Log를 생성한 컨트랙트의 주소
  • data : Log에 포함된 data
  • topics : 배열의 0번째 topic은, 위에서 getEventTopic으로 조회한 event id를, 1번째부터의 topic은 Event의 indexed 파라미터를 의미합니다!

Log의 파싱

이제 Event를 조회하였으니, Log에 포함된 data를 파싱하여
Event에 포함되었던 파라미터를 알아보는 부분을 진행해 보려 합니다!

Log에 포함된 data의 파싱은, Interface 클래스의 parseLog 메서드를 활용하여 진행합니다!

ethers 공식 문서 : Parsing

예시 및 호출 결과

// getLogs를 호출했을 때와 같은 방식으로 Interface 클래스를 만들어 줍니다!
const iface = new ethers.utils.Interface([ {
    "type":"event",
    "anonymous":false,
    "name":"Transfer",
    "inputs": [
      {
        	"type":"address",
        	"name":"src",
        	"indexed":true
      },
      {
        	"type":"address",
        	"name":"dst",
        	"indexed":true
      },
      {
        	"type":"uint256",
        	"name":"val"
      }
    ]
  }])

// Log에는 위 예시로 살펴보았던 Log 객체를 그대로 넣어줍니다!
const parsedLog = iface.parseLog(log)

호출 결과

LogDescription {
  eventFragment: {
    name: 'Transfer',
    anonymous: false,
    inputs: [ [ParamType], [ParamType], [ParamType] ],
    type: 'event',
    _isFragment: true,
    constructor: [Function: EventFragment] {
      from: [Function (anonymous)],
      fromObject: [Function (anonymous)],
      fromString: [Function (anonymous)],
      isEventFragment: [Function (anonymous)]
    },
    format: [Function (anonymous)]
  },
  name: 'Transfer',
  signature: 'Transfer(address,address,uint256)',
  topic: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
  args: [
    '0x479e1B71A702a595e19b6d5932CD5c863ab57ee0',
    '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57',
    BigNumber { _hex: '0x7c607447cbe26e32', _isBigNumber: true },
    from: '0x479e1B71A702a595e19b6d5932CD5c863ab57ee0',
    to: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57',
    amount: BigNumber { _hex: '0x7c607447cbe26e32', _isBigNumber: true }
  ]
}

저희는 Event에 포함된 argument가 궁금했던 것이므로, args에 포함된 BigNumber 타입을 string으로 변환하는 부분까지 진행해 보면, 아래와 같습니다! :)

const parsedLog = iface.parseLog(log)

const args = parsedLog.args;

// 프로퍼티 명으로 접근 가능!
const {from, to, amount} = args;
console.log(`from : ${from}`); // 0x479e1B71A702a595e19b6d5932CD5c863ab57ee0
console.log(`to : ${to}`); // 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57
console.log(`amount : ${amount}`); // 8962291110179401266

from 주소로부터, to 주소로 amount 만큼의 WMATIC을 전송한 Event인 것을 확인할 수 있었습니다!

마무리

본 포스팅에서는 ethers의 getLogs 메서드를 활용하여 Event를 조회하고, 포함된 data를 파싱하는 부분까지를 간략히 다루어 보았습니다!

본문에는 자세히 다루지 못했지만, getLogs 호출시 1번째부터의 topic에 indexed 파라미터를 함께 넣어주어,
특정한 indexed 파라미터로 발생한 Event만을 필터링할 수 있는 등, 사용하기에 따라 보다 자세한 Filtering이 가능하므로
자세한 활용에 대해서는 공식 문서를 참고하시면 더욱 좋을 것 같습니다! :)

profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글