@chainlink/ccip-sdk 1.0.0 → 1.1.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 (91) hide show
  1. package/dist/api/index.d.ts +4 -4
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +110 -11
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/types.d.ts +34 -0
  6. package/dist/api/types.d.ts.map +1 -1
  7. package/dist/chain.d.ts +93 -4
  8. package/dist/chain.d.ts.map +1 -1
  9. package/dist/chain.js +78 -8
  10. package/dist/chain.js.map +1 -1
  11. package/dist/errors/codes.d.ts +1 -1
  12. package/dist/errors/codes.d.ts.map +1 -1
  13. package/dist/errors/codes.js +2 -1
  14. package/dist/errors/codes.js.map +1 -1
  15. package/dist/errors/index.d.ts +2 -2
  16. package/dist/errors/index.d.ts.map +1 -1
  17. package/dist/errors/index.js +2 -2
  18. package/dist/errors/index.js.map +1 -1
  19. package/dist/errors/recovery.js +1 -1
  20. package/dist/errors/recovery.js.map +1 -1
  21. package/dist/errors/specialized.d.ts +22 -19
  22. package/dist/errors/specialized.d.ts.map +1 -1
  23. package/dist/errors/specialized.js +30 -25
  24. package/dist/errors/specialized.js.map +1 -1
  25. package/dist/evm/abi/OffRamp_2_0.d.ts +24 -12
  26. package/dist/evm/abi/OffRamp_2_0.d.ts.map +1 -1
  27. package/dist/evm/abi/OffRamp_2_0.js +16 -8
  28. package/dist/evm/abi/OffRamp_2_0.js.map +1 -1
  29. package/dist/evm/abi/TokenPool_2_0.d.ts +1552 -0
  30. package/dist/evm/abi/TokenPool_2_0.d.ts.map +1 -0
  31. package/dist/evm/abi/TokenPool_2_0.js +1637 -0
  32. package/dist/evm/abi/TokenPool_2_0.js.map +1 -0
  33. package/dist/evm/const.d.ts +1 -0
  34. package/dist/evm/const.d.ts.map +1 -1
  35. package/dist/evm/const.js +2 -0
  36. package/dist/evm/const.js.map +1 -1
  37. package/dist/evm/index.d.ts +10 -4
  38. package/dist/evm/index.d.ts.map +1 -1
  39. package/dist/evm/index.js +138 -41
  40. package/dist/evm/index.js.map +1 -1
  41. package/dist/evm/messages.d.ts +2 -33
  42. package/dist/evm/messages.d.ts.map +1 -1
  43. package/dist/evm/messages.js +0 -210
  44. package/dist/evm/messages.js.map +1 -1
  45. package/dist/gas.d.ts +4 -0
  46. package/dist/gas.d.ts.map +1 -1
  47. package/dist/gas.js +27 -21
  48. package/dist/gas.js.map +1 -1
  49. package/dist/index.d.ts +2 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/messages.d.ts +34 -0
  54. package/dist/messages.d.ts.map +1 -0
  55. package/dist/messages.js +211 -0
  56. package/dist/messages.js.map +1 -0
  57. package/dist/solana/cleanup.js +2 -2
  58. package/dist/solana/cleanup.js.map +1 -1
  59. package/dist/solana/exec.js +1 -1
  60. package/dist/solana/exec.js.map +1 -1
  61. package/dist/solana/index.d.ts +19 -19
  62. package/dist/solana/index.d.ts.map +1 -1
  63. package/dist/solana/index.js +14 -0
  64. package/dist/solana/index.js.map +1 -1
  65. package/dist/sui/index.d.ts.map +1 -1
  66. package/dist/sui/index.js +14 -1
  67. package/dist/sui/index.js.map +1 -1
  68. package/dist/ton/index.d.ts.map +1 -1
  69. package/dist/ton/index.js +3 -1
  70. package/dist/ton/index.js.map +1 -1
  71. package/package.json +5 -5
  72. package/src/api/index.ts +126 -11
  73. package/src/api/types.ts +43 -0
  74. package/src/chain.ts +131 -11
  75. package/src/errors/codes.ts +2 -1
  76. package/src/errors/index.ts +1 -1
  77. package/src/errors/recovery.ts +1 -1
  78. package/src/errors/specialized.ts +35 -30
  79. package/src/evm/abi/OffRamp_2_0.ts +16 -8
  80. package/src/evm/abi/TokenPool_2_0.ts +1636 -0
  81. package/src/evm/const.ts +2 -0
  82. package/src/evm/index.ts +230 -75
  83. package/src/evm/messages.ts +3 -285
  84. package/src/gas.ts +27 -19
  85. package/src/index.ts +2 -1
  86. package/src/messages.ts +278 -0
  87. package/src/solana/cleanup.ts +2 -2
  88. package/src/solana/exec.ts +1 -1
  89. package/src/solana/index.ts +17 -0
  90. package/src/sui/index.ts +17 -0
  91. package/src/ton/index.ts +5 -1
