💡 본 포스팅에서는
ethers js
를 사용하여 체인에서 발생한 event(Solidity)를 조회하는 기본적인 내용을 담으려 합니다.
본 시리즈의 모든 내용은 ethers js v5를 기준으로 작성하였습니다.
작성일(2024.04.19) 기준, ethers js는 6.12.0 버전까지 공개되었고,
ethers js를 설치하면 기본으로 6 버전이 설치됩니다!
본문의 내용은 해당 버전과 상이할 수 있습니다.
잘못된 내용에 대한 지적은 항상 감사드립니다!! 🙇
Solidity의 Event에 대한 개념 및 자세한 설명은 아래 글을 중점적으로 참고하였습니다!
보다 자세한 내용을 위해 참고하시면 좋을 것 같습니다!
[Medium] Solidity-events-Explained
→ 블록체인 상에서 어떤 동작이 발생했음을 기록하는 한 형태로, 외부에서 해당 형태(Event)로 된 데이터에 쉽게 접근할 수 있음!
contract SampleContract {
event SampleEvent(uint indexed sampleId, address sender, uint sampleAmount);
}
sampleId
, sender
, sampleAmount
3개의 파라미터를 갖는 SampleEvent
이라는 이름의 event를 정의했습니다!emit
키워드와 Event 이름을 사용합니다!function sampleTransaction(address sampleAddress, uint sampleAmount) {
...
emit SampleEvent(1, sampleAddress, sampleAmount);
}
sampleId
파라미터로 1, sender
파라미터로 sampleAddress, sampleAmount
파라미터로 sampleAmount
에 할당된 변수를 넣어 event를 발생시키고 있습니다!본 시리즈의 이전 글에서 살펴 본 바와 같이, 스마트 컨트랙트가 배포될 때는 해당 컨트랙트의 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"
}
]
}
]
이하에서는,
Transfer
이벤트를 조회한다고 가정하고 작성하려 합니다!스마트 컨트랙트의 Event를 조회하기 위해, ethers js에서 제공하는 getLogs
메서드를 사용합니다!
getLogs
메서드는 ethers 문서상 Filter
인터페이스 를 파라미터로 받아, 해당 파라미터(즉, Event들에 대한 Filter)에 해당하는 event를 반환합니다!
[ fromBlock ]
[ toBlock ]
[ address ]
[ topics ]
indexed
속성을 가진 파라미터가 위치합니다! 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
P.S : fromBlock
과 toBlock
의 차이가 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
}
]
예제 편의상, 위에서 조회된 7개의 Log 중 하나만을 대상으로
파싱하는 부분까지 진행해 보도록 하겠습니다! :)
{
blockNumber: 56077244,
blockHash: '0x0a69a345b8c407869142b26615a3a9c0be968705fb5af030bfbf9bc22d44c5b7',
transactionIndex: 114,
removed: false,
address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
data: '0x00000000000000000000000000000000000000000000000012fac38bc254a392',
topics: [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x000000000000000000000000b3866eb993e1aef93f219c3da0a71c3f11becbf2',
'0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564'
],
transactionHash: '0x4cbe1cc05ae564d9b3da0f5db162bd9266e0a22b5b3d1770597218c552ed8cc8',
logIndex: 424
}
getEventTopic
으로 조회한 event id를, 1번째부터의 topic은 Event의 indexed 파라미터를 의미합니다!이제 Event를 조회하였으니, Log에 포함된 data를 파싱하여
Event에 포함되었던 파라미터를 알아보는 부분을 진행해 보려 합니다!
Log에 포함된 data의 파싱은, Interface
클래스의 parseLog
메서드를 활용하여 진행합니다!
// 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이 가능하므로
자세한 활용에 대해서는 공식 문서를 참고하시면 더욱 좋을 것 같습니다! :)