@circle-fin/app-kit 1.12.0 → 1.12.1

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.
package/index.mjs CHANGED
@@ -10183,6 +10183,7 @@ function parseOrThrow(value, schema, context) {
10183
10183
  [Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
10184
10184
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
10185
10185
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
10186
+ [Blockchain.Celo_Alfajores_Testnet]: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B',
10186
10187
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
10187
10188
  [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
10188
10189
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
@@ -12014,6 +12015,44 @@ function assertBridgeParams(params, schema) {
12014
12015
  }
12015
12016
  }
12016
12017
 
12018
+ /**
12019
+ * Canonical list of actions that do not prepare or submit transactions.
12020
+ *
12021
+ * @internal
12022
+ */ const READ_ACTION_KEYS = [
12023
+ 'token.allowance',
12024
+ 'token.balanceOf',
12025
+ 'token.name',
12026
+ 'native.balanceOf',
12027
+ 'usdc.allowance',
12028
+ 'usdc.balanceOf',
12029
+ 'usdc.name',
12030
+ 'gateway.v1.isDelegate',
12031
+ 'gateway.v1.withdrawingBalance',
12032
+ 'gateway.v1.withdrawalBlock',
12033
+ 'gateway.v1.signBurnIntents'
12034
+ ];
12035
+ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
12036
+ /**
12037
+ * Check whether a runtime value identifies a read action.
12038
+ *
12039
+ * @param action - The value to classify.
12040
+ * @returns Whether the value is a registered read-action key.
12041
+ *
12042
+ * @example
12043
+ * ```typescript
12044
+ * import { isReadActionKey } from '@core/adapter'
12045
+ *
12046
+ * if (isReadActionKey(value)) {
12047
+ * await adapter.readAction(value, params, context)
12048
+ * }
12049
+ * ```
12050
+ *
12051
+ * @internal
12052
+ */ function isReadActionKey(action) {
12053
+ return typeof action === 'string' && READ_ACTION_KEY_SET.has(action);
12054
+ }
12055
+
12017
12056
  /**
12018
12057
  * Resolves an operation context into concrete chain and address values.
12019
12058
  *
@@ -12093,6 +12132,141 @@ function assertBridgeParams(params, schema) {
12093
12132
  };
12094
12133
  }
12095
12134
 
12135
+ /**
12136
+ * Create the standard error for a missing or non-read action.
12137
+ *
12138
+ * @param action - The unsupported action value.
12139
+ * @returns A fatal unsupported-action error.
12140
+ *
12141
+ * @internal
12142
+ */ function createUnsupportedReadActionError(action) {
12143
+ return new KitError({
12144
+ ...InputError.UNSUPPORTED_ACTION,
12145
+ recoverability: 'FATAL',
12146
+ message: `Read action "${String(action)}" is not registered in this adapter.`
12147
+ });
12148
+ }
12149
+ /**
12150
+ * Execute a read through the adapter's dedicated read seam when available.
12151
+ *
12152
+ * @remarks
12153
+ * Fall back to the legacy `prepareAction().execute()` contract so providers
12154
+ * remain runtime-compatible with adapter versions released before `readAction`.
12155
+ * Consumers must upgrade their adapter package for reads to bypass custom
12156
+ * `prepareAction` wrappers.
12157
+ *
12158
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
12159
+ * @typeParam TActionKey - The read action key.
12160
+ * @param adapter - The adapter that owns the read action.
12161
+ * @param action - The read action to execute.
12162
+ * @param params - The parameters for the read action.
12163
+ * @param ctx - The operation context.
12164
+ * @returns The raw read-action result.
12165
+ * @throws {KitError} When `action` is not a supported read-action key.
12166
+ *
12167
+ * @example
12168
+ * ```typescript
12169
+ * import { executeAdapterReadAction } from '@core/adapter'
12170
+ * import { Ethereum } from '@core/chains'
12171
+ *
12172
+ * const allowance = await executeAdapterReadAction(
12173
+ * adapter,
12174
+ * 'token.allowance',
12175
+ * { tokenAddress, delegate },
12176
+ * { chain: Ethereum },
12177
+ * )
12178
+ * ```
12179
+ *
12180
+ * @internal
12181
+ */ async function executeAdapterReadAction(adapter, action, params, ctx) {
12182
+ if (!isReadActionKey(action)) {
12183
+ throw createUnsupportedReadActionError(action);
12184
+ }
12185
+ const runtimeAdapter = adapter;
12186
+ if (typeof runtimeAdapter.readAction === 'function') {
12187
+ return runtimeAdapter.readAction(action, params, ctx);
12188
+ }
12189
+ let request;
12190
+ try {
12191
+ request = await adapter.prepareAction(action, params, ctx);
12192
+ } catch (error) {
12193
+ if (error instanceof Error && error.message === `Action ${action} is not supported`) {
12194
+ throw createUnsupportedReadActionError(action);
12195
+ }
12196
+ throw error;
12197
+ }
12198
+ return request.execute();
12199
+ }
12200
+ /**
12201
+ * Read and parse a token allowance while supporting older adapter versions.
12202
+ *
12203
+ * @remarks
12204
+ * Prefer the adapter's dedicated read seam and fall back to the legacy
12205
+ * `prepareAction().execute()` contract when the runtime adapter predates it.
12206
+ *
12207
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
12208
+ * @param adapter - The adapter that owns the allowance action.
12209
+ * @param params - The token and delegate whose allowance is being read.
12210
+ * @param ctx - The operation context.
12211
+ * @returns The non-negative allowance in token base units.
12212
+ * @throws {KitError} When the action is unsupported or its response is malformed.
12213
+ *
12214
+ * @example
12215
+ * ```typescript
12216
+ * import { readTokenAllowance } from '@core/adapter'
12217
+ * import { Ethereum } from '@core/chains'
12218
+ *
12219
+ * const allowance = await readTokenAllowance(
12220
+ * adapter,
12221
+ * { tokenAddress, delegate },
12222
+ * { chain: Ethereum },
12223
+ * )
12224
+ * ```
12225
+ *
12226
+ * @internal
12227
+ */ async function readTokenAllowance(adapter, params, ctx) {
12228
+ return parseAllowanceResponse(await executeAdapterReadAction(adapter, 'token.allowance', params, ctx));
12229
+ }
12230
+ /**
12231
+ * Parse a raw token allowance response into base units.
12232
+ *
12233
+ * @param allowanceRaw - The adapter response, optionally wrapped as an Amount output.
12234
+ * @returns The non-negative allowance as a bigint, or zero for a missing value.
12235
+ * @throws {KitError} When the response cannot represent a non-negative bigint.
12236
+ *
12237
+ * @example
12238
+ * ```typescript
12239
+ * import { parseAllowanceResponse } from '@core/adapter'
12240
+ *
12241
+ * const allowance = parseAllowanceResponse({ amount: { raw: 1000000n } })
12242
+ * ```
12243
+ */ function parseAllowanceResponse(allowanceRaw) {
12244
+ let value = allowanceRaw;
12245
+ if (typeof value === 'object' && value !== null && 'amount' in value) {
12246
+ const amount = value.amount;
12247
+ if (typeof amount === 'object' && amount !== null && 'raw' in amount) {
12248
+ value = amount.raw;
12249
+ }
12250
+ }
12251
+ if (value === undefined || value === null) {
12252
+ return 0n;
12253
+ }
12254
+ let allowance;
12255
+ if (typeof value === 'bigint') {
12256
+ allowance = value;
12257
+ } else if (typeof value === 'string') {
12258
+ try {
12259
+ allowance = BigInt(value);
12260
+ } catch {
12261
+ allowance = undefined;
12262
+ }
12263
+ }
12264
+ if (allowance === undefined || allowance < 0n) {
12265
+ throw createValidationFailedError$1('token.allowance', value, 'token.allowance response must be a non-negative bigint-compatible string or bigint');
12266
+ }
12267
+ return allowance;
12268
+ }
12269
+
12096
12270
  /**
12097
12271
  * Schema for validating hexadecimal strings with '0x' prefix.
12098
12272
  *
@@ -12302,16 +12476,15 @@ function assertBridgeParams(params, schema) {
12302
12476
  * ```
12303
12477
  */ const validateBalanceForTransaction = async (params)=>{
12304
12478
  const { amount, adapter, token, tokenAddress, operationContext } = params;
12305
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
12479
+ const balance = await executeAdapterReadAction(adapter, 'usdc.balanceOf', {
12306
12480
  walletAddress: operationContext.address
12307
12481
  }, operationContext);
12308
- const balance = await balancePrepared.execute();
12309
- if (BigInt(balance) < BigInt(amount)) {
12482
+ if (BigInt(String(balance)) < BigInt(amount)) {
12310
12483
  // Extract chain name from operationContext
12311
12484
  const chainName = extractChainInfo(operationContext.chain).name;
12312
12485
  // Create KitError with rich context in trace
12313
12486
  throw createInsufficientTokenBalanceError(chainName, token, {
12314
- balance: balance.toString(),
12487
+ balance: String(balance),
12315
12488
  amount,
12316
12489
  tokenAddress,
12317
12490
  walletAddress: operationContext.address
@@ -14743,6 +14916,25 @@ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 500_000n // buffered worst 474_078 (Sei 3
14743
14916
  ;
14744
14917
  const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears Cronos' calldata floor ~10x
14745
14918
  ;
14919
+ /**
14920
+ * The gas floor for each bridge step, keyed by step name.
14921
+ *
14922
+ * Two places need these and they must agree: each step module passes its floor
14923
+ * to `executePreparedChainRequest` for submission, and
14924
+ * `CCTPV2BridgingProvider.estimate()` quotes the resulting limit so a caller
14925
+ * can fund a wallet. A transaction is only admitted when the sender holds
14926
+ * `gasLimit * maxFeePerGas`, so a quote taken from anything other than the
14927
+ * submitted limit under-reports what the wallet actually needs — historically
14928
+ * the quote sat ~2.5x below the reserved limit.
14929
+ *
14930
+ * Both sides read this map so the two cannot drift apart. Change a floor here
14931
+ * and the quote moves with it; point one side at a different value and the
14932
+ * divergence is visible in review rather than silent at runtime.
14933
+ */ const BRIDGE_STEP_GAS_FLOORS_EVM = {
14934
+ approve: APPROVE_GAS_LIMIT_EVM,
14935
+ burn: DEPOSIT_FOR_BURN_GAS_LIMIT_EVM,
14936
+ mint: RECEIVE_MESSAGE_GAS_LIMIT_EVM
14937
+ };
14746
14938
  /**
14747
14939
  * The minimum finality threshold for CCTPv2 transfers.
14748
14940
  *
@@ -14761,1595 +14953,1595 @@ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears C
14761
14953
  };
14762
14954
 
14763
14955
  /**
14764
- * Default configuration values for the attestation fetcher.
14765
- * @internal
14766
- */ const DEFAULT_CONFIG$2 = {
14767
- timeout: 2_000,
14768
- maxRetries: 30 * 20,
14769
- retryDelay: 2_000,
14770
- headers: {
14771
- 'Content-Type': 'application/json'
14772
- }
14773
- };
14774
- /**
14775
- * Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
14776
- *
14777
- * Headers are merged independently so caller-supplied headers augment the
14778
- * defaults (such as `Content-Type`) rather than replacing them wholesale.
14779
- *
14780
- * @param config - Caller-provided polling configuration overrides
14781
- * @param internalDefaults - Internal defaults applied before `config` (for example a
14782
- * reduced `maxRetries` for one-shot requests); `config` still wins on conflict
14783
- * @returns The effective polling configuration
14784
- * @internal
14785
- */ const mergeAttestationConfig = (config, internalDefaults = {})=>({
14786
- ...DEFAULT_CONFIG$2,
14787
- ...internalDefaults,
14788
- ...config,
14789
- headers: {
14790
- ...DEFAULT_CONFIG$2.headers,
14791
- ...internalDefaults.headers,
14792
- ...config.headers
14793
- }
14794
- });
14795
- /**
14796
- * Type guard that verifies if an unknown value matches the AttestationMessage shape
14797
- * and has all required properties.
14798
- *
14799
- * @param obj - The value to check, typically an element from the messages array
14800
- * @returns True if the object matches the AttestationMessage shape, false otherwise
14801
- * @internal
14802
- */ const isValidAttestationMessage = (obj)=>{
14803
- return typeof obj === 'object' && obj !== null && 'message' in obj && 'eventNonce' in obj && 'attestation' in obj && 'decodedMessage' in obj && 'cctpVersion' in obj && 'status' in obj && typeof obj.status === 'string';
14804
- };
14805
- /**
14806
- * Type guard that verifies if an attestation message is complete.
14956
+ * CCTP bridge step names that can occur in the bridging flow.
14807
14957
  *
14808
- * @param message - The attestation message to check
14809
- * @returns True if the message status is 'complete', false otherwise
14810
- * @internal
14811
- */ const isCompleteAttestation = (message)=>{
14812
- return message.status === 'complete';
14958
+ * This object provides type safety for step names and represents all possible
14959
+ * steps that can be executed during a CCTP bridge operation. Using const assertions
14960
+ * makes this tree-shakable and follows modern TypeScript best practices.
14961
+ */ const CCTPv2StepName = {
14962
+ approve: 'approve',
14963
+ burn: 'burn',
14964
+ fetchAttestation: 'fetchAttestation',
14965
+ mint: 'mint',
14966
+ reAttest: 'reAttest'
14813
14967
  };
14814
14968
  /**
14815
- * Type guard that verifies if an unknown value has the correct structure
14816
- * for an AttestationResponse, regardless of attestation completion status.
14969
+ * Conditional step transition rules for CCTP bridge flow.
14817
14970
  *
14818
- * @param obj - The value to check, typically a parsed JSON response
14819
- * @returns True if the object matches the AttestationResponse shape
14820
- * @internal
14821
- */ const hasValidAttestationStructure = (obj)=>{
14822
- if (typeof obj !== 'object' || obj === null || !('messages' in obj) || !Array.isArray(obj.messages)) {
14823
- return false;
14824
- }
14825
- const messages = obj.messages;
14826
- // Validate all messages have the correct shape
14827
- return messages.every(isValidAttestationMessage);
14971
+ * Rules are evaluated in order - the first matching condition determines the next step.
14972
+ * This approach supports flexible flow logic and makes it easy to extend with new patterns.
14973
+ */ const STEP_TRANSITION_RULES = {
14974
+ // Starting state - no steps executed yet
14975
+ '': [
14976
+ {
14977
+ condition: ()=>true,
14978
+ nextStep: CCTPv2StepName.approve,
14979
+ reason: 'Start with approval step',
14980
+ isActionable: true
14981
+ }
14982
+ ],
14983
+ // After Approve step
14984
+ [CCTPv2StepName.approve]: [
14985
+ {
14986
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
14987
+ nextStep: CCTPv2StepName.burn,
14988
+ reason: 'Approval successful, proceed to burn',
14989
+ isActionable: true
14990
+ },
14991
+ {
14992
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
14993
+ nextStep: CCTPv2StepName.approve,
14994
+ reason: 'Retry failed approval',
14995
+ isActionable: true
14996
+ },
14997
+ {
14998
+ condition: (ctx)=>ctx.lastStep?.state === 'noop',
14999
+ nextStep: CCTPv2StepName.burn,
15000
+ reason: 'No approval needed, proceed to burn',
15001
+ isActionable: true
15002
+ },
15003
+ {
15004
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
15005
+ nextStep: CCTPv2StepName.approve,
15006
+ reason: 'Continue pending approval',
15007
+ isActionable: false
15008
+ }
15009
+ ],
15010
+ // After Burn step
15011
+ [CCTPv2StepName.burn]: [
15012
+ {
15013
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
15014
+ nextStep: CCTPv2StepName.fetchAttestation,
15015
+ reason: 'Burn successful, fetch attestation',
15016
+ isActionable: true
15017
+ },
15018
+ {
15019
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
15020
+ nextStep: CCTPv2StepName.burn,
15021
+ reason: 'Retry failed burn',
15022
+ isActionable: true
15023
+ },
15024
+ {
15025
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
15026
+ nextStep: CCTPv2StepName.burn,
15027
+ reason: 'Continue pending burn',
15028
+ isActionable: false
15029
+ }
15030
+ ],
15031
+ // After FetchAttestation step
15032
+ [CCTPv2StepName.fetchAttestation]: [
15033
+ {
15034
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
15035
+ nextStep: CCTPv2StepName.mint,
15036
+ reason: 'Attestation fetched, proceed to mint',
15037
+ isActionable: true
15038
+ },
15039
+ {
15040
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
15041
+ nextStep: CCTPv2StepName.fetchAttestation,
15042
+ reason: 'Retry fetching attestation',
15043
+ isActionable: true
15044
+ },
15045
+ {
15046
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
15047
+ nextStep: CCTPv2StepName.fetchAttestation,
15048
+ reason: 'Continue pending attestation fetch',
15049
+ isActionable: false
15050
+ }
15051
+ ],
15052
+ // After Mint step
15053
+ [CCTPv2StepName.mint]: [
15054
+ {
15055
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
15056
+ nextStep: null,
15057
+ reason: 'Bridge completed successfully',
15058
+ isActionable: false
15059
+ },
15060
+ {
15061
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
15062
+ nextStep: CCTPv2StepName.mint,
15063
+ reason: 'Retry failed mint',
15064
+ isActionable: true
15065
+ },
15066
+ {
15067
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
15068
+ nextStep: CCTPv2StepName.mint,
15069
+ reason: 'Continue pending mint',
15070
+ isActionable: false
15071
+ }
15072
+ ],
15073
+ // After ReAttest step
15074
+ [CCTPv2StepName.reAttest]: [
15075
+ {
15076
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
15077
+ nextStep: CCTPv2StepName.mint,
15078
+ reason: 'Re-attestation successful, proceed to mint',
15079
+ isActionable: true
15080
+ },
15081
+ {
15082
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
15083
+ nextStep: CCTPv2StepName.mint,
15084
+ reason: 'Re-attestation failed, retry mint to re-initiate recovery',
15085
+ isActionable: true
15086
+ },
15087
+ {
15088
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
15089
+ nextStep: CCTPv2StepName.mint,
15090
+ reason: 'Re-attestation pending, retry mint to re-initiate recovery',
15091
+ isActionable: true
15092
+ }
15093
+ ]
14828
15094
  };
14829
15095
  /**
14830
- * Type guard that verifies if an unknown value matches the AttestationResponse shape
14831
- * and contains a complete attestation.
15096
+ * Analyze bridge steps to determine retry feasibility and continuation point.
14832
15097
  *
14833
- * This function performs runtime validation to ensure that the provided value
14834
- * conforms to the expected structure of an AttestationResponse and has at least
14835
- * one complete attestation. It checks that:
14836
- * 1. The value has valid AttestationResponse structure
14837
- * 2. At least one message has status 'complete'
15098
+ * This function examines the current state of bridge steps to determine the optimal
15099
+ * continuation strategy. It uses a rule-based approach that makes it easy to extend
15100
+ * with new flow patterns and step types in the future.
14838
15101
  *
14839
- * @remarks
14840
- * This type guard is used internally by the attestation fetcher to validate
14841
- * responses from the IRIS API before processing them. It provides runtime
14842
- * type safety for data coming from the network and ensures we have a complete
14843
- * attestation before proceeding.
15102
+ * The current analysis supports the standard CCTP flow:
15103
+ * **Traditional flow**: Approve Burn FetchAttestation Mint
14844
15104
  *
14845
- * If the response has valid structure but no complete attestation yet,
14846
- * it throws a retryable error. If the response structure is invalid,
14847
- * it throws a non-retryable validation error.
15105
+ * Key features:
15106
+ * - Rule-based transitions: Easy to extend with new step types and logic
15107
+ * - Context-aware decisions: Considers execution history and step states
15108
+ * - Actionable logic: Distinguishes between steps requiring user action vs waiting
15109
+ * - Terminal states: Properly handles completion and non-actionable states
14848
15110
  *
14849
- * @param obj - The value to check, typically a parsed JSON response
14850
- * @returns True if the object matches the AttestationResponse shape and has a complete attestation
14851
- * @throws {Error} With "Invalid attestation response structure" if structure is invalid (non-retryable)
14852
- * @throws {Error} With "Attestation not ready" if no complete attestation yet (retryable)
15111
+ * @param bridgeResult - The bridge result containing step execution history.
15112
+ * @returns Analysis result with continuation step and actionability information.
15113
+ * @throws Error when bridgeResult is invalid or contains no steps array.
14853
15114
  *
14854
15115
  * @example
14855
15116
  * ```typescript
14856
- * const response = await fetch('https://iris-api.circle.com/...')
14857
- * const data = await response.json()
15117
+ * import { analyzeSteps } from './analyzeSteps'
14858
15118
  *
14859
- * if (isAttestationResponse(data)) {
14860
- * // TypeScript now knows data is AttestationResponse with at least one complete attestation
14861
- * const completeMessage = data.messages.find(msg => msg.status === 'complete')
14862
- * console.log('Found complete attestation:', completeMessage.attestation)
15119
+ * // Failed approval step (requires user action)
15120
+ * const bridgeResult = {
15121
+ * steps: [
15122
+ * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
15123
+ * ]
14863
15124
  * }
14864
- * ```
14865
- */ const isAttestationResponse = (obj)=>{
14866
- // First check if the structure is valid
14867
- if (!hasValidAttestationStructure(obj)) {
14868
- // If structure is invalid, this is a permanent failure - don't retry
14869
- throw new Error('Invalid attestation response structure');
14870
- }
14871
- // Then check if at least one message is complete
14872
- if (!obj.messages.some(isCompleteAttestation)) {
14873
- // If no complete message, this is a temporary state - allow retry
14874
- throw new Error('Attestation not ready');
14875
- }
14876
- return true;
14877
- };
14878
- /**
14879
- * Builds the IRIS API URL for fetching attestation data from Circle's CCTP service.
14880
- *
14881
- * Constructs a properly formatted URL for the IRIS API v2 endpoint that provides
14882
- * attestation messages for cross-chain transfers. The URL includes both the source
14883
- * domain identifier and the transaction hash as query parameters. The base URL
14884
- * is selected based on whether the operation is for testnet or mainnet.
14885
15125
  *
14886
- * @param sourceDomainId - The CCTP domain ID of the source chain (numeric or string)
14887
- * @param transactionHash - The transaction hash of the burn operation to fetch attestation for
14888
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
14889
- * @returns A fully qualified URL string for the IRIS API endpoint
15126
+ * const analysis = analyzeSteps(bridgeResult)
15127
+ * // Result: { continuationStep: 'Approve', isRetryable: true,
15128
+ * // reason: 'Retry failed approval' }
15129
+ * ```
14890
15130
  *
14891
15131
  * @example
14892
15132
  * ```typescript
14893
- * // Mainnet URL
14894
- * const mainnetUrl = buildIrisUrl(1, '0xabc...', false)
14895
- * // => 'https://iris-api.circle.com/v2/messages/1?transactionHash=0xabc...'
15133
+ * // Pending transaction (requires waiting, not actionable)
15134
+ * const bridgeResult = {
15135
+ * steps: [
15136
+ * { name: 'Approve', state: 'pending' }
15137
+ * ]
15138
+ * }
14896
15139
  *
14897
- * // Testnet URL
14898
- * const testnetUrl = buildIrisUrl(1, '0xdef...', true)
14899
- * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
15140
+ * const analysis = analyzeSteps(bridgeResult)
15141
+ * // Result: { continuationStep: 'Approve', isRetryable: false,
15142
+ * // reason: 'Continue pending approval' }
14900
15143
  * ```
14901
- */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
14902
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
14903
- const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
14904
- url.searchParams.set('transactionHash', transactionHash);
14905
- return url.toString();
14906
- };
14907
- /**
14908
- * Fetches attestation data from the IRIS API with retry and timeout handling.
14909
- *
14910
- * Polls the IRIS API until a complete attestation is available. The default
14911
- * window is sized for slow source chains where finality may take many
14912
- * confirmations.
14913
- *
14914
- * Defaults (see `DEFAULT_CONFIG`):
14915
- * - Per-attempt timeout: 2 000 ms (each HTTP request aborts after 2 s)
14916
- * - Retry delay: 2 000 ms between attempts
14917
- * - Max retries: 600 (30 × 20)
14918
- * - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
14919
- *
14920
- * @param sourceDomainId - The CCTP domain ID.
14921
- * @param transactionHash - The transaction hash to fetch attestation for.
14922
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
14923
- * @param config - Optional configuration overrides for the attestation fetcher
14924
- * @returns The attestation response data.
14925
- * @throws If the request fails, times out, or returns invalid data.
14926
15144
  *
14927
15145
  * @example
14928
15146
  * ```typescript
14929
- * // Fetch attestation for mainnet transaction
14930
- * const response = await fetchAttestation(1, '0xabc...', false)
14931
- * console.log(`Found ${response.messages.length} attestation messages`)
15147
+ * // Completed bridge (nothing to do)
15148
+ * const bridgeResult = {
15149
+ * steps: [
15150
+ * { name: 'Approve', state: 'success' },
15151
+ * { name: 'Burn', state: 'success' },
15152
+ * { name: 'FetchAttestation', state: 'success' },
15153
+ * { name: 'Mint', state: 'success' }
15154
+ * ]
15155
+ * }
14932
15156
  *
14933
- * // Fetch with custom timeout
14934
- * const response2 = await fetchAttestation(1, '0xdef...', true, {
14935
- * timeout: 5000,
14936
- * maxRetries: 5
14937
- * })
15157
+ * const analysis = analyzeSteps(bridgeResult)
15158
+ * // Result: { continuationStep: null, isRetryable: false,
15159
+ * // reason: 'Bridge completed successfully' }
14938
15160
  * ```
14939
- */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
14940
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
14941
- const effectiveConfig = mergeAttestationConfig(config);
14942
- return await pollApiGet(url, isAttestationResponse, effectiveConfig);
14943
- };
14944
- /**
14945
- * Type guard that validates attestation response structure without requiring completion status.
14946
- *
14947
- * This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
14948
- * attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
14949
- * this function does not throw if no complete attestation is found.
14950
- *
14951
- * @param obj - The value to check, typically a parsed JSON response
14952
- * @returns True if the object has valid attestation structure
14953
- * @throws {Error} With "Invalid attestation response structure" if structure is invalid
14954
- * @internal
14955
- */ const isAttestationResponseWithoutStatusCheck = (obj)=>{
14956
- if (!hasValidAttestationStructure(obj)) {
14957
- throw new Error('Invalid attestation response structure');
15161
+ */ const analyzeSteps = (bridgeResult)=>{
15162
+ // Input validation
15163
+ if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
15164
+ throw new Error('Invalid bridgeResult: must contain a steps array');
14958
15165
  }
14959
- return true;
15166
+ const { steps } = bridgeResult;
15167
+ // Build execution context from step history
15168
+ const context = buildFlowContext(steps);
15169
+ // Determine continuation logic using rule engine
15170
+ const continuation = determineContinuationFromRules(context);
15171
+ return {
15172
+ continuationStep: continuation.nextStep,
15173
+ isActionable: continuation.isActionable,
15174
+ completedSteps: Array.from(context.completedSteps),
15175
+ failedSteps: Array.from(context.failedSteps),
15176
+ reason: continuation.reason
15177
+ };
14960
15178
  };
14961
15179
  /**
14962
- * Fetches attestation data without requiring the attestation to be complete.
14963
- *
14964
- * This function is useful for retrieving attestation data (particularly the nonce)
14965
- * from an existing transaction, even if the attestation has expired or is pending.
14966
- * It uses minimal retries since we're fetching existing data, not waiting for completion.
14967
- *
14968
- * @param sourceDomainId - The CCTP domain ID of the source chain
14969
- * @param transactionHash - The transaction hash to fetch attestation for
14970
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
14971
- * @param config - Optional configuration overrides
14972
- * @returns The attestation response data (may contain incomplete/expired attestations)
14973
- * @throws If the request fails, times out, or returns invalid data
15180
+ * Build flow context from the execution history.
14974
15181
  *
14975
- * @example
14976
- * ```typescript
14977
- * // Fetch existing attestation to extract nonce for re-attestation
14978
- * const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
14979
- * const nonce = response.messages[0]?.eventNonce
14980
- * ```
14981
- */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
14982
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
14983
- // Use minimal retries since we're just fetching existing data
14984
- const effectiveConfig = mergeAttestationConfig(config, {
14985
- maxRetries: 3
14986
- });
14987
- return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
14988
- };
15182
+ * @param steps - Array of executed bridge steps.
15183
+ * @returns Flow context with execution state and history.
15184
+ */ function buildFlowContext(steps) {
15185
+ const completedSteps = new Set();
15186
+ const failedSteps = new Set();
15187
+ let lastStep;
15188
+ // Process step history to build context
15189
+ for (const step of steps){
15190
+ if (step.state === 'success' || step.state === 'noop') {
15191
+ completedSteps.add(step.name);
15192
+ } else if (step.state === 'error') {
15193
+ failedSteps.add(step.name);
15194
+ }
15195
+ // Track the last step for continuation logic
15196
+ lastStep = {
15197
+ name: step.name,
15198
+ state: step.state
15199
+ };
15200
+ }
15201
+ return {
15202
+ completedSteps,
15203
+ failedSteps,
15204
+ ...lastStep && {
15205
+ lastStep
15206
+ }
15207
+ };
15208
+ }
14989
15209
  /**
14990
- * Type guard that validates attestation response has expirationBlock === '0'.
14991
- *
14992
- * This is used after requestReAttestation() to poll until the attestation
14993
- * is fully re-processed and has a zero expiration block (never expires).
14994
- * The expiration block transitions from non-zero to zero when Circle
14995
- * completes processing the re-attestation request.
15210
+ * Determine continuation step using the rule engine.
14996
15211
  *
14997
- * @param obj - The value to check, typically a parsed JSON response
14998
- * @returns True if the attestation has expirationBlock === '0'
14999
- * @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
15212
+ * @param context - The flow context with execution history.
15213
+ * @returns Continuation decision with next step and actionability information.
15214
+ */ function determineContinuationFromRules(context) {
15215
+ const lastStepName = context.lastStep?.name;
15216
+ // Handle initial state when no steps have been executed
15217
+ if (lastStepName === undefined) {
15218
+ const rules = STEP_TRANSITION_RULES[''];
15219
+ const matchingRule = rules?.find((rule)=>rule.condition(context));
15220
+ if (!matchingRule) {
15221
+ return {
15222
+ nextStep: null,
15223
+ isActionable: false,
15224
+ reason: 'No initial state rule found'
15225
+ };
15226
+ }
15227
+ return {
15228
+ nextStep: matchingRule.nextStep,
15229
+ isActionable: matchingRule.isActionable,
15230
+ reason: matchingRule.reason
15231
+ };
15232
+ }
15233
+ // A step with an empty name is ambiguous and should be treated as an unrecoverable state.
15234
+ if (lastStepName === '') {
15235
+ return {
15236
+ nextStep: null,
15237
+ isActionable: false,
15238
+ reason: 'No transition rules defined for step with empty name'
15239
+ };
15240
+ }
15241
+ const rules = STEP_TRANSITION_RULES[lastStepName];
15242
+ if (!rules) {
15243
+ return {
15244
+ nextStep: null,
15245
+ isActionable: false,
15246
+ reason: `No transition rules defined for step: ${lastStepName}`
15247
+ };
15248
+ }
15249
+ // Find the first matching rule
15250
+ const matchingRule = rules.find((rule)=>rule.condition(context));
15251
+ if (!matchingRule) {
15252
+ return {
15253
+ nextStep: null,
15254
+ isActionable: false,
15255
+ reason: `No matching transition rule for current context`
15256
+ };
15257
+ }
15258
+ return {
15259
+ nextStep: matchingRule.nextStep,
15260
+ isActionable: matchingRule.isActionable,
15261
+ reason: matchingRule.reason
15262
+ };
15263
+ }
15264
+
15265
+ /**
15266
+ * Find a step by name in the bridge result.
15267
+ *
15268
+ * @param result - The bridge result to search.
15269
+ * @param stepName - The name of the step to find.
15270
+ * @returns The step if found, undefined otherwise.
15000
15271
  *
15001
15272
  * @example
15002
15273
  * ```typescript
15003
- * // After requesting re-attestation, use this to validate the response
15004
- * const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
15005
- * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
15274
+ * import { findStepByName } from './findStep'
15275
+ *
15276
+ * const burnStep = findStepByName(result, 'burn')
15277
+ * if (burnStep) {
15278
+ * console.log('Burn tx:', burnStep.txHash)
15279
+ * }
15006
15280
  * ```
15281
+ */ function findStepByName(result, stepName) {
15282
+ return result.steps.find((step)=>step.name === stepName);
15283
+ }
15284
+ /**
15285
+ * Find a pending step by name and return it with its index.
15007
15286
  *
15008
- * @internal
15009
- */ const isReAttestedAttestationResponse = (obj)=>{
15010
- // First validate the basic structure and completion status
15011
- // This will throw appropriate errors for invalid structure or incomplete attestation
15012
- if (!isAttestationResponse(obj)) ;
15013
- // Check if the first message has expirationBlock === '0'
15014
- const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
15015
- if (expirationBlock !== '0') {
15016
- // Re-attestation not yet complete - allow retry via polling
15017
- throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
15287
+ * Searches for a step that matches both the step name and has a pending state.
15288
+ *
15289
+ * @param result - The bridge result containing steps to search through.
15290
+ * @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
15291
+ * @returns An object containing the step and its index in the steps array.
15292
+ * @throws KitError if the specified pending step is not found.
15293
+ *
15294
+ * @example
15295
+ * ```typescript
15296
+ * import { findPendingStep } from './findStep'
15297
+ *
15298
+ * const { step, index } = findPendingStep(result, 'burn')
15299
+ * console.log('Pending step:', step.name, 'at index:', index)
15300
+ * ```
15301
+ */ function findPendingStep(result, stepName) {
15302
+ const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
15303
+ if (index === -1) {
15304
+ throw new KitError({
15305
+ ...InputError.VALIDATION_FAILED,
15306
+ recoverability: 'FATAL',
15307
+ message: `Pending step "${stepName}" not found in result`
15308
+ });
15018
15309
  }
15019
- return true;
15020
- };
15310
+ const step = result.steps[index];
15311
+ if (!step) {
15312
+ throw new KitError({
15313
+ ...InputError.VALIDATION_FAILED,
15314
+ recoverability: 'FATAL',
15315
+ message: 'Pending step is undefined'
15316
+ });
15317
+ }
15318
+ return {
15319
+ step,
15320
+ index
15321
+ };
15322
+ }
15021
15323
  /**
15022
- * Fetches attestation data and polls until expirationBlock === '0'.
15324
+ * Get the burn transaction hash from bridge result.
15023
15325
  *
15024
- * This function is used after calling requestReAttestation() to wait until
15025
- * the attestation is fully re-processed. The expirationBlock transitions
15026
- * from non-zero to zero when Circle completes the re-attestation.
15326
+ * @param result - The bridge result.
15327
+ * @returns The burn transaction hash, or undefined if not found.
15027
15328
  *
15028
- * @param sourceDomainId - The CCTP domain ID of the source chain
15029
- * @param transactionHash - The transaction hash to fetch attestation for
15030
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15031
- * @param config - Optional configuration overrides
15032
- * @returns The re-attested attestation response with expirationBlock === '0'
15033
- * @throws If the request fails, times out, or expirationBlock never becomes 0
15329
+ * @example
15330
+ * ```typescript
15331
+ * import { getBurnTxHash } from './findStep'
15332
+ *
15333
+ * const burnTxHash = getBurnTxHash(result)
15334
+ * if (burnTxHash) {
15335
+ * console.log('Burn tx hash:', burnTxHash)
15336
+ * }
15337
+ * ```
15338
+ */ function getBurnTxHash(result) {
15339
+ return findStepByName(result, CCTPv2StepName.burn)?.txHash;
15340
+ }
15341
+ /**
15342
+ * Get the attestation data from bridge result.
15343
+ *
15344
+ * @param result - The bridge result.
15345
+ * @returns The attestation data, or undefined if not found.
15034
15346
  *
15035
15347
  * @example
15036
15348
  * ```typescript
15037
- * // After requesting re-attestation
15038
- * await requestReAttestation(nonce, isTestnet)
15349
+ * import { getAttestationData } from './findStep'
15039
15350
  *
15040
- * // Poll until expirationBlock becomes 0
15041
- * const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
15042
- * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
15351
+ * const attestation = getAttestationData(result)
15352
+ * if (attestation) {
15353
+ * console.log('Attestation:', attestation.message)
15354
+ * }
15043
15355
  * ```
15044
- */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
15045
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
15046
- const effectiveConfig = mergeAttestationConfig(config);
15047
- return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
15048
- };
15356
+ */ function getAttestationData(result) {
15357
+ // Prefer reAttest data (most recent attestation after expiry)
15358
+ const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
15359
+ if (reAttestStep?.state === 'success' && reAttestStep.data) {
15360
+ return reAttestStep.data;
15361
+ }
15362
+ // Fall back to fetchAttestation step
15363
+ const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
15364
+ return fetchStep?.data;
15365
+ }
15366
+
15049
15367
  /**
15050
- * Builds the IRIS API URL for re-attestation requests.
15368
+ * Check if the analysis indicates a non-actionable pending state.
15051
15369
  *
15052
- * Constructs the URL for Circle's re-attestation endpoint that allows
15053
- * requesting a fresh attestation for an expired nonce.
15370
+ * A pending state is non-actionable when there's a continuation step but
15371
+ * the analysis marks it as not actionable, typically because we need to
15372
+ * wait for an ongoing operation to complete.
15054
15373
  *
15055
- * @param nonce - The nonce from the original attestation
15056
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15057
- * @returns A fully qualified URL string for the re-attestation endpoint
15374
+ * @param analysis - The step analysis result from analyzeSteps.
15375
+ * @param result - The bridge result to check for pending steps.
15376
+ * @returns True if there is a pending step that we should wait for.
15058
15377
  *
15059
15378
  * @example
15060
15379
  * ```typescript
15061
- * // Mainnet URL
15062
- * const mainnetUrl = buildReAttestUrl('0xabc', false)
15063
- * // => 'https://iris-api.circle.com/v2/reattest/0xabc'
15380
+ * import { hasPendingState } from './stepUtils'
15381
+ * import { analyzeSteps } from '../analyzeSteps'
15064
15382
  *
15065
- * // Testnet URL
15066
- * const testnetUrl = buildReAttestUrl('0xabc', true)
15067
- * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
15383
+ * const analysis = analyzeSteps(bridgeResult)
15384
+ * if (hasPendingState(analysis, bridgeResult)) {
15385
+ * // Wait for the pending operation to complete
15386
+ * }
15068
15387
  * ```
15069
- */ const buildReAttestUrl = (nonce, isTestnet)=>{
15070
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15071
- const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
15072
- return url.toString();
15073
- };
15074
- /**
15075
- * Type guard that validates the re-attestation API response structure.
15388
+ */ /**
15389
+ * Evaluate a transaction receipt and return the corresponding step state
15390
+ * and error message. Centralises the success/revert/unconfirmed logic so
15391
+ * every call-site behaves identically.
15076
15392
  *
15077
- * @param obj - The value to check, typically a parsed JSON response
15078
- * @returns True if the object matches the ReAttestationResponse shape
15079
- * @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
15080
- * @internal
15081
- */ const isReAttestationResponse = (obj)=>{
15082
- if (typeof obj !== 'object' || obj === null || !('message' in obj) || !('nonce' in obj) || typeof obj.message !== 'string' || typeof obj.nonce !== 'string') {
15083
- throw new Error('Invalid re-attestation response structure');
15393
+ * @param receipt - The transaction receipt containing status and block info.
15394
+ * @param txHash - The transaction hash used in error messages.
15395
+ * @returns An object with `state` and an optional `errorMessage`.
15396
+ *
15397
+ * @example
15398
+ * ```typescript
15399
+ * const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
15400
+ * step.state = outcome.state
15401
+ * if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
15402
+ * ```
15403
+ */ function evaluateTransactionOutcome(receipt, txHash) {
15404
+ if (receipt.status === 'success' && receipt.blockNumber) {
15405
+ return {
15406
+ state: 'success'
15407
+ };
15084
15408
  }
15085
- return true;
15086
- };
15409
+ return {
15410
+ state: 'error',
15411
+ errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
15412
+ };
15413
+ }
15414
+ function hasPendingState(analysis, result) {
15415
+ // Check if there's a continuation step that's marked as non-actionable
15416
+ if (analysis.continuationStep === null || analysis.isActionable) {
15417
+ return false;
15418
+ }
15419
+ // Verify that the continuation step actually exists and is in pending state
15420
+ const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
15421
+ return pendingStep !== undefined;
15422
+ }
15087
15423
  /**
15088
- * Requests re-attestation for an expired attestation nonce.
15089
- *
15090
- * This function calls Circle's re-attestation API endpoint to request a fresh
15091
- * attestation for a previously issued nonce. After calling this function,
15092
- * you should poll `fetchAttestation` to retrieve the new attestation.
15424
+ * Check if the step is the last one in the execution flow.
15093
15425
  *
15094
- * @param nonce - The nonce from the original (expired) attestation
15095
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15096
- * @param config - Optional configuration overrides for the request
15097
- * @returns The re-attestation response confirming the request was accepted
15098
- * @throws If the request fails, times out, or returns invalid data
15426
+ * @param step - The step object to check.
15427
+ * @param stepNames - The ordered list of step names in the execution flow.
15428
+ * @returns True if this is the last step in the flow.
15099
15429
  *
15100
15430
  * @example
15101
15431
  * ```typescript
15102
- * // Request re-attestation for an expired nonce
15103
- * const response = await requestReAttestation('0xabc', true)
15104
- * console.log(response.message) // "Re-attestation successfully requested for nonce."
15432
+ * import { isLastStep } from './stepUtils'
15105
15433
  *
15106
- * // After requesting re-attestation, poll for the new attestation
15107
- * const attestation = await fetchAttestation(domainId, txHash, true)
15434
+ * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
15435
+ * isLastStep({ name: 'mint' }, stepNames) // true
15436
+ * isLastStep({ name: 'burn' }, stepNames) // false
15108
15437
  * ```
15109
- */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
15110
- const url = buildReAttestUrl(nonce, isTestnet);
15111
- // Use minimal retries since we're just submitting a request, not polling for state
15112
- const effectiveConfig = mergeAttestationConfig(config, {
15113
- maxRetries: 3
15114
- });
15115
- return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
15116
- };
15117
-
15438
+ */ function isLastStep(step, stepNames) {
15439
+ const stepIndex = stepNames.indexOf(step.name);
15440
+ return stepIndex === -1 || stepIndex >= stepNames.length - 1;
15441
+ }
15118
15442
  /**
15119
- * Type guard that checks if the relayer has confirmed the mint transaction.
15443
+ * Wait for a pending transaction to complete.
15120
15444
  *
15121
- * This function validates that:
15122
- * 1. The response has valid AttestationResponse structure
15123
- * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
15445
+ * Poll the adapter until the transaction is confirmed on-chain and return
15446
+ * the updated step with success or error state based on the receipt.
15124
15447
  *
15125
- * If forwardState is 'FAILED', throws a non-retryable KitError.
15126
- * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
15448
+ * @param pendingStep - The full step object containing the transaction hash.
15449
+ * @param adapter - The adapter to use for waiting.
15450
+ * @param chain - The chain where the transaction was submitted.
15451
+ * @returns The updated step object with success or error state.
15127
15452
  *
15128
- * @param obj - The value to check, typically a parsed JSON response
15129
- * @returns True if the relayer has confirmed the mint
15130
- * @throws {KitError} With FATAL recoverability if structure is invalid
15131
- * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
15132
- * @throws {KitError} With RETRYABLE recoverability if still pending
15133
- * @internal
15134
- */ const isRelayerMintConfirmed = (obj)=>{
15135
- // First check if the structure is valid
15136
- if (!hasValidAttestationStructure(obj)) {
15137
- throw new KitError({
15138
- ...InputError.VALIDATION_FAILED,
15139
- recoverability: 'FATAL',
15140
- message: 'Invalid attestation response structure from IRIS API.'
15141
- });
15142
- }
15143
- // Find the first message (typically there's only one)
15144
- const message = obj.messages[0];
15145
- if (!message) {
15453
+ * @throws KitError when the pending step has no transaction hash.
15454
+ *
15455
+ * @example
15456
+ * ```typescript
15457
+ * import { waitForPendingTransaction } from './bridgeStepUtils'
15458
+ *
15459
+ * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
15460
+ * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
15461
+ * // updatedStep.state is now 'success' or 'error'
15462
+ * ```
15463
+ */ async function waitForPendingTransaction(pendingStep, adapter, chain) {
15464
+ if (!pendingStep.txHash) {
15146
15465
  throw new KitError({
15147
15466
  ...InputError.VALIDATION_FAILED,
15148
15467
  recoverability: 'FATAL',
15149
- message: 'No attestation messages found in IRIS API response.'
15150
- });
15151
- }
15152
- // Check for FAILED state - this is a permanent failure
15153
- if (message.forwardState === 'FAILED') {
15154
- throw new KitError({
15155
- ...NetworkError.RELAYER_FORWARD_FAILED,
15156
- recoverability: 'RESUMABLE',
15157
- message: 'Circle relayer failed to forward the mint transaction. The mint may still have succeeded if another party submitted it. Check the recipient wallet balance before retrying. If the mint did not occur, you can manually submit it using the attestation data in the error cause.',
15158
- cause: {
15159
- trace: {
15160
- eventNonce: message.eventNonce,
15161
- attestation: message.attestation,
15162
- message: message.message
15163
- }
15164
- }
15468
+ message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
15165
15469
  });
15166
15470
  }
15167
- // Check if mint is confirmed (or complete) with a valid transaction hash
15168
- // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
15169
- if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
15170
- return true;
15171
- }
15172
- // Still pending or not yet processed - throw RETRYABLE error to continue polling
15173
- throw new KitError({
15174
- ...NetworkError.RELAYER_PENDING,
15175
- recoverability: 'RETRYABLE',
15176
- message: 'Relayer mint not ready. Waiting for confirmation.'
15471
+ const txHash = pendingStep.txHash;
15472
+ const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
15473
+ isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
15474
+ chain: chain.name,
15475
+ txHash
15476
+ }))
15177
15477
  });
15178
- };
15478
+ const outcome = evaluateTransactionOutcome(txReceipt, txHash);
15479
+ return {
15480
+ ...pendingStep,
15481
+ state: outcome.state,
15482
+ data: txReceipt,
15483
+ explorerUrl: buildExplorerUrl(chain, txHash),
15484
+ ...outcome.errorMessage ? {
15485
+ errorMessage: outcome.errorMessage
15486
+ } : {}
15487
+ };
15488
+ }
15179
15489
  /**
15180
- * Polls the attestation API until the relayer's mint transaction is confirmed.
15490
+ * Wait for a pending step to complete.
15181
15491
  *
15182
- * This function is used when `useForwarder` is enabled. Instead of the user
15183
- * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
15184
- * This function polls until the relayer has submitted and confirmed the mint transaction.
15492
+ * For transaction steps: waits for the transaction to be confirmed.
15493
+ * For attestation: re-executes the attestation fetch.
15185
15494
  *
15186
- * @remarks
15187
- * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
15188
- * - Throws immediately if `forwardState` is 'FAILED'
15189
- * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
15190
- * - Returns the attestation message with `forwardTxHash` populated
15495
+ * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
15496
+ * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
15497
+ * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
15498
+ * @param adapter - The adapter to use.
15499
+ * @param chain - The chain where the step is executing.
15500
+ * @param context - The retry context.
15501
+ * @param result - The bridge result.
15502
+ * @param provider - The CCTP v2 bridging provider.
15503
+ * @returns The resolved step object with updated state.
15191
15504
  *
15192
- * @param sourceDomainId - The CCTP domain ID of the source chain
15193
- * @param transactionHash - The transaction hash of the burn operation
15194
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
15195
- * @param config - Optional configuration overrides for polling behavior
15196
- * @returns The attestation message with confirmed forwardTxHash
15197
- * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
15198
- * @throws {KitError} If timeout is reached while still pending
15505
+ * @throws KitError when fetching attestation but burn transaction hash is not found.
15199
15506
  *
15200
15507
  * @example
15201
15508
  * ```typescript
15202
- * const attestation = await fetchRelayerMint(0, '0xabc...', false)
15203
- * console.log('Relayer mint tx:', attestation.forwardTxHash)
15509
+ * import { waitForStepToComplete } from './bridgeStepUtils'
15510
+ *
15511
+ * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
15512
+ * const updatedStep = await waitForStepToComplete(
15513
+ * pendingStep,
15514
+ * adapter,
15515
+ * chain,
15516
+ * context,
15517
+ * result,
15518
+ * provider,
15519
+ * )
15520
+ * // updatedStep.state is now 'success' or 'error'
15204
15521
  * ```
15205
- */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
15206
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
15207
- const effectiveConfig = mergeAttestationConfig(config);
15208
- let response;
15209
- try {
15210
- response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
15211
- } catch (error) {
15212
- // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
15213
- if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
15522
+ */ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
15523
+ if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
15524
+ // For attestation, re-run the fetch (it has built-in polling)
15525
+ const burnTxHash = getBurnTxHash(result);
15526
+ if (!burnTxHash) {
15214
15527
  throw new KitError({
15215
- ...NetworkError.RELAYER_FORWARD_FAILED,
15216
- recoverability: error.recoverability,
15217
- message: error.message,
15218
- cause: {
15219
- ...error.cause,
15220
- trace: {
15221
- ...error.cause?.trace,
15222
- burnTxHash: transactionHash
15223
- }
15224
- }
15528
+ ...InputError.VALIDATION_FAILED,
15529
+ recoverability: 'FATAL',
15530
+ message: 'Cannot fetch attestation: burn transaction hash not found'
15225
15531
  });
15226
15532
  }
15227
- throw error;
15228
- }
15229
- // Return the first message (which should have forwardTxHash)
15230
- // Note: This check is needed for TypeScript type safety even though
15231
- // isRelayerMintConfirmed validates messages[0] exists. The type guard
15232
- // narrows the type at the call site, but TypeScript can't infer that
15233
- // the array still has elements after pollApiGet returns.
15234
- const message = response.messages[0];
15235
- if (!message) {
15236
- throw new KitError({
15237
- ...InputError.VALIDATION_FAILED,
15238
- recoverability: 'FATAL',
15239
- message: 'No attestation messages found in response after polling.'
15240
- });
15533
+ const sourceAddress = result.source.address;
15534
+ const attestation = await provider.fetchAttestation({
15535
+ chain: result.source.chain,
15536
+ adapter: context.from,
15537
+ address: sourceAddress
15538
+ }, burnTxHash);
15539
+ return {
15540
+ ...pendingStep,
15541
+ state: 'success',
15542
+ data: attestation
15543
+ };
15241
15544
  }
15242
- return message;
15243
- };
15545
+ // For transaction steps, wait for the transaction to complete
15546
+ return waitForPendingTransaction(pendingStep, adapter, chain);
15547
+ }
15244
15548
 
15245
- const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
15246
15549
  /**
15247
- * Asserts that the provided parameters match the CCTPv2 wallet context interface.
15248
- * The validation includes:
15249
- * - Basic wallet context validation (adapter, address, chain)
15250
- * - CCTPv2-specific chain validation (must be an EVM chain)
15550
+ * Multiplier applied to a successful gas estimate before it is submitted.
15251
15551
  *
15252
- * @param params - The parameters to validate
15253
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
15552
+ * Estimates are exact, not padded: Sei returns 109_739 for an approve that
15553
+ * consumes 107_717 (1.9% headroom). Chains that price storage in large steps
15554
+ * can exceed the estimate if state changes between estimation and inclusion,
15555
+ * so the estimate is padded before use.
15556
+ *
15557
+ * @remarks
15558
+ * This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
15559
+ * scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
15560
+ * covers a slot that exists at estimation time and is consumed before
15561
+ * inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
15562
+ * the estimate covers it. For burn the buffer does cover a step (25% of
15563
+ * ~300_000 exceeds 51_500).
15564
+ */ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
15565
+ /**
15566
+ * Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
15567
+ *
15568
+ * Estimates first so chains whose real cost exceeds the floor are covered by
15569
+ * their own measurement, and falls back to the floor whenever estimation is
15570
+ * unavailable or under-reports. Estimation failure is never fatal here: before
15571
+ * floors existed these requests were submitted with a pinned limit and no
15572
+ * estimate at all, so degrading to the floor is never worse than the previous
15573
+ * behaviour.
15574
+ *
15575
+ * @param request - The prepared EVM request to size a gas limit for
15576
+ * @param gasFloor - The minimum gas limit to submit, in gas units
15577
+ * @returns The gas limit to submit, in gas units
15578
+ * @throws Never — estimation failures degrade to `gasFloor`
15254
15579
  *
15255
15580
  * @example
15256
15581
  * ```typescript
15257
- * import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
15258
- * import { Ethereum } from '@core/chains'
15259
- *
15260
- * // Prepare wallet context
15261
- * const context = {
15262
- * adapter: {
15263
- * prepare: async () => ({ data: 'prepared transaction' }),
15264
- * waitForTransaction: async () => ({ status: 'confirmed' })
15265
- * },
15266
- * address: '0x1234567890123456789012345678901234567890',
15267
- * chain: {
15268
- * ...Ethereum,
15269
- * usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
15270
- * cctp: {
15271
- * domain: 1,
15272
- * contracts: {
15273
- * v2: {
15274
- * tokenMessenger: '0xTokenMessenger',
15275
- * messageTransmitter: '0xMessageTransmitter'
15276
- * }
15277
- * }
15278
- * }
15279
- * }
15280
- * }
15281
- *
15282
- * // This will throw if validation fails
15283
- * assertCCTPv2WalletContext(context)
15284
- *
15285
- * // If we get here, context is guaranteed to be valid
15286
- * console.log('CCTPv2 wallet context is valid')
15582
+ * const gasLimit = await resolveGasLimit(request, 150_000)
15287
15583
  * ```
15288
- */ function assertCCTPv2WalletContext(params) {
15289
- // First validate basic wallet context
15290
- validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
15291
- // After validation, we know params is WalletContext
15292
- const context = params;
15293
- // Validate USDC support
15294
- if (context.chain.usdcAddress === null) {
15295
- throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
15296
- }
15297
- // Validate CCTPv2 support
15298
- if (!isCCTPV2Supported(context.chain)) {
15299
- throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
15584
+ */ const resolveGasLimit = async (request, gasFloor)=>{
15585
+ try {
15586
+ // Deliberately called without a `fallback`: both the viem and ethers
15587
+ // adapters *return* the supplied fallback object when estimation reverts
15588
+ // rather than throwing, which would set the estimate to the floor and then
15589
+ // multiply it by the buffer below. Omitting it routes reverts through the
15590
+ // catch, so a failed estimate degrades to exactly the floor.
15591
+ const estimate = await request.estimate();
15592
+ // The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
15593
+ // typed `bigint`, but adapters are a public extension point and may be
15594
+ // implemented in plain JS, so a non-bigint `gas` would throw here
15595
+ // ("Cannot mix BigInt and other types"). Guarding it keeps the documented
15596
+ // contract — estimation never aborts a step, it degrades to the floor.
15597
+ const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
15598
+ // Convert before comparing: Math.max throws on BigInt operands, and gas
15599
+ // units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
15600
+ return Math.max(Number(buffered), gasFloor);
15601
+ } catch {
15602
+ // Estimation is best-effort; the floor is the known-safe value.
15603
+ return gasFloor;
15300
15604
  }
15301
- }
15302
-
15303
- const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
15605
+ };
15304
15606
  /**
15305
- * Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
15306
- * The validation includes:
15307
- * - Basic parameter structure and types
15308
- * - Amount validation (non-empty numeric string \> 0)
15309
- * - Wallet address format validation (must be valid Ethereum address)
15310
- * - Chain definition validation (must be a valid chain with required properties)
15311
- * - Adapter validation (must implement required methods)
15312
- * - Optional config validation (transfer speed and max fee)
15313
- * - Network compatibility (source and destination chains must both be testnet or both mainnet)
15314
- * - CCTPv2-specific wallet context validations
15607
+ * Executes a prepared chain request and returns the result as a bridge step.
15315
15608
  *
15316
- * @param params - The parameters to validate
15317
- * @throws {KitError} If validation fails, with details about which properties failed
15609
+ * This function takes a prepared chain request (containing transaction data) and executes
15610
+ * it using the appropriate adapter. It handles the execution details and formats
15611
+ * the result as a standardized bridge step with transaction details and explorer URLs.
15612
+ *
15613
+ * @param params - The execution parameters containing:
15614
+ * - `name`: The name of the step
15615
+ * - `request`: The prepared chain request containing transaction data
15616
+ * - `adapter`: The adapter that will execute the transaction
15617
+ * - `confirmations`: The number of confirmations to wait for (defaults to 1)
15618
+ * - `timeout`: The timeout for the request in milliseconds
15619
+ * - `gasFloor`: Optional minimum gas limit (number); the request is submitted
15620
+ * with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
15621
+ * @returns The bridge step with the transaction details and explorer URL
15622
+ * @throws If the transaction execution fails
15318
15623
  *
15319
15624
  * @example
15320
15625
  * ```typescript
15321
- * import { assertCCTPv2BridgeParams } from '@circle-fin/provider-cctp-v2'
15322
- * import { Ethereum, Base } from '@core/chains'
15323
- *
15324
- * // Prepare transfer parameters
15325
- * const params = {
15326
- * amount: '100.50',
15327
- * source: {
15328
- * adapter: sourceAdapter,
15329
- * address: '0xSourceAddress',
15330
- * chain: {
15331
- * ...Ethereum,
15332
- * cctp: {
15333
- * domain: 1,
15334
- * contracts: {
15335
- * v2: {
15336
- * tokenMessenger: '0xTokenMessenger',
15337
- * messageTransmitter: '0xMessageTransmitter'
15338
- * }
15339
- * }
15340
- * }
15341
- * }
15342
- * },
15343
- * destination: {
15344
- * adapter: destAdapter,
15345
- * address: '0xDestAddress',
15346
- * chain: {
15347
- * ...Base,
15348
- * cctp: {
15349
- * domain: 2,
15350
- * contracts: {
15351
- * v2: {
15352
- * tokenMessenger: '0xTokenMessenger',
15353
- * messageTransmitter: '0xMessageTransmitter'
15354
- * }
15355
- * }
15356
- * }
15357
- * }
15358
- * },
15359
- * token: 'USDC',
15360
- * config: {
15361
- * transferSpeed: 'FAST',
15362
- * maxFee: '1000000'
15363
- * }
15364
- * }
15365
- *
15366
- * // This will throw if validation fails
15367
- * assertCCTPv2BridgeParams(params)
15368
- *
15369
- * // If we get here, params is guaranteed to be valid
15370
- * console.log('CCTPv2 transfer parameters are valid')
15626
+ * const step = await executePreparedChainRequest({
15627
+ * name: 'approve',
15628
+ * request: preparedRequest,
15629
+ * adapter: adapter,
15630
+ * confirmations: 2,
15631
+ * timeout: 30000
15632
+ * })
15633
+ * console.log('Transaction hash:', step.txHash)
15371
15634
  * ```
15372
- */ function assertCCTPv2BridgeParams(params) {
15373
- // First validate basic bridge params
15374
- validateWithStateTracking(params, bridgeParamsSchema, 'CCTPv2 bridge parameters', assertCCTPv2BridgeParamsSymbol);
15375
- // After validation, we know params is CCTPV2BridgeParams
15376
- const bridgeParams = params;
15377
- // Enforce that source and destination chains are either testnet or mainnet
15378
- if (bridgeParams.source.chain.isTestnet !== bridgeParams.destination.chain.isTestnet) {
15379
- throw createNetworkMismatchError(bridgeParams.source.chain, bridgeParams.destination.chain);
15380
- }
15381
- assertCCTPV2Support(bridgeParams.source.chain, bridgeParams.destination.chain);
15382
- // Validate that the destination chain supports forwarding when forwarder is enabled
15383
- assertForwarderRouteSupport$1(bridgeParams.source.chain, bridgeParams.destination.chain, bridgeParams.destination.useForwarder);
15384
- /**
15385
- * Enforce that if fee is defined then feeRecipient must be defined.
15386
- * We do not do this in the validation function itself because we want to allow
15387
- * optional properties when calling `provider.bridge()` due to the custom fee
15388
- * configuration being possible at the kit level as well.
15389
- */ if (bridgeParams.config?.customFee?.value !== undefined && bridgeParams.config?.customFee?.recipientAddress === undefined) {
15390
- throw createValidationFailedError$1('recipientAddress', bridgeParams.config.customFee.value, 'Custom fee is defined but fee recipient is not. Please provide a fee recipient.');
15391
- }
15392
- // Check if this is a forwarder-only destination (no adapter, requires useForwarder: true)
15393
- const isForwarderOnly = bridgeParams.destination.useForwarder === true && !('adapter' in bridgeParams.destination && bridgeParams.destination.adapter);
15394
- // Forwarder-only destinations require recipientAddress
15395
- if (isForwarderOnly) {
15396
- if (!bridgeParams.destination.recipientAddress?.trim()) {
15397
- throw createValidationFailedError$1('recipientAddress', bridgeParams.destination.recipientAddress, 'recipientAddress is required when using forwarder without a destination adapter.');
15635
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
15636
+ const step = {
15637
+ name,
15638
+ state: 'pending'
15639
+ };
15640
+ try {
15641
+ /**
15642
+ * No-op requests are not executed.
15643
+ * We return a noop step instead.
15644
+ */ if (request.type === 'noop') {
15645
+ step.state = 'noop';
15646
+ return step;
15647
+ }
15648
+ const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
15649
+ gasLimit: await resolveGasLimit(request, gasFloor)
15650
+ }) : await request.execute();
15651
+ step.txHash = txHash;
15652
+ const retryOptions = {
15653
+ isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
15654
+ chain: chain.name,
15655
+ txHash
15656
+ }))
15657
+ };
15658
+ if (timeout !== undefined) {
15659
+ retryOptions.deadlineMs = Date.now() + timeout;
15660
+ }
15661
+ const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
15662
+ confirmations,
15663
+ timeout
15664
+ }, chain), retryOptions);
15665
+ const outcome = evaluateTransactionOutcome(transaction, txHash);
15666
+ step.state = outcome.state;
15667
+ step.data = transaction;
15668
+ // Generate explorer URL for the step
15669
+ step.explorerUrl = buildExplorerUrl(chain, txHash);
15670
+ if (outcome.errorMessage) {
15671
+ step.errorMessage = outcome.errorMessage;
15672
+ // Transaction was mined but reverted on-chain.
15673
+ step.errorCategory = 'chain_revert';
15674
+ }
15675
+ } catch (err) {
15676
+ step.state = 'error';
15677
+ step.error = err;
15678
+ // Sequential path does not yet attempt fine-grained classification of
15679
+ // pre-submission errors (user_rejected, capability errors, etc.). Mark
15680
+ // as `unknown` so consumers can at least detect the category is
15681
+ // populated uniformly across batched and sequential flows.
15682
+ step.errorCategory = 'unknown';
15683
+ // Optionally parse for common blockchain error formats
15684
+ if (err instanceof Error) {
15685
+ step.errorMessage = err.message;
15686
+ } else if (typeof err === 'object' && err != null && 'message' in err) {
15687
+ step.errorMessage = String(err.message);
15688
+ } else {
15689
+ step.errorMessage = `Unknown error occurred during ${name} step.`;
15398
15690
  }
15399
15691
  }
