Skip to content

Commit 5415541

Browse files
committed
Add encodeCallMsg field-context errors and logTriggerConfig helper
- encodeCallMsg now wraps hexToBase64 errors with field name context - Add logTriggerConfig() helper for validated hex-to-base64 log trigger config - Add validateHexByteLength for address (20 bytes) and topic (32 bytes) validation - Add 17 new tests for encodeCallMsg error context and logTriggerConfig
1 parent 6ecd37b commit 5415541

2 files changed

Lines changed: 252 additions & 7 deletions

File tree

packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isChainSelectorSupported,
1010
LAST_FINALIZED_BLOCK_NUMBER,
1111
LATEST_BLOCK_NUMBER,
12+
logTriggerConfig,
1213
type ProtoBigInt,
1314
prepareReportRequest,
1415
protoBigIntToBigint,
@@ -268,6 +269,144 @@ describe('blockchain-helpers', () => {
268269
})
269270
})
270271

272+
describe('encodeCallMsg error context', () => {
273+
test('should include field name in error for invalid from address', () => {
274+
const payload: EncodeCallMsgPayload = {
275+
from: 'not-hex' as `0x${string}`,
276+
to: '0x0000000000000000000000000000000000000000',
277+
data: '0x',
278+
}
279+
expect(() => encodeCallMsg(payload)).toThrow("Invalid hex in 'from' field of CallMsg")
280+
})
281+
282+
test('should include field name in error for invalid to address', () => {
283+
const payload: EncodeCallMsgPayload = {
284+
from: '0x0000000000000000000000000000000000000000',
285+
to: 'bad-hex' as `0x${string}`,
286+
data: '0x',
287+
}
288+
expect(() => encodeCallMsg(payload)).toThrow("Invalid hex in 'to' field of CallMsg")
289+
})
290+
291+
test('should include field name in error for invalid data', () => {
292+
const payload: EncodeCallMsgPayload = {
293+
from: '0x0000000000000000000000000000000000000000',
294+
to: '0x0000000000000000000000000000000000000000',
295+
data: 'not-hex' as `0x${string}`,
296+
}
297+
expect(() => encodeCallMsg(payload)).toThrow("Invalid hex in 'data' field of CallMsg")
298+
})
299+
})
300+
301+
describe('logTriggerConfig', () => {
302+
const VALID_ADDRESS = '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9'
303+
const VALID_TOPIC =
304+
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
305+
306+
test('should encode a single address', () => {
307+
const result = logTriggerConfig({ addresses: [VALID_ADDRESS] })
308+
expect(result.addresses).toHaveLength(1)
309+
expect(typeof result.addresses[0]).toBe('string') // base64
310+
})
311+
312+
test('should encode multiple addresses', () => {
313+
const result = logTriggerConfig({
314+
addresses: [VALID_ADDRESS, '0x0000000000000000000000000000000000000000'],
315+
})
316+
expect(result.addresses).toHaveLength(2)
317+
})
318+
319+
test('should encode topics with correct structure', () => {
320+
const result = logTriggerConfig({
321+
addresses: [VALID_ADDRESS],
322+
topics: [[VALID_TOPIC]],
323+
})
324+
expect(result.topics).toHaveLength(1)
325+
expect(result.topics![0].values).toHaveLength(1)
326+
expect(typeof result.topics![0].values[0]).toBe('string') // base64
327+
})
328+
329+
test('should encode multiple topic slots', () => {
330+
const result = logTriggerConfig({
331+
addresses: [VALID_ADDRESS],
332+
topics: [[VALID_TOPIC], [VALID_TOPIC, VALID_TOPIC]],
333+
})
334+
expect(result.topics).toHaveLength(2)
335+
expect(result.topics![0].values).toHaveLength(1)
336+
expect(result.topics![1].values).toHaveLength(2)
337+
})
338+
339+
test('should omit topics when not provided', () => {
340+
const result = logTriggerConfig({ addresses: [VALID_ADDRESS] })
341+
expect(result.topics).toBeUndefined()
342+
})
343+
344+
test('should set confidence level', () => {
345+
const result = logTriggerConfig({
346+
addresses: [VALID_ADDRESS],
347+
confidence: 'LATEST',
348+
})
349+
expect(result.confidence).toBe('CONFIDENCE_LEVEL_LATEST')
350+
})
351+
352+
test('should set FINALIZED confidence level', () => {
353+
const result = logTriggerConfig({
354+
addresses: [VALID_ADDRESS],
355+
confidence: 'FINALIZED',
356+
})
357+
expect(result.confidence).toBe('CONFIDENCE_LEVEL_FINALIZED')
358+
})
359+
360+
test('should omit confidence when not provided', () => {
361+
const result = logTriggerConfig({ addresses: [VALID_ADDRESS] })
362+
expect(result.confidence).toBeUndefined()
363+
})
364+
365+
test('should throw for empty addresses array', () => {
366+
expect(() => logTriggerConfig({ addresses: [] })).toThrow(
367+
'logTriggerConfig requires at least one address',
368+
)
369+
})
370+
371+
test('should throw for invalid hex address', () => {
372+
expect(() =>
373+
logTriggerConfig({ addresses: ['not-hex' as `0x${string}`] }),
374+
).toThrow('Invalid address at index 0')
375+
})
376+
377+
test('should throw for address with wrong byte length', () => {
378+
expect(() =>
379+
logTriggerConfig({ addresses: ['0x1234' as `0x${string}`] }),
380+
).toThrow('expected 20 bytes')
381+
})
382+
383+
test('should throw for topic with wrong byte length', () => {
384+
expect(() =>
385+
logTriggerConfig({
386+
addresses: [VALID_ADDRESS],
387+
topics: [['0x1234' as `0x${string}`]],
388+
}),
389+
).toThrow('expected 32 bytes')
390+
})
391+
392+
test('should include index in address error', () => {
393+
expect(() =>
394+
logTriggerConfig({
395+
addresses: [VALID_ADDRESS, 'bad' as `0x${string}`],
396+
}),
397+
).toThrow('Invalid address at index 1')
398+
})
399+
400+
test('should include slot and value index in topic error', () => {
401+
expect(() =>
402+
logTriggerConfig({
403+
addresses: [VALID_ADDRESS],
404+
topics: [[VALID_TOPIC], ['bad' as `0x${string}`]],
405+
}),
406+
).toThrow('Invalid topic at topics[1][0]')
407+
})
408+
})
409+
271410
describe('isChainSelectorSupported', () => {
272411
test('should return true for supported chain selectors', () => {
273412
// Get all supported chain selectors from EVMClient

packages/cre-sdk/src/sdk/utils/capabilities/blockchain/blockchain-helpers.ts

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { create, toJson } from '@bufbuild/protobuf'
2-
import type { CallMsgJson } from '@cre/generated/capabilities/blockchain/evm/v1alpha/client_pb'
2+
import type {
3+
CallMsgJson,
4+
ConfidenceLevelJson,
5+
FilterLogTriggerRequestJson,
6+
} from '@cre/generated/capabilities/blockchain/evm/v1alpha/client_pb'
37
import type { ReportRequestJson } from '@cre/generated/sdk/v1alpha/sdk_pb'
48
import { BigIntSchema, type BigInt as GeneratedBigInt } from '@cre/generated/values/v1/values_pb'
59
import { EVMClient } from '@cre/sdk/cre'
6-
import { bigintToBytes, bytesToBigint, hexToBase64 } from '@cre/sdk/utils/hex-utils'
10+
import { bigintToBytes, bytesToBigint, hexToBase64, hexToBytes } from '@cre/sdk/utils/hex-utils'
711
import type { Address, Hex } from 'viem'
812

913
/**
@@ -118,11 +122,23 @@ export interface EncodeCallMsgPayload {
118122
* @param payload - The call message payload to encode.
119123
* @returns The encoded call message payload.
120124
*/
121-
export const encodeCallMsg = (payload: EncodeCallMsgPayload): CallMsgJson => ({
122-
from: hexToBase64(payload.from),
123-
to: hexToBase64(payload.to),
124-
data: hexToBase64(payload.data),
125-
})
125+
export const encodeCallMsg = (payload: EncodeCallMsgPayload): CallMsgJson => {
126+
const encodeField = (fieldName: string, value: string): string => {
127+
try {
128+
return hexToBase64(value)
129+
} catch (e) {
130+
throw new Error(
131+
`Invalid hex in '${fieldName}' field of CallMsg: ${e instanceof Error ? e.message : String(e)}`,
132+
)
133+
}
134+
}
135+
136+
return {
137+
from: encodeField('from', payload.from),
138+
to: encodeField('to', payload.to),
139+
data: encodeField('data', payload.data),
140+
}
141+
}
126142

127143
/**
128144
* Default values expected by the EVM capability for report encoding.
@@ -148,5 +164,95 @@ export const prepareReportRequest = (
148164
...reportEncoder,
149165
})
150166

167+
/**
168+
* Validates a hex string and checks that the decoded bytes have the expected length.
169+
*/
170+
const validateHexByteLength = (hex: string, expectedBytes: number, fieldLabel: string): string => {
171+
const bytes = hexToBytes(hex)
172+
if (bytes.length !== expectedBytes) {
173+
throw new Error(
174+
`Invalid ${fieldLabel}: expected ${expectedBytes} bytes, got ${bytes.length} bytes from '${hex.length > 200 ? hex.slice(0, 200) + '...' : hex}'. EVM ${fieldLabel}s must be exactly ${expectedBytes} bytes.`,
175+
)
176+
}
177+
return hexToBase64(hex)
178+
}
179+
180+
export interface LogTriggerConfigOptions {
181+
/** EVM addresses to monitor — hex strings with 0x prefix (20 bytes each) */
182+
addresses: Hex[]
183+
/** Topic filters — array of up to 4 arrays of hex topic values (32 bytes each).
184+
* - topics[0]: event signatures (keccak256 hashes), at least one required
185+
* - topics[1]: possible values for first indexed arg (optional)
186+
* - topics[2]: possible values for second indexed arg (optional)
187+
* - topics[3]: possible values for third indexed arg (optional)
188+
*/
189+
topics?: Hex[][]
190+
/** Confidence level for log finality. Defaults to SAFE. */
191+
confidence?: 'SAFE' | 'LATEST' | 'FINALIZED'
192+
}
193+
194+
/**
195+
* Creates a log trigger configuration from hex-encoded addresses and topics.
196+
*
197+
* This helper converts hex addresses and topic hashes to the base64-encoded format
198+
* expected by the EVM capability's `FilterLogTriggerRequest`, and validates that
199+
* addresses are 20 bytes and topics are 32 bytes.
200+
*
201+
* @example
202+
* const WETH = '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9'
203+
* const TRANSFER = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
204+
*
205+
* handler(
206+
* evmClient.logTrigger(logTriggerConfig({
207+
* addresses: [WETH],
208+
* topics: [[TRANSFER]],
209+
* confidence: 'LATEST',
210+
* })),
211+
* onLogTrigger,
212+
* )
213+
*
214+
* @param opts - Hex-encoded addresses, topic filters, and optional confidence level.
215+
* @returns The `FilterLogTriggerRequestJson` ready to pass to `evmClient.logTrigger()`.
216+
*/
217+
export const logTriggerConfig = (opts: LogTriggerConfigOptions): FilterLogTriggerRequestJson => {
218+
if (!opts.addresses || opts.addresses.length === 0) {
219+
throw new Error(
220+
'logTriggerConfig requires at least one address. Provide an array of hex-encoded EVM addresses (20 bytes each).',
221+
)
222+
}
223+
224+
const addresses = opts.addresses.map((addr, i) => {
225+
try {
226+
return validateHexByteLength(addr, 20, 'address')
227+
} catch (e) {
228+
throw new Error(
229+
`Invalid address at index ${i}: ${e instanceof Error ? e.message : String(e)}`,
230+
)
231+
}
232+
})
233+
234+
const topics = opts.topics?.map((topicSlot, slotIndex) => ({
235+
values: topicSlot.map((topic, valueIndex) => {
236+
try {
237+
return validateHexByteLength(topic, 32, 'topic')
238+
} catch (e) {
239+
throw new Error(
240+
`Invalid topic at topics[${slotIndex}][${valueIndex}]: ${e instanceof Error ? e.message : String(e)}`,
241+
)
242+
}
243+
}),
244+
}))
245+
246+
const confidence: ConfidenceLevelJson | undefined = opts.confidence
247+
? `CONFIDENCE_LEVEL_${opts.confidence}`
248+
: undefined
249+
250+
return {
251+
addresses,
252+
...(topics ? { topics } : {}),
253+
...(confidence ? { confidence } : {}),
254+
}
255+
}
256+
151257
export const isChainSelectorSupported = (chainSelectorName: string) =>
152258
Object.keys(EVMClient.SUPPORTED_CHAIN_SELECTORS).includes(chainSelectorName)

0 commit comments

Comments
 (0)