@@ -23,12 +23,19 @@ This guide explains the two key parts of working with log triggers:
2323
2424You create an EVM Log trigger by calling the ` EVMClient.logTrigger() ` method with a ` FilterLogTriggerRequest ` configuration. This configuration specifies which contract addresses and event topics to listen for.
2525
26+ <Aside type = " note" title = " Base64 Encoding Required" >
27+ ** All addresses and topic values must be base64 encoded** using the ` hexToBase64() ` helper function from the CRE SDK.
28+ While the workflow simulator accepts raw hex strings for convenience during development, ** deployed workflows require
29+ base64 encoding** . Always use ` hexToBase64() ` on addresses and topic values to ensure your workflow works in both
30+ simulation and production.
31+ </Aside >
32+
2633### Basic configuration
2734
2835The simplest configuration listens for ** all events** from specific contract addresses:
2936
3037``` typescript
31- import { cre , getNetwork , type Runtime , type EVMLog , Runner , bytesToHex } from " @chainlink/cre-sdk"
38+ import { cre , getNetwork , type Runtime , type EVMLog , Runner , bytesToHex , hexToBase64 } from " @chainlink/cre-sdk"
3239
3340type Config = {
3441 chainSelectorName: string
@@ -58,7 +65,7 @@ const initWorkflow = (config: Config) => {
5865 return [
5966 cre .handler (
6067 evmClient .logTrigger ({
61- addresses: [config .contractAddress ],
68+ addresses: [hexToBase64 ( config .contractAddress ) ],
6269 }),
6370 onLogTrigger
6471 ),
@@ -75,10 +82,11 @@ main()
7582
7683### Filtering by event type
7784
78- To listen for ** specific event types** , you need to provide the event's signature hash as the first topic (` Topics[0] ` ). You can compute this using viem's ` keccak256 ` and ` toHex ` functions:
85+ To listen for ** specific event types** , you need to provide the event's signature hash as the first topic (` Topics[0] ` ). You can compute this using viem's ` keccak256 ` and ` toBytes ` functions:
7986
8087``` typescript
81- import { keccak256 , toHex } from " viem"
88+ import { keccak256 , toBytes } from " viem"
89+ import { hexToBase64 } from " @chainlink/cre-sdk"
8290
8391const initWorkflow = (config : Config ) => {
8492 const network = getNetwork ({
@@ -94,14 +102,14 @@ const initWorkflow = (config: Config) => {
94102 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
95103
96104 // Compute the event signature hash for Transfer(address,address,uint256)
97- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
105+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
98106
99107 return [
100108 cre .handler (
101109 evmClient .logTrigger ({
102- addresses: [config .contractAddress ],
110+ addresses: [hexToBase64 ( config .contractAddress ) ],
103111 topics: [
104- { values: [transferEventHash ] }, // Listen only for Transfer events
112+ { values: [hexToBase64 ( transferEventHash ) ] }, // Listen only for Transfer events
105113 ],
106114 }),
107115 onLogTrigger
@@ -118,13 +126,27 @@ EVM events can have up to 3 `indexed` parameters (in addition to the event signa
118126
119127- ** ` addresses ` ** : The trigger fires if the event is emitted from ** any** contract in this list (** OR** logic).
120128- ** ` topics ` ** : An event must match the conditions for ** all** defined topic slots (** AND** logic between topics). Within a single topic, you can provide multiple values, and it will match if the event's topic is ** any** of those values (** OR** logic within a topic).
129+ - ** Wildcarding topics** : To skip filtering on a specific topic position, omit it from the topics array or provide an empty values array ` { values: [] } ` . For example, to filter on topic 1 and topic 3 but not topic 2, you would provide ` [topic0, topic1, { values: [] }, topic3] ` .
130+
131+ <Aside type = " caution" title = " Topic values must be padded to 32 bytes and base64 encoded" >
132+ EVM logs always store indexed parameters as ** 32-byte values** . When filtering on topics 1, 2, or 3:
133+
134+ 1 . ** Pad your values to 32 bytes** using ` padHex(value, { size: 32 }) ` (e.g., addresses are 20 bytes and must be padded)
135+ 1 . ** Convert to base64** using ` hexToBase64() `
136+
137+ If you don't pad correctly, your filter won't match the actual log topics and the trigger will not fire.
138+
139+ Topic 0 (the event signature from ` keccak256 ` ) is already 32 bytes and doesn't need padding.
140+
141+ </Aside >
121142
122143#### Example 1: Filtering on a single indexed parameter
123144
124145To trigger only on ` Transfer ` events where the ` from ` address is a specific value:
125146
126147``` typescript
127- import { keccak256 , toHex , pad } from " viem"
148+ import { keccak256 , toBytes , padHex } from " viem"
149+ import { hexToBase64 } from " @chainlink/cre-sdk"
128150
129151const initWorkflow = (config : Config ) => {
130152 const network = getNetwork ({
@@ -139,16 +161,16 @@ const initWorkflow = (config: Config) => {
139161
140162 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
141163
142- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
143- const aliceAddress = " 0xAlice..."
164+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
165+ const aliceAddress = " 0xAlice..." as ` 0x${ string } `
144166
145167 return [
146168 cre .handler (
147169 evmClient .logTrigger ({
148- addresses: [config .contractAddress ],
170+ addresses: [hexToBase64 ( config .contractAddress ) ],
149171 topics: [
150- { values: [transferEventHash ] }, // Topic 0: Event signature (Transfer)
151- { values: [pad ( aliceAddress )] }, // Topic 1: from = Alice
172+ { values: [hexToBase64 ( transferEventHash ) ] }, // Topic 0: Event signature (Transfer)
173+ { values: [hexToBase64 ( padHex ( aliceAddress , { size: 32 }) )] }, // Topic 1: from = Alice
152174 ],
153175 }),
154176 onLogTrigger
@@ -157,18 +179,21 @@ const initWorkflow = (config: Config) => {
157179}
158180```
159181
182+ { /* prettier-ignore */ }
160183<Aside type = " note" title = " Indexed Parameters and Topics" >
161- Only parameters marked as ` indexed ` in the Solidity event definition can be filtered using topics. The event signature
162- is always ` Topics[0] ` . Subsequent indexed parameters are ` Topics[1] ` , ` Topics[2] ` , and ` Topics[3] ` . Address values
163- must be padded to 32 bytes using viem's ` pad() ` function.
184+ Only parameters marked as ` indexed ` in the Solidity event definition can be filtered using topics. The event signature is always ` Topics[0] ` . Subsequent indexed parameters are ` Topics[1] ` , ` Topics[2] ` , and ` Topics[3] ` . Encoding different types:
185+ - ** Addresses** : Cast as `` `0x${string}` `` , use ` padHex(address, { size: 32 }) ` then ` hexToBase64() `
186+ - ** uint256** : Use ` padHex(numberToHex(value), { size: 32 }) ` then ` hexToBase64() `
187+ - ** bytes32** : Ensure it's 32 bytes, then use ` hexToBase64() ` directly
164188</Aside >
165189
166190#### Example 2: "AND" filtering
167191
168192To trigger on ` Transfer ` events where ` from ` is Alice ** AND** ` to ` is Bob:
169193
170194``` typescript
171- import { keccak256 , toHex , pad } from " viem"
195+ import { keccak256 , toBytes , padHex } from " viem"
196+ import { hexToBase64 } from " @chainlink/cre-sdk"
172197
173198const initWorkflow = (config : Config ) => {
174199 const network = getNetwork ({
@@ -183,18 +208,18 @@ const initWorkflow = (config: Config) => {
183208
184209 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
185210
186- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
187- const aliceAddress = " 0xAlice..."
188- const bobAddress = " 0xBob..."
211+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
212+ const aliceAddress = " 0xAlice..." as ` 0x${ string } `
213+ const bobAddress = " 0xBob..." as ` 0x${ string } `
189214
190215 return [
191216 cre .handler (
192217 evmClient .logTrigger ({
193- addresses: [config .contractAddress ],
218+ addresses: [hexToBase64 ( config .contractAddress ) ],
194219 topics: [
195- { values: [transferEventHash ] }, // Topic 0: Event signature (Transfer)
196- { values: [pad ( aliceAddress )] }, // Topic 1: from = Alice
197- { values: [pad ( bobAddress )] }, // Topic 2: to = Bob
220+ { values: [hexToBase64 ( transferEventHash ) ] }, // Topic 0: Event signature (Transfer)
221+ { values: [hexToBase64 ( padHex ( aliceAddress , { size: 32 }) )] }, // Topic 1: from = Alice
222+ { values: [hexToBase64 ( padHex ( bobAddress , { size: 32 }) )] }, // Topic 2: to = Bob
198223 ],
199224 }),
200225 onLogTrigger
@@ -208,7 +233,8 @@ const initWorkflow = (config: Config) => {
208233To trigger on ` Transfer ` events where ` from ` is ** either** Alice ** OR** Charlie:
209234
210235``` typescript
211- import { keccak256 , toHex , pad } from " viem"
236+ import { keccak256 , toBytes , padHex } from " viem"
237+ import { hexToBase64 } from " @chainlink/cre-sdk"
212238
213239const initWorkflow = (config : Config ) => {
214240 const network = getNetwork ({
@@ -223,17 +249,22 @@ const initWorkflow = (config: Config) => {
223249
224250 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
225251
226- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
227- const aliceAddress = " 0xAlice..."
228- const charlieAddress = " 0xCharlie..."
252+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
253+ const aliceAddress = " 0xAlice..." as ` 0x${ string } `
254+ const charlieAddress = " 0xCharlie..." as ` 0x${ string } `
229255
230256 return [
231257 cre .handler (
232258 evmClient .logTrigger ({
233- addresses: [config .contractAddress ],
259+ addresses: [hexToBase64 ( config .contractAddress ) ],
234260 topics: [
235- { values: [transferEventHash ] }, // Topic 0: Event signature (Transfer)
236- { values: [pad (aliceAddress ), pad (charlieAddress )] }, // Topic 1: from = Alice OR Charlie
261+ { values: [hexToBase64 (transferEventHash )] }, // Topic 0: Event signature (Transfer)
262+ {
263+ values: [
264+ hexToBase64 (padHex (aliceAddress , { size: 32 })),
265+ hexToBase64 (padHex (charlieAddress , { size: 32 })),
266+ ],
267+ }, // Topic 1: from = Alice OR Charlie
237268 ],
238269 }),
239270 onLogTrigger
@@ -247,7 +278,8 @@ const initWorkflow = (config: Config) => {
247278To listen for ** multiple event types** from a single contract, provide multiple event signature hashes in ` Topics[0] ` :
248279
249280``` typescript
250- import { keccak256 , toHex } from " viem"
281+ import { keccak256 , toBytes } from " viem"
282+ import { hexToBase64 } from " @chainlink/cre-sdk"
251283
252284const initWorkflow = (config : Config ) => {
253285 const network = getNetwork ({
@@ -262,15 +294,15 @@ const initWorkflow = (config: Config) => {
262294
263295 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
264296
265- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
266- const approvalEventHash = keccak256 (toHex (" Approval(address,address,uint256)" ))
297+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
298+ const approvalEventHash = keccak256 (toBytes (" Approval(address,address,uint256)" ))
267299
268300 return [
269301 cre .handler (
270302 evmClient .logTrigger ({
271- addresses: [config .contractAddress ],
303+ addresses: [hexToBase64 ( config .contractAddress ) ],
272304 topics: [
273- { values: [transferEventHash , approvalEventHash ] }, // Listen for Transfer OR Approval
305+ { values: [hexToBase64 ( transferEventHash ), hexToBase64 ( approvalEventHash ) ] }, // Listen for Transfer OR Approval
274306 ],
275307 }),
276308 onLogTrigger
@@ -284,6 +316,46 @@ const initWorkflow = (config: Config) => {
284316To listen for the ** same event from multiple contracts** , provide multiple addresses:
285317
286318``` typescript
319+ import { keccak256 , toBytes } from " viem"
320+ import { hexToBase64 } from " @chainlink/cre-sdk"
321+
322+ const initWorkflow = (config : Config ) => {
323+ const network = getNetwork ({
324+ chainFamily: " evm" ,
325+ chainSelectorName: config .chainSelectorName ,
326+ isTestnet: true ,
327+ })
328+
329+ if (! network ) {
330+ throw new Error (` Network not found: ${config .chainSelectorName } ` )
331+ }
332+
333+ const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
334+
335+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
336+
337+ return [
338+ cre .handler (
339+ evmClient .logTrigger ({
340+ addresses: [hexToBase64 (" 0xTokenA..." ), hexToBase64 (" 0xTokenB..." ), hexToBase64 (" 0xTokenC..." )],
341+ topics: [
342+ { values: [hexToBase64 (transferEventHash )] }, // Listen for Transfer events from any of these contracts
343+ ],
344+ }),
345+ onLogTrigger
346+ ),
347+ ]
348+ }
349+ ```
350+
351+ #### Example 6: Filtering on uint256 indexed parameter
352+
353+ To filter on indexed ` uint256 ` or other numeric types, convert them to a 32-byte hex value:
354+
355+ ``` typescript
356+ import { keccak256 , toBytes , numberToHex , padHex } from " viem"
357+ import { hexToBase64 } from " @chainlink/cre-sdk"
358+
287359const initWorkflow = (config : Config ) => {
288360 const network = getNetwork ({
289361 chainFamily: " evm" ,
@@ -297,14 +369,19 @@ const initWorkflow = (config: Config) => {
297369
298370 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
299371
300- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
372+ // Example: event ValueChanged(address indexed user, uint256 indexed newValue)
373+ const eventHash = keccak256 (toBytes (" ValueChanged(address,uint256)" ))
374+ const userAddress = padHex (" 0xUser..." as ` 0x${string } ` , { size: 32 })
375+ const targetValue = padHex (numberToHex (12345 ), { size: 32 })
301376
302377 return [
303378 cre .handler (
304379 evmClient .logTrigger ({
305- addresses: [" 0xTokenA... " , " 0xTokenB... " , " 0xTokenC... " ],
380+ addresses: [hexToBase64 ( config . contractAddress ) ],
306381 topics: [
307- { values: [transferEventHash ] }, // Listen for Transfer events from any of these contracts
382+ { values: [hexToBase64 (eventHash )] }, // Topic 0: Event signature
383+ { values: [hexToBase64 (userAddress )] }, // Topic 1: user address
384+ { values: [hexToBase64 (targetValue )] }, // Topic 2: newValue = 12345
308385 ],
309386 }),
310387 onLogTrigger
@@ -313,13 +390,19 @@ const initWorkflow = (config: Config) => {
313390}
314391```
315392
393+ <Aside type = " note" title = " Converting Numbers to Topics" >
394+ For indexed ` uint256 ` parameters, use ` numberToHex() ` to convert the number to hex, then ` padHex() ` to ensure it's 32
395+ bytes, and finally ` hexToBase64() ` to encode it for the trigger configuration. For ` bytes32 ` parameters, ensure
396+ they're already 32 bytes and apply ` hexToBase64() ` directly.
397+ </Aside >
398+
316399### Confidence level
317400
318401You can set the block confirmation level by adding the ` confidence ` field to the trigger configuration:
319402
320403``` typescript
321404evmClient .logTrigger ({
322- addresses: [config .contractAddress ],
405+ addresses: [hexToBase64 ( config .contractAddress ) ],
323406 confidence: " CONFIDENCE_LEVEL_FINALIZED" , // Wait for finalized blocks
324407})
325408```
@@ -432,7 +515,7 @@ const onLogTrigger = (runtime: Runtime<Config>, log: EVMLog): string => {
432515<Aside type = " note" title = " Type Assertion for Topics" >
433516 The type assertion
434517 ```
435- as [\ `0x${string}\ `, ...\ `0x${string}\ `[]]
518+ as [`0x${string}`, ...`0x${string}`[]]
436519 ```
437520
438521tells TypeScript that ` topics ` is a non-empty array of
@@ -472,8 +555,8 @@ const onLogTrigger = (runtime: Runtime<Config>, log: EVMLog): string => {
472555Here's a complete example that listens for ERC20 ` Transfer ` events and decodes them:
473556
474557``` typescript
475- import { cre , getNetwork , type Runtime , type EVMLog , Runner , bytesToHex } from " @chainlink/cre-sdk"
476- import { keccak256 , toHex , decodeEventLog , parseAbi } from " viem"
558+ import { cre , getNetwork , type Runtime , type EVMLog , Runner , bytesToHex , hexToBase64 } from " @chainlink/cre-sdk"
559+ import { keccak256 , toBytes , decodeEventLog , parseAbi } from " viem"
477560
478561type Config = {
479562 chainSelectorName: string
@@ -510,13 +593,13 @@ const initWorkflow = (config: Config) => {
510593 }
511594
512595 const evmClient = new cre .capabilities .EVMClient (network .chainSelector .selector )
513- const transferEventHash = keccak256 (toHex (" Transfer(address,address,uint256)" ))
596+ const transferEventHash = keccak256 (toBytes (" Transfer(address,address,uint256)" ))
514597
515598 return [
516599 cre .handler (
517600 evmClient .logTrigger ({
518- addresses: [config .tokenAddress ],
519- topics: [{ values: [transferEventHash ] }],
601+ addresses: [hexToBase64 ( config .tokenAddress ) ],
602+ topics: [{ values: [hexToBase64 ( transferEventHash ) ] }],
520603 confidence: " CONFIDENCE_LEVEL_FINALIZED" ,
521604 }),
522605 onLogTrigger
0 commit comments