15400
- // Validate CCTP v2 specific requirements for source wallet
15401
- assertCCTPv2WalletContext(bridgeParams.source);
15402
- // Validate that source adapter supports the chain (defense-in-depth)
15403
- bridgeParams.source.adapter.validateChainSupport(bridgeParams.source.chain);
15404
- // Only validate destination wallet context and adapter if not forwarder-only
15405
- if (!isForwarderOnly) {
15406
- assertCCTPv2WalletContext(bridgeParams.destination);
15407
- // Validate that destination adapter supports the chain (defense-in-depth)
15408
- bridgeParams.destination.adapter.validateChainSupport(bridgeParams.destination.chain);
15409
- }
15692
+ return step;
15410
15693
  }
15694
+
15411
15695
  /**
15412
- * Validate CCTP v2 support on both chains
15413
- */ /**
15414
- * Throws a KitError if the given chain does not support CCTP v2.
15415
- *
15416
- * @param chain - The chain to check for CCTP v2 support
15417
- * @param otherChain - The other chain in the route (for error context)
15418
- * @param isSource - Whether this is the source chain (for error context)
15419
- */ function assertCCTPV2Support(source, destination) {
15420
- if (!isCCTPV2Supported(source) || !isCCTPV2Supported(destination)) {
15421
- throw createUnsupportedRouteError(source.name, destination.name);
15696
+ * Default configuration values for the attestation fetcher.
15697
+ * @internal
15698
+ */ const DEFAULT_CONFIG$2 = {
15699
+ timeout: 2_000,
15700
+ maxRetries: 30 * 20,
15701
+ retryDelay: 2_000,
15702
+ headers: {
15703
+ 'Content-Type': 'application/json'
15422
15704
  }
15423
- }
15705
+ };
15424
15706
  /**
15425
- * Validates that the forwarder (relaying) feature is compatible with the route.
15707
+ * Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
15426
15708
  *
15427
- * Checks the destination chain's `cctp.forwarderSupported.destination` property
15428
- * to determine whether the chain supports receiving forwarded transfers.
15709
+ * Headers are merged independently so caller-supplied headers augment the
15710
+ * defaults (such as `Content-Type`) rather than replacing them wholesale.
15429
15711
  *
15430
- * @param source - The source chain definition
15431
- * @param destination - The destination chain definition
15432
- * @param useForwarder - Whether the forwarder is enabled on the destination
15433
- * @throws {KitError} If the forwarder is enabled and the destination chain does not support forwarding
15434
- */ function assertForwarderRouteSupport$1(source, destination, useForwarder) {
15435
- if (useForwarder === true && !destination.cctp?.forwarderSupported.destination) {
15436
- throw new KitError({
15437
- ...InputError.UNSUPPORTED_ROUTE,
15438
- recoverability: 'FATAL',
15439
- message: `Route from ${source.name} to ${destination.name} with forwarder is not supported (destination chain does not support forwarding).`,
15440
- cause: {
15441
- trace: {
15442
- source: source.name,
15443
- destination: destination.name
15444
- }
15445
- }
15446
- });
15447
- }
15448
- }
15449
-
15712
+ * @param config - Caller-provided polling configuration overrides
15713
+ * @param internalDefaults - Internal defaults applied before `config` (for example a
15714
+ * reduced `maxRetries` for one-shot requests); `config` still wins on conflict
15715
+ * @returns The effective polling configuration
15716
+ * @internal
15717
+ */ const mergeAttestationConfig = (config, internalDefaults = {})=>({
15718
+ ...DEFAULT_CONFIG$2,
15719
+ ...internalDefaults,
15720
+ ...config,
15721
+ headers: {
15722
+ ...DEFAULT_CONFIG$2.headers,
15723
+ ...internalDefaults.headers,
15724
+ ...config.headers
15725
+ }
15726
+ });
15450
15727
  /**
15451
- * Checks if a decoded attestation field matches the corresponding transfer parameter.
15452
- * If the values do not match, appends a descriptive error message to the errors array.
15728
+ * Type guard that verifies if an unknown value matches the AttestationMessage shape
15729
+ * and has all required properties.
15453
15730
  *
15454
- * @param field - The name of the field being compared (for error reporting)
15455
- * @param decoded - The value decoded from the attestation message
15456
- * @param param - The expected value from the transfer parameters
15457
- * @param errors - The array to which error messages will be appended if a mismatch is found
15458
- */ function checkFieldMismatch(field, decoded, param, errors) {
15459
- if (decoded !== param) {
15460
- errors.push(`${field} mismatch: decoded=${String(decoded)}, params=${String(param)}`);
15461
- }
15462
- }
15731
+ * @param obj - The value to check, typically an element from the messages array
15732
+ * @returns True if the object matches the AttestationMessage shape, false otherwise
15733
+ * @internal
15734
+ */ const isValidAttestationMessage = (obj)=>{
15735
+ return typeof obj === 'object' && obj !== null && 'message' in obj && 'eventNonce' in obj && 'attestation' in obj && 'decodedMessage' in obj && 'cctpVersion' in obj && 'status' in obj && typeof obj.status === 'string';
15736
+ };
15463
15737
  /**
15464
- * Asserts that the decoded message from attestation matches the provided transfer params.
15465
- * Throws KitError if any field mismatches, with clear error messages.
15738
+ * Type guard that verifies if an attestation message is complete.
15466
15739
  *
15467
- * @param attestation - The attestation message containing the decoded message
15468
- * @param params - The transfer parameters to validate against
15469
- * @throws {@link KitError} If any field mismatches
15470
- */ async function assertCCTPv2AttestationParams(attestation, params) {
15471
- const errors = [];
15472
- const message = attestation.decodedMessage;
15473
- const messageBody = message.decodedMessageBody;
15474
- // Use recipientAddress if provided, otherwise use destination.address
15475
- const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
15476
- const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
15477
- let sender;
15478
- if (hasCustomContractSupport(params.source.chain, 'bridge')) {
15479
- if (params.source.chain.type === 'solana') {
15480
- // Solana: User Bridge contract → CCTP (user remains sender)
15481
- sender = params.source.address;
15482
- } else {
15483
- // Other chains (like EVM): Bridge contract → CCTP (bridge contract becomes sender)
15484
- sender = params.source.chain.kitContracts?.bridge;
15485
- }
15486
- } else {
15487
- sender = params.source.address;
15488
- }
15489
- checkFieldMismatch('sourceDomain', message.sourceDomain, params.source.chain.cctp.domain.toString(), errors);
15490
- checkFieldMismatch('destinationDomain', message.destinationDomain, params.destination.chain.cctp.domain.toString(), errors);
15491
- checkFieldMismatch('minFinalityThreshold', message.minFinalityThreshold, CCTPv2MinFinalityThreshold[params.config.transferSpeed ?? 'FAST'].toString(), errors);
15492
- checkFieldMismatch('sender', params.source.chain.type === 'evm' ? messageBody.messageSender.toLowerCase() : messageBody.messageSender, params.source.chain.type === 'evm' ? sender?.toLowerCase() : sender, errors);
15493
- checkFieldMismatch('recipient', params.destination.chain.type === 'evm' ? messageBody.mintRecipient.toLowerCase() : messageBody.mintRecipient, params.destination.chain.type === 'evm' ? mintRecipient.toLowerCase() : mintRecipient, errors);
15494
- checkFieldMismatch('amount', messageBody.amount, params.amount.toString(), errors);
15495
- checkFieldMismatch('burnToken', messageBody.burnToken.toLowerCase(), params.source.chain.usdcAddress.toLowerCase(), errors);
15496
- if (errors.length > 0) {
15497
- const errorMessage = 'Attestation validation failed: received attestation does not match expected transfer parameters';
15498
- const firstError = errors[0] ?? '';
15499
- throw new KitError({
15500
- ...InputError.VALIDATION_FAILED,
15501
- recoverability: 'FATAL',
15502
- message: `${errorMessage}: ${firstError}`,
15503
- cause: {
15504
- trace: {
15505
- validationErrors: errors
15506
- }
15507
- }
15508
- });
15740
+ * @param message - The attestation message to check
15741
+ * @returns True if the message status is 'complete', false otherwise
15742
+ * @internal
15743
+ */ const isCompleteAttestation = (message)=>{
15744
+ return message.status === 'complete';
15745
+ };
15746
+ /**
15747
+ * Type guard that verifies if an unknown value has the correct structure
15748
+ * for an AttestationResponse, regardless of attestation completion status.
15749
+ *
15750
+ * @param obj - The value to check, typically a parsed JSON response
15751
+ * @returns True if the object matches the AttestationResponse shape
15752
+ * @internal
15753
+ */ const hasValidAttestationStructure = (obj)=>{
15754
+ if (typeof obj !== 'object' || obj === null || !('messages' in obj) || !Array.isArray(obj.messages)) {
15755
+ return false;
15509
15756
  }
15510
- }
15511
-
15757
+ const messages = obj.messages;
15758
+ // Validate all messages have the correct shape
15759
+ return messages.every(isValidAttestationMessage);
15760
+ };
15512
15761
  /**
15513
- * Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
15762
+ * Type guard that verifies if an unknown value matches the AttestationResponse shape
15763
+ * and contains a complete attestation.
15514
15764
  *
15515
- * Validates the full public-boundary input before any field destructuring,
15516
- * `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
15517
- * inputs always produce typed `KitError` validation failures.
15765
+ * This function performs runtime validation to ensure that the provided value
15766
+ * conforms to the expected structure of an AttestationResponse and has at least
15767
+ * one complete attestation. It checks that:
15768
+ * 1. The value has valid AttestationResponse structure
15769
+ * 2. At least one message has status 'complete'
15518
15770
  *
15519
- * Checks performed (in order):
15520
- * - `params` must be a non-null plain object
15521
- * - `source` valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
15522
- * - `destinationChain` present and supports CCTP v2
15523
- * - source and destination chains must both be testnet or both mainnet
15524
- * - source and destination chains must differ
15525
- * - `executor` — non-empty string
15526
- * - `amount` — bigint or non-empty string coercible to bigint
15527
- * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
15528
- * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
15529
- * - `claim.signedQuote` — valid `0x`-prefixed hex string
15530
- * - `claim.refundAddress` — valid EVM address
15531
- * - `hookData` — valid `0x`-prefixed hex string when present
15771
+ * @remarks
15772
+ * This type guard is used internally by the attestation fetcher to validate
15773
+ * responses from the IRIS API before processing them. It provides runtime
15774
+ * type safety for data coming from the network and ensures we have a complete
15775
+ * attestation before proceeding.
15532
15776
  *
15533
- * @param params - The value to validate.
15534
- * @throws {KitError} If any field is missing or invalid.
15777
+ * If the response has valid structure but no complete attestation yet,
15778
+ * it throws a retryable error. If the response structure is invalid,
15779
+ * it throws a non-retryable validation error.
15780
+ *
15781
+ * @param obj - The value to check, typically a parsed JSON response
15782
+ * @returns True if the object matches the AttestationResponse shape and has a complete attestation
15783
+ * @throws {Error} With "Invalid attestation response structure" if structure is invalid (non-retryable)
15784
+ * @throws {Error} With "Attestation not ready" if no complete attestation yet (retryable)
15535
15785
  *
15536
15786
  * @example
15537
15787
  * ```typescript
15538
- * assertBurnWithFeesParams(params)
15539
- * // params is now typed as BurnWithFeesParams and safe to use
15540
- * const { source, destinationChain, amount } = params
15788
+ * const response = await fetch('https://iris-api.circle.com/...')
15789
+ * const data = await response.json()
15790
+ *
15791
+ * if (isAttestationResponse(data)) {
15792
+ * // TypeScript now knows data is AttestationResponse with at least one complete attestation
15793
+ * const completeMessage = data.messages.find(msg => msg.status === 'complete')
15794
+ * console.log('Found complete attestation:', completeMessage.attestation)
15795
+ * }
15541
15796
  * ```
15542
- */ function assertBurnWithFeesParams(params) {
15543
- if (params === null || typeof params !== 'object' || Array.isArray(params)) {
15544
- throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
15545
- }
15546
- const p = params;
15547
- // Source wallet context
15548
- assertCCTPv2WalletContext(p['source']);
15549
- const source = p['source'];
15550
- // destinationChain
15551
- const destinationChain = p['destinationChain'];
15552
- if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
15553
- throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
15554
- }
15555
- if (!isCCTPV2Supported(destinationChain)) {
15556
- throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
15557
- }
15558
- const dest = destinationChain;
15559
- // Testnet / mainnet mismatch
15560
- if (source.chain.isTestnet !== dest.isTestnet) {
15561
- throw createNetworkMismatchError(source.chain, dest);
15562
- }
15563
- // Same-chain guard
15564
- if (source.chain.name === dest.name) {
15565
- throw createUnsupportedRouteError(source.chain.name, dest.name);
15566
- }
15567
- // executor
15568
- const executor = p['executor'];
15569
- if (typeof executor !== 'string' || executor === '') {
15570
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
15571
- }
15572
- // amount
15573
- const rawAmount = p['amount'];
15574
- if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
15575
- throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
15576
- }
15577
- try {
15578
- BigInt(rawAmount);
15579
- } catch {
15580
- throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
15581
- }
15582
- // feeTotalAmount
15583
- const rawFee = p['feeTotalAmount'];
15584
- if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
15585
- throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
15586
- }
15587
- try {
15588
- BigInt(rawFee);
15589
- } catch {
15590
- throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
15591
- }
15592
- // feeToken
15593
- if (!evmAddressSchema.safeParse(p['feeToken']).success) {
15594
- throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
15595
- }
15596
- // claim
15597
- const rawClaim = p['claim'];
15598
- if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
15599
- throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
15600
- }
15601
- const claim = rawClaim;
15602
- if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
15603
- throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
15797
+ */ const isAttestationResponse = (obj)=>{
15798
+ // First check if the structure is valid
15799
+ if (!hasValidAttestationStructure(obj)) {
15800
+ // If structure is invalid, this is a permanent failure - don't retry
15801
+ throw new Error('Invalid attestation response structure');
15604
15802
  }
15605
- if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
15606
- throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
15803
+ // Then check if at least one message is complete
15804
+ if (!obj.messages.some(isCompleteAttestation)) {
15805
+ // If no complete message, this is a temporary state - allow retry
15806
+ throw new Error('Attestation not ready');
15607
15807
  }
15608
- // hookData (optional)
15609
- const hookData = p['hookData'];
15610
- if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
15611
- throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
15808
+ return true;
15809
+ };
15810
+ /**
15811
+ * Builds the IRIS API URL for fetching attestation data from Circle's CCTP service.
15812
+ *
15813
+ * Constructs a properly formatted URL for the IRIS API v2 endpoint that provides
15814
+ * attestation messages for cross-chain transfers. The URL includes both the source
15815
+ * domain identifier and the transaction hash as query parameters. The base URL
15816
+ * is selected based on whether the operation is for testnet or mainnet.
15817
+ *
15818
+ * @param sourceDomainId - The CCTP domain ID of the source chain (numeric or string)
15819
+ * @param transactionHash - The transaction hash of the burn operation to fetch attestation for
15820
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15821
+ * @returns A fully qualified URL string for the IRIS API endpoint
15822
+ *
15823
+ * @example
15824
+ * ```typescript
15825
+ * // Mainnet URL
15826
+ * const mainnetUrl = buildIrisUrl(1, '0xabc...', false)
15827
+ * // => 'https://iris-api.circle.com/v2/messages/1?transactionHash=0xabc...'
15828
+ *
15829
+ * // Testnet URL
15830
+ * const testnetUrl = buildIrisUrl(1, '0xdef...', true)
15831
+ * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
15832
+ * ```
15833
+ */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
15834
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15835
+ const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
15836
+ url.searchParams.set('transactionHash', transactionHash);
15837
+ return url.toString();
15838
+ };
15839
+ /**
15840
+ * Fetches attestation data from the IRIS API with retry and timeout handling.
15841
+ *
15842
+ * Polls the IRIS API until a complete attestation is available. The default
15843
+ * window is sized for slow source chains where finality may take many
15844
+ * confirmations.
15845
+ *
15846
+ * Defaults (see `DEFAULT_CONFIG`):
15847
+ * - Per-attempt timeout: 2 000 ms (each HTTP request aborts after 2 s)
15848
+ * - Retry delay: 2 000 ms between attempts
15849
+ * - Max retries: 600 (30 × 20)
15850
+ * - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
15851
+ *
15852
+ * @param sourceDomainId - The CCTP domain ID.
15853
+ * @param transactionHash - The transaction hash to fetch attestation for.
15854
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15855
+ * @param config - Optional configuration overrides for the attestation fetcher
15856
+ * @returns The attestation response data.
15857
+ * @throws If the request fails, times out, or returns invalid data.
15858
+ *
15859
+ * @example
15860
+ * ```typescript
15861
+ * // Fetch attestation for mainnet transaction
15862
+ * const response = await fetchAttestation(1, '0xabc...', false)
15863
+ * console.log(`Found ${response.messages.length} attestation messages`)
15864
+ *
15865
+ * // Fetch with custom timeout
15866
+ * const response2 = await fetchAttestation(1, '0xdef...', true, {
15867
+ * timeout: 5000,
15868
+ * maxRetries: 5
15869
+ * })
15870
+ * ```
15871
+ */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
15872
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
15873
+ const effectiveConfig = mergeAttestationConfig(config);
15874
+ return await pollApiGet(url, isAttestationResponse, effectiveConfig);
15875
+ };
15876
+ /**
15877
+ * Type guard that validates attestation response structure without requiring completion status.
15878
+ *
15879
+ * This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
15880
+ * attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
15881
+ * this function does not throw if no complete attestation is found.
15882
+ *
15883
+ * @param obj - The value to check, typically a parsed JSON response
15884
+ * @returns True if the object has valid attestation structure
15885
+ * @throws {Error} With "Invalid attestation response structure" if structure is invalid
15886
+ * @internal
15887
+ */ const isAttestationResponseWithoutStatusCheck = (obj)=>{
15888
+ if (!hasValidAttestationStructure(obj)) {
15889
+ throw new Error('Invalid attestation response structure');
15612
15890
  }
15613
- }
15614
-
15891
+ return true;
15892
+ };
15615
15893
  /**
15616
- * CCTP bridge step names that can occur in the bridging flow.
15894
+ * Fetches attestation data without requiring the attestation to be complete.
15617
15895
  *
15618
- * This object provides type safety for step names and represents all possible
15619
- * steps that can be executed during a CCTP bridge operation. Using const assertions
15620
- * makes this tree-shakable and follows modern TypeScript best practices.
15621
- */ const CCTPv2StepName = {
15622
- approve: 'approve',
15623
- burn: 'burn',
15624
- fetchAttestation: 'fetchAttestation',
15625
- mint: 'mint',
15626
- reAttest: 'reAttest'
15896
+ * This function is useful for retrieving attestation data (particularly the nonce)
15897
+ * from an existing transaction, even if the attestation has expired or is pending.
15898
+ * It uses minimal retries since we're fetching existing data, not waiting for completion.
15899
+ *
15900
+ * @param sourceDomainId - The CCTP domain ID of the source chain
15901
+ * @param transactionHash - The transaction hash to fetch attestation for
15902
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15903
+ * @param config - Optional configuration overrides
15904
+ * @returns The attestation response data (may contain incomplete/expired attestations)
15905
+ * @throws If the request fails, times out, or returns invalid data
15906
+ *
15907
+ * @example
15908
+ * ```typescript
15909
+ * // Fetch existing attestation to extract nonce for re-attestation
15910
+ * const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
15911
+ * const nonce = response.messages[0]?.eventNonce
15912
+ * ```
15913
+ */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
15914
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
15915
+ // Use minimal retries since we're just fetching existing data
15916
+ const effectiveConfig = mergeAttestationConfig(config, {
15917
+ maxRetries: 3
15918
+ });
15919
+ return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
15627
15920
  };
