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