@chainlink/ccip-sdk 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/api/index.d.ts +165 -15
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +236 -61
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/types.d.ts +119 -1
  6. package/dist/api/types.d.ts.map +1 -1
  7. package/dist/chain.d.ts +53 -27
  8. package/dist/chain.d.ts.map +1 -1
  9. package/dist/chain.js +71 -16
  10. package/dist/chain.js.map +1 -1
  11. package/dist/errors/codes.d.ts +1 -0
  12. package/dist/errors/codes.d.ts.map +1 -1
  13. package/dist/errors/codes.js +1 -0
  14. package/dist/errors/codes.js.map +1 -1
  15. package/dist/errors/index.d.ts +1 -1
  16. package/dist/errors/index.d.ts.map +1 -1
  17. package/dist/errors/index.js +1 -1
  18. package/dist/errors/index.js.map +1 -1
  19. package/dist/errors/recovery.d.ts.map +1 -1
  20. package/dist/errors/recovery.js +1 -0
  21. package/dist/errors/recovery.js.map +1 -1
  22. package/dist/errors/specialized.d.ts +21 -0
  23. package/dist/errors/specialized.d.ts.map +1 -1
  24. package/dist/errors/specialized.js +31 -1
  25. package/dist/errors/specialized.js.map +1 -1
  26. package/dist/evm/abi/OffRamp_2_0.d.ts +18 -17
  27. package/dist/evm/abi/OffRamp_2_0.d.ts.map +1 -1
  28. package/dist/evm/abi/OffRamp_2_0.js +19 -21
  29. package/dist/evm/abi/OffRamp_2_0.js.map +1 -1
  30. package/dist/evm/abi/TokenPool_2_0.d.ts +0 -4
  31. package/dist/evm/abi/TokenPool_2_0.d.ts.map +1 -1
  32. package/dist/evm/abi/TokenPool_2_0.js +0 -1
  33. package/dist/evm/abi/TokenPool_2_0.js.map +1 -1
  34. package/dist/evm/gas.d.ts +14 -4
  35. package/dist/evm/gas.d.ts.map +1 -1
  36. package/dist/evm/gas.js +7 -6
  37. package/dist/evm/gas.js.map +1 -1
  38. package/dist/evm/index.d.ts +39 -8
  39. package/dist/evm/index.d.ts.map +1 -1
  40. package/dist/evm/index.js +106 -27
  41. package/dist/evm/index.js.map +1 -1
  42. package/dist/extra-args.d.ts +18 -8
  43. package/dist/extra-args.d.ts.map +1 -1
  44. package/dist/extra-args.js +6 -6
  45. package/dist/extra-args.js.map +1 -1
  46. package/dist/gas.d.ts +1 -1
  47. package/dist/gas.d.ts.map +1 -1
  48. package/dist/gas.js +7 -2
  49. package/dist/gas.js.map +1 -1
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/requests.d.ts +11 -5
  54. package/dist/requests.d.ts.map +1 -1
  55. package/dist/requests.js +4 -7
  56. package/dist/requests.js.map +1 -1
  57. package/dist/solana/index.d.ts +2 -2
  58. package/dist/solana/utils.js +2 -2
  59. package/dist/solana/utils.js.map +1 -1
  60. package/dist/ton/index.d.ts.map +1 -1
  61. package/dist/ton/index.js +34 -26
  62. package/dist/ton/index.js.map +1 -1
  63. package/dist/utils.d.ts +10 -4
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/utils.js +10 -4
  66. package/dist/utils.js.map +1 -1
  67. package/package.json +7 -7
  68. package/src/api/index.ts +271 -59
  69. package/src/api/types.ts +126 -1
  70. package/src/chain.ts +120 -42
  71. package/src/errors/codes.ts +1 -0
  72. package/src/errors/index.ts +1 -0
  73. package/src/errors/recovery.ts +2 -0
  74. package/src/errors/specialized.ts +33 -1
  75. package/src/evm/abi/OffRamp_2_0.ts +19 -21
  76. package/src/evm/abi/TokenPool_2_0.ts +0 -1
  77. package/src/evm/gas.ts +18 -20
  78. package/src/evm/index.ts +126 -26
  79. package/src/extra-args.ts +18 -8
  80. package/src/gas.ts +8 -3
  81. package/src/index.ts +4 -0
  82. package/src/requests.ts +18 -12
  83. package/src/solana/utils.ts +2 -2
  84. package/src/ton/index.ts +47 -26
  85. package/src/utils.ts +10 -4