15628
15921
  /**
15629
- * Conditional step transition rules for CCTP bridge flow.
15922
+ * Type guard that validates attestation response has expirationBlock === '0'.
15630
15923
  *
15631
- * Rules are evaluated in order - the first matching condition determines the next step.
15632
- * This approach supports flexible flow logic and makes it easy to extend with new patterns.
15633
- */ const STEP_TRANSITION_RULES = {
15634
- // Starting state - no steps executed yet
15635
- '': [
15636
- {
15637
- condition: ()=>true,
15638
- nextStep: CCTPv2StepName.approve,
15639
- reason: 'Start with approval step',
15640
- isActionable: true
15641
- }
15642
- ],
15643
- // After Approve step
15644
- [CCTPv2StepName.approve]: [
15645
- {
15646
- condition: (ctx)=>ctx.lastStep?.state === 'success',
15647
- nextStep: CCTPv2StepName.burn,
15648
- reason: 'Approval successful, proceed to burn',
15649
- isActionable: true
15650
- },
15651
- {
15652
- condition: (ctx)=>ctx.lastStep?.state === 'error',
15653
- nextStep: CCTPv2StepName.approve,
15654
- reason: 'Retry failed approval',
15655
- isActionable: true
15656
- },
15657
- {
15658
- condition: (ctx)=>ctx.lastStep?.state === 'noop',
15659
- nextStep: CCTPv2StepName.burn,
15660
- reason: 'No approval needed, proceed to burn',
15661
- isActionable: true
15662
- },
15663
- {
15664
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
15665
- nextStep: CCTPv2StepName.approve,
15666
- reason: 'Continue pending approval',
15667
- isActionable: false
15668
- }
15669
- ],
15670
- // After Burn step
15671
- [CCTPv2StepName.burn]: [
15672
- {
15673
- condition: (ctx)=>ctx.lastStep?.state === 'success',
15674
- nextStep: CCTPv2StepName.fetchAttestation,
15675
- reason: 'Burn successful, fetch attestation',
15676
- isActionable: true
15677
- },
15678
- {
15679
- condition: (ctx)=>ctx.lastStep?.state === 'error',
15680
- nextStep: CCTPv2StepName.burn,
15681
- reason: 'Retry failed burn',
15682
- isActionable: true
15683
- },
15684
- {
15685
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
15686
- nextStep: CCTPv2StepName.burn,
15687
- reason: 'Continue pending burn',
15688
- isActionable: false
15689
- }
15690
- ],
15691
- // After FetchAttestation step
15692
- [CCTPv2StepName.fetchAttestation]: [
15693
- {
15694
- condition: (ctx)=>ctx.lastStep?.state === 'success',
15695
- nextStep: CCTPv2StepName.mint,
15696
- reason: 'Attestation fetched, proceed to mint',
15697
- isActionable: true
15698
- },
15699
- {
15700
- condition: (ctx)=>ctx.lastStep?.state === 'error',
15701
- nextStep: CCTPv2StepName.fetchAttestation,
15702
- reason: 'Retry fetching attestation',
15703
- isActionable: true
15704
- },
15705
- {
15706
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
15707
- nextStep: CCTPv2StepName.fetchAttestation,
15708
- reason: 'Continue pending attestation fetch',
15709
- isActionable: false
15710
- }
15711
- ],
15712
- // After Mint step
15713
- [CCTPv2StepName.mint]: [
15714
- {
15715
- condition: (ctx)=>ctx.lastStep?.state === 'success',
15716
- nextStep: null,
15717
- reason: 'Bridge completed successfully',
15718
- isActionable: false
15719
- },
15720
- {
15721
- condition: (ctx)=>ctx.lastStep?.state === 'error',
15722
- nextStep: CCTPv2StepName.mint,
15723
- reason: 'Retry failed mint',
15724
- isActionable: true
15725
- },
15726
- {
15727
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
15728
- nextStep: CCTPv2StepName.mint,
15729
- reason: 'Continue pending mint',
15730
- isActionable: false
15731
- }
15732
- ],
15733
- // After ReAttest step
15734
- [CCTPv2StepName.reAttest]: [
15735
- {
15736
- condition: (ctx)=>ctx.lastStep?.state === 'success',
15737
- nextStep: CCTPv2StepName.mint,
15738
- reason: 'Re-attestation successful, proceed to mint',
15739
- isActionable: true
15740
- },
15741
- {
15742
- condition: (ctx)=>ctx.lastStep?.state === 'error',
15743
- nextStep: CCTPv2StepName.mint,
15744
- reason: 'Re-attestation failed, retry mint to re-initiate recovery',
15745
- isActionable: true
15746
- },
15747
- {
15748
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
15749
- nextStep: CCTPv2StepName.mint,
15750
- reason: 'Re-attestation pending, retry mint to re-initiate recovery',
15751
- isActionable: true
15752
- }
15753
- ]
15924
+ * This is used after requestReAttestation() to poll until the attestation
15925
+ * is fully re-processed and has a zero expiration block (never expires).
15926
+ * The expiration block transitions from non-zero to zero when Circle
15927
+ * completes processing the re-attestation request.
15928
+ *
15929
+ * @param obj - The value to check, typically a parsed JSON response
15930
+ * @returns True if the attestation has expirationBlock === '0'
15931
+ * @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
15932
+ *
15933
+ * @example
15934
+ * ```typescript
15935
+ * // After requesting re-attestation, use this to validate the response
15936
+ * const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
15937
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
15938
+ * ```
15939
+ *
15940
+ * @internal
15941
+ */ const isReAttestedAttestationResponse = (obj)=>{
15942
+ // First validate the basic structure and completion status
15943
+ // This will throw appropriate errors for invalid structure or incomplete attestation
15944
+ if (!isAttestationResponse(obj)) ;
15945
+ // Check if the first message has expirationBlock === '0'
15946
+ const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
15947
+ if (expirationBlock !== '0') {
15948
+ // Re-attestation not yet complete - allow retry via polling
15949
+ throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
15950
+ }
15951
+ return true;
15754
15952
  };
15755
15953
  /**
15756
- * Analyze bridge steps to determine retry feasibility and continuation point.
15757
- *
15758
- * This function examines the current state of bridge steps to determine the optimal
15759
- * continuation strategy. It uses a rule-based approach that makes it easy to extend
15760
- * with new flow patterns and step types in the future.
15761
- *
15762
- * The current analysis supports the standard CCTP flow:
15763
- * **Traditional flow**: Approve → Burn → FetchAttestation → Mint
15954
+ * Fetches attestation data and polls until expirationBlock === '0'.
15764
15955
  *
15765
- * Key features:
15766
- * - Rule-based transitions: Easy to extend with new step types and logic
15767
- * - Context-aware decisions: Considers execution history and step states
15768
- * - Actionable logic: Distinguishes between steps requiring user action vs waiting
15769
- * - Terminal states: Properly handles completion and non-actionable states
15956
+ * This function is used after calling requestReAttestation() to wait until
15957
+ * the attestation is fully re-processed. The expirationBlock transitions
15958
+ * from non-zero to zero when Circle completes the re-attestation.
15770
15959
  *
15771
- * @param bridgeResult - The bridge result containing step execution history.
15772
- * @returns Analysis result with continuation step and actionability information.
15773
- * @throws Error when bridgeResult is invalid or contains no steps array.
15960
+ * @param sourceDomainId - The CCTP domain ID of the source chain
15961
+ * @param transactionHash - The transaction hash to fetch attestation for
15962
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15963
+ * @param config - Optional configuration overrides
15964
+ * @returns The re-attested attestation response with expirationBlock === '0'
15965
+ * @throws If the request fails, times out, or expirationBlock never becomes 0
15774
15966
  *
15775
15967
  * @example
15776
15968
  * ```typescript
15777
- * import { analyzeSteps } from './analyzeSteps'
15778
- *
15779
- * // Failed approval step (requires user action)
15780
- * const bridgeResult = {
15781
- * steps: [
15782
- * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
15783
- * ]
15784
- * }
15969
+ * // After requesting re-attestation
15970
+ * await requestReAttestation(nonce, isTestnet)
15785
15971
  *
15786
- * const analysis = analyzeSteps(bridgeResult)
15787
- * // Result: { continuationStep: 'Approve', isRetryable: true,
15788
- * // reason: 'Retry failed approval' }
15972
+ * // Poll until expirationBlock becomes 0
15973
+ * const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
15974
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
15789
15975
  * ```
15976
+ */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
15977
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
15978
+ const effectiveConfig = mergeAttestationConfig(config);
15979
+ return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
15980
+ };
15981
+ /**
15982
+ * Builds the IRIS API URL for re-attestation requests.
15790
15983
  *
15791
- * @example
15792
- * ```typescript
15793
- * // Pending transaction (requires waiting, not actionable)
15794
- * const bridgeResult = {
15795
- * steps: [
15796
- * { name: 'Approve', state: 'pending' }
15797
- * ]
15798
- * }
15984
+ * Constructs the URL for Circle's re-attestation endpoint that allows
15985
+ * requesting a fresh attestation for an expired nonce.
15799
15986
  *
15800
- * const analysis = analyzeSteps(bridgeResult)
15801
- * // Result: { continuationStep: 'Approve', isRetryable: false,
15802
- * // reason: 'Continue pending approval' }
15803
- * ```
15987
+ * @param nonce - The nonce from the original attestation
15988
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
15989
+ * @returns A fully qualified URL string for the re-attestation endpoint
15804
15990
  *
15805
15991
  * @example
15806
15992
  * ```typescript
15807
- * // Completed bridge (nothing to do)
15808
- * const bridgeResult = {
15809
- * steps: [
15810
- * { name: 'Approve', state: 'success' },
15811
- * { name: 'Burn', state: 'success' },
15812
- * { name: 'FetchAttestation', state: 'success' },
15813
- * { name: 'Mint', state: 'success' }
15814
- * ]
15815
- * }
15993
+ * // Mainnet URL
15994
+ * const mainnetUrl = buildReAttestUrl('0xabc', false)
15995
+ * // => 'https://iris-api.circle.com/v2/reattest/0xabc'
15816
15996
  *
15817
- * const analysis = analyzeSteps(bridgeResult)
15818
- * // Result: { continuationStep: null, isRetryable: false,
15819
- * // reason: 'Bridge completed successfully' }
15997
+ * // Testnet URL
15998
+ * const testnetUrl = buildReAttestUrl('0xabc', true)
15999
+ * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
15820
16000
  * ```
15821
- */ const analyzeSteps = (bridgeResult)=>{
15822
- // Input validation
15823
- if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
15824
- throw new Error('Invalid bridgeResult: must contain a steps array');
15825
- }
15826
- const { steps } = bridgeResult;
15827
- // Build execution context from step history
15828
- const context = buildFlowContext(steps);
15829
- // Determine continuation logic using rule engine
15830
- const continuation = determineContinuationFromRules(context);
15831
- return {
15832
- continuationStep: continuation.nextStep,
15833
- isActionable: continuation.isActionable,
15834
- completedSteps: Array.from(context.completedSteps),
15835
- failedSteps: Array.from(context.failedSteps),
15836
- reason: continuation.reason
15837
- };
16001
+ */ const buildReAttestUrl = (nonce, isTestnet)=>{
16002
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
16003
+ const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
16004
+ return url.toString();
15838
16005
  };
