Skip to content

Commit a0334e7

Browse files
authored
TS EVM log trigger: Add base64 encoding (#3342)
* TS EVM log triggers to use base64 encoding * TS EVM log triggers to use base64 encoding * padding & base64 encoding caution note * padding & base64 encoding caution note
1 parent 0bc85ba commit a0334e7

File tree

2 files changed

+258
-92
lines changed

2 files changed

+258
-92
lines changed

src/content/cre/guides/workflow/using-triggers/evm-log-trigger-ts.mdx

Lines changed: 129 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@ This guide explains the two key parts of working with log triggers:
2323

2424
You 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

2835
The 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

3340
type 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

8391
const 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

124145
To 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

129151
const 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

168192
To 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

173198
const 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) => {
208233
To 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

213239
const 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) => {
247278
To 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

252284
const 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) => {
284316
To 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+
287359
const 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

318401
You can set the block confirmation level by adding the `confidence` field to the trigger configuration:
319402

320403
```typescript
321404
evmClient.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

438521
tells TypeScript that `topics` is a non-empty array of
@@ -472,8 +555,8 @@ const onLogTrigger = (runtime: Runtime<Config>, log: EVMLog): string => {
472555
Here'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

478561
type 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

Comments
 (0)