package/src/api/index.ts CHANGED
@@ -2,23 +2,26 @@ import { memoize } from 'micro-memoize'
2
2
  import type { SetRequired } from 'type-fest'
3
3
 
4
4
  import {
5
+ CCIPApiClientNotAvailableError,
5
6
  CCIPHttpError,
6
7
  CCIPLaneNotFoundError,
7
8
  CCIPMessageIdNotFoundError,
8
9
  CCIPMessageNotFoundInTxError,
9
- CCIPNotImplementedError,
10
10
  CCIPTimeoutError,
11
11
  CCIPUnexpectedPaginationError,
12
12
  } from '../errors/index.ts'
13
13
  import { HttpStatus } from '../http-status.ts'
14
+ import { decodeMessageV1 } from '../messages.ts'
14
15
  import { decodeMessage } from '../requests.ts'
15
16
  import {
16
17
  type CCIPMessage,
17
18
  type CCIPRequest,
18
19
  type ChainLog,
19
20
  type ExecutionInput,
21
+ type Lane,
20
22
  type Logger,
21
23
  type NetworkInfo,
24
+ type OffchainTokenData,
22
25
  type WithLogger,
23
26
  CCIPVersion,
24
27
  ChainFamily,
@@ -29,11 +32,13 @@ import { bigIntReviver, parseJson } from '../utils.ts'
29
32
  import type {
30
33
  APIErrorResponse,
31
34
  LaneLatencyResponse,
35
+ RawExecutionInputsResult,
32
36
  RawLaneLatencyResponse,
33
37
  RawMessageResponse,
34
38
  RawMessagesResponse,
35
39
  RawNetworkInfo,
36
40
  } from './types.ts'
41
+ import { calculateManualExecProof } from '../execution.ts'
37
42
 
38
43
  export type { APICCIPRequestMetadata, APIErrorResponse, LaneLatencyResponse } from './types.ts'
39
44
 
@@ -46,7 +51,7 @@ export const DEFAULT_TIMEOUT_MS = 30000
46
51
  /** SDK version string for telemetry header */
47
52
  // generate:nofail
48
53
  // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
49
- export const SDK_VERSION = '1.0.0-4cc6ccc'
54
+ export const SDK_VERSION = '1.1.0-30c18a9'
50
55
  // generate:end
51
56
 
52
57
  /** SDK telemetry header name */
@@ -134,7 +139,7 @@ export class CCIPAPIClient {
134
139
  static {
135
140
  CCIPAPIClient.fromUrl = memoize(
136
141
  (baseUrl?: string, ctx?: CCIPAPIClientContext) => new CCIPAPIClient(baseUrl, ctx),
137
- { maxArgs: 1 },
142
+ { maxArgs: 1, transformKey: ([baseUrl]) => [baseUrl ?? DEFAULT_API_BASE_URL] },
138
143
  )
139
144
  }
140
145
 
@@ -144,10 +149,26 @@ export class CCIPAPIClient {
144
149
  * @param ctx - Optional context with logger and custom fetch
145
150
  */
146
151
  constructor(baseUrl?: string, ctx?: CCIPAPIClientContext) {
152
+ if (typeof baseUrl === 'boolean' || (baseUrl as unknown) === null)
153
+ throw new CCIPApiClientNotAvailableError({ context: { baseUrl } }) // shouldn't happen
147
154
  this.baseUrl = baseUrl ?? DEFAULT_API_BASE_URL
148
155
  this.logger = ctx?.logger ?? console
149
156
  this.timeoutMs = ctx?.timeoutMs ?? DEFAULT_TIMEOUT_MS
150
157
  this._fetch = ctx?.fetch ?? globalThis.fetch.bind(globalThis)
158
+
159
+ this.getMessageById = memoize(this.getMessageById.bind(this), {
160
+ async: true,
161
+ expires: 4_000,
162
+ maxArgs: 1,
163
+ maxSize: 100,
164
+ })
165
+
166
+ this.getExecutionInput = memoize(this.getExecutionInput.bind(this), {
167
+ async: true,
168
+ expires: 4_000,
169
+ maxArgs: 1,
170
+ maxSize: 100,
171
+ })
151
172
  }
152
173
 
153
174
  /**
@@ -437,15 +458,109 @@ export class CCIPAPIClient {
437
458
  /**
438
459
  * Fetches the execution input for a given message by id.
439
460
  * @param messageId - The ID of the message to fetch the execution input for.
440
- * @returns Either `{ encodedMessage, verifications }` or `{ message, offchainTokenData, ...proof }`, and offRamp
461
+ * @returns Either `{ encodedMessage, verifications }` or `{ message, offchainTokenData, ...proof }`, offRamp and lane
441
462
  */
442
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
443
- getExecutionInput(messageId: string): Promise<ExecutionInput & { offRamp: string }> {
444
- throw new CCIPNotImplementedError(`CCIPAPIClient.getExecutionInput`)
445
- // TODO: fetch (memoized) request with metadata from `getMessageById`
446
- // TODO: if request doesn't contain everything needed (e.g. <v2.0), fetch `batch` and
447
- // `offchainTokenData` from `/execution-input`
448
- // TODO: if <v2.0, `calculateManualExecProof` and return `offRamp` and `input`
463
+ async getExecutionInput(messageId: string): Promise<ExecutionInput & Lane & { offRamp: string }> {
464
+ const url = `${this.baseUrl}/v2/messages/${encodeURIComponent(messageId)}/execution-inputs`
465
+
466
+ this.logger.debug(`CCIPAPIClient: GET ${url}`)
467
+
468
+ const response = await this._fetchWithTimeout(url, 'getExecutionInput')
469
+ if (!response.ok) {
470
+ // Try to parse structured error response from API
471
+ let apiError: APIErrorResponse | undefined
472
+ try {
473
+ apiError = parseJson<APIErrorResponse>(await response.text())
474
+ } catch {
475
+ // Response body not JSON, use HTTP status only
476
+ }
477
+
478
+ // 404 - Message not found
479
+ if (response.status === HttpStatus.NOT_FOUND) {
480
+ throw new CCIPMessageIdNotFoundError(messageId, {
481
+ context: apiError
482
+ ? {
483
+ apiErrorCode: apiError.error,
484
+ apiErrorMessage: apiError.message,
485
+ }
486
+ : undefined,
487
+ })
488
+ }
489
+
490
+ // Generic HTTP error for other cases
491
+ throw new CCIPHttpError(response.status, response.statusText, {
492
+ context: apiError
493
+ ? {
494
+ apiErrorCode: apiError.error,
495
+ apiErrorMessage: apiError.message,
496
+ }
497
+ : undefined,
498
+ })
499
+ }
500
+
501
+ const raw = JSON.parse(await response.text(), bigIntReviver) as RawExecutionInputsResult
502
+ this.logger.debug('getExecutionInput raw response:', raw)
503
+
504
+ const offRamp = raw.offramp
505
+ let lane: Lane
506
+ if ('encodedMessage' in raw) {
507
+ // CCIP 2.0 messages use MessageV1Codec, which is chain-independent serialization
508
+ const {
509
+ sourceChainSelector,
510
+ destChainSelector,
511
+ onRampAddress: onRamp,
512
+ } = decodeMessageV1(raw.encodedMessage)
513
+ return {
514
+ sourceChainSelector,
515
+ destChainSelector,
516
+ onRamp,
517
+ offRamp,
518
+ version: CCIPVersion.V2_0,
519
+ encodedMessage: raw.encodedMessage,
520
+ verifications: (raw.ccvData ?? []).map((ccvData, i) => ({
521
+ ccvData,
522
+ destAddress: raw.verifierAddresses[i]!,
523
+ })),
524
+ }
525
+ }
526
+
527
+ const messagesInBatch = raw.messageBatch.map(decodeMessage)
528
+ const message = messagesInBatch.find((message) => message.messageId === messageId)!
529
+ if ('onramp' in raw && raw.onramp && raw.version) {
530
+ lane = {
531
+ sourceChainSelector: raw.sourceChainSelector,
532
+ destChainSelector: raw.destChainSelector,
533
+ onRamp: raw.onramp,
534
+ version: raw.version as CCIPVersion,
535
+ }
536
+ } else {
537
+ ;({ lane } = await this.getMessageById(messageId))
538
+ }
539
+
540
+ const proof = calculateManualExecProof(messagesInBatch, lane, messageId, raw.merkleRoot, this)
541
+
542
+ const rawMessage = raw.messageBatch.find((message) => message.messageId === messageId)!
543
+ const offchainTokenData: OffchainTokenData[] = rawMessage.tokenAmounts.map(() => undefined)
544
+ if (rawMessage.usdcData?.status === 'complete')
545
+ offchainTokenData[0] = {
546
+ _tag: 'usdc',
547
+ message: rawMessage.usdcData.message_bytes_hex!,
548
+ attestation: rawMessage.usdcData.attestation!,
549
+ }
550
+ else if (rawMessage.lbtcData?.status === 'NOTARIZATION_STATUS_SESSION_APPROVED')
551
+ offchainTokenData[0] = {
552
+ _tag: 'lbtc',
553
+ message_hash: rawMessage.lbtcData.message_hash!,
554
+ attestation: rawMessage.lbtcData.attestation!,
555
+ }
556
+
557
+ return {
558
+ offRamp,
559
+ ...lane,
560
+ message,
561
+ offchainTokenData,
562
+ ...proof,
563
+ } as ExecutionInput & Lane & { offRamp: string }
449
564
  }
450
565
 
451
566
  /**
package/src/api/types.ts CHANGED
@@ -182,3 +182,46 @@ export type APICCIPRequestMetadata = {
182
182
  /** Destination network metadata. */
183
183
  destNetworkInfo: NetworkInfo
184
184
  }
185
+
186
+ // ============================================================================
187
+ // GET /v2/messages/${messageId}/execution-inputs search endpoint types
188
+ // ============================================================================
189
+
190
+ /** Raw API response from GET /v2/messages/:messageId/execution-inputs */
191
+ export type RawExecutionInputsResult = {
192
+ offramp: string
193
+ } & (
194
+ | {
195
+ onramp: string
196
+ sourceChainSelector: bigint
197
+ destChainSelector: bigint
198
+ version: string
199
+ }
200
+ | object
201
+ ) &
202
+ (
203
+ | {
204
+ merkleRoot?: string
205
+ messageBatch: {
206
+ [key: string]: unknown
207
+ messageId: string
208
+ tokenAmounts: { token: string; amount: string }[]
209
+ usdcData?: {
210
+ status: 'pending_confirmations' | 'complete'
211
+ attestation?: string
212
+ message_bytes_hex?: string
213
+ }
214
+ lbtcData?: {
215
+ status: 'NOTARIZATION_STATUS_SESSION_APPROVED' | 'NOTARIZATION_STATUS_SESSION_PENDING'
216
+ attestation?: string
217
+ message_hash?: string
218
+ }
219
+ }[]
220
+ }
221
+ | {
222
+ encodedMessage: string
223
+ verificationComplete?: boolean
224
+ ccvData?: string[]
225
+ verifierAddresses: string[]
226
+ }
227
+ )
package/src/chain.ts CHANGED
@@ -6,8 +6,10 @@ import type { UnsignedAptosTx } from './aptos/types.ts'
6
6
  import { getOnchainCommitReport } from './commits.ts'
7
7
  import {
8
8
  CCIPApiClientNotAvailableError,
9
+ CCIPArgumentInvalidError,
9
10
  CCIPChainFamilyMismatchError,
10
11
  CCIPExecTxRevertedError,
12
+ CCIPNotImplementedError,
11
13
  CCIPTokenPoolChainConfigNotFoundError,
12
14
  CCIPTransactionNotFinalizedError,
13
15
  } from './errors/index.ts'
@@ -50,8 +52,12 @@ import {
50
52
  } from './types.ts'
51
53
  import { networkInfo, util, withRetry } from './utils.ts'
52
54
 
53
- /** Field names unique to GenericExtraArgsV3 (not present in V2). */
54
- const V3_ONLY_FIELDS = [
55
+ /** All valid field names for GenericExtraArgsV2. */
56
+ const V2_FIELDS = new Set(['gasLimit', 'allowOutOfOrderExecution'])
57
+
58
+ /** All valid field names for GenericExtraArgsV3. */
59
+ const V3_FIELDS = new Set([
60
+ 'gasLimit',
55
61
  'blockConfirmations',
56
62
  'ccvs',
57
63
  'ccvArgs',
@@ -59,12 +65,26 @@ const V3_ONLY_FIELDS = [
59
65
  'executorArgs',
60
66
  'tokenReceiver',
61
67
  'tokenArgs',
62
- ] as const
68
+ ])
69
+
70
+ /** Throw if any key in extraArgs is not in the allowed set. */
71
+ function assertNoUnknownFields(
72
+ extraArgs: Partial<ExtraArgs>,
73
+ allowed: Set<string>,
74
+ variant: string,
75
+ ): void {
76
+ const unknown = Object.keys(extraArgs).filter((k) => !allowed.has(k))
77
+ if (unknown.length)
78
+ throw new CCIPArgumentInvalidError(
79
+ 'extraArgs',
80
+ `unknown field(s) for ${variant}: ${unknown.map((k) => JSON.stringify(k)).join(', ')}`,
81
+ )
82
+ }
63
83
 
64
- /** Check if extraArgs contains any V3-only fields. */
84
+ /** Check if extraArgs contains any V3-only fields (i.e. fields in V3 but not in V2). */
65
85
  function hasV3ExtraArgs(extraArgs: Partial<ExtraArgs> | undefined): boolean {
66
86
  if (!extraArgs) return false
67
- return V3_ONLY_FIELDS.some((field) => field in extraArgs)
87
+ return Object.keys(extraArgs).some((k) => V3_FIELDS.has(k) && !V2_FIELDS.has(k))
68
88
  }
69
89
 
70
90
  /**
@@ -94,12 +114,13 @@ export type ChainContext = WithLogger & {
94
114
  * CCIP API client instance for lane information queries.
95
115
  *
96
116
  * - `undefined` (default): Creates CCIPAPIClient with {@link DEFAULT_API_BASE_URL}
117
+ * - `string`: Creates CCIPAPIClient with provided URL
97
118
  * - `CCIPAPIClient`: Uses provided instance (allows custom URL, fetch, etc.)
98
119
  * - `null`: Disables API client entirely (getLaneLatency() will throw)
99
120
  *
100
121
  * Default: `undefined` (auto-create with production endpoint)
101
122
  */
102
- apiClient?: CCIPAPIClient | null
123
+ apiClient?: CCIPAPIClient | string | null
103
124
 
104
125
  /**
105
126
  * Retry configuration for API fallback operations.
@@ -173,6 +194,52 @@ export type TokenInfo = {
173
194
  readonly name?: string
174
195
  }
175
196
 
197
+ /**
198
+ * Available lane feature keys.
199
+ * These represent features or thresholds that can be configured per-lane.
200
+ */
201
+ export const LaneFeature = {
202
+ /**
203
+ * Minimum block confirmations for Faster Time to Finality (FTF).
204
+ * - **absent**: the lane does not support FTF (pre-v2.0 lane).
205
+ * - **0**: the lane supports FTF, but it is not enabled for this
206
+ * token (e.g. the token pool predates FTF, or FTF is configured
207
+ * to use default finality only).
208
+ * - **\> 0**: FTF is enabled; this is the minimum number of block
209
+ * confirmations required to use it.
210
+ */
211
+ MIN_BLOCK_CONFIRMATIONS: 'MIN_BLOCK_CONFIRMATIONS',
212
+ /**
213
+ * Rate limiter bucket state for the lane/token with default finality.
214
+ */
215
+ RATE_LIMITS: 'RATE_LIMITS',
216
+ /**
217
+ * Rate limiter bucket state when using non-default finality (FTF).
218
+ * Only meaningful when FTF is supported on this lane, i.e.
219
+ * {@link LaneFeature.MIN_BLOCK_CONFIRMATIONS} is present and \> 0.
220
+ * If absent, the default rate limits ({@link LaneFeature.RATE_LIMITS}) apply even when using custom finality.
221
+ */
222
+ CUSTOM_BLOCK_CONFIRMATIONS_RATE_LIMITS: 'CUSTOM_BLOCK_CONFIRMATIONS_RATE_LIMITS',
223
+ } as const
224
+ /** Type representing one of the lane feature keys. */
225
+ export type LaneFeature = (typeof LaneFeature)[keyof typeof LaneFeature]
226
+
227
+ /**
228
+ * Lane features record.
229
+ * Maps feature keys to their values.
230
+ */
231
+ export interface LaneFeatures extends Record<LaneFeature, unknown> {
232
+ /** Minimum block confirmations for FTF. */
233
+ MIN_BLOCK_CONFIRMATIONS: number
234
+ /** Rate limiter bucket state for the lane/token with default finality. */
235
+ RATE_LIMITS: RateLimiterState
236
+ /**
237
+ * Rate limiter bucket state when using non-default finality (FTF).
238
+ * If absent, the default rate limits ({@link LaneFeatures.RATE_LIMITS}) apply even when using custom finality.
239
+ */
240
+ CUSTOM_BLOCK_CONFIRMATIONS_RATE_LIMITS: RateLimiterState
241
+ }
242
+
176
243
  /**
177
244
  * Options for getBalance query.
178
245
  */
@@ -231,11 +298,19 @@ export type TokenPoolRemote = {
231
298
  * - Version management (different pool implementations)
232
299
  */
233
300
  remotePools: string[]
234
- /** Inbound rate limiter state for tokens coming into this chain. */
235
- inboundRateLimiterState: RateLimiterState
236
301
  /** Outbound rate limiter state for tokens leaving this chain. */
237
302
  outboundRateLimiterState: RateLimiterState
238
- }
303
+ /** Inbound rate limiter state for tokens coming into this chain. */
304
+ inboundRateLimiterState: RateLimiterState
305
+ } & (
306
+ | {
307
+ /** Outbound rate limiter state for tokens leaving this chain (FTF/v2). */
308
+ customBlockConfirmationsOutboundRateLimiterState: RateLimiterState
309
+ /** Inbound rate limiter state for tokens coming into this chain (FTF/v2). */
310
+ customBlockConfirmationsInboundRateLimiterState: RateLimiterState
311
+ }
312
+ | object
313
+ )
239
314
 
240
315
  /**
241
316
  * Token pool configuration returned by {@link Chain.getTokenPoolConfig}.
@@ -256,6 +331,13 @@ export type TokenPoolConfig = {
256
331
  * May be undefined for older pool implementations that don't expose this method.
257
332
  */
258
333
  typeAndVersion?: string
334
+ /**
335
+ * Min custom block confirmations for Faster Time to Finality (FTF),
336
+ * if TokenPool version \>= v2.0.0 and FTF is supported on this lane.
337
+ * `0` indicates FTF is supported but not enabled for this token; `>0` indicates FTF is enabled
338
+ * with this many minimum confirmations.
339
+ */
340
+ minBlockConfirmations?: number
259
341
  }
260
342
 
261
343
  /**
@@ -362,11 +444,11 @@ export abstract class Chain<F extends ChainFamily = ChainFamily> {
362
444
  if (apiClient === null) {
363
445
  this.apiClient = null // Explicit opt-out
364
446
  this.apiRetryConfig = null // No retry config needed without API client
365
- } else if (apiClient !== undefined) {
447
+ } else if (apiClient && typeof apiClient !== 'string') {
366
448
  this.apiClient = apiClient // Use provided instance
367
449
  this.apiRetryConfig = { ...DEFAULT_API_RETRY_CONFIG, ...apiRetryConfig }
368
450
  } else {
369
- this.apiClient = CCIPAPIClient.fromUrl(undefined, { logger }) // Default
451
+ this.apiClient = CCIPAPIClient.fromUrl(apiClient, ctx) // default=undefined or provided string as URL
370
452
  this.apiRetryConfig = { ...DEFAULT_API_RETRY_CONFIG, ...apiRetryConfig }
371
453
  }
372
454
  }
@@ -1050,6 +1132,41 @@ export abstract class Chain<F extends ChainFamily = ChainFamily> {
1050
1132
  return this.apiClient.getLaneLatency(this.network.chainSelector, destChainSelector)
1051
1133
  }
1052
1134
 
1135
+ /**
1136
+ * Retrieve features for a lane (router/destChainSelector/token triplet).
1137
+ *
1138
+ * @param _opts - Options containing router address, destChainSelector, and optional token
1139
+ * address (the token to be transferred in a hypothetical message on this lane)
1140
+ * @returns Promise resolving to partial lane features record
1141
+ *
1142
+ * @throws {@link CCIPNotImplementedError} if not implemented for this chain family
1143
+ *
1144
+ * @example Get lane features
1145
+ * ```typescript
1146
+ * const features = await chain.getLaneFeatures({
1147
+ * router: '0x...',
1148
+ * destChainSelector: 4949039107694359620n,
1149
+ * })
1150
+ * // MIN_BLOCK_CONFIRMATIONS has three states:
1151
+ * // - undefined: FTF is not supported on this lane (pre-v2.0)
1152
+ * // - 0: the lane supports FTF, but it is not enabled for this token
1153
+ * // - > 0: FTF is enabled with this many block confirmations
1154
+ * const ftf = features.MIN_BLOCK_CONFIRMATIONS
1155
+ * if (ftf != null && ftf > 0) {
1156
+ * console.log(`FTF enabled with ${ftf} confirmations`)
1157
+ * } else if (ftf === 0) {
1158
+ * console.log('FTF supported on this lane but not enabled for this token')
1159
+ * }
1160
+ * ```
1161
+ */
1162
+ getLaneFeatures(_opts: {
1163
+ router: string
1164
+ destChainSelector: bigint
1165
+ token?: string
1166
+ }): Promise<Partial<LaneFeatures>> {
1167
+ return Promise.reject(new CCIPNotImplementedError('getLaneFeatures'))
1168
+ }
1169
+
1053
1170
  /**
1054
1171
  * Default/generic implementation of getExecutionReceipts.
1055
1172
  * Yields execution receipts for a given offRamp.
@@ -1311,6 +1428,8 @@ export abstract class Chain<F extends ChainFamily = ChainFamily> {
1311
1428
 
1312
1429
  // Detect if user wants V3 by checking for any V3-only field
1313
1430
  if (hasV3ExtraArgs(message.extraArgs)) {
1431
+ if (message.extraArgs)
1432
+ assertNoUnknownFields(message.extraArgs, V3_FIELDS, 'GenericExtraArgsV3')
1314
1433
  // V3 defaults (GenericExtraArgsV3)
1315
1434
  return {
1316
1435
  ...message,
@@ -1328,6 +1447,7 @@ export abstract class Chain<F extends ChainFamily = ChainFamily> {
1328
1447
  }
1329
1448
  }
1330
1449
 
1450
+ if (message.extraArgs) assertNoUnknownFields(message.extraArgs, V2_FIELDS, 'EVMExtraArgsV2')
1331
1451
  // Default to V2 (GenericExtraArgsV2, aka EVMExtraArgsV2)
1332
1452
  return {
1333
1453
  ...message,
@@ -27,6 +27,7 @@ export const CCIPErrorCode = {
27
27
  MESSAGE_NOT_IN_BATCH: 'MESSAGE_NOT_IN_BATCH',
28
28
  MESSAGE_CHAIN_MISMATCH: 'MESSAGE_CHAIN_MISMATCH',
29
29
  MESSAGE_RETRIEVAL_FAILED: 'MESSAGE_RETRIEVAL_FAILED',
30
+ MESSAGE_NOT_VERIFIED_YET: 'MESSAGE_NOT_VERIFIED_YET',
30
31
 
31
32
  // Lane & Routing
32
33
  OFFRAMP_NOT_FOUND: 'OFFRAMP_NOT_FOUND',
@@ -109,7 +110,6 @@ export const CCIPErrorCode = {
109
110
  LBTC_ATTESTATION_NOT_FOUND: 'LBTC_ATTESTATION_NOT_FOUND',
110
111
  LBTC_ATTESTATION_NOT_APPROVED: 'LBTC_ATTESTATION_NOT_APPROVED',
111
112
  CCTP_DECODE_FAILED: 'CCTP_DECODE_FAILED',
112
- CCTP_MULTIPLE_EVENTS: 'CCTP_MULTIPLE_EVENTS',
113
113
 
114
114
  // Log & Event
115
115
  LOG_DATA_INVALID: 'LOG_DATA_INVALID',
@@ -178,6 +178,7 @@ export const TRANSIENT_ERROR_CODES = new Set<CCIPErrorCode>([
178
178
  CCIPErrorCode.TRANSACTION_NOT_FINALIZED,
179
179
  CCIPErrorCode.MESSAGE_ID_NOT_FOUND,
180
180
  CCIPErrorCode.MESSAGE_BATCH_INCOMPLETE,
181
+ CCIPErrorCode.MESSAGE_NOT_VERIFIED_YET,
181
182
  CCIPErrorCode.COMMIT_NOT_FOUND,
182
183
  CCIPErrorCode.RECEIPT_NOT_FOUND,
183
184
  CCIPErrorCode.USDC_ATTESTATION_FAILED,
@@ -31,6 +31,7 @@ export {
31
31
  CCIPMessageInvalidError,
32
32
  CCIPMessageNotFoundInTxError,
33
33
  CCIPMessageNotInBatchError,
34
+ CCIPMessageNotVerifiedYetError,
34
35
  CCIPMessageRetrievalError,
35
36
  } from './specialized.ts'
36
37
 
@@ -100,7 +101,6 @@ export {
100
101
  export {
101
102
  CCIPBlockTimeNotFoundError,
102
103
  CCIPCctpDecodeError,
103
- CCIPCctpMultipleEventsError,
104
104
  CCIPExecutionReportChainMismatchError,
105
105
  CCIPExecutionStateInvalidError,
106
106
  CCIPExtraArgsLengthInvalidError,
@@ -37,6 +37,7 @@ export const DEFAULT_RECOVERY_HINTS: Partial<Record<CCIPErrorCode, string>> = {
37
37
  'Verify you are using the correct destination chain. Check that sourceChainSelector and destChainSelector match your lane.',
38
38
  MESSAGE_RETRIEVAL_FAILED:
39
39
  'Both API and RPC failed to retrieve the message. Verify the transaction hash is correct and the transaction is confirmed. Check RPC and network connectivity.',
40
+ MESSAGE_NOT_VERIFIED_YET: 'Message not yet committed or verified; wait and retry.',
40
41
  MESSAGE_VERSION_INVALID:
41
42
  'Ensure the source chain onRamp uses CCIP v1.6. Older message versions are not compatible with this destination.',
42
43
 
@@ -129,7 +130,6 @@ export const DEFAULT_RECOVERY_HINTS: Partial<Record<CCIPErrorCode, string>> = {
129
130
  LBTC_ATTESTATION_NOT_APPROVED: 'LBTC attestation not yet approved. Wait for notarization.',
130
131
  CCTP_DECODE_FAILED:
131
132
  'Ensure the transaction contains a valid CCTP MessageSent event. Verify this is a USDC transfer.',
132
- CCTP_MULTIPLE_EVENTS: 'Multiple CCTP events found. Expected only one per transaction.',
133
133
 
134
134
  LOG_DATA_INVALID: 'Ensure the log data is a valid hex string from a transaction receipt.',
135
135
  LOG_DATA_MISSING: 'Log data is missing or not a string.',
@@ -380,6 +380,41 @@ export class CCIPMessageRetrievalError extends CCIPError {
380
380
  }
381
381
  }
382
382
 
383
+ /**
384
+ * Thrown when a CCIP message has not been verified yet by the offchain system.
385
+ * This is a transient error - the message needs time to be verified before execution input can be retrieved.
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * try {
390
+ * const execInput = await api.getExecutionInput(messageId)
391
+ * } catch (error) {
392
+ * if (error instanceof CCIPMessageNotVerifiedYetError) {
393
+ * console.log(`Message not verified yet, retry after ${error.retryAfterMs}ms`)
394
+ * await sleep(error.retryAfterMs ?? 15000)
395
+ * // Retry the request
396
+ * }
397
+ * }
398
+ * ```
399
+ */
400
+ export class CCIPMessageNotVerifiedYetError extends CCIPError {
401
+ override readonly name = 'CCIPMessageNotVerifiedYetError'
402
+ /** Creates a message not verified yet error. */
403
+ constructor(messageId: string, options?: CCIPErrorOptions) {
404
+ super(
405
+ CCIPErrorCode.MESSAGE_NOT_VERIFIED_YET,
406
+ `Message ${messageId} has not been verified yet. The offchain verification system needs time to process this message.`,
407
+ {
408
+ ...options,
409
+ isTransient: true,
410
+ retryAfterMs: 15000,
411
+ recovery: 'Wait for the message to be verified by the offchain system, then retry.',
412
+ context: { ...options?.context, messageId },
413
+ },
414
+ )
415
+ }
416
+ }
417
+
383
418
  // Lane & Routing
384
419
 
385
420
  /**
@@ -3238,36 +3273,6 @@ export class CCIPSolanaLaneVersionUnsupportedError extends CCIPError {
3238
3273
  }
3239
3274
  }
3240
3275
 
3241
- /**
3242
- * Thrown when multiple CCTP events found in transaction.
3243
- *
3244
- * @example
3245
- * ```typescript
3246
- * try {
3247
- * const cctpData = await chain.getOffchainTokenData(request)
3248
- * } catch (error) {
3249
- * if (error instanceof CCIPCctpMultipleEventsError) {
3250
- * console.log(`Found ${error.context.count} events, expected 1`)
3251
- * }
3252
- * }
3253
- * ```
3254
- */
3255
- export class CCIPCctpMultipleEventsError extends CCIPError {
3256
- override readonly name = 'CCIPCctpMultipleEventsError'
3257
- /** Creates a CCTP multiple events error. */
3258
- constructor(count: number, txSignature: string, options?: CCIPErrorOptions) {
3259
- super(
3260
- CCIPErrorCode.CCTP_MULTIPLE_EVENTS,
3261
- `Expected only 1 CcipCctpMessageSentEvent, found ${count} in transaction ${txSignature}`,
3262
- {
3263
- ...options,
3264
- isTransient: false,
3265
- context: { ...options?.context, count, txSignature },
3266
- },
3267
- )
3268
- }
3269
- }
3270
-
3271
3276
  /**
3272
3277
  * Thrown when compute units exceed limit.
3273
3278
  *
@@ -576,14 +576,6 @@ export default [
576
576
  name: 'DuplicateCCVNotAllowed',
577
577
  inputs: [{ name: 'ccvAddress', type: 'address', internalType: 'address' }],
578
578
  },
579
- {
580
- type: 'error',
581
- name: 'ExecutionError',
582
- inputs: [
583
- { name: 'messageId', type: 'bytes32', internalType: 'bytes32' },
584
- { name: 'err', type: 'bytes', internalType: 'bytes' },
585
- ],
586
- },
587
579
  { type: 'error', name: 'GasCannotBeZero', inputs: [] },
588
580
  {
589
581
  type: 'error',
@@ -664,6 +656,14 @@ export default [
664
656
  name: 'InvalidOnRamp',
665
657
  inputs: [{ name: 'got', type: 'bytes', internalType: 'bytes' }],
666
658
  },
659
+ {
660
+ type: 'error',
661
+ name: 'InvalidOptionalThreshold',
662
+ inputs: [
663
+ { name: 'wanted', type: 'uint8', internalType: 'uint8' },
664
+ { name: 'got', type: 'uint256', internalType: 'uint256' },
665
+ ],
666
+ },
667
667
  {
668
668
  type: 'error',
669
669
  name: 'InvalidVerifierResultsLength',
@@ -678,6 +678,14 @@ export default [
678
678
  name: 'MustSpecifyDefaultOrRequiredCCVs',
679
679
  inputs: [],
680
680
  },
681
+ {
682
+ type: 'error',
683
+ name: 'NoStateProgressMade',
684
+ inputs: [
685
+ { name: 'messageId', type: 'bytes32', internalType: 'bytes32' },
686
+ { name: 'err', type: 'bytes', internalType: 'bytes' },
687
+ ],
688
+ },
681
689
  {
682
690
  type: 'error',
683
691
  name: 'NotACompatiblePool',