15839
16006
  /**
15840
- * Build flow context from the execution history.
16007
+ * Type guard that validates the re-attestation API response structure.
15841
16008
  *
15842
- * @param steps - Array of executed bridge steps.
15843
- * @returns Flow context with execution state and history.
15844
- */ function buildFlowContext(steps) {
15845
- const completedSteps = new Set();
15846
- const failedSteps = new Set();
15847
- let lastStep;
15848
- // Process step history to build context
15849
- for (const step of steps){
15850
- if (step.state === 'success' || step.state === 'noop') {
15851
- completedSteps.add(step.name);
15852
- } else if (step.state === 'error') {
15853
- failedSteps.add(step.name);
15854
- }
15855
- // Track the last step for continuation logic
15856
- lastStep = {
15857
- name: step.name,
15858
- state: step.state
15859
- };
16009
+ * @param obj - The value to check, typically a parsed JSON response
16010
+ * @returns True if the object matches the ReAttestationResponse shape
16011
+ * @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
16012
+ * @internal
16013
+ */ const isReAttestationResponse = (obj)=>{
16014
+ if (typeof obj !== 'object' || obj === null || !('message' in obj) || !('nonce' in obj) || typeof obj.message !== 'string' || typeof obj.nonce !== 'string') {
16015
+ throw new Error('Invalid re-attestation response structure');
15860
16016
  }
15861
- return {
15862
- completedSteps,
15863
- failedSteps,
15864
- ...lastStep && {
15865
- lastStep
15866
- }
15867
- };
15868
- }
16017
+ return true;
16018
+ };
15869
16019
  /**
15870
- * Determine continuation step using the rule engine.
16020
+ * Requests re-attestation for an expired attestation nonce.
15871
16021
  *
15872
- * @param context - The flow context with execution history.
15873
- * @returns Continuation decision with next step and actionability information.
15874
- */ function determineContinuationFromRules(context) {
15875
- const lastStepName = context.lastStep?.name;
15876
- // Handle initial state when no steps have been executed
15877
- if (lastStepName === undefined) {
15878
- const rules = STEP_TRANSITION_RULES[''];
15879
- const matchingRule = rules?.find((rule)=>rule.condition(context));
15880
- if (!matchingRule) {
15881
- return {
15882
- nextStep: null,
15883
- isActionable: false,
15884
- reason: 'No initial state rule found'
15885
- };
15886
- }
15887
- return {
15888
- nextStep: matchingRule.nextStep,
15889
- isActionable: matchingRule.isActionable,
15890
- reason: matchingRule.reason
15891
- };
15892
- }
15893
- // A step with an empty name is ambiguous and should be treated as an unrecoverable state.
15894
- if (lastStepName === '') {
15895
- return {
15896
- nextStep: null,
15897
- isActionable: false,
15898
- reason: 'No transition rules defined for step with empty name'
15899
- };
15900
- }
15901
- const rules = STEP_TRANSITION_RULES[lastStepName];
15902
- if (!rules) {
15903
- return {
15904
- nextStep: null,
15905
- isActionable: false,
15906
- reason: `No transition rules defined for step: ${lastStepName}`
15907
- };
15908
- }
15909
- // Find the first matching rule
15910
- const matchingRule = rules.find((rule)=>rule.condition(context));
15911
- if (!matchingRule) {
15912
- return {
15913
- nextStep: null,
15914
- isActionable: false,
15915
- reason: `No matching transition rule for current context`
15916
- };
15917
- }
15918
- return {
15919
- nextStep: matchingRule.nextStep,
15920
- isActionable: matchingRule.isActionable,
15921
- reason: matchingRule.reason
15922
- };
15923
- }
15924
-
15925
- /**
15926
- * Find a step by name in the bridge result.
16022
+ * This function calls Circle's re-attestation API endpoint to request a fresh
16023
+ * attestation for a previously issued nonce. After calling this function,
16024
+ * you should poll `fetchAttestation` to retrieve the new attestation.
15927
16025
  *
15928
- * @param result - The bridge result to search.
15929
- * @param stepName - The name of the step to find.
15930
- * @returns The step if found, undefined otherwise.
16026
+ * @param nonce - The nonce from the original (expired) attestation
16027
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
16028
+ * @param config - Optional configuration overrides for the request
16029
+ * @returns The re-attestation response confirming the request was accepted
16030
+ * @throws If the request fails, times out, or returns invalid data
15931
16031
  *
15932
16032
  * @example
15933
16033
  * ```typescript
15934
- * import { findStepByName } from './findStep'
16034
+ * // Request re-attestation for an expired nonce
16035
+ * const response = await requestReAttestation('0xabc', true)
16036
+ * console.log(response.message) // "Re-attestation successfully requested for nonce."
15935
16037
  *
15936
- * const burnStep = findStepByName(result, 'burn')
15937
- * if (burnStep) {
15938
- * console.log('Burn tx:', burnStep.txHash)
15939
- * }
16038
+ * // After requesting re-attestation, poll for the new attestation
16039
+ * const attestation = await fetchAttestation(domainId, txHash, true)
15940
16040
  * ```
15941
- */ function findStepByName(result, stepName) {
15942
- return result.steps.find((step)=>step.name === stepName);
15943
- }
16041
+ */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
16042
+ const url = buildReAttestUrl(nonce, isTestnet);
16043
+ // Use minimal retries since we're just submitting a request, not polling for state
16044
+ const effectiveConfig = mergeAttestationConfig(config, {
16045
+ maxRetries: 3
16046
+ });
16047
+ return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
16048
+ };
16049
+
15944
16050
  /**
15945
- * Find a pending step by name and return it with its index.
15946
- *
15947
- * Searches for a step that matches both the step name and has a pending state.
16051
+ * Type guard that checks if the relayer has confirmed the mint transaction.
15948
16052
  *
15949
- * @param result - The bridge result containing steps to search through.
15950
- * @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
15951
- * @returns An object containing the step and its index in the steps array.
15952
- * @throws KitError if the specified pending step is not found.
16053
+ * This function validates that:
16054
+ * 1. The response has valid AttestationResponse structure
16055
+ * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
15953
16056
  *
15954
- * @example
15955
- * ```typescript
15956
- * import { findPendingStep } from './findStep'
16057
+ * If forwardState is 'FAILED', throws a non-retryable KitError.
16058
+ * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
15957
16059
  *
15958
- * const { step, index } = findPendingStep(result, 'burn')
15959
- * console.log('Pending step:', step.name, 'at index:', index)
15960
- * ```
15961
- */ function findPendingStep(result, stepName) {
15962
- const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
15963
- if (index === -1) {
16060
+ * @param obj - The value to check, typically a parsed JSON response
16061
+ * @returns True if the relayer has confirmed the mint
16062
+ * @throws {KitError} With FATAL recoverability if structure is invalid
16063
+ * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
16064
+ * @throws {KitError} With RETRYABLE recoverability if still pending
16065
+ * @internal
16066
+ */ const isRelayerMintConfirmed = (obj)=>{
16067
+ // First check if the structure is valid
16068
+ if (!hasValidAttestationStructure(obj)) {
15964
16069
  throw new KitError({
15965
16070
  ...InputError.VALIDATION_FAILED,
15966
16071
  recoverability: 'FATAL',
15967
- message: `Pending step "${stepName}" not found in result`
16072
+ message: 'Invalid attestation response structure from IRIS API.'
15968
16073
  });
15969
16074
  }
15970
- const step = result.steps[index];
15971
- if (!step) {
16075
+ // Find the first message (typically there's only one)
16076
+ const message = obj.messages[0];
16077
+ if (!message) {
15972
16078
  throw new KitError({
15973
16079
  ...InputError.VALIDATION_FAILED,
15974
16080
  recoverability: 'FATAL',
15975
- message: 'Pending step is undefined'
16081
+ message: 'No attestation messages found in IRIS API response.'
15976
16082
  });
15977
16083
  }
15978
- return {
15979
- step,
15980
- index
15981
- };
15982
- }
16084
+ // Check for FAILED state - this is a permanent failure
16085
+ if (message.forwardState === 'FAILED') {
16086
+ throw new KitError({
16087
+ ...NetworkError.RELAYER_FORWARD_FAILED,
16088
+ recoverability: 'RESUMABLE',
16089
+ message: 'Circle relayer failed to forward the mint transaction. The mint may still have succeeded if another party submitted it. Check the recipient wallet balance before retrying. If the mint did not occur, you can manually submit it using the attestation data in the error cause.',
16090
+ cause: {
16091
+ trace: {
16092
+ eventNonce: message.eventNonce,
16093
+ attestation: message.attestation,
16094
+ message: message.message
16095
+ }
16096
+ }
16097
+ });
16098
+ }
16099
+ // Check if mint is confirmed (or complete) with a valid transaction hash
16100
+ // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
16101
+ if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
16102
+ return true;
16103
+ }
16104
+ // Still pending or not yet processed - throw RETRYABLE error to continue polling
16105
+ throw new KitError({
16106
+ ...NetworkError.RELAYER_PENDING,
16107
+ recoverability: 'RETRYABLE',
16108
+ message: 'Relayer mint not ready. Waiting for confirmation.'
16109
+ });
16110
+ };
15983
16111
  /**
15984
- * Get the burn transaction hash from bridge result.
16112
+ * Polls the attestation API until the relayer's mint transaction is confirmed.
15985
16113
  *
15986
- * @param result - The bridge result.
15987
- * @returns The burn transaction hash, or undefined if not found.
16114
+ * This function is used when `useForwarder` is enabled. Instead of the user
16115
+ * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
16116
+ * This function polls until the relayer has submitted and confirmed the mint transaction.
16117
+ *
16118
+ * @remarks
16119
+ * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
16120
+ * - Throws immediately if `forwardState` is 'FAILED'
16121
+ * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
16122
+ * - Returns the attestation message with `forwardTxHash` populated
16123
+ *
16124
+ * @param sourceDomainId - The CCTP domain ID of the source chain
16125
+ * @param transactionHash - The transaction hash of the burn operation
16126
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
16127
+ * @param config - Optional configuration overrides for polling behavior
16128
+ * @returns The attestation message with confirmed forwardTxHash
16129
+ * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
16130
+ * @throws {KitError} If timeout is reached while still pending
15988
16131
  *
15989
16132
  * @example
15990
16133
  * ```typescript
15991
- * import { getBurnTxHash } from './findStep'
15992
- *
15993
- * const burnTxHash = getBurnTxHash(result)
15994
- * if (burnTxHash) {
15995
- * console.log('Burn tx hash:', burnTxHash)
15996
- * }
16134
+ * const attestation = await fetchRelayerMint(0, '0xabc...', false)
16135
+ * console.log('Relayer mint tx:', attestation.forwardTxHash)
15997
16136
  * ```
15998
- */ function getBurnTxHash(result) {
15999
- return findStepByName(result, CCTPv2StepName.burn)?.txHash;
16000
- }
16137
+ */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
16138
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
16139
+ const effectiveConfig = mergeAttestationConfig(config);
16140
+ let response;
16141
+ try {
16142
+ response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
16143
+ } catch (error) {
16144
+ // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
16145
+ if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
16146
+ throw new KitError({
16147
+ ...NetworkError.RELAYER_FORWARD_FAILED,
16148
+ recoverability: error.recoverability,
16149
+ message: error.message,
16150
+ cause: {
16151
+ ...error.cause,
16152
+ trace: {
16153
+ ...error.cause?.trace,
16154
+ burnTxHash: transactionHash
16155
+ }
16156
+ }
16157
+ });
16158
+ }
16159
+ throw error;
16160
+ }
16161
+ // Return the first message (which should have forwardTxHash)
16162
+ // Note: This check is needed for TypeScript type safety even though
16163
+ // isRelayerMintConfirmed validates messages[0] exists. The type guard
16164
+ // narrows the type at the call site, but TypeScript can't infer that
16165
+ // the array still has elements after pollApiGet returns.
16166
+ const message = response.messages[0];
16167
+ if (!message) {
16168
+ throw new KitError({
16169
+ ...InputError.VALIDATION_FAILED,
16170
+ recoverability: 'FATAL',
16171
+ message: 'No attestation messages found in response after polling.'
16172
+ });
16173
+ }
16174
+ return message;
16175
+ };
16176
+
16177
+ const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
16001
16178
  /**
16002
- * Get the attestation data from bridge result.
16179
+ * Asserts that the provided parameters match the CCTPv2 wallet context interface.
16180
+ * The validation includes:
16181
+ * - Basic wallet context validation (adapter, address, chain)
16182
+ * - CCTPv2-specific chain validation (must be an EVM chain)
16003
16183
  *
16004
- * @param result - The bridge result.
16005
- * @returns The attestation data, or undefined if not found.
16184
+ * @param params - The parameters to validate
16185
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
16006
16186
  *
16007
16187
  * @example
16008
16188
  * ```typescript
16009
- * import { getAttestationData } from './findStep'
16189
+ * import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
16190
+ * import { Ethereum } from '@core/chains'
16010
16191
  *
16011
- * const attestation = getAttestationData(result)
16012
- * if (attestation) {
16013
- * console.log('Attestation:', attestation.message)
16192
+ * // Prepare wallet context
16193
+ * const context = {
16194
+ * adapter: {
16195
+ * prepare: async () => ({ data: 'prepared transaction' }),
16196
+ * waitForTransaction: async () => ({ status: 'confirmed' })
16197
+ * },
16198
+ * address: '0x1234567890123456789012345678901234567890',
16199
+ * chain: {
16200
+ * ...Ethereum,
16201
+ * usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
16202
+ * cctp: {
16203
+ * domain: 1,
16204
+ * contracts: {
16205
+ * v2: {
16206
+ * tokenMessenger: '0xTokenMessenger',
16207
+ * messageTransmitter: '0xMessageTransmitter'
16208
+ * }
16209
+ * }
16210
+ * }
16211
+ * }
16014
16212
  * }
16213
+ *
16214
+ * // This will throw if validation fails
16215
+ * assertCCTPv2WalletContext(context)
16216
+ *
16217
+ * // If we get here, context is guaranteed to be valid
16218
+ * console.log('CCTPv2 wallet context is valid')
16015
16219
  * ```
16016
- */ function getAttestationData(result) {
16017
- // Prefer reAttest data (most recent attestation after expiry)
16018
- const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
16019
- if (reAttestStep?.state === 'success' && reAttestStep.data) {
16020
- return reAttestStep.data;
16220
+ */ function assertCCTPv2WalletContext(params) {
16221
+ // First validate basic wallet context
16222
+ validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
16223
+ // After validation, we know params is WalletContext
16224
+ const context = params;
16225
+ // Validate USDC support
16226
+ if (context.chain.usdcAddress === null) {
16227
+ throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
16228
+ }
16229
+ // Validate CCTPv2 support
16230
+ if (!isCCTPV2Supported(context.chain)) {
16231
+ throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
16021
16232
  }
16022
- // Fall back to fetchAttestation step
16023
- const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
16024
- return fetchStep?.data;
16025
16233
  }
16026
16234
 
16235
+ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16027
16236
  /**
16028
- * Check if the analysis indicates a non-actionable pending state.
16029
- *
16030
- * A pending state is non-actionable when there's a continuation step but
16031
- * the analysis marks it as not actionable, typically because we need to
16032
- * wait for an ongoing operation to complete.
16237
+ * Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
16238
+ * The validation includes:
16239
+ * - Basic parameter structure and types
16240
+ * - Amount validation (non-empty numeric string \> 0)
16241
+ * - Wallet address format validation (must be valid Ethereum address)
16242
+ * - Chain definition validation (must be a valid chain with required properties)
16243
+ * - Adapter validation (must implement required methods)
16244
+ * - Optional config validation (transfer speed and max fee)
16245
+ * - Network compatibility (source and destination chains must both be testnet or both mainnet)
16246
+ * - CCTPv2-specific wallet context validations
16033
16247
  *
16034
- * @param analysis - The step analysis result from analyzeSteps.
16035
- * @param result - The bridge result to check for pending steps.
16036
- * @returns True if there is a pending step that we should wait for.
16248
+ * @param params - The parameters to validate
16249
+ * @throws {KitError} If validation fails, with details about which properties failed
16037
16250
  *
16038
16251
  * @example
16039
16252
  * ```typescript
16040
- * import { hasPendingState } from './stepUtils'
16041
- * import { analyzeSteps } from '../analyzeSteps'
16253
+ * import { assertCCTPv2BridgeParams } from '@circle-fin/provider-cctp-v2'
16254
+ * import { Ethereum, Base } from '@core/chains'
16042
16255
  *
16043
- * const analysis = analyzeSteps(bridgeResult)
16044
- * if (hasPendingState(analysis, bridgeResult)) {
16045
- * // Wait for the pending operation to complete
16046
- * }
16047
- * ```
16048
- */ /**
16049
- * Evaluate a transaction receipt and return the corresponding step state
16050
- * and error message. Centralises the success/revert/unconfirmed logic so
16051
- * every call-site behaves identically.
16256
+ * // Prepare transfer parameters
16257
+ * const params = {
16258
+ * amount: '100.50',
16259
+ * source: {
16260
+ * adapter: sourceAdapter,
16261
+ * address: '0xSourceAddress',
16262
+ * chain: {
16263
+ * ...Ethereum,
16264
+ * cctp: {
16265
+ * domain: 1,
16266
+ * contracts: {
16267
+ * v2: {
16268
+ * tokenMessenger: '0xTokenMessenger',
16269
+ * messageTransmitter: '0xMessageTransmitter'
16270
+ * }
16271
+ * }
16272
+ * }
16273
+ * }
16274
+ * },
16275
+ * destination: {
16276
+ * adapter: destAdapter,
16277
+ * address: '0xDestAddress',
16278
+ * chain: {
16279
+ * ...Base,
16280
+ * cctp: {
16281
+ * domain: 2,
16282
+ * contracts: {
16283
+ * v2: {
16284
+ * tokenMessenger: '0xTokenMessenger',
16285
+ * messageTransmitter: '0xMessageTransmitter'
16286
+ * }
16287
+ * }
16288
+ * }
16289
+ * }
16290
+ * },
16291
+ * token: 'USDC',
16292
+ * config: {
16293
+ * transferSpeed: 'FAST',
16294
+ * maxFee: '1000000'
16295
+ * }
16296
+ * }
16052
16297
  *
16053
- * @param receipt - The transaction receipt containing status and block info.
16054
- * @param txHash - The transaction hash used in error messages.
16055
- * @returns An object with `state` and an optional `errorMessage`.
16298
+ * // This will throw if validation fails
16299
+ * assertCCTPv2BridgeParams(params)
16056
16300
  *
16057
- * @example
16058
- * ```typescript
16059
- * const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
16060
- * step.state = outcome.state
16061
- * if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
16301
+ * // If we get here, params is guaranteed to be valid
16302
+ * console.log('CCTPv2 transfer parameters are valid')
16062
16303
  * ```
16063
- */ function evaluateTransactionOutcome(receipt, txHash) {
16064
- if (receipt.status === 'success' && receipt.blockNumber) {
16065
- return {
16066
- state: 'success'
16067
- };
16304
+ */ function assertCCTPv2BridgeParams(params) {
16305
+ // First validate basic bridge params
16306
+ validateWithStateTracking(params, bridgeParamsSchema, 'CCTPv2 bridge parameters', assertCCTPv2BridgeParamsSymbol);
16307
+ // After validation, we know params is CCTPV2BridgeParams
16308
+ const bridgeParams = params;
16309
+ // Enforce that source and destination chains are either testnet or mainnet
16310
+ if (bridgeParams.source.chain.isTestnet !== bridgeParams.destination.chain.isTestnet) {
16311
+ throw createNetworkMismatchError(bridgeParams.source.chain, bridgeParams.destination.chain);
16068
16312
  }
16069
- return {
16070
- state: 'error',
16071
- errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
16072
- };
16073
- }
16074
- function hasPendingState(analysis, result) {
16075
- // Check if there's a continuation step that's marked as non-actionable
16076
- if (analysis.continuationStep === null || analysis.isActionable) {
16077
- return false;
16313
+ assertCCTPV2Support(bridgeParams.source.chain, bridgeParams.destination.chain);
16314
+ // Validate that the destination chain supports forwarding when forwarder is enabled
16315
+ assertForwarderRouteSupport$1(bridgeParams.source.chain, bridgeParams.destination.chain, bridgeParams.destination.useForwarder);
16316
+ /**
16317
+ * Enforce that if fee is defined then feeRecipient must be defined.
16318
+ * We do not do this in the validation function itself because we want to allow
16319
+ * optional properties when calling `provider.bridge()` due to the custom fee
16320
+ * configuration being possible at the kit level as well.
16321
+ */ if (bridgeParams.config?.customFee?.value !== undefined && bridgeParams.config?.customFee?.recipientAddress === undefined) {
16322
+ throw createValidationFailedError$1('recipientAddress', bridgeParams.config.customFee.value, 'Custom fee is defined but fee recipient is not. Please provide a fee recipient.');
16323
+ }
16324
+ // Check if this is a forwarder-only destination (no adapter, requires useForwarder: true)
16325
+ const isForwarderOnly = bridgeParams.destination.useForwarder === true && !('adapter' in bridgeParams.destination && bridgeParams.destination.adapter);
16326
+ // Forwarder-only destinations require recipientAddress
16327
+ if (isForwarderOnly) {
16328
+ if (!bridgeParams.destination.recipientAddress?.trim()) {
16329
+ throw createValidationFailedError$1('recipientAddress', bridgeParams.destination.recipientAddress, 'recipientAddress is required when using forwarder without a destination adapter.');
16330
+ }
16331
+ }
16332
+ // Validate CCTP v2 specific requirements for source wallet
16333
+ assertCCTPv2WalletContext(bridgeParams.source);
16334
+ // Validate that source adapter supports the chain (defense-in-depth)
16335
+ bridgeParams.source.adapter.validateChainSupport(bridgeParams.source.chain);
16336
+ // Only validate destination wallet context and adapter if not forwarder-only
16337
+ if (!isForwarderOnly) {
16338
+ assertCCTPv2WalletContext(bridgeParams.destination);
16339
+ // Validate that destination adapter supports the chain (defense-in-depth)
16340
+ bridgeParams.destination.adapter.validateChainSupport(bridgeParams.destination.chain);
16078
16341
  }
16079
- // Verify that the continuation step actually exists and is in pending state
16080
- const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
16081
- return pendingStep !== undefined;
16082
16342
  }
16083
16343
  /**
16084
- * Check if the step is the last one in the execution flow.
16085
- *
16086
- * @param step - The step object to check.
16087
- * @param stepNames - The ordered list of step names in the execution flow.
16088
- * @returns True if this is the last step in the flow.
16089
- *
16090
- * @example
16091
- * ```typescript
16092
- * import { isLastStep } from './stepUtils'
16344
+ * Validate CCTP v2 support on both chains
16345
+ */ /**
16346
+ * Throws a KitError if the given chain does not support CCTP v2.
16093
16347
  *
16094
- * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
16095
- * isLastStep({ name: 'mint' }, stepNames) // true
16096
- * isLastStep({ name: 'burn' }, stepNames) // false
16097
- * ```
16098
- */ function isLastStep(step, stepNames) {
16099
- const stepIndex = stepNames.indexOf(step.name);
16100
- return stepIndex === -1 || stepIndex >= stepNames.length - 1;
16348
+ * @param chain - The chain to check for CCTP v2 support
16349
+ * @param otherChain - The other chain in the route (for error context)
16350
+ * @param isSource - Whether this is the source chain (for error context)
16351
+ */ function assertCCTPV2Support(source, destination) {
16352
+ if (!isCCTPV2Supported(source) || !isCCTPV2Supported(destination)) {
16353
+ throw createUnsupportedRouteError(source.name, destination.name);
16354
+ }
16101
16355
  }
16102
16356
  /**
16103
- * Wait for a pending transaction to complete.
16104
- *
16105
- * Poll the adapter until the transaction is confirmed on-chain and return
16106
- * the updated step with success or error state based on the receipt.
16107
- *
16108
- * @param pendingStep - The full step object containing the transaction hash.
16109
- * @param adapter - The adapter to use for waiting.
16110
- * @param chain - The chain where the transaction was submitted.
16111
- * @returns The updated step object with success or error state.
16112
- *
16113
- * @throws KitError when the pending step has no transaction hash.
16357
+ * Validates that the forwarder (relaying) feature is compatible with the route.
16114
16358
  *
16115
- * @example
16116
- * ```typescript
16117
- * import { waitForPendingTransaction } from './bridgeStepUtils'
16359
+ * Checks the destination chain's `cctp.forwarderSupported.destination` property
16360
+ * to determine whether the chain supports receiving forwarded transfers.
16118
16361
  *
16119
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
16120
- * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
16121
- * // updatedStep.state is now 'success' or 'error'
16122
- * ```
16123
- */ async function waitForPendingTransaction(pendingStep, adapter, chain) {
16124
- if (!pendingStep.txHash) {
16362
+ * @param source - The source chain definition
16363
+ * @param destination - The destination chain definition
16364
+ * @param useForwarder - Whether the forwarder is enabled on the destination
16365
+ * @throws {KitError} If the forwarder is enabled and the destination chain does not support forwarding
16366
+ */ function assertForwarderRouteSupport$1(source, destination, useForwarder) {
16367
+ if (useForwarder === true && !destination.cctp?.forwarderSupported.destination) {
16125
16368
  throw new KitError({
16126
- ...InputError.VALIDATION_FAILED,
16369
+ ...InputError.UNSUPPORTED_ROUTE,
16127
16370
  recoverability: 'FATAL',
16128
- message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
16371
+ message: `Route from ${source.name} to ${destination.name} with forwarder is not supported (destination chain does not support forwarding).`,
16372
+ cause: {
16373
+ trace: {
16374
+ source: source.name,
16375
+ destination: destination.name
16376
+ }
16377
+ }
16129
16378
  });
16130
16379
  }
16131
- const txHash = pendingStep.txHash;
16132
- const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
16133
- isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
16134
- chain: chain.name,
16135
- txHash
16136
- }))
16137
- });
16138
- const outcome = evaluateTransactionOutcome(txReceipt, txHash);
16139
- return {
16140
- ...pendingStep,
16141
- state: outcome.state,
16142
- data: txReceipt,
16143
- explorerUrl: buildExplorerUrl(chain, txHash),
16144
- ...outcome.errorMessage ? {
16145
- errorMessage: outcome.errorMessage
16146
- } : {}
16147
- };
16148
16380
  }
16381
+
16149
16382
  /**
16150
- * Wait for a pending step to complete.
16151
- *
16152
- * For transaction steps: waits for the transaction to be confirmed.
16153
- * For attestation: re-executes the attestation fetch.
16154
- *
16155
- * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
16156
- * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
16157
- * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
16158
- * @param adapter - The adapter to use.
16159
- * @param chain - The chain where the step is executing.
16160
- * @param context - The retry context.
16161
- * @param result - The bridge result.
16162
- * @param provider - The CCTP v2 bridging provider.
16163
- * @returns The resolved step object with updated state.
16164
- *
16165
- * @throws KitError when fetching attestation but burn transaction hash is not found.
16383
+ * Checks if a decoded attestation field matches the corresponding transfer parameter.
16384
+ * If the values do not match, appends a descriptive error message to the errors array.
16166
16385
  *
16167
- * @example
16168
- * ```typescript
16169
- * import { waitForStepToComplete } from './bridgeStepUtils'
16386
+ * @param field - The name of the field being compared (for error reporting)
16387
+ * @param decoded - The value decoded from the attestation message
16388
+ * @param param - The expected value from the transfer parameters
16389
+ * @param errors - The array to which error messages will be appended if a mismatch is found
16390
+ */ function checkFieldMismatch(field, decoded, param, errors) {
16391
+ if (decoded !== param) {
16392
+ errors.push(`${field} mismatch: decoded=${String(decoded)}, params=${String(param)}`);
16393
+ }
16394
+ }
16395
+ /**
16396
+ * Asserts that the decoded message from attestation matches the provided transfer params.
16397
+ * Throws KitError if any field mismatches, with clear error messages.
16170
16398
  *
16171
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
16172
- * const updatedStep = await waitForStepToComplete(
16173
- * pendingStep,
16174
- * adapter,
16175
- * chain,
16176
- * context,
16177
- * result,
16178
- * provider,
16179
- * )
16180
- * // updatedStep.state is now 'success' or 'error'
16181
- * ```
16182
- */ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
16183
- if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
16184
- // For attestation, re-run the fetch (it has built-in polling)
16185
- const burnTxHash = getBurnTxHash(result);
16186
- if (!burnTxHash) {
16187
- throw new KitError({
16188
- ...InputError.VALIDATION_FAILED,
16189
- recoverability: 'FATAL',
16190
- message: 'Cannot fetch attestation: burn transaction hash not found'
16191
- });
16399
+ * @param attestation - The attestation message containing the decoded message
16400
+ * @param params - The transfer parameters to validate against
16401
+ * @throws {@link KitError} If any field mismatches
16402
+ */ async function assertCCTPv2AttestationParams(attestation, params) {
16403
+ const errors = [];
16404
+ const message = attestation.decodedMessage;
16405
+ const messageBody = message.decodedMessageBody;
16406
+ // Use recipientAddress if provided, otherwise use destination.address
16407
+ const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
16408
+ const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
16409
+ let sender;
16410
+ if (hasCustomContractSupport(params.source.chain, 'bridge')) {
16411
+ if (params.source.chain.type === 'solana') {
16412
+ // Solana: User Bridge contract → CCTP (user remains sender)
16413
+ sender = params.source.address;
16414
+ } else {
16415
+ // Other chains (like EVM): Bridge contract → CCTP (bridge contract becomes sender)
16416
+ sender = params.source.chain.kitContracts?.bridge;
16192
16417
  }
16193
- const sourceAddress = result.source.address;
16194
- const attestation = await provider.fetchAttestation({
16195
- chain: result.source.chain,
16196
- adapter: context.from,
16197
- address: sourceAddress
16198
- }, burnTxHash);
16199
- return {
16200
- ...pendingStep,
16201
- state: 'success',
16202
- data: attestation
16203
- };
16418
+ } else {
16419
+ sender = params.source.address;
16420
+ }
16421
+ checkFieldMismatch('sourceDomain', message.sourceDomain, params.source.chain.cctp.domain.toString(), errors);
16422
+ checkFieldMismatch('destinationDomain', message.destinationDomain, params.destination.chain.cctp.domain.toString(), errors);
16423
+ checkFieldMismatch('minFinalityThreshold', message.minFinalityThreshold, CCTPv2MinFinalityThreshold[params.config.transferSpeed ?? 'FAST'].toString(), errors);
16424
+ checkFieldMismatch('sender', params.source.chain.type === 'evm' ? messageBody.messageSender.toLowerCase() : messageBody.messageSender, params.source.chain.type === 'evm' ? sender?.toLowerCase() : sender, errors);
16425
+ checkFieldMismatch('recipient', params.destination.chain.type === 'evm' ? messageBody.mintRecipient.toLowerCase() : messageBody.mintRecipient, params.destination.chain.type === 'evm' ? mintRecipient.toLowerCase() : mintRecipient, errors);
16426
+ checkFieldMismatch('amount', messageBody.amount, params.amount.toString(), errors);
16427
+ checkFieldMismatch('burnToken', messageBody.burnToken.toLowerCase(), params.source.chain.usdcAddress.toLowerCase(), errors);
16428
+ if (errors.length > 0) {
16429
+ const errorMessage = 'Attestation validation failed: received attestation does not match expected transfer parameters';
16430
+ const firstError = errors[0] ?? '';
16431
+ throw new KitError({
16432
+ ...InputError.VALIDATION_FAILED,
16433
+ recoverability: 'FATAL',
16434
+ message: `${errorMessage}: ${firstError}`,
16435
+ cause: {
16436
+ trace: {
16437
+ validationErrors: errors
16438
+ }
16439
+ }
16440
+ });
16204
16441
  }
16205
- // For transaction steps, wait for the transaction to complete
16206
- return waitForPendingTransaction(pendingStep, adapter, chain);
16207
16442
  }
16208
16443
 
16209
16444
  /**
16210
- * Multiplier applied to a successful gas estimate before it is submitted.
16211
- *
16212
- * Estimates are exact, not padded: Sei returns 109_739 for an approve that
16213
- * consumes 107_717 (1.9% headroom). Chains that price storage in large steps
16214
- * can exceed the estimate if state changes between estimation and inclusion,
16215
- * so the estimate is padded before use.
16445
+ * Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
16216
16446
  *
16217
- * @remarks
16218
- * This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
16219
- * scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
16220
- * covers a slot that exists at estimation time and is consumed before
16221
- * inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
16222
- * the estimate covers it. For burn the buffer does cover a step (25% of
16223
- * ~300_000 exceeds 51_500).
16224
- */ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
16225
- /**
16226
- * Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
16447
+ * Validates the full public-boundary input before any field destructuring,
16448
+ * `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
16449
+ * inputs always produce typed `KitError` validation failures.
16227
16450
  *
16228
- * Estimates first so chains whose real cost exceeds the floor are covered by
16229
- * their own measurement, and falls back to the floor whenever estimation is
16230
- * unavailable or under-reports. Estimation failure is never fatal here: before
16231
- * floors existed these requests were submitted with a pinned limit and no
16232
- * estimate at all, so degrading to the floor is never worse than the previous
16233
- * behaviour.
16451
+ * Checks performed (in order):
16452
+ * - `params` must be a non-null plain object
16453
+ * - `source` valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
16454
+ * - `destinationChain` present and supports CCTP v2
16455
+ * - source and destination chains must both be testnet or both mainnet
16456
+ * - source and destination chains must differ
16457
+ * - `executor` — non-empty string
16458
+ * - `amount` — bigint or non-empty string coercible to bigint
16459
+ * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
16460
+ * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
16461
+ * - `claim.signedQuote` — valid `0x`-prefixed hex string
16462
+ * - `claim.refundAddress` — valid EVM address
16463
+ * - `hookData` — valid `0x`-prefixed hex string when present
16234
16464
  *
16235
- * @param request - The prepared EVM request to size a gas limit for
16236
- * @param gasFloor - The minimum gas limit to submit, in gas units
16237
- * @returns The gas limit to submit, in gas units
16238
- * @throws Never — estimation failures degrade to `gasFloor`
16465
+ * @param params - The value to validate.
16466
+ * @throws {KitError} If any field is missing or invalid.
16239
16467
  *
16240
16468
  * @example
16241
16469
  * ```typescript
16242
- * const gasLimit = await resolveGasLimit(request, 150_000)
16470
+ * assertBurnWithFeesParams(params)
16471
+ * // params is now typed as BurnWithFeesParams and safe to use
16472
+ * const { source, destinationChain, amount } = params
16243
16473
  * ```
16244
- */ const resolveGasLimit = async (request, gasFloor)=>{
16474
+ */ function assertBurnWithFeesParams(params) {
16475
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
16476
+ throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
16477
+ }
16478
+ const p = params;
16479
+ // Source wallet context
16480
+ assertCCTPv2WalletContext(p['source']);
16481
+ const source = p['source'];
16482
+ // destinationChain
16483
+ const destinationChain = p['destinationChain'];
16484
+ if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
16485
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
16486
+ }
16487
+ if (!isCCTPV2Supported(destinationChain)) {
16488
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
16489
+ }
16490
+ const dest = destinationChain;
16491
+ // Testnet / mainnet mismatch
16492
+ if (source.chain.isTestnet !== dest.isTestnet) {
16493
+ throw createNetworkMismatchError(source.chain, dest);
16494
+ }
16495
+ // Same-chain guard
16496
+ if (source.chain.name === dest.name) {
16497
+ throw createUnsupportedRouteError(source.chain.name, dest.name);
16498
+ }
16499
+ // executor
16500
+ const executor = p['executor'];
16501
+ if (typeof executor !== 'string' || executor === '') {
16502
+ throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
16503
+ }
16504
+ // amount
16505
+ const rawAmount = p['amount'];
16506
+ if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
16507
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
16508
+ }
16245
16509
  try {
16246
- // Deliberately called without a `fallback`: both the viem and ethers
16247
- // adapters *return* the supplied fallback object when estimation reverts
16248
- // rather than throwing, which would set the estimate to the floor and then
16249
- // multiply it by the buffer below. Omitting it routes reverts through the
16250
- // catch, so a failed estimate degrades to exactly the floor.
16251
- const estimate = await request.estimate();
16252
- // The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
16253
- // typed `bigint`, but adapters are a public extension point and may be
16254
- // implemented in plain JS, so a non-bigint `gas` would throw here
16255
- // ("Cannot mix BigInt and other types"). Guarding it keeps the documented
16256
- // contract — estimation never aborts a step, it degrades to the floor.
16257
- const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
16258
- // Convert before comparing: Math.max throws on BigInt operands, and gas
16259
- // units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
16260
- return Math.max(Number(buffered), gasFloor);
16510
+ BigInt(rawAmount);
16261
16511
  } catch {
16262
- // Estimation is best-effort; the floor is the known-safe value.
16263
- return gasFloor;
16512
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
16513
+ }
16514
+ // feeTotalAmount
16515
+ const rawFee = p['feeTotalAmount'];
16516
+ if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
16517
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
16264
16518
  }
16265
- };
16266
- /**
16267
- * Executes a prepared chain request and returns the result as a bridge step.
16268
- *
16269
- * This function takes a prepared chain request (containing transaction data) and executes
16270
- * it using the appropriate adapter. It handles the execution details and formats
16271
- * the result as a standardized bridge step with transaction details and explorer URLs.
16272
- *
16273
- * @param params - The execution parameters containing:
16274
- * - `name`: The name of the step
16275
- * - `request`: The prepared chain request containing transaction data
16276
- * - `adapter`: The adapter that will execute the transaction
16277
- * - `confirmations`: The number of confirmations to wait for (defaults to 1)
16278
- * - `timeout`: The timeout for the request in milliseconds
16279
- * - `gasFloor`: Optional minimum gas limit (number); the request is submitted
16280
- * with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
16281
- * @returns The bridge step with the transaction details and explorer URL
16282
- * @throws If the transaction execution fails
16283
- *
16284
- * @example
16285
- * ```typescript
16286
- * const step = await executePreparedChainRequest({
16287
- * name: 'approve',
16288
- * request: preparedRequest,
16289
- * adapter: adapter,
16290
- * confirmations: 2,
16291
- * timeout: 30000
16292
- * })
16293
- * console.log('Transaction hash:', step.txHash)
16294
- * ```
16295
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
16296
- const step = {
16297
- name,
16298
- state: 'pending'
16299
- };
16300
16519
  try {
16301
- /**
16302
- * No-op requests are not executed.
16303
- * We return a noop step instead.
16304
- */ if (request.type === 'noop') {
16305
- step.state = 'noop';
16306
- return step;
16307
- }
16308
- const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
16309
- gasLimit: await resolveGasLimit(request, gasFloor)
16310
- }) : await request.execute();
16311
- step.txHash = txHash;
16312
- const retryOptions = {
16313
- isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
16314
- chain: chain.name,
16315
- txHash
16316
- }))
16317
- };
16318
- if (timeout !== undefined) {
16319
- retryOptions.deadlineMs = Date.now() + timeout;
16320
- }
16321
- const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
16322
- confirmations,
16323
- timeout
16324
- }, chain), retryOptions);
16325
- const outcome = evaluateTransactionOutcome(transaction, txHash);
16326
- step.state = outcome.state;
16327
- step.data = transaction;
16328
- // Generate explorer URL for the step
16329
- step.explorerUrl = buildExplorerUrl(chain, txHash);
16330
- if (outcome.errorMessage) {
16331
- step.errorMessage = outcome.errorMessage;
16332
- // Transaction was mined but reverted on-chain.
16333
- step.errorCategory = 'chain_revert';
16334
- }
16335
- } catch (err) {
16336
- step.state = 'error';
16337
- step.error = err;
16338
- // Sequential path does not yet attempt fine-grained classification of
16339
- // pre-submission errors (user_rejected, capability errors, etc.). Mark
16340
- // as `unknown` so consumers can at least detect the category is
16341
- // populated uniformly across batched and sequential flows.
16342
- step.errorCategory = 'unknown';
16343
- // Optionally parse for common blockchain error formats
16344
- if (err instanceof Error) {
16345
- step.errorMessage = err.message;
16346
- } else if (typeof err === 'object' && err != null && 'message' in err) {
16347
- step.errorMessage = String(err.message);
16348
- } else {
16349
- step.errorMessage = `Unknown error occurred during ${name} step.`;
16350
- }
16520
+ BigInt(rawFee);
16521
+ } catch {
16522
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
16523
+ }
16524
+ // feeToken
16525
+ if (!evmAddressSchema.safeParse(p['feeToken']).success) {
16526
+ throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
16527
+ }
16528
+ // claim
16529
+ const rawClaim = p['claim'];
16530
+ if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
16531
+ throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
16532
+ }
16533
+ const claim = rawClaim;
16534
+ if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
16535
+ throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
16536
+ }
16537
+ if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
16538
+ throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
16539
+ }
16540
+ // hookData (optional)
16541
+ const hookData = p['hookData'];
16542
+ if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
16543
+ throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
16351
16544
  }