package/src/api/index.ts CHANGED
@@ -2,6 +2,7 @@ import { memoize } from 'micro-memoize'
2
2
  import type { SetRequired } from 'type-fest'
3
3
 
4
4
  import {
5
+ CCIPAbortError,
5
6
  CCIPApiClientNotAvailableError,
6
7
  CCIPHttpError,
7
8
  CCIPLaneNotFoundError,
@@ -28,10 +29,13 @@ import {
28
29
  MessageStatus,
29
30
  NetworkType,
30
31
  } from '../types.ts'
31
- import { bigIntReviver, parseJson } from '../utils.ts'
32
+ import { bigIntReviver, decodeAddress, parseJson } from '../utils.ts'
32
33
  import type {
33
34
  APIErrorResponse,
34
35
  LaneLatencyResponse,
36
+ MessageSearchFilters,
37
+ MessageSearchPage,
38
+ MessageSearchResult,
35
39
  RawExecutionInputsResult,
36
40
  RawLaneLatencyResponse,
37
41
  RawMessageResponse,
@@ -40,7 +44,14 @@ import type {
40
44
  } from './types.ts'
41
45
  import { calculateManualExecProof } from '../execution.ts'
42
46
 
43
- export type { APICCIPRequestMetadata, APIErrorResponse, LaneLatencyResponse } from './types.ts'
47
+ export type {
48
+ APICCIPRequestMetadata,
49
+ APIErrorResponse,
50
+ LaneLatencyResponse,
51
+ MessageSearchFilters,
52
+ MessageSearchPage,
53
+ MessageSearchResult,
54
+ } from './types.ts'
44
55
 
45
56
  /** Default CCIP API base URL */
46
57
  export const DEFAULT_API_BASE_URL = 'https://api.ccip.chain.link'
@@ -51,7 +62,7 @@ export const DEFAULT_TIMEOUT_MS = 30000
51
62
  /** SDK version string for telemetry header */
52
63
  // generate:nofail
53
64
  // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
54
- export const SDK_VERSION = '1.1.1-593a607'
65
+ export const SDK_VERSION = '1.2.0-7dcdd16'
55
66
  // generate:end
56
67
 
57
68
  /** SDK telemetry header name */
@@ -87,6 +98,7 @@ const validateMessageStatus = (value: string, logger: Logger): MessageStatus =>
87
98
 
88
99
  const ensureNetworkInfo = (o: RawNetworkInfo, logger: Logger): NetworkInfo => {
89
100
  return Object.assign(o, {
101
+ chainSelector: BigInt(o.chainSelector),
90
102
  networkType: o.name.includes('-mainnet') ? NetworkType.Mainnet : NetworkType.Testnet,
91
103
  ...(!('family' in o) && { family: validateChainFamily(o.chainFamily, logger) }),
92
104
  }) as unknown as NetworkInfo
@@ -191,25 +203,33 @@ export class CCIPAPIClient {
191
203
  * @throws CCIPTimeoutError if request times out
192
204
  * @internal
193
205
  */
194
- private async _fetchWithTimeout(url: string, operation: string): Promise<Response> {
195
- const controller = new AbortController()
196
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs)
206
+ private async _fetchWithTimeout(
207
+ url: string,
208
+ operation: string,
209
+ signal?: AbortSignal,
210
+ ): Promise<Response> {
211
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs)
212
+ const combinedSignal = signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal
197
213
 
198
214
  try {
199
215
  return await this._fetch(url, {
200
- signal: controller.signal,
216
+ signal: combinedSignal,
201
217
  headers: {
202
218
  'Content-Type': 'application/json',
203
219
  [SDK_VERSION_HEADER]: `CCIP SDK v${SDK_VERSION}`,
204
220
  },
205
221
  })
206
222
  } catch (error) {
207
- if (error instanceof Error && error.name === 'AbortError') {
223
+ if (
224
+ error instanceof Error &&
225
+ (error.name === 'AbortError' || error.name === 'TimeoutError')
226
+ ) {
227
+ if (signal?.aborted) {
228
+ throw new CCIPAbortError(operation)
229
+ }
208
230
  throw new CCIPTimeoutError(operation, this.timeoutMs)
209
231
  }
210
232
  throw error
211
- } finally {
212
- clearTimeout(timeoutId)
213
233
  }
214
234
  }
215
235
 
@@ -218,10 +238,15 @@ export class CCIPAPIClient {
218
238
  *
219
239
  * @param sourceChainSelector - Source chain selector (bigint)
220
240
  * @param destChainSelector - Destination chain selector (bigint)
241
+ * @param numberOfBlocks - Optional number of block confirmations for latency calculation.
242
+ * When omitted or 0, uses the lane's default finality. When provided as a positive
243
+ * integer, the API returns latency for that custom finality value (sent as `numOfBlocks`
244
+ * query parameter).
221
245
  * @returns Promise resolving to {@link LaneLatencyResponse} with totalMs
222
246
  *
223
247
  * @throws {@link CCIPLaneNotFoundError} when lane not found (404)
224
248
  * @throws {@link CCIPTimeoutError} if request times out
249
+ * @throws {@link CCIPAbortError} if request is aborted via signal
225
250
  * @throws {@link CCIPHttpError} on other HTTP errors with context:
226
251
  * - `status` - HTTP status code (e.g., 500)
227
252
  * - `statusText` - HTTP status message
@@ -237,6 +262,15 @@ export class CCIPAPIClient {
237
262
  * console.log(`Estimated delivery: ${Math.round(latency.totalMs / 60000)} minutes`)
238
263
  * ```
239
264
  *
265
+ * @example Custom block confirmations
266
+ * ```typescript
267
+ * const latency = await api.getLaneLatency(
268
+ * 5009297550715157269n, // Ethereum mainnet
269
+ * 4949039107694359620n, // Arbitrum mainnet
270
+ * 10, // 10 block confirmations
271
+ * )
272
+ * ```
273
+ *
240
274
  * @example Handling specific API errors
241
275
  * ```typescript
242
276
  * try {
@@ -251,14 +285,19 @@ export class CCIPAPIClient {
251
285
  async getLaneLatency(
252
286
  sourceChainSelector: bigint,
253
287
  destChainSelector: bigint,
288
+ numberOfBlocks?: number,
289
+ options?: { signal?: AbortSignal },
254
290
  ): Promise<LaneLatencyResponse> {
255
291
  const url = new URL(`${this.baseUrl}/v2/lanes/latency`)
256
292
  url.searchParams.set('sourceChainSelector', sourceChainSelector.toString())
257
293
  url.searchParams.set('destChainSelector', destChainSelector.toString())
294
+ if (numberOfBlocks) {
295
+ url.searchParams.set('numOfBlocks', numberOfBlocks.toString())
296
+ }
258
297
 
259
298
  this.logger.debug(`CCIPAPIClient: GET ${url.toString()}`)
260
299
 
261
- const response = await this._fetchWithTimeout(url.toString(), 'getLaneLatency')
300
+ const response = await this._fetchWithTimeout(url.toString(), 'getLaneLatency', options?.signal)
262
301
 
263
302
  if (!response.ok) {
264
303
  // Try to parse structured error response from API
@@ -308,6 +347,7 @@ export class CCIPAPIClient {
308
347
  *
309
348
  * @throws {@link CCIPMessageIdNotFoundError} when message not found (404)
310
349
  * @throws {@link CCIPTimeoutError} if request times out
350
+ * @throws {@link CCIPAbortError} if request is aborted via signal
311
351
  * @throws {@link CCIPHttpError} on HTTP errors with context:
312
352
  * - `status` - HTTP status code
313
353
  * - `statusText` - HTTP status message
@@ -334,12 +374,15 @@ export class CCIPAPIClient {
334
374
  * }
335
375
  * ```
336
376
  */
337
- async getMessageById(messageId: string): Promise<SetRequired<CCIPRequest, 'metadata'>> {
377
+ async getMessageById(
378
+ messageId: string,
379
+ options?: { signal?: AbortSignal },
380
+ ): Promise<SetRequired<CCIPRequest, 'metadata'>> {
338
381
  const url = `${this.baseUrl}/v2/messages/${encodeURIComponent(messageId)}`
339
382
 
340
383
  this.logger.debug(`CCIPAPIClient: GET ${url}`)
341
384
 
342
- const response = await this._fetchWithTimeout(url, 'getMessageById')
385
+ const response = await this._fetchWithTimeout(url, 'getMessageById', options?.signal)
343
386
 
344
387
  if (!response.ok) {
345
388
  // Try to parse structured error response from API
@@ -379,93 +422,260 @@ export class CCIPAPIClient {
379
422
  }
380
423
 
381
424
  /**
382
- * Fetches message IDs from a source transaction hash.
425
+ * Searches CCIP messages using filters with cursor-based pagination.
383
426
  *
384
- * @param txHash - Source transaction hash (EVM hex or Solana Base58)
385
- * @returns Promise resolving to array of message IDs
427
+ * @param filters - Optional search filters. Ignored when `options.cursor` is provided
428
+ * (the cursor already encodes the original filters).
429
+ * @param options - Optional pagination options: `limit` (max results per page) and
430
+ * `cursor` (opaque token from a previous {@link MessageSearchPage} for the next page).
431
+ * @returns Promise resolving to a {@link MessageSearchPage} with results and pagination info.
386
432
  *
387
- * @throws {@link CCIPMessageNotFoundInTxError} when no messages found (404 or empty)
388
- * @throws {@link CCIPUnexpectedPaginationError} when hasNextPage is true
389
- * @throws {@link CCIPTimeoutError} if request times out
390
- * @throws {@link CCIPHttpError} on HTTP errors
433
+ * @remarks
434
+ * A 404 response is treated as "no results found" and returns an empty page,
435
+ * unlike {@link CCIPAPIClient.getMessageById} which throws on 404.
436
+ * When paginating with a cursor, the `filters` parameter is ignored because
437
+ * the cursor encodes the original filters.
391
438
  *
392
- * @example Basic usage
439
+ * @throws {@link CCIPTimeoutError} if request times out.
440
+ * @throws {@link CCIPAbortError} if request is aborted via signal.
441
+ * @throws {@link CCIPHttpError} on HTTP errors (4xx/5xx, except 404 which returns empty).
442
+ *
443
+ * @see {@link MessageSearchFilters} — available filter fields
444
+ * @see {@link MessageSearchPage} — return type with pagination
445
+ * @see {@link CCIPAPIClient.searchAllMessages} — async generator that handles pagination automatically
446
+ * @see {@link CCIPAPIClient.getMessageById} — fetch full message details for a search result
447
+ * @see {@link CCIPAPIClient.getMessageIdsInTx} — convenience wrapper using `sourceTransactionHash`
448
+ *
449
+ * @example Search by sender
393
450
  * ```typescript
394
- * const messageIds = await api.getMessageIdsInTx(
395
- * '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
396
- * )
397
- * console.log(`Found ${messageIds.length} messages`)
451
+ * const page = await api.searchMessages({
452
+ * sender: '0x9d087fC03ae39b088326b67fA3C788236645b717',
453
+ * })
454
+ * for (const msg of page.data) {
455
+ * console.log(`${msg.messageId}: ${msg.status}`)
456
+ * }
457
+ * ```
458
+ *
459
+ * @example Paginate through all results
460
+ * ```typescript
461
+ * let page = await api.searchMessages({ sender: '0x...' }, { limit: 10 })
462
+ * const all = [...page.data]
463
+ * while (page.hasNextPage) {
464
+ * page = await api.searchMessages(undefined, { cursor: page.cursor! })
465
+ * all.push(...page.data)
466
+ * }
467
+ * ```
468
+ *
469
+ * @example Filter by lane and sender
470
+ * ```typescript
471
+ * const page = await api.searchMessages({
472
+ * sender: '0x9d087fC03ae39b088326b67fA3C788236645b717',
473
+ * sourceChainSelector: 16015286601757825753n,
474
+ * destChainSelector: 14767482510784806043n,
475
+ * })
398
476
  * ```
399
477
  */
400
- async getMessageIdsInTx(txHash: string): Promise<string[]> {
478
+ async searchMessages(
479
+ filters?: MessageSearchFilters,
480
+ options?: { limit?: number; cursor?: string; signal?: AbortSignal },
481
+ ): Promise<MessageSearchPage> {
401
482
  const url = new URL(`${this.baseUrl}/v2/messages`)
402
- url.searchParams.set('sourceTransactionHash', txHash)
403
- url.searchParams.set('limit', '100')
483
+
484
+ if (options?.cursor) {
485
+ // Cursor encodes all original filters — only send cursor (and optional limit)
486
+ url.searchParams.set('cursor', options.cursor)
487
+ } else if (filters) {
488
+ if (filters.sender) url.searchParams.set('sender', filters.sender)
489
+ if (filters.receiver) url.searchParams.set('receiver', filters.receiver)
490
+ if (filters.sourceChainSelector != null)
491
+ url.searchParams.set('sourceChainSelector', filters.sourceChainSelector.toString())
492
+ if (filters.destChainSelector != null)
493
+ url.searchParams.set('destChainSelector', filters.destChainSelector.toString())
494
+ if (filters.sourceTransactionHash)
495
+ url.searchParams.set('sourceTransactionHash', filters.sourceTransactionHash)
496
+ if (filters.readyForManualExecOnly != null)
497
+ url.searchParams.set('readyForManualExecOnly', String(filters.readyForManualExecOnly))
498
+ }
499
+
500
+ if (options?.limit != null) url.searchParams.set('limit', options.limit.toString())
404
501
 
405
502
  this.logger.debug(`CCIPAPIClient: GET ${url.toString()}`)
406
503
 
407
- const response = await this._fetchWithTimeout(url.toString(), 'getMessageIdsInTx')
504
+ const response = await this._fetchWithTimeout(url.toString(), 'searchMessages', options?.signal)
408
505
 
409
506
  if (!response.ok) {
410
- // Try to parse structured error response from API
507
+ // 404 empty results (search found nothing)
508
+ if (response.status === HttpStatus.NOT_FOUND) {
509
+ return { data: [], hasNextPage: false }
510
+ }
511
+
411
512
  let apiError: APIErrorResponse | undefined
412
513
  try {
413
514
  apiError = parseJson<APIErrorResponse>(await response.text())
414
515
  } catch {
415
- // Response body not JSON, use HTTP status only
416
- }
417
-
418
- // 404 - No messages found
419
- if (response.status === HttpStatus.NOT_FOUND) {
420
- throw new CCIPMessageNotFoundInTxError(txHash, {
421
- context: apiError
422
- ? {
423
- apiErrorCode: apiError.error,
424
- apiErrorMessage: apiError.message,
425
- }
426
- : undefined,
427
- })
516
+ // Response body not JSON
428
517
  }
429
518
 
430
- // Generic HTTP error for other cases
431
519
  throw new CCIPHttpError(response.status, response.statusText, {
432
520
  context: apiError
433
- ? {
434
- apiErrorCode: apiError.error,
435
- apiErrorMessage: apiError.message,
436
- }
521
+ ? { apiErrorCode: apiError.error, apiErrorMessage: apiError.message }
437
522
  : undefined,
438
523
  })
439
524
  }
440
525
 
441
526
  const raw = parseJson<RawMessagesResponse>(await response.text())
442
527
 
443
- this.logger.debug('getMessageIdsInTx raw response:', raw)
528
+ this.logger.debug('searchMessages raw response:', raw)
444
529
 
445
- // Handle empty results
446
- if (raw.data.length === 0) {
530
+ return {
531
+ data: raw.data.map((msg) => {
532
+ const sourceInfo = ensureNetworkInfo(msg.sourceNetworkInfo, this.logger)
533
+ const destInfo = ensureNetworkInfo(msg.destNetworkInfo, this.logger)
534
+ return {
535
+ ...msg,
536
+ status: validateMessageStatus(msg.status, this.logger),
537
+ sourceNetworkInfo: sourceInfo,
538
+ destNetworkInfo: destInfo,
539
+ sender: decodeAddress(msg.sender, sourceInfo.family),
540
+ receiver: decodeAddress(msg.receiver, destInfo.family),
541
+ origin: decodeAddress(msg.origin, sourceInfo.family),
542
+ }
543
+ }),
544
+ hasNextPage: raw.pagination.hasNextPage,
545
+ cursor: raw.pagination.cursor ?? undefined,
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Async generator that streams all messages matching the given filters,
551
+ * handling cursor-based pagination automatically.
552
+ *
553
+ * @param filters - Optional search filters (same as {@link CCIPAPIClient.searchMessages}).
554
+ * @param options - Optional `limit` controlling the per-page fetch size (number of
555
+ * results fetched per API call). The total number of results is controlled by the
556
+ * consumer — break out of the loop to stop early.
557
+ * @returns AsyncGenerator yielding {@link MessageSearchResult} one at a time, across all pages.
558
+ *
559
+ * @throws {@link CCIPTimeoutError} if a page request times out.
560
+ * @throws {@link CCIPAbortError} if a page request is aborted via signal.
561
+ * @throws {@link CCIPHttpError} on HTTP errors (4xx/5xx, except 404 which yields nothing).
562
+ *
563
+ * @see {@link CCIPAPIClient.searchMessages} — for page-level control and explicit cursor handling
564
+ * @see {@link CCIPAPIClient.getMessageById} — fetch full message details for a search result
565
+ *
566
+ * @example Iterate all messages for a sender
567
+ * ```typescript
568
+ * for await (const msg of api.searchAllMessages({ sender: '0x...' })) {
569
+ * console.log(`${msg.messageId}: ${msg.status}`)
570
+ * }
571
+ * ```
572
+ *
573
+ * @example Stop after collecting 5 results
574
+ * ```typescript
575
+ * const results: MessageSearchResult[] = []
576
+ * for await (const msg of api.searchAllMessages({ sender: '0x...' })) {
577
+ * results.push(msg)
578
+ * if (results.length >= 5) break
579
+ * }
580
+ * ```
581
+ */
582
+ async *searchAllMessages(
583
+ filters?: MessageSearchFilters,
584
+ options?: { limit?: number; signal?: AbortSignal },
585
+ ): AsyncGenerator<MessageSearchResult> {
586
+ let cursor: string | undefined
587
+ do {
588
+ const page = await this.searchMessages(filters, {
589
+ limit: options?.limit,
590
+ cursor,
591
+ signal: options?.signal,
592
+ })
593
+ yield* page.data
594
+ cursor = page.cursor
595
+ } while (cursor)
596
+ }
597
+
598
+ /**
599
+ * Fetches message IDs from a source transaction hash.
600
+ *
601
+ * @remarks
602
+ * Uses {@link CCIPAPIClient.searchMessages} internally with `sourceTransactionHash` filter and `limit: 100`.
603
+ *
604
+ * @param txHash - Source transaction hash.
605
+ * @returns Promise resolving to array of message IDs.
606
+ *
607
+ * @throws {@link CCIPMessageNotFoundInTxError} when no messages found (404 or empty).
608
+ * @throws {@link CCIPUnexpectedPaginationError} when hasNextPage is true.
609
+ * @throws {@link CCIPTimeoutError} if request times out.
610
+ * @throws {@link CCIPAbortError} if request is aborted via signal.
611
+ * @throws {@link CCIPHttpError} on HTTP errors.
612
+ *
613
+ * @example Basic usage
614
+ * ```typescript
615
+ * const messageIds = await api.getMessageIdsInTx(
616
+ * '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
617
+ * )
618
+ * console.log(`Found ${messageIds.length} messages`)
619
+ * ```
620
+ *
621
+ * @example Fetch full details for each message
622
+ * ```typescript
623
+ * const api = CCIPAPIClient.fromUrl()
624
+ * const messageIds = await api.getMessageIdsInTx(txHash)
625
+ * for (const id of messageIds) {
626
+ * const request = await api.getMessageById(id)
627
+ * console.log(`${id}: ${request.metadata.status}`)
628
+ * }
629
+ * ```
630
+ */
631
+ async getMessageIdsInTx(txHash: string, options?: { signal?: AbortSignal }): Promise<string[]> {
632
+ const result = await this.searchMessages(
633
+ { sourceTransactionHash: txHash },
634
+ { limit: 100, signal: options?.signal },
635
+ )
636
+
637
+ if (result.data.length === 0) {
447
638
  throw new CCIPMessageNotFoundInTxError(txHash)
448
639
  }
449
640
 
450
- // Handle unexpected pagination (more than 100 messages)
451
- if (raw.pagination.hasNextPage) {
452
- throw new CCIPUnexpectedPaginationError(txHash, raw.data.length)
641
+ if (result.hasNextPage) {
642
+ throw new CCIPUnexpectedPaginationError(txHash, result.data.length)
453
643
  }
454
644
 
455
- return raw.data.map((msg) => msg.messageId)
645
+ return result.data.map((msg) => msg.messageId)
456
646
  }
457
647
 
458
648
  /**
459
649
  * Fetches the execution input for a given message by id.
460
- * @param messageId - The ID of the message to fetch the execution input for.
461
- * @returns Either `{ encodedMessage, verifications }` or `{ message, offchainTokenData, ...proof }`, offRamp and lane
650
+ * For v2.0 messages, returns `{ encodedMessage, verifications }`.
651
+ * For pre-v2 messages, returns `{ message, offchainTokenData, proofs, ... }` with merkle proof.
652
+ *
653
+ * @param messageId - The CCIP message ID (32-byte hex string)
654
+ * @returns Execution input with offRamp address and lane info
655
+ *
656
+ * @throws {@link CCIPMessageIdNotFoundError} when message not found (404)
657
+ * @throws {@link CCIPTimeoutError} if request times out
658
+ * @throws {@link CCIPAbortError} if request is aborted via signal
659
+ * @throws {@link CCIPHttpError} on other HTTP errors
660
+ *
661
+ * @example
662
+ * ```typescript
663
+ * const api = CCIPAPIClient.fromUrl()
664
+ * const execInput = await api.getExecutionInput('0x1234...')
665
+ * // Use with dest.execute():
666
+ * const { offRamp, ...input } = execInput
667
+ * await dest.execute({ offRamp, input, wallet })
668
+ * ```
462
669
  */
463
- async getExecutionInput(messageId: string): Promise<ExecutionInput & Lane & { offRamp: string }> {
670
+ async getExecutionInput(
671
+ messageId: string,
672
+ options?: { signal?: AbortSignal },
673
+ ): Promise<ExecutionInput & Lane & { offRamp: string }> {
464
674
  const url = `${this.baseUrl}/v2/messages/${encodeURIComponent(messageId)}/execution-inputs`
465
675
 
466
676
  this.logger.debug(`CCIPAPIClient: GET ${url}`)
467
677
 
468
- const response = await this._fetchWithTimeout(url, 'getExecutionInput')
678
+ const response = await this._fetchWithTimeout(url, 'getExecutionInput', options?.signal)
469
679
  if (!response.ok) {
470
680
  // Try to parse structured error response from API
471
681
  let apiError: APIErrorResponse | undefined
@@ -578,6 +788,7 @@ export class CCIPAPIClient {
578
788
  status,
579
789
  origin,
580
790
  onramp,
791
+ offramp,
581
792
  version,
582
793
  readyForManualExecution,
583
794
  sendTransactionHash,
@@ -647,6 +858,7 @@ export class CCIPAPIClient {
647
858
  deliveryTime,
648
859
  sourceNetworkInfo: ensureNetworkInfo(sourceNetworkInfo, this.logger),
649
860
  destNetworkInfo: ensureNetworkInfo(destNetworkInfo, this.logger),
861
+ offRamp: offramp,
650
862
  },
651
863
  }
652
864
  }
package/src/api/types.ts CHANGED
@@ -105,6 +105,7 @@ export type RawMessageResponse = {
105
105
  origin: string
106
106
  sequenceNumber: string
107
107
  onramp: string
108
+ offramp?: string
108
109
  sendBlockNumber: bigint
109
110
  sendLogIndex: bigint
110
111
  // Optional fields
@@ -144,6 +145,121 @@ export type RawMessagesResponse = {
144
145
  }
145
146
  }
146
147
 
148
+ // ============================================================================
149
+ // searchMessages public types
150
+ // ============================================================================
151
+
152
+ /**
153
+ * Filters for searching CCIP messages via the API.
154
+ *
155
+ * All fields are optional — omit a field to leave it unfiltered.
156
+ * Chain selectors are accepted as `bigint` and converted to strings for the API.
157
+ *
158
+ * @see {@link CCIPAPIClient.searchMessages}
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const api = CCIPAPIClient.fromUrl()
163
+ * // Find messages from a specific sender on a specific lane
164
+ * const page = await api.searchMessages({
165
+ * sender: '0x9d087fC03ae39b088326b67fA3C788236645b717',
166
+ * sourceChainSelector: 16015286601757825753n,
167
+ * destChainSelector: 14767482510784806043n,
168
+ * })
169
+ * ```
170
+ */
171
+ export type MessageSearchFilters = {
172
+ /** Filter by sender address */
173
+ sender?: string
174
+ /** Filter by receiver address */
175
+ receiver?: string
176
+ /** Filter by source chain selector */
177
+ sourceChainSelector?: bigint
178
+ /** Filter by destination chain selector */
179
+ destChainSelector?: bigint
180
+ /** Filter by source transaction hash */
181
+ sourceTransactionHash?: string
182
+ /** When `true`, return only messages eligible for manual execution (stuck/failed messages) */
183
+ readyForManualExecOnly?: boolean
184
+ }
185
+
186
+ /**
187
+ * A single message search result from the CCIP API.
188
+ *
189
+ * @remarks
190
+ * This is a lightweight summary — it does not include `extraArgs`, `tokenAmounts`,
191
+ * `fees`, or other detailed fields available via {@link CCIPAPIClient.getMessageById}.
192
+ *
193
+ * @see {@link CCIPAPIClient.getMessageById} — to fetch full message details
194
+ * @see {@link CCIPAPIClient.searchMessages}
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const page = await api.searchMessages({ sender: '0x...' })
199
+ * for (const msg of page.data) {
200
+ * console.log(`${msg.messageId}: ${msg.status} (${msg.sourceNetworkInfo.name} → ${msg.destNetworkInfo.name})`)
201
+ * }
202
+ * ```
203
+ */
204
+ export type MessageSearchResult = {
205
+ /** Unique CCIP message ID (0x-prefixed, 32-byte hex string) */
206
+ messageId: string
207
+ /** Transaction originator address (EOA that submitted the send transaction) */
208
+ origin: string
209
+ /** Message sender address */
210
+ sender: string
211
+ /** Message receiver address */
212
+ receiver: string
213
+ /** Message lifecycle status */
214
+ status: MessageStatus
215
+ /** Source network metadata */
216
+ sourceNetworkInfo: NetworkInfo
217
+ /** Destination network metadata */
218
+ destNetworkInfo: NetworkInfo
219
+ /** Source chain transaction hash */
220
+ sendTransactionHash: string
221
+ /** ISO 8601 timestamp of the send transaction */
222
+ sendTimestamp: string
223
+ }
224
+
225
+ /**
226
+ * A page of message search results with cursor-based pagination.
227
+ *
228
+ * @remarks
229
+ * When `hasNextPage` is `true`, pass the `cursor` value to
230
+ * {@link CCIPAPIClient.searchMessages} to fetch the next page.
231
+ * The cursor encodes all original filters, so you do not need
232
+ * to re-supply them when paginating.
233
+ *
234
+ * @see {@link MessageSearchFilters}
235
+ * @see {@link CCIPAPIClient.searchMessages}
236
+ * @see {@link CCIPAPIClient.searchAllMessages} — async generator alternative that handles
237
+ * pagination automatically
238
+ *
239
+ * @example Manual pagination
240
+ * ```typescript
241
+ * let page = await api.searchMessages({ sender: '0x...' }, { limit: 10 })
242
+ * while (page.hasNextPage) {
243
+ * page = await api.searchMessages(undefined, { cursor: page.cursor! })
244
+ * }
245
+ * ```
246
+ *
247
+ * @example Automatic pagination (preferred for most use cases)
248
+ * ```typescript
249
+ * for await (const msg of api.searchAllMessages({ sender: '0x...' })) {
250
+ * console.log(msg.messageId)
251
+ * }
252
+ * ```
253
+ */
254
+ export type MessageSearchPage = {
255
+ /** Array of message search results */
256
+ data: MessageSearchResult[]
257
+ /** Whether more results are available */
258
+ hasNextPage: boolean
259
+ /** Opaque cursor for fetching the next page */
260
+ cursor?: string
261
+ }
262
+
147
263
  // ============================================================================
148
264
  // APICCIPRequest type - derived from CCIPRequest
149
265
  // ============================================================================
@@ -181,13 +297,22 @@ export type APICCIPRequestMetadata = {
181
297
  sourceNetworkInfo: NetworkInfo
182
298
  /** Destination network metadata. */
183
299
  destNetworkInfo: NetworkInfo
300
+ /** OffRamp address on dest */
301
+ offRamp?: string
184
302
  }
185
303
 
186
304
  // ============================================================================
187
305
  // GET /v2/messages/${messageId}/execution-inputs search endpoint types
188
306
  // ============================================================================
189
307
 
190
- /** Raw API response from GET /v2/messages/:messageId/execution-inputs */
308
+ /**
309
+ * Raw API response from GET /v2/messages/:messageId/execution-inputs.
310
+ *
311
+ * @remarks
312
+ * The response has two union branches:
313
+ * - **v2.0+**: contains `encodedMessage` (MessageV1Codec-serialized), optional `ccvData` array, and `verifierAddresses`
314
+ * - **pre-v2**: contains `messageBatch` array with decoded messages, `merkleRoot`, and optional USDC/LBTC attestation data
315
+ */
191
316
  export type RawExecutionInputsResult = {
192
317
  offramp: string
193
318
  } & (