16352
- return step;
16353
16545
  }
16354
16546
 
16355
16547
  /**
@@ -16380,7 +16572,7 @@ function hasPendingState(analysis, result) {
16380
16572
  adapter: params.source.adapter,
16381
16573
  chain: params.source.chain,
16382
16574
  request: await provider.approve(params.source, approvalAmount),
16383
- gasFloor: Number(APPROVE_GAS_LIMIT_EVM)
16575
+ gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.approve)
16384
16576
  });
16385
16577
  }
16386
16578
 
@@ -16408,7 +16600,7 @@ function hasPendingState(analysis, result) {
16408
16600
  adapter: params.source.adapter,
16409
16601
  chain: params.source.chain,
16410
16602
  request: await provider.burn(params),
16411
- gasFloor: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
16603
+ gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.burn)
16412
16604
  });
16413
16605
  }
16414
16606
 
@@ -16504,7 +16696,7 @@ function hasPendingState(analysis, result) {
16504
16696
  // eth_estimateGas does not account for, returning a below-floor value
16505
16697
  // without reverting. The floor covers those; chains that cost more than the
16506
16698
  // floor are covered by their own estimate.
16507
- gasFloor: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
16699
+ gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.mint)
16508
16700
  });
16509
16701
  // Add forwarded: false for non-relayer mints
16510
16702
  return {
@@ -17027,7 +17219,7 @@ const mockAttestationMessage = {
17027
17219
  return step;
17028
17220
  }
17029
17221
 
17030
- var version$4 = "1.10.2";
17222
+ var version$4 = "1.11.0";
17031
17223
  var pkg$4 = {
17032
17224
  version: version$4};
17033
17225
 
@@ -17986,10 +18178,39 @@ function assertCCTPV2Config(config) {
17986
18178
  // CCTP-specific transfer params validation (includes base validation)
17987
18179
  assertCCTPv2BridgeParams(params);
17988
18180
  const { source, destination, amount } = params;
17989
- const estimateBurn = async ()=>{
17990
- const burn = await this.burn(params);
17991
- return await burn.estimate(undefined, await source.adapter.calculateTransactionFee(hasCustomContractSupport(source.chain, 'bridge') ? CUSTOM_BURN_GAS_ESTIMATE_EVM : DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM, undefined, source.chain));
18181
+ /**
18182
+ * Price the gas a step will RESERVE on-chain, not what it will spend.
18183
+ *
18184
+ * A transaction is only admitted when the sender holds
18185
+ * `gasLimit * gasPrice`, and `executePreparedChainRequest` submits
18186
+ * `max(estimate * buffer, floor)`. Quoting a bare `estimate()` therefore
18187
+ * under-reports what a wallet must hold to send at all: the floor governs
18188
+ * on virtually every chain, which put the burn quote ~2.5x below the real
18189
+ * requirement and left anyone funding from it unable to submit.
18190
+ *
18191
+ * Delegates to the same `resolveGasLimit` the execute path calls, so a
18192
+ * quote and the limit later submitted for that step cannot drift apart.
18193
+ * That also means an estimation failure degrades to the floor here exactly
18194
+ * as it does on execution, rather than surfacing as a failed quote — the
18195
+ * floor is what would be submitted, so it is the honest number to quote.
18196
+ *
18197
+ * Non-EVM requests are unchanged: `executePreparedChainRequest` only
18198
+ * applies a floor when `request.type === 'evm'`, so there is no reserved
18199
+ * limit to quote on other chains.
18200
+ */ const quoteReservedGas = async (request, gasFloor, priceGas, // Ignored on EVM — only evaluated and used on non-EVM paths.
18201
+ nonEvmFallbackGasEstimate)=>{
18202
+ if (request.type !== 'evm') {
18203
+ return nonEvmFallbackGasEstimate === undefined ? await request.estimate() : await request.estimate(undefined, await priceGas(nonEvmFallbackGasEstimate()));
18204
+ }
18205
+ // resolveGasLimit returns number; gas units are well below Number.MAX_SAFE_INTEGER,
18206
+ // so the Number() → resolveGasLimit → BigInt() round-trip is lossless.
18207
+ return await priceGas(BigInt(await resolveGasLimit(request, Number(gasFloor))));
17992
18208
  };
18209
+ const priceGasFor = (ctx)=>async (gasUnits)=>await ctx.adapter.calculateTransactionFee(gasUnits, undefined, ctx.chain);
18210
+ const priceSourceGas = priceGasFor(source);
18211
+ const priceDestinationGas = priceGasFor(destination);
18212
+ const estimateApprove = async ()=>await quoteReservedGas(await this.approve(source, amount), BRIDGE_STEP_GAS_FLOORS_EVM.approve, priceSourceGas);
18213
+ const estimateBurn = async ()=>await quoteReservedGas(await this.burn(params), BRIDGE_STEP_GAS_FLOORS_EVM.burn, priceSourceGas, ()=>hasCustomContractSupport(source.chain, 'bridge') ? CUSTOM_BURN_GAS_ESTIMATE_EVM : DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM);
17993
18214
  // Only estimate Mint gas when not using forwarder (user pays gas)
17994
18215
  // When useForwarder=true, Circle's Orbit relayer handles and pays for the mint
17995
18216
  const useForwarder = destination.useForwarder === true;
@@ -17998,12 +18219,11 @@ function assertCCTPV2Config(config) {
17998
18219
  return null // Skip mint estimation when forwarder handles it
17999
18220
  ;
18000
18221
  }
18001
- const mint = await this.mint(source, destination, mockAttestationMessage);
18002
- return await mint.estimate(undefined, await destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, undefined, destination.chain));
18222
+ return await quoteReservedGas(await this.mint(source, destination, mockAttestationMessage), BRIDGE_STEP_GAS_FLOORS_EVM.mint, priceDestinationGas, ()=>RECEIVE_MESSAGE_GAS_ESTIMATE_EVM);
18003
18223
  };
18004
18224
  // Parallelize all independent async operations
18005
18225
  const [approveEstimate, depositForBurnFee, receiveMessageFee, feeEstimates] = await Promise.allSettled([
18006
- this.approve(source, amount).then(async (approve)=>approve.estimate()),
18226
+ estimateApprove(),
18007
18227
  estimateBurn(),
18008
18228
  estimateMint(),
18009
18229
  this.getMaxFee(params)
@@ -19574,7 +19794,7 @@ function assertCCTPV2Config(config) {
19574
19794
  registerKit(`${pkg$5.name}/${pkg$5.version}`);
19575
19795
 
19576
19796
  var name$3 = "@circle-fin/swap-kit";
19577
- var version$3 = "1.5.1";
19797
+ var version$3 = "1.5.2";
19578
19798
  var pkg$3 = {
19579
19799
  name: name$3,
19580
19800
  version: version$3};
@@ -26672,14 +26892,13 @@ const TOKEN_REGISTRY$3 = createTokenRegistry();
26672
26892
  return;
26673
26893
  }
26674
26894
  // For non-native tokens (SPL tokens, ERC-20 tokens), check the token balance
26675
- const balancePrepared = await adapter.prepareAction('token.balanceOf', {
26895
+ const balance = await executeAdapterReadAction(adapter, 'token.balanceOf', {
26676
26896
  tokenAddress: tokenInAddress,
26677
26897
  walletAddress
26678
26898
  }, context);
26679
- const balance = await balancePrepared.execute();
26680
26899
  // Compare balances
26681
26900
  const requiredAmount = BigInt(amount);
26682
- const currentBalance = BigInt(balance);
26901
+ const currentBalance = BigInt(String(balance));
26683
26902
  if (currentBalance < requiredAmount) {
26684
26903
  throw new KitError({
26685
26904
  ...BalanceError.INSUFFICIENT_TOKEN,
@@ -27840,11 +28059,10 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
27840
28059
  * @throws Error if the adapter fails to prepare or execute approval transactions
27841
28060
  * @throws Error if transaction confirmation fails or times out
27842
28061
  */ async handleUsdtApproval(adapter, chain, executionCtx, adapterContractAddress, resolvedContext, executedTransactions) {
27843
- const allowanceRequest = await adapter.prepareAction('token.allowance', {
28062
+ const current = await readTokenAllowance(adapter, {
27844
28063
  tokenAddress: executionCtx.tokenInAddress,
27845
28064
  delegate: adapterContractAddress
27846
28065
  }, resolvedContext);
27847
- const current = BigInt(await allowanceRequest.execute());
27848
28066
  const required = BigInt(executionCtx.amount);
27849
28067
  if (current >= required) {
27850
28068
  // Sufficient allowance - proceed to swap
@@ -32725,7 +32943,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32725
32943
  };
32726
32944
 
32727
32945
  var name$2 = "@circle-fin/earn-kit";
32728
- var version$2 = "1.5.0";
32946
+ var version$2 = "1.5.1";
32729
32947
  var pkg$2 = {
32730
32948
  name: name$2,
32731
32949
  version: version$2};
@@ -32972,30 +33190,6 @@ function toSdkChain(chain) {
32972
33190
  return assertHexAddress('chain.kitContracts.adapter', adapterContractAddress, `Adapter contract for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
32973
33191
  }
32974
33192
 
32975
- /**
32976
- * Parse the raw `token.allowance` adapter response into a bigint.
32977
- *
32978
- * @internal
32979
- */ function parseAllowanceResponse(allowanceRaw) {
32980
- if (allowanceRaw === undefined || allowanceRaw === null) {
32981
- return 0n;
32982
- }
32983
- let allowance;
32984
- if (typeof allowanceRaw === 'bigint') {
32985
- allowance = allowanceRaw;
32986
- } else if (typeof allowanceRaw === 'string') {
32987
- try {
32988
- allowance = BigInt(allowanceRaw);
32989
- } catch {
32990
- allowance = undefined;
32991
- }
32992
- }
32993
- if (allowance === undefined || allowance < 0n) {
32994
- throw createValidationFailedError$1('token.allowance', allowanceRaw, 'token.allowance response must be a non-negative bigint-compatible string or bigint');
32995
- }
32996
- return allowance;
32997
- }
32998
-
32999
33193
  /**
33000
33194
  * Safety multiplier applied to locally estimated gas for earn transactions.
33001
33195
  *
@@ -33217,17 +33411,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
33217
33411
  if (requiredAllowance <= 0n) {
33218
33412
  return undefined;
33219
33413
  }
33220
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
33221
- tokenAddress,
33222
- delegate
33223
- }, {
33224
- chain,
33225
- address
33226
- });
33227
- // Each execute() is a fresh allowance read at `latest`, so the same prepared
33228
- // action serves both the pre-approval decision read and the post-approval
33229
- // propagation polls.
33230
- const readAllowance = async ()=>parseAllowanceResponse(await allowancePrepared.execute());
33414
+ // Each call is a fresh allowance read at `latest`, so the same function serves
33415
+ // both the pre-approval decision and post-approval propagation polls.
33416
+ const readAllowance = async ()=>readTokenAllowance(adapter, {
33417
+ tokenAddress,
33418
+ delegate
33419
+ }, {
33420
+ chain,
33421
+ address
33422
+ });
33231
33423
  const currentAllowance = await readAllowance();
33232
33424
  if (currentAllowance >= requiredAllowance) {
33233
33425
  return undefined;
@@ -34310,14 +34502,13 @@ function throwBatchFailure(result, executeReceipt, chain, actionKey, revertMessa
34310
34502
  // approveAllowanceIfNeeded guard and avoids an increaseAllowance underflow
34311
34503
  // (requiredAllowance - currentAllowance would be negative, which reverts as
34312
34504
  // an out-of-range uint256).
34313
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
34505
+ const currentAllowance = await readTokenAllowance(adapter, {
34314
34506
  tokenAddress: approvalToken,
34315
34507
  delegate
34316
34508
  }, {
34317
34509
  chain,
34318
34510
  address
34319
34511
  });
34320
- const currentAllowance = parseAllowanceResponse(await allowancePrepared.execute());
34321
34512
  const approvalNeeded = currentAllowance < requiredAllowance;
34322
34513
  const executePrepared = await adapter.prepareAction(actionKey, {
34323
34514
  executeParams,
@@ -35694,7 +35885,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
35694
35885
  }
35695
35886
 
35696
35887
  var name$1 = "@circle-fin/provider-earn-service";
35697
- var version$1 = "1.4.0";
35888
+ var version$1 = "1.4.1";
35698
35889
  var pkg$1 = {
35699
35890
  name: name$1,
35700
35891
  version: version$1};
@@ -40890,7 +41081,7 @@ const tokens = createTokenRegistry();
40890
41081
  * token: 'USDC'
40891
41082
  * })
40892
41083
  *
40893
- * console.log('Bridge completed:', result.hash)
41084
+ * console.log(`Bridged ${result.amount} ${result.token} (${result.state})`)
40894
41085
  * ```
40895
41086
  */ const bridge = async (context, params)=>{
40896
41087
  const kit = createBridgeKit(context);
@@ -41559,7 +41750,7 @@ async function deposit$2(context, params) {
41559
41750
  }
41560
41751
 
41561
41752
  var name = "@circle-fin/unified-balance-kit";
41562
- var version = "1.4.0";
41753
+ var version = "1.4.1";
41563
41754
  var pkg = {
41564
41755
  name: name,
41565
41756
  version: version};
@@ -45797,8 +45988,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45797
45988
  chain
45798
45989
  };
45799
45990
  // Step 1: Quick check at latest block (no HTTP call)
45800
- const latestRequest = await adapter.prepareAction('gateway.v1.isDelegate', baseActionParams, operationContext);
45801
- const latestResult = await latestRequest.execute();
45991
+ const latestResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', baseActionParams, operationContext);
45802
45992
  if (String(latestResult).toLowerCase() !== 'true') {
45803
45993
  return 'none';
45804
45994
  }
@@ -45807,13 +45997,12 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45807
45997
  // Solana uses confirmed vs finalized commitment as a proxy for
45808
45998
  // Gateway finality. This is conservative — can only over-report
45809
45999
  // 'pending', never falsely report 'ready'.
45810
- const finalizedRequest = await adapter.prepareAction('gateway.v1.isDelegate', {
45811
- ...baseActionParams,
45812
- commitment: 'finalized'
45813
- }, operationContext);
45814
46000
  let finalizedResult;
45815
46001
  try {
45816
- finalizedResult = await finalizedRequest.execute();
46002
+ finalizedResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', {
46003
+ ...baseActionParams,
46004
+ commitment: 'finalized'
46005
+ }, operationContext);
45817
46006
  } catch (error) {
45818
46007
  if (isBlockRangeError(error)) {
45819
46008
  return 'pending';
@@ -45824,10 +46013,6 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45824
46013
  }
45825
46014
  // EVM: use processedHeight from /v1/info
45826
46015
  const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain);
45827
- const finalizedRequest = await adapter.prepareAction('gateway.v1.isDelegate', {
45828
- ...baseActionParams,
45829
- blockNumber: processedHeight
45830
- }, operationContext);
45831
46016
  // If the RPC node lags Gateway's indexer view, the historical read at
45832
46017
  // processedHeight may throw a block-range error. This is safe to treat
45833
46018
  // as 'pending' because processedHeight comes from Gateway's /v1/info
@@ -45836,7 +46021,10 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45836
46021
  // Re-throw structural errors to avoid masking real bugs.
45837
46022
  let finalizedResult;
45838
46023
  try {
45839
- finalizedResult = await finalizedRequest.execute();
46024
+ finalizedResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', {
46025
+ ...baseActionParams,
46026
+ blockNumber: processedHeight
46027
+ }, operationContext);
45840
46028
  } catch (error) {
45841
46029
  if (isBlockRangeError(error)) {
45842
46030
  return 'pending';
@@ -45897,8 +46085,8 @@ function parseAmountSafe(amount) {
45897
46085
  chain
45898
46086
  };
45899
46087
  const [withdrawingRaw, withdrawalBlockRaw] = await Promise.all([
45900
- adapter.prepareAction('gateway.v1.withdrawingBalance', readParams, operationContext).then(async (req)=>req.execute()),
45901
- adapter.prepareAction('gateway.v1.withdrawalBlock', readParams, operationContext).then(async (req)=>req.execute())
46088
+ executeAdapterReadAction(adapter, 'gateway.v1.withdrawingBalance', readParams, operationContext),
46089
+ executeAdapterReadAction(adapter, 'gateway.v1.withdrawalBlock', readParams, operationContext)
45902
46090
  ]);
45903
46091
  const withdrawingValue = safeBigInt(String(withdrawingRaw), 'withdrawingBalance');
45904
46092
  const withdrawalBlockValue = safeBigInt(String(withdrawalBlockRaw), 'withdrawalBlock');
@@ -45938,12 +46126,11 @@ function parseAmountSafe(amount) {
45938
46126
  const tokenAddress = getTokenAddress(chain, params.token);
45939
46127
  // Read the pending balance before withdrawing — the contract resets it to 0
45940
46128
  // after withdraw() executes, so this is the only way to capture the amount.
45941
- const withdrawingBalanceReq = await adapter.prepareAction('gateway.v1.withdrawingBalance', {
46129
+ const withdrawingRaw = await executeAdapterReadAction(adapter, 'gateway.v1.withdrawingBalance', {
45942
46130
  token: tokenAddress,
45943
46131
  depositor: signerAddress,
45944
46132
  chain
45945
46133
  }, operationContext);
45946
- const withdrawingRaw = await withdrawingBalanceReq.execute();
45947
46134
  const withdrawingValue = safeBigInt(String(withdrawingRaw), 'withdrawingBalance');
45948
46135
  if (withdrawingValue === 0n) {
45949
46136
  throw new KitError({
@@ -49179,7 +49366,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49179
49366
  * token: 'USDC'
49180
49367
  * })
49181
49368
  *
49182
- * console.log('Bridge completed:', result.hash)
49369
+ * console.log(`Bridged ${result.amount} ${result.token} (${result.state})`)
49183
49370
  * ```
49184
49371
  */ async bridge(params) {
49185
49372
  return bridge(this.context, params);