@circle-fin/app-kit 1.11.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.
@@ -38,6 +38,7 @@ import { PublicKey } from '@solana/web3.js';
38
38
  import 'bn.js';
39
39
  import '@coral-xyz/anchor';
40
40
  import '@noble/curves/ed25519';
41
+ import 'viem';
41
42
  import { keccak256 } from '@ethersproject/keccak256';
42
43
 
43
44
  // Import global type declarations
@@ -2555,6 +2556,8 @@ class KitError extends Error {
2555
2556
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2556
2557
  Blockchain["XDC"] = "XDC";
2557
2558
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2559
+ Blockchain["X_Layer"] = "X_Layer";
2560
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2558
2561
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2559
2562
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2560
2563
  })(Blockchain || (Blockchain = {}));
@@ -2608,6 +2611,7 @@ var BridgeChain;
2608
2611
  BridgeChain["Unichain"] = "Unichain";
2609
2612
  BridgeChain["World_Chain"] = "World_Chain";
2610
2613
  BridgeChain["XDC"] = "XDC";
2614
+ BridgeChain["X_Layer"] = "X_Layer";
2611
2615
  // Testnet chains with CCTPv2 support
2612
2616
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2613
2617
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2633,6 +2637,7 @@ var BridgeChain;
2633
2637
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2634
2638
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2635
2639
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2640
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2636
2641
  })(BridgeChain || (BridgeChain = {}));
2637
2642
  var UnifiedBalanceChain;
2638
2643
  (function(UnifiedBalanceChain) {
@@ -5170,7 +5175,8 @@ var EarnChain;
5170
5175
  isTestnet: true,
5171
5176
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
5172
5177
  rpcEndpoints: [
5173
- 'https://rpc-amoy.polygon.technology'
5178
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
5179
+ 'https://polygon-amoy.drpc.org'
5174
5180
  ],
5175
5181
  eurcAddress: null,
5176
5182
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -6035,6 +6041,104 @@ var EarnChain;
6035
6041
  }
6036
6042
  });
6037
6043
 
6044
+ /**
6045
+ * X Layer Mainnet chain definition
6046
+ * @remarks
6047
+ * This represents the official production network for the X Layer blockchain.
6048
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
6049
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
6050
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
6051
+ */ const XLayer = defineChain({
6052
+ type: 'evm',
6053
+ chain: Blockchain.X_Layer,
6054
+ name: 'X Layer',
6055
+ title: 'X Layer Mainnet',
6056
+ nativeCurrency: {
6057
+ name: 'OKB',
6058
+ symbol: 'OKB',
6059
+ decimals: 18
6060
+ },
6061
+ chainId: 196,
6062
+ isTestnet: false,
6063
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
6064
+ rpcEndpoints: [
6065
+ 'https://xlayerrpc.okx.com'
6066
+ ],
6067
+ eurcAddress: null,
6068
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
6069
+ usdtAddress: null,
6070
+ cctp: {
6071
+ domain: 37,
6072
+ contracts: {
6073
+ v2: {
6074
+ type: 'split',
6075
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6076
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6077
+ confirmations: 65,
6078
+ fastConfirmations: 1
6079
+ }
6080
+ },
6081
+ forwarderSupported: {
6082
+ source: false,
6083
+ destination: false
6084
+ }
6085
+ },
6086
+ kitContracts: {
6087
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
6088
+ }
6089
+ });
6090
+
6091
+ /**
6092
+ * X Layer Testnet chain definition
6093
+ * @remarks
6094
+ * This represents the official test network for the X Layer blockchain.
6095
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
6096
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
6097
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
6098
+ */ const XLayerTestnet = defineChain({
6099
+ type: 'evm',
6100
+ chain: Blockchain.X_Layer_Testnet,
6101
+ name: 'X Layer Testnet',
6102
+ title: 'X Layer Testnet',
6103
+ nativeCurrency: {
6104
+ name: 'OKB',
6105
+ symbol: 'OKB',
6106
+ decimals: 18
6107
+ },
6108
+ chainId: 1952,
6109
+ isTestnet: true,
6110
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
6111
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
6112
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
6113
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
6114
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
6115
+ rpcEndpoints: [
6116
+ 'https://testrpc.xlayer.tech'
6117
+ ],
6118
+ eurcAddress: null,
6119
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
6120
+ usdtAddress: null,
6121
+ cctp: {
6122
+ domain: 37,
6123
+ contracts: {
6124
+ v2: {
6125
+ type: 'split',
6126
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6127
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6128
+ confirmations: 65,
6129
+ fastConfirmations: 1
6130
+ }
6131
+ },
6132
+ forwarderSupported: {
6133
+ source: false,
6134
+ destination: false
6135
+ }
6136
+ },
6137
+ kitContracts: {
6138
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
6139
+ }
6140
+ });
6141
+
6038
6142
  /**
6039
6143
  * ZKSync Era Mainnet chain definition
6040
6144
  * @remarks
@@ -6154,6 +6258,8 @@ var Chains = /*#__PURE__*/Object.freeze({
6154
6258
  WorldChainSepolia: WorldChainSepolia,
6155
6259
  XDC: XDC,
6156
6260
  XDCApothem: XDCApothem,
6261
+ XLayer: XLayer,
6262
+ XLayerTestnet: XLayerTestnet,
6157
6263
  ZKSyncEra: ZKSyncEra,
6158
6264
  ZKSyncEraSepolia: ZKSyncEraSepolia
6159
6265
  });
@@ -8208,6 +8314,7 @@ const swapTokenEnumSchema = z.enum([
8208
8314
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
8209
8315
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
8210
8316
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
8317
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
8211
8318
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
8212
8319
  // =========================================================================
8213
8320
  // Testnets (alphabetically sorted)
@@ -8216,6 +8323,7 @@ const swapTokenEnumSchema = z.enum([
8216
8323
  [Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
8217
8324
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
8218
8325
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
8326
+ [Blockchain.Celo_Alfajores_Testnet]: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B',
8219
8327
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
8220
8328
  [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
8221
8329
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
@@ -8242,6 +8350,7 @@ const swapTokenEnumSchema = z.enum([
8242
8350
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
8243
8351
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
8244
8352
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8353
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
8245
8354
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
8246
8355
  }
8247
8356
  };
@@ -9653,7 +9762,7 @@ function resolveOptions(options) {
9653
9762
  }
9654
9763
 
9655
9764
  var name$2 = "@circle-fin/bridge-kit";
9656
- var version$3 = "1.12.2";
9765
+ var version$3 = "1.13.0";
9657
9766
  var pkg$3 = {
9658
9767
  name: name$2,
9659
9768
  version: version$3};
@@ -9690,13 +9799,21 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
9690
9799
  computeFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
9691
9800
  calculateFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
9692
9801
  resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
9693
- }).strict().refine((data)=>{
9802
+ }).strict().superRefine((data, ctx)=>{
9694
9803
  const hasComputeFee = data.computeFee !== undefined;
9695
9804
  const hasCalculateFee = data.calculateFee !== undefined;
9696
- // XOR: exactly one must be provided
9697
- return hasComputeFee !== hasCalculateFee;
9698
- }, {
9699
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
9805
+ if (hasComputeFee && hasCalculateFee) {
9806
+ ctx.addIssue({
9807
+ code: z.ZodIssueCode.custom,
9808
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
9809
+ });
9810
+ }
9811
+ if (!hasComputeFee && !hasCalculateFee) {
9812
+ ctx.addIssue({
9813
+ code: z.ZodIssueCode.custom,
9814
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
9815
+ });
9816
+ }
9700
9817
  });
9701
9818
  /**
9702
9819
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
@@ -9794,6 +9911,44 @@ function assertBridgeParams(params, schema) {
9794
9911
  }
9795
9912
  }
9796
9913
 
9914
+ /**
9915
+ * Canonical list of actions that do not prepare or submit transactions.
9916
+ *
9917
+ * @internal
9918
+ */ const READ_ACTION_KEYS = [
9919
+ 'token.allowance',
9920
+ 'token.balanceOf',
9921
+ 'token.name',
9922
+ 'native.balanceOf',
9923
+ 'usdc.allowance',
9924
+ 'usdc.balanceOf',
9925
+ 'usdc.name',
9926
+ 'gateway.v1.isDelegate',
9927
+ 'gateway.v1.withdrawingBalance',
9928
+ 'gateway.v1.withdrawalBlock',
9929
+ 'gateway.v1.signBurnIntents'
9930
+ ];
9931
+ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
9932
+ /**
9933
+ * Check whether a runtime value identifies a read action.
9934
+ *
9935
+ * @param action - The value to classify.
9936
+ * @returns Whether the value is a registered read-action key.
9937
+ *
9938
+ * @example
9939
+ * ```typescript
9940
+ * import { isReadActionKey } from '@core/adapter'
9941
+ *
9942
+ * if (isReadActionKey(value)) {
9943
+ * await adapter.readAction(value, params, context)
9944
+ * }
9945
+ * ```
9946
+ *
9947
+ * @internal
9948
+ */ function isReadActionKey(action) {
9949
+ return READ_ACTION_KEY_SET.has(action);
9950
+ }
9951
+
9797
9952
  /**
9798
9953
  * Resolves an operation context into concrete chain and address values.
9799
9954
  *
@@ -9873,6 +10028,72 @@ function assertBridgeParams(params, schema) {
9873
10028
  };
9874
10029
  }
9875
10030
 
10031
+ /**
10032
+ * Create the standard error for a missing or non-read action.
10033
+ *
10034
+ * @param action - The unsupported action value.
10035
+ * @returns A fatal unsupported-action error.
10036
+ *
10037
+ * @internal
10038
+ */ function createUnsupportedReadActionError(action) {
10039
+ return new KitError({
10040
+ ...InputError.UNSUPPORTED_ACTION,
10041
+ recoverability: 'FATAL',
10042
+ message: `Read action "${String(action)}" is not registered in this adapter.`
10043
+ });
10044
+ }
10045
+ /**
10046
+ * Execute a read through the adapter's dedicated read seam when available.
10047
+ *
10048
+ * @remarks
10049
+ * Fall back to the legacy `prepareAction().execute()` contract so providers
10050
+ * remain runtime-compatible with adapter versions released before `readAction`.
10051
+ * Consumers must upgrade their adapter package for reads to bypass custom
10052
+ * `prepareAction` wrappers.
10053
+ *
10054
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
10055
+ * @typeParam TActionKey - The read action key.
10056
+ * @param adapter - The adapter that owns the read action.
10057
+ * @param action - The read action to execute.
10058
+ * @param params - The parameters for the read action.
10059
+ * @param ctx - The operation context.
10060
+ * @returns The raw read-action result.
10061
+ * @throws {KitError} When `action` is not a supported read-action key.
10062
+ *
10063
+ * @example
10064
+ * ```typescript
10065
+ * import { executeAdapterReadAction } from '@core/adapter'
10066
+ * import { Ethereum } from '@core/chains'
10067
+ *
10068
+ * const allowance = await executeAdapterReadAction(
10069
+ * adapter,
10070
+ * 'token.allowance',
10071
+ * { tokenAddress, delegate },
10072
+ * { chain: Ethereum },
10073
+ * )
10074
+ * ```
10075
+ *
10076
+ * @internal
10077
+ */ async function executeAdapterReadAction(adapter, action, params, ctx) {
10078
+ if (!isReadActionKey(action)) {
10079
+ throw createUnsupportedReadActionError(action);
10080
+ }
10081
+ const runtimeAdapter = adapter;
10082
+ if (typeof runtimeAdapter.readAction === 'function') {
10083
+ return runtimeAdapter.readAction(action, params, ctx);
10084
+ }
10085
+ let request;
10086
+ try {
10087
+ request = await adapter.prepareAction(action, params, ctx);
10088
+ } catch (error) {
10089
+ if (error instanceof Error && error.message === `Action ${action} is not supported`) {
10090
+ throw createUnsupportedReadActionError(action);
10091
+ }
10092
+ throw error;
10093
+ }
10094
+ return request.execute();
10095
+ }
10096
+
9876
10097
  /**
9877
10098
  * Schema for validating hexadecimal strings with '0x' prefix.
9878
10099
  *
@@ -10082,16 +10303,15 @@ function assertBridgeParams(params, schema) {
10082
10303
  * ```
10083
10304
  */ const validateBalanceForTransaction = async (params)=>{
10084
10305
  const { amount, adapter, token, tokenAddress, operationContext } = params;
10085
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
10306
+ const balance = await executeAdapterReadAction(adapter, 'usdc.balanceOf', {
10086
10307
  walletAddress: operationContext.address
10087
10308
  }, operationContext);
10088
- const balance = await balancePrepared.execute();
10089
- if (BigInt(balance) < BigInt(amount)) {
10309
+ if (BigInt(String(balance)) < BigInt(amount)) {
10090
10310
  // Extract chain name from operationContext
10091
10311
  const chainName = extractChainInfo(operationContext.chain).name;
10092
10312
  // Create KitError with rich context in trace
10093
10313
  throw createInsufficientTokenBalanceError(chainName, token, {
10094
- balance: balance.toString(),
10314
+ balance: String(balance),
10095
10315
  amount,
10096
10316
  tokenAddress,
10097
10317
  walletAddress: operationContext.address
@@ -12457,15 +12677,52 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
12457
12677
  ;
12458
12678
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
12459
12679
  ;
12460
- // Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
12461
- // chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
12462
- // Kept separate from the fee-estimate averages above.
12463
- const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
12680
+ // Gas FLOORS, not ceilings kept separate from the fee-estimate averages
12681
+ // above. `executePreparedChainRequest` submits
12682
+ // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
12683
+ // is covered by its own estimate, and a chain whose estimator under-reports
12684
+ // (Cronos: returns 30_600 where the EIP-7623 calldata floor is 45_000) is
12685
+ // covered by the floor.
12686
+ //
12687
+ // Two distinct chain surcharges drive these numbers, both measured live:
12688
+ // Sei — ~+51_500 per NEWLY CREATED storage slot (73_595 vs vanilla 22_100);
12689
+ // no flat per-tx surcharge (31_535, identical to Base).
12690
+ // Edge — ~+53_200 flat on EVERY tx (84_751 vs Base 31_535); storage priced
12691
+ // normally. Edge therefore fails warm as well as cold.
12692
+ // A floor must clear the worst COLD cost, since a slot that exists at estimate
12693
+ // time can be consumed before inclusion and cost a full step more on execution.
12694
+ // Each floor is therefore derived from the worst observed estimate *after* the
12695
+ // 1.25x buffer, plus headroom — sizing it below the buffered value would leave
12696
+ // the estimate governing and defeat the point of the floor.
12697
+ //
12698
+ // The `*_GAS_LIMIT_EVM` names are kept despite these being floors: they are
12699
+ // exported, so renaming to `*_GAS_FLOOR_EVM` would be a breaking change for
12700
+ // consumers. Read "LIMIT" here as "the limit we submit", never as a ceiling.
12701
+ const APPROVE_GAS_LIMIT_EVM = 150_000n // buffered worst cold 149_355 (Edge Testnet 119_484 x 1.25) + drift headroom
12464
12702
  ;
12465
- const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
12703
+ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 500_000n // buffered worst 474_078 (Sei 379_263 x 1.25) + ~26k headroom
12466
12704
  ;
12467
- const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
12705
+ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears Cronos' calldata floor ~10x
12468
12706
  ;
12707
+ /**
12708
+ * The gas floor for each bridge step, keyed by step name.
12709
+ *
12710
+ * Two places need these and they must agree: each step module passes its floor
12711
+ * to `executePreparedChainRequest` for submission, and
12712
+ * `CCTPV2BridgingProvider.estimate()` quotes the resulting limit so a caller
12713
+ * can fund a wallet. A transaction is only admitted when the sender holds
12714
+ * `gasLimit * maxFeePerGas`, so a quote taken from anything other than the
12715
+ * submitted limit under-reports what the wallet actually needs — historically
12716
+ * the quote sat ~2.5x below the reserved limit.
12717
+ *
12718
+ * Both sides read this map so the two cannot drift apart. Change a floor here
12719
+ * and the quote moves with it; point one side at a different value and the
12720
+ * divergence is visible in review rather than silent at runtime.
12721
+ */ const BRIDGE_STEP_GAS_FLOORS_EVM = {
12722
+ approve: APPROVE_GAS_LIMIT_EVM,
12723
+ burn: DEPOSIT_FOR_BURN_GAS_LIMIT_EVM,
12724
+ mint: RECEIVE_MESSAGE_GAS_LIMIT_EVM
12725
+ };
12469
12726
  /**
12470
12727
  * The minimum finality threshold for CCTPv2 transfers.
12471
12728
  *
@@ -12484,1538 +12741,1595 @@ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
12484
12741
  };
12485
12742
 
12486
12743
  /**
12487
- * Default configuration values for the attestation fetcher.
12488
- * @internal
12489
- */ const DEFAULT_CONFIG = {
12490
- timeout: 2_000,
12491
- maxRetries: 30 * 20,
12492
- retryDelay: 2_000,
12493
- headers: {
12494
- 'Content-Type': 'application/json'
12495
- }
12496
- };
12497
- /**
12498
- * Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
12499
- *
12500
- * Headers are merged independently so caller-supplied headers augment the
12501
- * defaults (such as `Content-Type`) rather than replacing them wholesale.
12502
- *
12503
- * @param config - Caller-provided polling configuration overrides
12504
- * @param internalDefaults - Internal defaults applied before `config` (for example a
12505
- * reduced `maxRetries` for one-shot requests); `config` still wins on conflict
12506
- * @returns The effective polling configuration
12507
- * @internal
12508
- */ const mergeAttestationConfig = (config, internalDefaults = {})=>({
12509
- ...DEFAULT_CONFIG,
12510
- ...internalDefaults,
12511
- ...config,
12512
- headers: {
12513
- ...DEFAULT_CONFIG.headers,
12514
- ...internalDefaults.headers,
12515
- ...config.headers
12516
- }
12517
- });
12518
- /**
12519
- * Type guard that verifies if an unknown value matches the AttestationMessage shape
12520
- * and has all required properties.
12521
- *
12522
- * @param obj - The value to check, typically an element from the messages array
12523
- * @returns True if the object matches the AttestationMessage shape, false otherwise
12524
- * @internal
12525
- */ const isValidAttestationMessage = (obj)=>{
12526
- 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';
12527
- };
12528
- /**
12529
- * Type guard that verifies if an attestation message is complete.
12530
- *
12531
- * @param message - The attestation message to check
12532
- * @returns True if the message status is 'complete', false otherwise
12533
- * @internal
12534
- */ const isCompleteAttestation = (message)=>{
12535
- return message.status === 'complete';
12536
- };
12537
- /**
12538
- * Type guard that verifies if an unknown value has the correct structure
12539
- * for an AttestationResponse, regardless of attestation completion status.
12540
- *
12541
- * @param obj - The value to check, typically a parsed JSON response
12542
- * @returns True if the object matches the AttestationResponse shape
12543
- * @internal
12544
- */ const hasValidAttestationStructure = (obj)=>{
12545
- if (typeof obj !== 'object' || obj === null || !('messages' in obj) || !Array.isArray(obj.messages)) {
12546
- return false;
12547
- }
12548
- const messages = obj.messages;
12549
- // Validate all messages have the correct shape
12550
- return messages.every(isValidAttestationMessage);
12551
- };
12552
- /**
12553
- * Type guard that verifies if an unknown value matches the AttestationResponse shape
12554
- * and contains a complete attestation.
12555
- *
12556
- * This function performs runtime validation to ensure that the provided value
12557
- * conforms to the expected structure of an AttestationResponse and has at least
12558
- * one complete attestation. It checks that:
12559
- * 1. The value has valid AttestationResponse structure
12560
- * 2. At least one message has status 'complete'
12561
- *
12562
- * @remarks
12563
- * This type guard is used internally by the attestation fetcher to validate
12564
- * responses from the IRIS API before processing them. It provides runtime
12565
- * type safety for data coming from the network and ensures we have a complete
12566
- * attestation before proceeding.
12567
- *
12568
- * If the response has valid structure but no complete attestation yet,
12569
- * it throws a retryable error. If the response structure is invalid,
12570
- * it throws a non-retryable validation error.
12571
- *
12572
- * @param obj - The value to check, typically a parsed JSON response
12573
- * @returns True if the object matches the AttestationResponse shape and has a complete attestation
12574
- * @throws {Error} With "Invalid attestation response structure" if structure is invalid (non-retryable)
12575
- * @throws {Error} With "Attestation not ready" if no complete attestation yet (retryable)
12576
- *
12577
- * @example
12578
- * ```typescript
12579
- * const response = await fetch('https://iris-api.circle.com/...')
12580
- * const data = await response.json()
12744
+ * CCTP bridge step names that can occur in the bridging flow.
12581
12745
  *
12582
- * if (isAttestationResponse(data)) {
12583
- * // TypeScript now knows data is AttestationResponse with at least one complete attestation
12584
- * const completeMessage = data.messages.find(msg => msg.status === 'complete')
12585
- * console.log('Found complete attestation:', completeMessage.attestation)
12586
- * }
12587
- * ```
12588
- */ const isAttestationResponse = (obj)=>{
12589
- // First check if the structure is valid
12590
- if (!hasValidAttestationStructure(obj)) {
12591
- // If structure is invalid, this is a permanent failure - don't retry
12592
- throw new Error('Invalid attestation response structure');
12593
- }
12594
- // Then check if at least one message is complete
12595
- if (!obj.messages.some(isCompleteAttestation)) {
12596
- // If no complete message, this is a temporary state - allow retry
12597
- throw new Error('Attestation not ready');
12598
- }
12599
- return true;
12746
+ * This object provides type safety for step names and represents all possible
12747
+ * steps that can be executed during a CCTP bridge operation. Using const assertions
12748
+ * makes this tree-shakable and follows modern TypeScript best practices.
12749
+ */ const CCTPv2StepName = {
12750
+ approve: 'approve',
12751
+ burn: 'burn',
12752
+ fetchAttestation: 'fetchAttestation',
12753
+ mint: 'mint',
12754
+ reAttest: 'reAttest'
12600
12755
  };
12601
12756
  /**
12602
- * Builds the IRIS API URL for fetching attestation data from Circle's CCTP service.
12603
- *
12604
- * Constructs a properly formatted URL for the IRIS API v2 endpoint that provides
12605
- * attestation messages for cross-chain transfers. The URL includes both the source
12606
- * domain identifier and the transaction hash as query parameters. The base URL
12607
- * is selected based on whether the operation is for testnet or mainnet.
12608
- *
12609
- * @param sourceDomainId - The CCTP domain ID of the source chain (numeric or string)
12610
- * @param transactionHash - The transaction hash of the burn operation to fetch attestation for
12611
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
12612
- * @returns A fully qualified URL string for the IRIS API endpoint
12757
+ * Conditional step transition rules for CCTP bridge flow.
12613
12758
  *
12614
- * @example
12615
- * ```typescript
12616
- * // Mainnet URL
12617
- * const mainnetUrl = buildIrisUrl(1, '0xabc...', false)
12618
- * // => 'https://iris-api.circle.com/v2/messages/1?transactionHash=0xabc...'
12619
- *
12620
- * // Testnet URL
12621
- * const testnetUrl = buildIrisUrl(1, '0xdef...', true)
12622
- * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
12623
- * ```
12624
- */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
12625
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
12626
- const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
12627
- url.searchParams.set('transactionHash', transactionHash);
12628
- return url.toString();
12759
+ * Rules are evaluated in order - the first matching condition determines the next step.
12760
+ * This approach supports flexible flow logic and makes it easy to extend with new patterns.
12761
+ */ const STEP_TRANSITION_RULES = {
12762
+ // Starting state - no steps executed yet
12763
+ '': [
12764
+ {
12765
+ condition: ()=>true,
12766
+ nextStep: CCTPv2StepName.approve,
12767
+ reason: 'Start with approval step',
12768
+ isActionable: true
12769
+ }
12770
+ ],
12771
+ // After Approve step
12772
+ [CCTPv2StepName.approve]: [
12773
+ {
12774
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
12775
+ nextStep: CCTPv2StepName.burn,
12776
+ reason: 'Approval successful, proceed to burn',
12777
+ isActionable: true
12778
+ },
12779
+ {
12780
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
12781
+ nextStep: CCTPv2StepName.approve,
12782
+ reason: 'Retry failed approval',
12783
+ isActionable: true
12784
+ },
12785
+ {
12786
+ condition: (ctx)=>ctx.lastStep?.state === 'noop',
12787
+ nextStep: CCTPv2StepName.burn,
12788
+ reason: 'No approval needed, proceed to burn',
12789
+ isActionable: true
12790
+ },
12791
+ {
12792
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
12793
+ nextStep: CCTPv2StepName.approve,
12794
+ reason: 'Continue pending approval',
12795
+ isActionable: false
12796
+ }
12797
+ ],
12798
+ // After Burn step
12799
+ [CCTPv2StepName.burn]: [
12800
+ {
12801
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
12802
+ nextStep: CCTPv2StepName.fetchAttestation,
12803
+ reason: 'Burn successful, fetch attestation',
12804
+ isActionable: true
12805
+ },
12806
+ {
12807
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
12808
+ nextStep: CCTPv2StepName.burn,
12809
+ reason: 'Retry failed burn',
12810
+ isActionable: true
12811
+ },
12812
+ {
12813
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
12814
+ nextStep: CCTPv2StepName.burn,
12815
+ reason: 'Continue pending burn',
12816
+ isActionable: false
12817
+ }
12818
+ ],
12819
+ // After FetchAttestation step
12820
+ [CCTPv2StepName.fetchAttestation]: [
12821
+ {
12822
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
12823
+ nextStep: CCTPv2StepName.mint,
12824
+ reason: 'Attestation fetched, proceed to mint',
12825
+ isActionable: true
12826
+ },
12827
+ {
12828
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
12829
+ nextStep: CCTPv2StepName.fetchAttestation,
12830
+ reason: 'Retry fetching attestation',
12831
+ isActionable: true
12832
+ },
12833
+ {
12834
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
12835
+ nextStep: CCTPv2StepName.fetchAttestation,
12836
+ reason: 'Continue pending attestation fetch',
12837
+ isActionable: false
12838
+ }
12839
+ ],
12840
+ // After Mint step
12841
+ [CCTPv2StepName.mint]: [
12842
+ {
12843
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
12844
+ nextStep: null,
12845
+ reason: 'Bridge completed successfully',
12846
+ isActionable: false
12847
+ },
12848
+ {
12849
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
12850
+ nextStep: CCTPv2StepName.mint,
12851
+ reason: 'Retry failed mint',
12852
+ isActionable: true
12853
+ },
12854
+ {
12855
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
12856
+ nextStep: CCTPv2StepName.mint,
12857
+ reason: 'Continue pending mint',
12858
+ isActionable: false
12859
+ }
12860
+ ],
12861
+ // After ReAttest step
12862
+ [CCTPv2StepName.reAttest]: [
12863
+ {
12864
+ condition: (ctx)=>ctx.lastStep?.state === 'success',
12865
+ nextStep: CCTPv2StepName.mint,
12866
+ reason: 'Re-attestation successful, proceed to mint',
12867
+ isActionable: true
12868
+ },
12869
+ {
12870
+ condition: (ctx)=>ctx.lastStep?.state === 'error',
12871
+ nextStep: CCTPv2StepName.mint,
12872
+ reason: 'Re-attestation failed, retry mint to re-initiate recovery',
12873
+ isActionable: true
12874
+ },
12875
+ {
12876
+ condition: (ctx)=>ctx.lastStep?.state === 'pending',
12877
+ nextStep: CCTPv2StepName.mint,
12878
+ reason: 'Re-attestation pending, retry mint to re-initiate recovery',
12879
+ isActionable: true
12880
+ }
12881
+ ]
12629
12882
  };
12630
12883
  /**
12631
- * Fetches attestation data from the IRIS API with retry and timeout handling.
12884
+ * Analyze bridge steps to determine retry feasibility and continuation point.
12632
12885
  *
12633
- * Polls the IRIS API until a complete attestation is available. The default
12634
- * window is sized for slow source chains where finality may take many
12635
- * confirmations.
12886
+ * This function examines the current state of bridge steps to determine the optimal
12887
+ * continuation strategy. It uses a rule-based approach that makes it easy to extend
12888
+ * with new flow patterns and step types in the future.
12636
12889
  *
12637
- * Defaults (see `DEFAULT_CONFIG`):
12638
- * - Per-attempt timeout: 2 000 ms (each HTTP request aborts after 2 s)
12639
- * - Retry delay: 2 000 ms between attempts
12640
- * - Max retries: 600 (30 × 20)
12641
- * - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
12890
+ * The current analysis supports the standard CCTP flow:
12891
+ * **Traditional flow**: Approve Burn FetchAttestation Mint
12642
12892
  *
12643
- * @param sourceDomainId - The CCTP domain ID.
12644
- * @param transactionHash - The transaction hash to fetch attestation for.
12645
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
12646
- * @param config - Optional configuration overrides for the attestation fetcher
12647
- * @returns The attestation response data.
12648
- * @throws If the request fails, times out, or returns invalid data.
12893
+ * Key features:
12894
+ * - Rule-based transitions: Easy to extend with new step types and logic
12895
+ * - Context-aware decisions: Considers execution history and step states
12896
+ * - Actionable logic: Distinguishes between steps requiring user action vs waiting
12897
+ * - Terminal states: Properly handles completion and non-actionable states
12898
+ *
12899
+ * @param bridgeResult - The bridge result containing step execution history.
12900
+ * @returns Analysis result with continuation step and actionability information.
12901
+ * @throws Error when bridgeResult is invalid or contains no steps array.
12649
12902
  *
12650
12903
  * @example
12651
12904
  * ```typescript
12652
- * // Fetch attestation for mainnet transaction
12653
- * const response = await fetchAttestation(1, '0xabc...', false)
12654
- * console.log(`Found ${response.messages.length} attestation messages`)
12655
- *
12656
- * // Fetch with custom timeout
12657
- * const response2 = await fetchAttestation(1, '0xdef...', true, {
12658
- * timeout: 5000,
12659
- * maxRetries: 5
12660
- * })
12661
- * ```
12662
- */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
12663
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
12664
- const effectiveConfig = mergeAttestationConfig(config);
12665
- return await pollApiGet(url, isAttestationResponse, effectiveConfig);
12666
- };
12667
- /**
12668
- * Type guard that validates attestation response structure without requiring completion status.
12669
- *
12670
- * This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
12671
- * attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
12672
- * this function does not throw if no complete attestation is found.
12673
- *
12674
- * @param obj - The value to check, typically a parsed JSON response
12675
- * @returns True if the object has valid attestation structure
12676
- * @throws {Error} With "Invalid attestation response structure" if structure is invalid
12677
- * @internal
12678
- */ const isAttestationResponseWithoutStatusCheck = (obj)=>{
12679
- if (!hasValidAttestationStructure(obj)) {
12680
- throw new Error('Invalid attestation response structure');
12681
- }
12682
- return true;
12683
- };
12684
- /**
12685
- * Fetches attestation data without requiring the attestation to be complete.
12905
+ * import { analyzeSteps } from './analyzeSteps'
12686
12906
  *
12687
- * This function is useful for retrieving attestation data (particularly the nonce)
12688
- * from an existing transaction, even if the attestation has expired or is pending.
12689
- * It uses minimal retries since we're fetching existing data, not waiting for completion.
12907
+ * // Failed approval step (requires user action)
12908
+ * const bridgeResult = {
12909
+ * steps: [
12910
+ * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
12911
+ * ]
12912
+ * }
12690
12913
  *
12691
- * @param sourceDomainId - The CCTP domain ID of the source chain
12692
- * @param transactionHash - The transaction hash to fetch attestation for
12693
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
12694
- * @param config - Optional configuration overrides
12695
- * @returns The attestation response data (may contain incomplete/expired attestations)
12696
- * @throws If the request fails, times out, or returns invalid data
12914
+ * const analysis = analyzeSteps(bridgeResult)
12915
+ * // Result: { continuationStep: 'Approve', isRetryable: true,
12916
+ * // reason: 'Retry failed approval' }
12917
+ * ```
12697
12918
  *
12698
12919
  * @example
12699
12920
  * ```typescript
12700
- * // Fetch existing attestation to extract nonce for re-attestation
12701
- * const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
12702
- * const nonce = response.messages[0]?.eventNonce
12703
- * ```
12704
- */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
12705
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
12706
- // Use minimal retries since we're just fetching existing data
12707
- const effectiveConfig = mergeAttestationConfig(config, {
12708
- maxRetries: 3
12709
- });
12710
- return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
12711
- };
12712
- /**
12713
- * Type guard that validates attestation response has expirationBlock === '0'.
12714
- *
12715
- * This is used after requestReAttestation() to poll until the attestation
12716
- * is fully re-processed and has a zero expiration block (never expires).
12717
- * The expiration block transitions from non-zero to zero when Circle
12718
- * completes processing the re-attestation request.
12921
+ * // Pending transaction (requires waiting, not actionable)
12922
+ * const bridgeResult = {
12923
+ * steps: [
12924
+ * { name: 'Approve', state: 'pending' }
12925
+ * ]
12926
+ * }
12719
12927
  *
12720
- * @param obj - The value to check, typically a parsed JSON response
12721
- * @returns True if the attestation has expirationBlock === '0'
12722
- * @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
12928
+ * const analysis = analyzeSteps(bridgeResult)
12929
+ * // Result: { continuationStep: 'Approve', isRetryable: false,
12930
+ * // reason: 'Continue pending approval' }
12931
+ * ```
12723
12932
  *
12724
12933
  * @example
12725
12934
  * ```typescript
12726
- * // After requesting re-attestation, use this to validate the response
12727
- * const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
12728
- * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
12729
- * ```
12935
+ * // Completed bridge (nothing to do)
12936
+ * const bridgeResult = {
12937
+ * steps: [
12938
+ * { name: 'Approve', state: 'success' },
12939
+ * { name: 'Burn', state: 'success' },
12940
+ * { name: 'FetchAttestation', state: 'success' },
12941
+ * { name: 'Mint', state: 'success' }
12942
+ * ]
12943
+ * }
12730
12944
  *
12731
- * @internal
12732
- */ const isReAttestedAttestationResponse = (obj)=>{
12733
- // First validate the basic structure and completion status
12734
- // This will throw appropriate errors for invalid structure or incomplete attestation
12735
- if (!isAttestationResponse(obj)) ;
12736
- // Check if the first message has expirationBlock === '0'
12737
- const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
12738
- if (expirationBlock !== '0') {
12739
- // Re-attestation not yet complete - allow retry via polling
12740
- throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
12945
+ * const analysis = analyzeSteps(bridgeResult)
12946
+ * // Result: { continuationStep: null, isRetryable: false,
12947
+ * // reason: 'Bridge completed successfully' }
12948
+ * ```
12949
+ */ const analyzeSteps = (bridgeResult)=>{
12950
+ // Input validation
12951
+ if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
12952
+ throw new Error('Invalid bridgeResult: must contain a steps array');
12741
12953
  }
12742
- return true;
12954
+ const { steps } = bridgeResult;
12955
+ // Build execution context from step history
12956
+ const context = buildFlowContext(steps);
12957
+ // Determine continuation logic using rule engine
12958
+ const continuation = determineContinuationFromRules(context);
12959
+ return {
12960
+ continuationStep: continuation.nextStep,
12961
+ isActionable: continuation.isActionable,
12962
+ completedSteps: Array.from(context.completedSteps),
12963
+ failedSteps: Array.from(context.failedSteps),
12964
+ reason: continuation.reason
12965
+ };
12743
12966
  };
12744
12967
  /**
12745
- * Fetches attestation data and polls until expirationBlock === '0'.
12746
- *
12747
- * This function is used after calling requestReAttestation() to wait until
12748
- * the attestation is fully re-processed. The expirationBlock transitions
12749
- * from non-zero to zero when Circle completes the re-attestation.
12750
- *
12751
- * @param sourceDomainId - The CCTP domain ID of the source chain
12752
- * @param transactionHash - The transaction hash to fetch attestation for
12753
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
12754
- * @param config - Optional configuration overrides
12755
- * @returns The re-attested attestation response with expirationBlock === '0'
12756
- * @throws If the request fails, times out, or expirationBlock never becomes 0
12757
- *
12758
- * @example
12759
- * ```typescript
12760
- * // After requesting re-attestation
12761
- * await requestReAttestation(nonce, isTestnet)
12968
+ * Build flow context from the execution history.
12762
12969
  *
12763
- * // Poll until expirationBlock becomes 0
12764
- * const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
12765
- * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
12766
- * ```
12767
- */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
12768
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
12769
- const effectiveConfig = mergeAttestationConfig(config);
12770
- return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
12771
- };
12970
+ * @param steps - Array of executed bridge steps.
12971
+ * @returns Flow context with execution state and history.
12972
+ */ function buildFlowContext(steps) {
12973
+ const completedSteps = new Set();
12974
+ const failedSteps = new Set();
12975
+ let lastStep;
12976
+ // Process step history to build context
12977
+ for (const step of steps){
12978
+ if (step.state === 'success' || step.state === 'noop') {
12979
+ completedSteps.add(step.name);
12980
+ } else if (step.state === 'error') {
12981
+ failedSteps.add(step.name);
12982
+ }
12983
+ // Track the last step for continuation logic
12984
+ lastStep = {
12985
+ name: step.name,
12986
+ state: step.state
12987
+ };
12988
+ }
12989
+ return {
12990
+ completedSteps,
12991
+ failedSteps,
12992
+ ...lastStep && {
12993
+ lastStep
12994
+ }
12995
+ };
12996
+ }
12772
12997
  /**
12773
- * Builds the IRIS API URL for re-attestation requests.
12998
+ * Determine continuation step using the rule engine.
12774
12999
  *
12775
- * Constructs the URL for Circle's re-attestation endpoint that allows
12776
- * requesting a fresh attestation for an expired nonce.
13000
+ * @param context - The flow context with execution history.
13001
+ * @returns Continuation decision with next step and actionability information.
13002
+ */ function determineContinuationFromRules(context) {
13003
+ const lastStepName = context.lastStep?.name;
13004
+ // Handle initial state when no steps have been executed
13005
+ if (lastStepName === undefined) {
13006
+ const rules = STEP_TRANSITION_RULES[''];
13007
+ const matchingRule = rules?.find((rule)=>rule.condition(context));
13008
+ if (!matchingRule) {
13009
+ return {
13010
+ nextStep: null,
13011
+ isActionable: false,
13012
+ reason: 'No initial state rule found'
13013
+ };
13014
+ }
13015
+ return {
13016
+ nextStep: matchingRule.nextStep,
13017
+ isActionable: matchingRule.isActionable,
13018
+ reason: matchingRule.reason
13019
+ };
13020
+ }
13021
+ // A step with an empty name is ambiguous and should be treated as an unrecoverable state.
13022
+ if (lastStepName === '') {
13023
+ return {
13024
+ nextStep: null,
13025
+ isActionable: false,
13026
+ reason: 'No transition rules defined for step with empty name'
13027
+ };
13028
+ }
13029
+ const rules = STEP_TRANSITION_RULES[lastStepName];
13030
+ if (!rules) {
13031
+ return {
13032
+ nextStep: null,
13033
+ isActionable: false,
13034
+ reason: `No transition rules defined for step: ${lastStepName}`
13035
+ };
13036
+ }
13037
+ // Find the first matching rule
13038
+ const matchingRule = rules.find((rule)=>rule.condition(context));
13039
+ if (!matchingRule) {
13040
+ return {
13041
+ nextStep: null,
13042
+ isActionable: false,
13043
+ reason: `No matching transition rule for current context`
13044
+ };
13045
+ }
13046
+ return {
13047
+ nextStep: matchingRule.nextStep,
13048
+ isActionable: matchingRule.isActionable,
13049
+ reason: matchingRule.reason
13050
+ };
13051
+ }
13052
+
13053
+ /**
13054
+ * Find a step by name in the bridge result.
12777
13055
  *
12778
- * @param nonce - The nonce from the original attestation
12779
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
12780
- * @returns A fully qualified URL string for the re-attestation endpoint
13056
+ * @param result - The bridge result to search.
13057
+ * @param stepName - The name of the step to find.
13058
+ * @returns The step if found, undefined otherwise.
12781
13059
  *
12782
13060
  * @example
12783
13061
  * ```typescript
12784
- * // Mainnet URL
12785
- * const mainnetUrl = buildReAttestUrl('0xabc', false)
12786
- * // => 'https://iris-api.circle.com/v2/reattest/0xabc'
13062
+ * import { findStepByName } from './findStep'
12787
13063
  *
12788
- * // Testnet URL
12789
- * const testnetUrl = buildReAttestUrl('0xabc', true)
12790
- * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
13064
+ * const burnStep = findStepByName(result, 'burn')
13065
+ * if (burnStep) {
13066
+ * console.log('Burn tx:', burnStep.txHash)
13067
+ * }
12791
13068
  * ```
12792
- */ const buildReAttestUrl = (nonce, isTestnet)=>{
12793
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
12794
- const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
12795
- return url.toString();
12796
- };
12797
- /**
12798
- * Type guard that validates the re-attestation API response structure.
12799
- *
12800
- * @param obj - The value to check, typically a parsed JSON response
12801
- * @returns True if the object matches the ReAttestationResponse shape
12802
- * @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
12803
- * @internal
12804
- */ const isReAttestationResponse = (obj)=>{
12805
- if (typeof obj !== 'object' || obj === null || !('message' in obj) || !('nonce' in obj) || typeof obj.message !== 'string' || typeof obj.nonce !== 'string') {
12806
- throw new Error('Invalid re-attestation response structure');
12807
- }
12808
- return true;
12809
- };
13069
+ */ function findStepByName(result, stepName) {
13070
+ return result.steps.find((step)=>step.name === stepName);
13071
+ }
12810
13072
  /**
12811
- * Requests re-attestation for an expired attestation nonce.
13073
+ * Find a pending step by name and return it with its index.
12812
13074
  *
12813
- * This function calls Circle's re-attestation API endpoint to request a fresh
12814
- * attestation for a previously issued nonce. After calling this function,
12815
- * you should poll `fetchAttestation` to retrieve the new attestation.
13075
+ * Searches for a step that matches both the step name and has a pending state.
12816
13076
  *
12817
- * @param nonce - The nonce from the original (expired) attestation
12818
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
12819
- * @param config - Optional configuration overrides for the request
12820
- * @returns The re-attestation response confirming the request was accepted
12821
- * @throws If the request fails, times out, or returns invalid data
13077
+ * @param result - The bridge result containing steps to search through.
13078
+ * @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
13079
+ * @returns An object containing the step and its index in the steps array.
13080
+ * @throws KitError if the specified pending step is not found.
12822
13081
  *
12823
13082
  * @example
12824
13083
  * ```typescript
12825
- * // Request re-attestation for an expired nonce
12826
- * const response = await requestReAttestation('0xabc', true)
12827
- * console.log(response.message) // "Re-attestation successfully requested for nonce."
13084
+ * import { findPendingStep } from './findStep'
12828
13085
  *
12829
- * // After requesting re-attestation, poll for the new attestation
12830
- * const attestation = await fetchAttestation(domainId, txHash, true)
13086
+ * const { step, index } = findPendingStep(result, 'burn')
13087
+ * console.log('Pending step:', step.name, 'at index:', index)
12831
13088
  * ```
12832
- */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
12833
- const url = buildReAttestUrl(nonce, isTestnet);
12834
- // Use minimal retries since we're just submitting a request, not polling for state
12835
- const effectiveConfig = mergeAttestationConfig(config, {
12836
- maxRetries: 3
12837
- });
12838
- return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
12839
- };
12840
-
12841
- /**
12842
- * Type guard that checks if the relayer has confirmed the mint transaction.
12843
- *
12844
- * This function validates that:
12845
- * 1. The response has valid AttestationResponse structure
12846
- * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
12847
- *
12848
- * If forwardState is 'FAILED', throws a non-retryable KitError.
12849
- * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
12850
- *
12851
- * @param obj - The value to check, typically a parsed JSON response
12852
- * @returns True if the relayer has confirmed the mint
12853
- * @throws {KitError} With FATAL recoverability if structure is invalid
12854
- * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
12855
- * @throws {KitError} With RETRYABLE recoverability if still pending
12856
- * @internal
12857
- */ const isRelayerMintConfirmed = (obj)=>{
12858
- // First check if the structure is valid
12859
- if (!hasValidAttestationStructure(obj)) {
13089
+ */ function findPendingStep(result, stepName) {
13090
+ const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
13091
+ if (index === -1) {
12860
13092
  throw new KitError({
12861
13093
  ...InputError.VALIDATION_FAILED,
12862
13094
  recoverability: 'FATAL',
12863
- message: 'Invalid attestation response structure from IRIS API.'
13095
+ message: `Pending step "${stepName}" not found in result`
12864
13096
  });
12865
13097
  }
12866
- // Find the first message (typically there's only one)
12867
- const message = obj.messages[0];
12868
- if (!message) {
13098
+ const step = result.steps[index];
13099
+ if (!step) {
12869
13100
  throw new KitError({
12870
13101
  ...InputError.VALIDATION_FAILED,
12871
13102
  recoverability: 'FATAL',
12872
- message: 'No attestation messages found in IRIS API response.'
13103
+ message: 'Pending step is undefined'
12873
13104
  });
12874
13105
  }
12875
- // Check for FAILED state - this is a permanent failure
12876
- if (message.forwardState === 'FAILED') {
12877
- throw new KitError({
12878
- ...NetworkError.RELAYER_FORWARD_FAILED,
12879
- recoverability: 'RESUMABLE',
12880
- 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.',
12881
- cause: {
12882
- trace: {
12883
- eventNonce: message.eventNonce,
12884
- attestation: message.attestation,
12885
- message: message.message
12886
- }
12887
- }
12888
- });
12889
- }
12890
- // Check if mint is confirmed (or complete) with a valid transaction hash
12891
- // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
12892
- if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
12893
- return true;
12894
- }
12895
- // Still pending or not yet processed - throw RETRYABLE error to continue polling
12896
- throw new KitError({
12897
- ...NetworkError.RELAYER_PENDING,
12898
- recoverability: 'RETRYABLE',
12899
- message: 'Relayer mint not ready. Waiting for confirmation.'
12900
- });
12901
- };
13106
+ return {
13107
+ step,
13108
+ index
13109
+ };
13110
+ }
12902
13111
  /**
12903
- * Polls the attestation API until the relayer's mint transaction is confirmed.
13112
+ * Get the burn transaction hash from bridge result.
12904
13113
  *
12905
- * This function is used when `useForwarder` is enabled. Instead of the user
12906
- * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
12907
- * This function polls until the relayer has submitted and confirmed the mint transaction.
13114
+ * @param result - The bridge result.
13115
+ * @returns The burn transaction hash, or undefined if not found.
12908
13116
  *
12909
- * @remarks
12910
- * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
12911
- * - Throws immediately if `forwardState` is 'FAILED'
12912
- * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
12913
- * - Returns the attestation message with `forwardTxHash` populated
13117
+ * @example
13118
+ * ```typescript
13119
+ * import { getBurnTxHash } from './findStep'
12914
13120
  *
12915
- * @param sourceDomainId - The CCTP domain ID of the source chain
12916
- * @param transactionHash - The transaction hash of the burn operation
12917
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
12918
- * @param config - Optional configuration overrides for polling behavior
12919
- * @returns The attestation message with confirmed forwardTxHash
12920
- * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
12921
- * @throws {KitError} If timeout is reached while still pending
13121
+ * const burnTxHash = getBurnTxHash(result)
13122
+ * if (burnTxHash) {
13123
+ * console.log('Burn tx hash:', burnTxHash)
13124
+ * }
13125
+ * ```
13126
+ */ function getBurnTxHash(result) {
13127
+ return findStepByName(result, CCTPv2StepName.burn)?.txHash;
13128
+ }
13129
+ /**
13130
+ * Get the attestation data from bridge result.
13131
+ *
13132
+ * @param result - The bridge result.
13133
+ * @returns The attestation data, or undefined if not found.
12922
13134
  *
12923
13135
  * @example
12924
13136
  * ```typescript
12925
- * const attestation = await fetchRelayerMint(0, '0xabc...', false)
12926
- * console.log('Relayer mint tx:', attestation.forwardTxHash)
13137
+ * import { getAttestationData } from './findStep'
13138
+ *
13139
+ * const attestation = getAttestationData(result)
13140
+ * if (attestation) {
13141
+ * console.log('Attestation:', attestation.message)
13142
+ * }
12927
13143
  * ```
12928
- */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
12929
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
12930
- const effectiveConfig = mergeAttestationConfig(config);
12931
- let response;
12932
- try {
12933
- response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
12934
- } catch (error) {
12935
- // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
12936
- if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
12937
- throw new KitError({
12938
- ...NetworkError.RELAYER_FORWARD_FAILED,
12939
- recoverability: error.recoverability,
12940
- message: error.message,
12941
- cause: {
12942
- ...error.cause,
12943
- trace: {
12944
- ...error.cause?.trace,
12945
- burnTxHash: transactionHash
12946
- }
12947
- }
12948
- });
12949
- }
12950
- throw error;
12951
- }
12952
- // Return the first message (which should have forwardTxHash)
12953
- // Note: This check is needed for TypeScript type safety even though
12954
- // isRelayerMintConfirmed validates messages[0] exists. The type guard
12955
- // narrows the type at the call site, but TypeScript can't infer that
12956
- // the array still has elements after pollApiGet returns.
12957
- const message = response.messages[0];
12958
- if (!message) {
12959
- throw new KitError({
12960
- ...InputError.VALIDATION_FAILED,
12961
- recoverability: 'FATAL',
12962
- message: 'No attestation messages found in response after polling.'
12963
- });
13144
+ */ function getAttestationData(result) {
13145
+ // Prefer reAttest data (most recent attestation after expiry)
13146
+ const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
13147
+ if (reAttestStep?.state === 'success' && reAttestStep.data) {
13148
+ return reAttestStep.data;
12964
13149
  }
12965
- return message;
12966
- };
13150
+ // Fall back to fetchAttestation step
13151
+ const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
13152
+ return fetchStep?.data;
13153
+ }
12967
13154
 
12968
- const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
12969
13155
  /**
12970
- * Asserts that the provided parameters match the CCTPv2 wallet context interface.
12971
- * The validation includes:
12972
- * - Basic wallet context validation (adapter, address, chain)
12973
- * - CCTPv2-specific chain validation (must be an EVM chain)
13156
+ * Check if the analysis indicates a non-actionable pending state.
12974
13157
  *
12975
- * @param params - The parameters to validate
12976
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
13158
+ * A pending state is non-actionable when there's a continuation step but
13159
+ * the analysis marks it as not actionable, typically because we need to
13160
+ * wait for an ongoing operation to complete.
13161
+ *
13162
+ * @param analysis - The step analysis result from analyzeSteps.
13163
+ * @param result - The bridge result to check for pending steps.
13164
+ * @returns True if there is a pending step that we should wait for.
12977
13165
  *
12978
13166
  * @example
12979
13167
  * ```typescript
12980
- * import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
12981
- * import { Ethereum } from '@core/chains'
13168
+ * import { hasPendingState } from './stepUtils'
13169
+ * import { analyzeSteps } from '../analyzeSteps'
12982
13170
  *
12983
- * // Prepare wallet context
12984
- * const context = {
12985
- * adapter: {
12986
- * prepare: async () => ({ data: 'prepared transaction' }),
12987
- * waitForTransaction: async () => ({ status: 'confirmed' })
12988
- * },
12989
- * address: '0x1234567890123456789012345678901234567890',
12990
- * chain: {
12991
- * ...Ethereum,
12992
- * usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
12993
- * cctp: {
12994
- * domain: 1,
12995
- * contracts: {
12996
- * v2: {
12997
- * tokenMessenger: '0xTokenMessenger',
12998
- * messageTransmitter: '0xMessageTransmitter'
12999
- * }
13000
- * }
13001
- * }
13002
- * }
13171
+ * const analysis = analyzeSteps(bridgeResult)
13172
+ * if (hasPendingState(analysis, bridgeResult)) {
13173
+ * // Wait for the pending operation to complete
13003
13174
  * }
13175
+ * ```
13176
+ */ /**
13177
+ * Evaluate a transaction receipt and return the corresponding step state
13178
+ * and error message. Centralises the success/revert/unconfirmed logic so
13179
+ * every call-site behaves identically.
13004
13180
  *
13005
- * // This will throw if validation fails
13006
- * assertCCTPv2WalletContext(context)
13181
+ * @param receipt - The transaction receipt containing status and block info.
13182
+ * @param txHash - The transaction hash used in error messages.
13183
+ * @returns An object with `state` and an optional `errorMessage`.
13007
13184
  *
13008
- * // If we get here, context is guaranteed to be valid
13009
- * console.log('CCTPv2 wallet context is valid')
13185
+ * @example
13186
+ * ```typescript
13187
+ * const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
13188
+ * step.state = outcome.state
13189
+ * if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
13010
13190
  * ```
13011
- */ function assertCCTPv2WalletContext(params) {
13012
- // First validate basic wallet context
13013
- validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
13014
- // After validation, we know params is WalletContext
13015
- const context = params;
13016
- // Validate USDC support
13017
- if (context.chain.usdcAddress === null) {
13018
- throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
13191
+ */ function evaluateTransactionOutcome(receipt, txHash) {
13192
+ if (receipt.status === 'success' && receipt.blockNumber) {
13193
+ return {
13194
+ state: 'success'
13195
+ };
13019
13196
  }
13020
- // Validate CCTPv2 support
13021
- if (!isCCTPV2Supported(context.chain)) {
13022
- throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
13197
+ return {
13198
+ state: 'error',
13199
+ errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
13200
+ };
13201
+ }
13202
+ function hasPendingState(analysis, result) {
13203
+ // Check if there's a continuation step that's marked as non-actionable
13204
+ if (analysis.continuationStep === null || analysis.isActionable) {
13205
+ return false;
13023
13206
  }
13207
+ // Verify that the continuation step actually exists and is in pending state
13208
+ const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
13209
+ return pendingStep !== undefined;
13024
13210
  }
13025
-
13026
- const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
13027
13211
  /**
13028
- * Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
13029
- * The validation includes:
13030
- * - Basic parameter structure and types
13031
- * - Amount validation (non-empty numeric string \> 0)
13032
- * - Wallet address format validation (must be valid Ethereum address)
13033
- * - Chain definition validation (must be a valid chain with required properties)
13034
- * - Adapter validation (must implement required methods)
13035
- * - Optional config validation (transfer speed and max fee)
13036
- * - Network compatibility (source and destination chains must both be testnet or both mainnet)
13037
- * - CCTPv2-specific wallet context validations
13212
+ * Check if the step is the last one in the execution flow.
13038
13213
  *
13039
- * @param params - The parameters to validate
13040
- * @throws {KitError} If validation fails, with details about which properties failed
13214
+ * @param step - The step object to check.
13215
+ * @param stepNames - The ordered list of step names in the execution flow.
13216
+ * @returns True if this is the last step in the flow.
13041
13217
  *
13042
13218
  * @example
13043
13219
  * ```typescript
13044
- * import { assertCCTPv2BridgeParams } from '@circle-fin/provider-cctp-v2'
13045
- * import { Ethereum, Base } from '@core/chains'
13220
+ * import { isLastStep } from './stepUtils'
13046
13221
  *
13047
- * // Prepare transfer parameters
13048
- * const params = {
13049
- * amount: '100.50',
13050
- * source: {
13051
- * adapter: sourceAdapter,
13052
- * address: '0xSourceAddress',
13053
- * chain: {
13054
- * ...Ethereum,
13055
- * cctp: {
13056
- * domain: 1,
13057
- * contracts: {
13058
- * v2: {
13059
- * tokenMessenger: '0xTokenMessenger',
13060
- * messageTransmitter: '0xMessageTransmitter'
13061
- * }
13062
- * }
13063
- * }
13064
- * }
13065
- * },
13066
- * destination: {
13067
- * adapter: destAdapter,
13068
- * address: '0xDestAddress',
13069
- * chain: {
13070
- * ...Base,
13071
- * cctp: {
13072
- * domain: 2,
13073
- * contracts: {
13074
- * v2: {
13075
- * tokenMessenger: '0xTokenMessenger',
13076
- * messageTransmitter: '0xMessageTransmitter'
13077
- * }
13078
- * }
13079
- * }
13080
- * }
13081
- * },
13082
- * token: 'USDC',
13083
- * config: {
13084
- * transferSpeed: 'FAST',
13085
- * maxFee: '1000000'
13086
- * }
13087
- * }
13088
- *
13089
- * // This will throw if validation fails
13090
- * assertCCTPv2BridgeParams(params)
13091
- *
13092
- * // If we get here, params is guaranteed to be valid
13093
- * console.log('CCTPv2 transfer parameters are valid')
13222
+ * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
13223
+ * isLastStep({ name: 'mint' }, stepNames) // true
13224
+ * isLastStep({ name: 'burn' }, stepNames) // false
13094
13225
  * ```
13095
- */ function assertCCTPv2BridgeParams(params) {
13096
- // First validate basic bridge params
13097
- validateWithStateTracking(params, bridgeParamsSchema, 'CCTPv2 bridge parameters', assertCCTPv2BridgeParamsSymbol);
13098
- // After validation, we know params is CCTPV2BridgeParams
13099
- const bridgeParams = params;
13100
- // Enforce that source and destination chains are either testnet or mainnet
13101
- if (bridgeParams.source.chain.isTestnet !== bridgeParams.destination.chain.isTestnet) {
13102
- throw createNetworkMismatchError(bridgeParams.source.chain, bridgeParams.destination.chain);
13103
- }
13104
- assertCCTPV2Support(bridgeParams.source.chain, bridgeParams.destination.chain);
13105
- // Validate that the destination chain supports forwarding when forwarder is enabled
13106
- assertForwarderRouteSupport(bridgeParams.source.chain, bridgeParams.destination.chain, bridgeParams.destination.useForwarder);
13107
- /**
13108
- * Enforce that if fee is defined then feeRecipient must be defined.
13109
- * We do not do this in the validation function itself because we want to allow
13110
- * optional properties when calling `provider.bridge()` due to the custom fee
13111
- * configuration being possible at the kit level as well.
13112
- */ if (bridgeParams.config?.customFee?.value !== undefined && bridgeParams.config?.customFee?.recipientAddress === undefined) {
13113
- throw createValidationFailedError$1('recipientAddress', bridgeParams.config.customFee.value, 'Custom fee is defined but fee recipient is not. Please provide a fee recipient.');
13114
- }
13115
- // Check if this is a forwarder-only destination (no adapter, requires useForwarder: true)
13116
- const isForwarderOnly = bridgeParams.destination.useForwarder === true && !('adapter' in bridgeParams.destination && bridgeParams.destination.adapter);
13117
- // Forwarder-only destinations require recipientAddress
13118
- if (isForwarderOnly) {
13119
- if (!bridgeParams.destination.recipientAddress?.trim()) {
13120
- throw createValidationFailedError$1('recipientAddress', bridgeParams.destination.recipientAddress, 'recipientAddress is required when using forwarder without a destination adapter.');
13121
- }
13122
- }
13123
- // Validate CCTP v2 specific requirements for source wallet
13124
- assertCCTPv2WalletContext(bridgeParams.source);
13125
- // Validate that source adapter supports the chain (defense-in-depth)
13126
- bridgeParams.source.adapter.validateChainSupport(bridgeParams.source.chain);
13127
- // Only validate destination wallet context and adapter if not forwarder-only
13128
- if (!isForwarderOnly) {
13129
- assertCCTPv2WalletContext(bridgeParams.destination);
13130
- // Validate that destination adapter supports the chain (defense-in-depth)
13131
- bridgeParams.destination.adapter.validateChainSupport(bridgeParams.destination.chain);
13132
- }
13226
+ */ function isLastStep(step, stepNames) {
13227
+ const stepIndex = stepNames.indexOf(step.name);
13228
+ return stepIndex === -1 || stepIndex >= stepNames.length - 1;
13133
13229
  }
13134
13230
  /**
13135
- * Validate CCTP v2 support on both chains
13136
- */ /**
13137
- * Throws a KitError if the given chain does not support CCTP v2.
13231
+ * Wait for a pending transaction to complete.
13138
13232
  *
13139
- * @param chain - The chain to check for CCTP v2 support
13140
- * @param otherChain - The other chain in the route (for error context)
13141
- * @param isSource - Whether this is the source chain (for error context)
13142
- */ function assertCCTPV2Support(source, destination) {
13143
- if (!isCCTPV2Supported(source) || !isCCTPV2Supported(destination)) {
13144
- throw createUnsupportedRouteError(source.name, destination.name);
13145
- }
13146
- }
13147
- /**
13148
- * Validates that the forwarder (relaying) feature is compatible with the route.
13233
+ * Poll the adapter until the transaction is confirmed on-chain and return
13234
+ * the updated step with success or error state based on the receipt.
13149
13235
  *
13150
- * Checks the destination chain's `cctp.forwarderSupported.destination` property
13151
- * to determine whether the chain supports receiving forwarded transfers.
13236
+ * @param pendingStep - The full step object containing the transaction hash.
13237
+ * @param adapter - The adapter to use for waiting.
13238
+ * @param chain - The chain where the transaction was submitted.
13239
+ * @returns The updated step object with success or error state.
13152
13240
  *
13153
- * @param source - The source chain definition
13154
- * @param destination - The destination chain definition
13155
- * @param useForwarder - Whether the forwarder is enabled on the destination
13156
- * @throws {KitError} If the forwarder is enabled and the destination chain does not support forwarding
13157
- */ function assertForwarderRouteSupport(source, destination, useForwarder) {
13158
- if (useForwarder === true && !destination.cctp?.forwarderSupported.destination) {
13159
- throw new KitError({
13160
- ...InputError.UNSUPPORTED_ROUTE,
13161
- recoverability: 'FATAL',
13162
- message: `Route from ${source.name} to ${destination.name} with forwarder is not supported (destination chain does not support forwarding).`,
13163
- cause: {
13164
- trace: {
13165
- source: source.name,
13166
- destination: destination.name
13167
- }
13168
- }
13169
- });
13170
- }
13171
- }
13172
-
13173
- /**
13174
- * Checks if a decoded attestation field matches the corresponding transfer parameter.
13175
- * If the values do not match, appends a descriptive error message to the errors array.
13241
+ * @throws KitError when the pending step has no transaction hash.
13176
13242
  *
13177
- * @param field - The name of the field being compared (for error reporting)
13178
- * @param decoded - The value decoded from the attestation message
13179
- * @param param - The expected value from the transfer parameters
13180
- * @param errors - The array to which error messages will be appended if a mismatch is found
13181
- */ function checkFieldMismatch(field, decoded, param, errors) {
13182
- if (decoded !== param) {
13183
- errors.push(`${field} mismatch: decoded=${String(decoded)}, params=${String(param)}`);
13184
- }
13185
- }
13186
- /**
13187
- * Asserts that the decoded message from attestation matches the provided transfer params.
13188
- * Throws KitError if any field mismatches, with clear error messages.
13243
+ * @example
13244
+ * ```typescript
13245
+ * import { waitForPendingTransaction } from './bridgeStepUtils'
13189
13246
  *
13190
- * @param attestation - The attestation message containing the decoded message
13191
- * @param params - The transfer parameters to validate against
13192
- * @throws {@link KitError} If any field mismatches
13193
- */ async function assertCCTPv2AttestationParams(attestation, params) {
13194
- const errors = [];
13195
- const message = attestation.decodedMessage;
13196
- const messageBody = message.decodedMessageBody;
13197
- // Use recipientAddress if provided, otherwise use destination.address
13198
- const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
13199
- const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
13200
- let sender;
13201
- if (hasCustomContractSupport(params.source.chain, 'bridge')) {
13202
- if (params.source.chain.type === 'solana') {
13203
- // Solana: User → Bridge contract → CCTP (user remains sender)
13204
- sender = params.source.address;
13205
- } else {
13206
- // Other chains (like EVM): Bridge contract → CCTP (bridge contract becomes sender)
13207
- sender = params.source.chain.kitContracts?.bridge;
13208
- }
13209
- } else {
13210
- sender = params.source.address;
13211
- }
13212
- checkFieldMismatch('sourceDomain', message.sourceDomain, params.source.chain.cctp.domain.toString(), errors);
13213
- checkFieldMismatch('destinationDomain', message.destinationDomain, params.destination.chain.cctp.domain.toString(), errors);
13214
- checkFieldMismatch('minFinalityThreshold', message.minFinalityThreshold, CCTPv2MinFinalityThreshold[params.config.transferSpeed ?? 'FAST'].toString(), errors);
13215
- checkFieldMismatch('sender', params.source.chain.type === 'evm' ? messageBody.messageSender.toLowerCase() : messageBody.messageSender, params.source.chain.type === 'evm' ? sender?.toLowerCase() : sender, errors);
13216
- checkFieldMismatch('recipient', params.destination.chain.type === 'evm' ? messageBody.mintRecipient.toLowerCase() : messageBody.mintRecipient, params.destination.chain.type === 'evm' ? mintRecipient.toLowerCase() : mintRecipient, errors);
13217
- checkFieldMismatch('amount', messageBody.amount, params.amount.toString(), errors);
13218
- checkFieldMismatch('burnToken', messageBody.burnToken.toLowerCase(), params.source.chain.usdcAddress.toLowerCase(), errors);
13219
- if (errors.length > 0) {
13220
- const errorMessage = 'Attestation validation failed: received attestation does not match expected transfer parameters';
13221
- const firstError = errors[0] ?? '';
13247
+ * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
13248
+ * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
13249
+ * // updatedStep.state is now 'success' or 'error'
13250
+ * ```
13251
+ */ async function waitForPendingTransaction(pendingStep, adapter, chain) {
13252
+ if (!pendingStep.txHash) {
13222
13253
  throw new KitError({
13223
13254
  ...InputError.VALIDATION_FAILED,
13224
13255
  recoverability: 'FATAL',
13225
- message: `${errorMessage}: ${firstError}`,
13226
- cause: {
13227
- trace: {
13228
- validationErrors: errors
13229
- }
13230
- }
13256
+ message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
13231
13257
  });
13232
13258
  }
13259
+ const txHash = pendingStep.txHash;
13260
+ const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
13261
+ isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
13262
+ chain: chain.name,
13263
+ txHash
13264
+ }))
13265
+ });
13266
+ const outcome = evaluateTransactionOutcome(txReceipt, txHash);
13267
+ return {
13268
+ ...pendingStep,
13269
+ state: outcome.state,
13270
+ data: txReceipt,
13271
+ explorerUrl: buildExplorerUrl(chain, txHash),
13272
+ ...outcome.errorMessage ? {
13273
+ errorMessage: outcome.errorMessage
13274
+ } : {}
13275
+ };
13233
13276
  }
13234
-
13235
13277
  /**
13236
- * Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
13278
+ * Wait for a pending step to complete.
13237
13279
  *
13238
- * Validates the full public-boundary input before any field destructuring,
13239
- * `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
13240
- * inputs always produce typed `KitError` validation failures.
13280
+ * For transaction steps: waits for the transaction to be confirmed.
13281
+ * For attestation: re-executes the attestation fetch.
13241
13282
  *
13242
- * Checks performed (in order):
13243
- * - `params` must be a non-null plain object
13244
- * - `source` valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
13245
- * - `destinationChain` present and supports CCTP v2
13246
- * - source and destination chains must both be testnet or both mainnet
13247
- * - source and destination chains must differ
13248
- * - `executor` non-empty string
13249
- * - `amount` bigint or non-empty string coercible to bigint
13250
- * - `feeTotalAmount` bigint or non-empty string coercible to bigint
13251
- * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
13252
- * - `claim.signedQuote` — valid `0x`-prefixed hex string
13253
- * - `claim.refundAddress` — valid EVM address
13254
- * - `hookData` — valid `0x`-prefixed hex string when present
13283
+ * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
13284
+ * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
13285
+ * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
13286
+ * @param adapter - The adapter to use.
13287
+ * @param chain - The chain where the step is executing.
13288
+ * @param context - The retry context.
13289
+ * @param result - The bridge result.
13290
+ * @param provider - The CCTP v2 bridging provider.
13291
+ * @returns The resolved step object with updated state.
13255
13292
  *
13256
- * @param params - The value to validate.
13257
- * @throws {KitError} If any field is missing or invalid.
13293
+ * @throws KitError when fetching attestation but burn transaction hash is not found.
13258
13294
  *
13259
13295
  * @example
13260
13296
  * ```typescript
13261
- * assertBurnWithFeesParams(params)
13262
- * // params is now typed as BurnWithFeesParams and safe to use
13263
- * const { source, destinationChain, amount } = params
13297
+ * import { waitForStepToComplete } from './bridgeStepUtils'
13298
+ *
13299
+ * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
13300
+ * const updatedStep = await waitForStepToComplete(
13301
+ * pendingStep,
13302
+ * adapter,
13303
+ * chain,
13304
+ * context,
13305
+ * result,
13306
+ * provider,
13307
+ * )
13308
+ * // updatedStep.state is now 'success' or 'error'
13264
13309
  * ```
13265
- */ function assertBurnWithFeesParams(params) {
13266
- if (params === null || typeof params !== 'object' || Array.isArray(params)) {
13267
- throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
13268
- }
13269
- const p = params;
13270
- // Source wallet context
13271
- assertCCTPv2WalletContext(p['source']);
13272
- const source = p['source'];
13273
- // destinationChain
13274
- const destinationChain = p['destinationChain'];
13275
- if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
13276
- throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
13277
- }
13278
- if (!isCCTPV2Supported(destinationChain)) {
13279
- throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
13280
- }
13281
- const dest = destinationChain;
13282
- // Testnet / mainnet mismatch
13283
- if (source.chain.isTestnet !== dest.isTestnet) {
13284
- throw createNetworkMismatchError(source.chain, dest);
13285
- }
13286
- // Same-chain guard
13287
- if (source.chain.name === dest.name) {
13288
- throw createUnsupportedRouteError(source.chain.name, dest.name);
13289
- }
13290
- // executor
13291
- const executor = p['executor'];
13292
- if (typeof executor !== 'string' || executor === '') {
13293
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
13294
- }
13295
- // amount
13296
- const rawAmount = p['amount'];
13297
- if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
13298
- throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
13310
+ */ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
13311
+ if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
13312
+ // For attestation, re-run the fetch (it has built-in polling)
13313
+ const burnTxHash = getBurnTxHash(result);
13314
+ if (!burnTxHash) {
13315
+ throw new KitError({
13316
+ ...InputError.VALIDATION_FAILED,
13317
+ recoverability: 'FATAL',
13318
+ message: 'Cannot fetch attestation: burn transaction hash not found'
13319
+ });
13320
+ }
13321
+ const sourceAddress = result.source.address;
13322
+ const attestation = await provider.fetchAttestation({
13323
+ chain: result.source.chain,
13324
+ adapter: context.from,
13325
+ address: sourceAddress
13326
+ }, burnTxHash);
13327
+ return {
13328
+ ...pendingStep,
13329
+ state: 'success',
13330
+ data: attestation
13331
+ };
13299
13332
  }
13333
+ // For transaction steps, wait for the transaction to complete
13334
+ return waitForPendingTransaction(pendingStep, adapter, chain);
13335
+ }
13336
+
13337
+ /**
13338
+ * Multiplier applied to a successful gas estimate before it is submitted.
13339
+ *
13340
+ * Estimates are exact, not padded: Sei returns 109_739 for an approve that
13341
+ * consumes 107_717 (1.9% headroom). Chains that price storage in large steps
13342
+ * can exceed the estimate if state changes between estimation and inclusion,
13343
+ * so the estimate is padded before use.
13344
+ *
13345
+ * @remarks
13346
+ * This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
13347
+ * scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
13348
+ * covers a slot that exists at estimation time and is consumed before
13349
+ * inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
13350
+ * the estimate covers it. For burn the buffer does cover a step (25% of
13351
+ * ~300_000 exceeds 51_500).
13352
+ */ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
13353
+ /**
13354
+ * Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
13355
+ *
13356
+ * Estimates first so chains whose real cost exceeds the floor are covered by
13357
+ * their own measurement, and falls back to the floor whenever estimation is
13358
+ * unavailable or under-reports. Estimation failure is never fatal here: before
13359
+ * floors existed these requests were submitted with a pinned limit and no
13360
+ * estimate at all, so degrading to the floor is never worse than the previous
13361
+ * behaviour.
13362
+ *
13363
+ * @param request - The prepared EVM request to size a gas limit for
13364
+ * @param gasFloor - The minimum gas limit to submit, in gas units
13365
+ * @returns The gas limit to submit, in gas units
13366
+ * @throws Never — estimation failures degrade to `gasFloor`
13367
+ *
13368
+ * @example
13369
+ * ```typescript
13370
+ * const gasLimit = await resolveGasLimit(request, 150_000)
13371
+ * ```
13372
+ */ const resolveGasLimit = async (request, gasFloor)=>{
13300
13373
  try {
13301
- BigInt(rawAmount);
13374
+ // Deliberately called without a `fallback`: both the viem and ethers
13375
+ // adapters *return* the supplied fallback object when estimation reverts
13376
+ // rather than throwing, which would set the estimate to the floor and then
13377
+ // multiply it by the buffer below. Omitting it routes reverts through the
13378
+ // catch, so a failed estimate degrades to exactly the floor.
13379
+ const estimate = await request.estimate();
13380
+ // The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
13381
+ // typed `bigint`, but adapters are a public extension point and may be
13382
+ // implemented in plain JS, so a non-bigint `gas` would throw here
13383
+ // ("Cannot mix BigInt and other types"). Guarding it keeps the documented
13384
+ // contract — estimation never aborts a step, it degrades to the floor.
13385
+ const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
13386
+ // Convert before comparing: Math.max throws on BigInt operands, and gas
13387
+ // units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
13388
+ return Math.max(Number(buffered), gasFloor);
13302
13389
  } catch {
13303
- throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
13304
- }
13305
- // feeTotalAmount
13306
- const rawFee = p['feeTotalAmount'];
13307
- if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
13308
- throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
13390
+ // Estimation is best-effort; the floor is the known-safe value.
13391
+ return gasFloor;
13309
13392
  }
13393
+ };
13394
+ /**
13395
+ * Executes a prepared chain request and returns the result as a bridge step.
13396
+ *
13397
+ * This function takes a prepared chain request (containing transaction data) and executes
13398
+ * it using the appropriate adapter. It handles the execution details and formats
13399
+ * the result as a standardized bridge step with transaction details and explorer URLs.
13400
+ *
13401
+ * @param params - The execution parameters containing:
13402
+ * - `name`: The name of the step
13403
+ * - `request`: The prepared chain request containing transaction data
13404
+ * - `adapter`: The adapter that will execute the transaction
13405
+ * - `confirmations`: The number of confirmations to wait for (defaults to 1)
13406
+ * - `timeout`: The timeout for the request in milliseconds
13407
+ * - `gasFloor`: Optional minimum gas limit (number); the request is submitted
13408
+ * with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
13409
+ * @returns The bridge step with the transaction details and explorer URL
13410
+ * @throws If the transaction execution fails
13411
+ *
13412
+ * @example
13413
+ * ```typescript
13414
+ * const step = await executePreparedChainRequest({
13415
+ * name: 'approve',
13416
+ * request: preparedRequest,
13417
+ * adapter: adapter,
13418
+ * confirmations: 2,
13419
+ * timeout: 30000
13420
+ * })
13421
+ * console.log('Transaction hash:', step.txHash)
13422
+ * ```
13423
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
13424
+ const step = {
13425
+ name,
13426
+ state: 'pending'
13427
+ };
13310
13428
  try {
13311
- BigInt(rawFee);
13312
- } catch {
13313
- throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
13314
- }
13315
- // feeToken
13316
- if (!evmAddressSchema.safeParse(p['feeToken']).success) {
13317
- throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
13429
+ /**
13430
+ * No-op requests are not executed.
13431
+ * We return a noop step instead.
13432
+ */ if (request.type === 'noop') {
13433
+ step.state = 'noop';
13434
+ return step;
13435
+ }
13436
+ const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
13437
+ gasLimit: await resolveGasLimit(request, gasFloor)
13438
+ }) : await request.execute();
13439
+ step.txHash = txHash;
13440
+ const retryOptions = {
13441
+ isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
13442
+ chain: chain.name,
13443
+ txHash
13444
+ }))
13445
+ };
13446
+ if (timeout !== undefined) {
13447
+ retryOptions.deadlineMs = Date.now() + timeout;
13448
+ }
13449
+ const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
13450
+ confirmations,
13451
+ timeout
13452
+ }, chain), retryOptions);
13453
+ const outcome = evaluateTransactionOutcome(transaction, txHash);
13454
+ step.state = outcome.state;
13455
+ step.data = transaction;
13456
+ // Generate explorer URL for the step
13457
+ step.explorerUrl = buildExplorerUrl(chain, txHash);
13458
+ if (outcome.errorMessage) {
13459
+ step.errorMessage = outcome.errorMessage;
13460
+ // Transaction was mined but reverted on-chain.
13461
+ step.errorCategory = 'chain_revert';
13462
+ }
13463
+ } catch (err) {
13464
+ step.state = 'error';
13465
+ step.error = err;
13466
+ // Sequential path does not yet attempt fine-grained classification of
13467
+ // pre-submission errors (user_rejected, capability errors, etc.). Mark
13468
+ // as `unknown` so consumers can at least detect the category is
13469
+ // populated uniformly across batched and sequential flows.
13470
+ step.errorCategory = 'unknown';
13471
+ // Optionally parse for common blockchain error formats
13472
+ if (err instanceof Error) {
13473
+ step.errorMessage = err.message;
13474
+ } else if (typeof err === 'object' && err != null && 'message' in err) {
13475
+ step.errorMessage = String(err.message);
13476
+ } else {
13477
+ step.errorMessage = `Unknown error occurred during ${name} step.`;
13478
+ }
13318
13479
  }
13319
- // claim
13320
- const rawClaim = p['claim'];
13321
- if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
13322
- throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
13480
+ return step;
13481
+ }
13482
+
13483
+ /**
13484
+ * Default configuration values for the attestation fetcher.
13485
+ * @internal
13486
+ */ const DEFAULT_CONFIG = {
13487
+ timeout: 2_000,
13488
+ maxRetries: 30 * 20,
13489
+ retryDelay: 2_000,
13490
+ headers: {
13491
+ 'Content-Type': 'application/json'
13323
13492
  }
13324
- const claim = rawClaim;
13325
- if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
13326
- throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
13493
+ };
13494
+ /**
13495
+ * Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
13496
+ *
13497
+ * Headers are merged independently so caller-supplied headers augment the
13498
+ * defaults (such as `Content-Type`) rather than replacing them wholesale.
13499
+ *
13500
+ * @param config - Caller-provided polling configuration overrides
13501
+ * @param internalDefaults - Internal defaults applied before `config` (for example a
13502
+ * reduced `maxRetries` for one-shot requests); `config` still wins on conflict
13503
+ * @returns The effective polling configuration
13504
+ * @internal
13505
+ */ const mergeAttestationConfig = (config, internalDefaults = {})=>({
13506
+ ...DEFAULT_CONFIG,
13507
+ ...internalDefaults,
13508
+ ...config,
13509
+ headers: {
13510
+ ...DEFAULT_CONFIG.headers,
13511
+ ...internalDefaults.headers,
13512
+ ...config.headers
13513
+ }
13514
+ });
13515
+ /**
13516
+ * Type guard that verifies if an unknown value matches the AttestationMessage shape
13517
+ * and has all required properties.
13518
+ *
13519
+ * @param obj - The value to check, typically an element from the messages array
13520
+ * @returns True if the object matches the AttestationMessage shape, false otherwise
13521
+ * @internal
13522
+ */ const isValidAttestationMessage = (obj)=>{
13523
+ 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';
13524
+ };
13525
+ /**
13526
+ * Type guard that verifies if an attestation message is complete.
13527
+ *
13528
+ * @param message - The attestation message to check
13529
+ * @returns True if the message status is 'complete', false otherwise
13530
+ * @internal
13531
+ */ const isCompleteAttestation = (message)=>{
13532
+ return message.status === 'complete';
13533
+ };
13534
+ /**
13535
+ * Type guard that verifies if an unknown value has the correct structure
13536
+ * for an AttestationResponse, regardless of attestation completion status.
13537
+ *
13538
+ * @param obj - The value to check, typically a parsed JSON response
13539
+ * @returns True if the object matches the AttestationResponse shape
13540
+ * @internal
13541
+ */ const hasValidAttestationStructure = (obj)=>{
13542
+ if (typeof obj !== 'object' || obj === null || !('messages' in obj) || !Array.isArray(obj.messages)) {
13543
+ return false;
13327
13544
  }
13328
- if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
13329
- throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
13545
+ const messages = obj.messages;
13546
+ // Validate all messages have the correct shape
13547
+ return messages.every(isValidAttestationMessage);
13548
+ };
13549
+ /**
13550
+ * Type guard that verifies if an unknown value matches the AttestationResponse shape
13551
+ * and contains a complete attestation.
13552
+ *
13553
+ * This function performs runtime validation to ensure that the provided value
13554
+ * conforms to the expected structure of an AttestationResponse and has at least
13555
+ * one complete attestation. It checks that:
13556
+ * 1. The value has valid AttestationResponse structure
13557
+ * 2. At least one message has status 'complete'
13558
+ *
13559
+ * @remarks
13560
+ * This type guard is used internally by the attestation fetcher to validate
13561
+ * responses from the IRIS API before processing them. It provides runtime
13562
+ * type safety for data coming from the network and ensures we have a complete
13563
+ * attestation before proceeding.
13564
+ *
13565
+ * If the response has valid structure but no complete attestation yet,
13566
+ * it throws a retryable error. If the response structure is invalid,
13567
+ * it throws a non-retryable validation error.
13568
+ *
13569
+ * @param obj - The value to check, typically a parsed JSON response
13570
+ * @returns True if the object matches the AttestationResponse shape and has a complete attestation
13571
+ * @throws {Error} With "Invalid attestation response structure" if structure is invalid (non-retryable)
13572
+ * @throws {Error} With "Attestation not ready" if no complete attestation yet (retryable)
13573
+ *
13574
+ * @example
13575
+ * ```typescript
13576
+ * const response = await fetch('https://iris-api.circle.com/...')
13577
+ * const data = await response.json()
13578
+ *
13579
+ * if (isAttestationResponse(data)) {
13580
+ * // TypeScript now knows data is AttestationResponse with at least one complete attestation
13581
+ * const completeMessage = data.messages.find(msg => msg.status === 'complete')
13582
+ * console.log('Found complete attestation:', completeMessage.attestation)
13583
+ * }
13584
+ * ```
13585
+ */ const isAttestationResponse = (obj)=>{
13586
+ // First check if the structure is valid
13587
+ if (!hasValidAttestationStructure(obj)) {
13588
+ // If structure is invalid, this is a permanent failure - don't retry
13589
+ throw new Error('Invalid attestation response structure');
13330
13590
  }
13331
- // hookData (optional)
13332
- const hookData = p['hookData'];
13333
- if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
13334
- throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
13591
+ // Then check if at least one message is complete
13592
+ if (!obj.messages.some(isCompleteAttestation)) {
13593
+ // If no complete message, this is a temporary state - allow retry
13594
+ throw new Error('Attestation not ready');
13335
13595
  }
13336
- }
13337
-
13596
+ return true;
13597
+ };
13338
13598
  /**
13339
- * CCTP bridge step names that can occur in the bridging flow.
13599
+ * Builds the IRIS API URL for fetching attestation data from Circle's CCTP service.
13340
13600
  *
13341
- * This object provides type safety for step names and represents all possible
13342
- * steps that can be executed during a CCTP bridge operation. Using const assertions
13343
- * makes this tree-shakable and follows modern TypeScript best practices.
13344
- */ const CCTPv2StepName = {
13345
- approve: 'approve',
13346
- burn: 'burn',
13347
- fetchAttestation: 'fetchAttestation',
13348
- mint: 'mint',
13349
- reAttest: 'reAttest'
13601
+ * Constructs a properly formatted URL for the IRIS API v2 endpoint that provides
13602
+ * attestation messages for cross-chain transfers. The URL includes both the source
13603
+ * domain identifier and the transaction hash as query parameters. The base URL
13604
+ * is selected based on whether the operation is for testnet or mainnet.
13605
+ *
13606
+ * @param sourceDomainId - The CCTP domain ID of the source chain (numeric or string)
13607
+ * @param transactionHash - The transaction hash of the burn operation to fetch attestation for
13608
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
13609
+ * @returns A fully qualified URL string for the IRIS API endpoint
13610
+ *
13611
+ * @example
13612
+ * ```typescript
13613
+ * // Mainnet URL
13614
+ * const mainnetUrl = buildIrisUrl(1, '0xabc...', false)
13615
+ * // => 'https://iris-api.circle.com/v2/messages/1?transactionHash=0xabc...'
13616
+ *
13617
+ * // Testnet URL
13618
+ * const testnetUrl = buildIrisUrl(1, '0xdef...', true)
13619
+ * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
13620
+ * ```
13621
+ */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
13622
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
13623
+ const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
13624
+ url.searchParams.set('transactionHash', transactionHash);
13625
+ return url.toString();
13350
13626
  };
13351
13627
  /**
13352
- * Conditional step transition rules for CCTP bridge flow.
13628
+ * Fetches attestation data from the IRIS API with retry and timeout handling.
13353
13629
  *
13354
- * Rules are evaluated in order - the first matching condition determines the next step.
13355
- * This approach supports flexible flow logic and makes it easy to extend with new patterns.
13356
- */ const STEP_TRANSITION_RULES = {
13357
- // Starting state - no steps executed yet
13358
- '': [
13359
- {
13360
- condition: ()=>true,
13361
- nextStep: CCTPv2StepName.approve,
13362
- reason: 'Start with approval step',
13363
- isActionable: true
13364
- }
13365
- ],
13366
- // After Approve step
13367
- [CCTPv2StepName.approve]: [
13368
- {
13369
- condition: (ctx)=>ctx.lastStep?.state === 'success',
13370
- nextStep: CCTPv2StepName.burn,
13371
- reason: 'Approval successful, proceed to burn',
13372
- isActionable: true
13373
- },
13374
- {
13375
- condition: (ctx)=>ctx.lastStep?.state === 'error',
13376
- nextStep: CCTPv2StepName.approve,
13377
- reason: 'Retry failed approval',
13378
- isActionable: true
13379
- },
13380
- {
13381
- condition: (ctx)=>ctx.lastStep?.state === 'noop',
13382
- nextStep: CCTPv2StepName.burn,
13383
- reason: 'No approval needed, proceed to burn',
13384
- isActionable: true
13385
- },
13386
- {
13387
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
13388
- nextStep: CCTPv2StepName.approve,
13389
- reason: 'Continue pending approval',
13390
- isActionable: false
13391
- }
13392
- ],
13393
- // After Burn step
13394
- [CCTPv2StepName.burn]: [
13395
- {
13396
- condition: (ctx)=>ctx.lastStep?.state === 'success',
13397
- nextStep: CCTPv2StepName.fetchAttestation,
13398
- reason: 'Burn successful, fetch attestation',
13399
- isActionable: true
13400
- },
13401
- {
13402
- condition: (ctx)=>ctx.lastStep?.state === 'error',
13403
- nextStep: CCTPv2StepName.burn,
13404
- reason: 'Retry failed burn',
13405
- isActionable: true
13406
- },
13407
- {
13408
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
13409
- nextStep: CCTPv2StepName.burn,
13410
- reason: 'Continue pending burn',
13411
- isActionable: false
13412
- }
13413
- ],
13414
- // After FetchAttestation step
13415
- [CCTPv2StepName.fetchAttestation]: [
13416
- {
13417
- condition: (ctx)=>ctx.lastStep?.state === 'success',
13418
- nextStep: CCTPv2StepName.mint,
13419
- reason: 'Attestation fetched, proceed to mint',
13420
- isActionable: true
13421
- },
13422
- {
13423
- condition: (ctx)=>ctx.lastStep?.state === 'error',
13424
- nextStep: CCTPv2StepName.fetchAttestation,
13425
- reason: 'Retry fetching attestation',
13426
- isActionable: true
13427
- },
13428
- {
13429
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
13430
- nextStep: CCTPv2StepName.fetchAttestation,
13431
- reason: 'Continue pending attestation fetch',
13432
- isActionable: false
13433
- }
13434
- ],
13435
- // After Mint step
13436
- [CCTPv2StepName.mint]: [
13437
- {
13438
- condition: (ctx)=>ctx.lastStep?.state === 'success',
13439
- nextStep: null,
13440
- reason: 'Bridge completed successfully',
13441
- isActionable: false
13442
- },
13443
- {
13444
- condition: (ctx)=>ctx.lastStep?.state === 'error',
13445
- nextStep: CCTPv2StepName.mint,
13446
- reason: 'Retry failed mint',
13447
- isActionable: true
13448
- },
13449
- {
13450
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
13451
- nextStep: CCTPv2StepName.mint,
13452
- reason: 'Continue pending mint',
13453
- isActionable: false
13454
- }
13455
- ],
13456
- // After ReAttest step
13457
- [CCTPv2StepName.reAttest]: [
13458
- {
13459
- condition: (ctx)=>ctx.lastStep?.state === 'success',
13460
- nextStep: CCTPv2StepName.mint,
13461
- reason: 'Re-attestation successful, proceed to mint',
13462
- isActionable: true
13463
- },
13464
- {
13465
- condition: (ctx)=>ctx.lastStep?.state === 'error',
13466
- nextStep: CCTPv2StepName.mint,
13467
- reason: 'Re-attestation failed, retry mint to re-initiate recovery',
13468
- isActionable: true
13469
- },
13470
- {
13471
- condition: (ctx)=>ctx.lastStep?.state === 'pending',
13472
- nextStep: CCTPv2StepName.mint,
13473
- reason: 'Re-attestation pending, retry mint to re-initiate recovery',
13474
- isActionable: true
13475
- }
13476
- ]
13630
+ * Polls the IRIS API until a complete attestation is available. The default
13631
+ * window is sized for slow source chains where finality may take many
13632
+ * confirmations.
13633
+ *
13634
+ * Defaults (see `DEFAULT_CONFIG`):
13635
+ * - Per-attempt timeout: 2 000 ms (each HTTP request aborts after 2 s)
13636
+ * - Retry delay: 2 000 ms between attempts
13637
+ * - Max retries: 600 (30 × 20)
13638
+ * - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
13639
+ *
13640
+ * @param sourceDomainId - The CCTP domain ID.
13641
+ * @param transactionHash - The transaction hash to fetch attestation for.
13642
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
13643
+ * @param config - Optional configuration overrides for the attestation fetcher
13644
+ * @returns The attestation response data.
13645
+ * @throws If the request fails, times out, or returns invalid data.
13646
+ *
13647
+ * @example
13648
+ * ```typescript
13649
+ * // Fetch attestation for mainnet transaction
13650
+ * const response = await fetchAttestation(1, '0xabc...', false)
13651
+ * console.log(`Found ${response.messages.length} attestation messages`)
13652
+ *
13653
+ * // Fetch with custom timeout
13654
+ * const response2 = await fetchAttestation(1, '0xdef...', true, {
13655
+ * timeout: 5000,
13656
+ * maxRetries: 5
13657
+ * })
13658
+ * ```
13659
+ */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
13660
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
13661
+ const effectiveConfig = mergeAttestationConfig(config);
13662
+ return await pollApiGet(url, isAttestationResponse, effectiveConfig);
13477
13663
  };
13478
13664
  /**
13479
- * Analyze bridge steps to determine retry feasibility and continuation point.
13665
+ * Type guard that validates attestation response structure without requiring completion status.
13480
13666
  *
13481
- * This function examines the current state of bridge steps to determine the optimal
13482
- * continuation strategy. It uses a rule-based approach that makes it easy to extend
13483
- * with new flow patterns and step types in the future.
13667
+ * This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
13668
+ * attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
13669
+ * this function does not throw if no complete attestation is found.
13484
13670
  *
13485
- * The current analysis supports the standard CCTP flow:
13486
- * **Traditional flow**: Approve Burn FetchAttestation Mint
13671
+ * @param obj - The value to check, typically a parsed JSON response
13672
+ * @returns True if the object has valid attestation structure
13673
+ * @throws {Error} With "Invalid attestation response structure" if structure is invalid
13674
+ * @internal
13675
+ */ const isAttestationResponseWithoutStatusCheck = (obj)=>{
13676
+ if (!hasValidAttestationStructure(obj)) {
13677
+ throw new Error('Invalid attestation response structure');
13678
+ }
13679
+ return true;
13680
+ };
13681
+ /**
13682
+ * Fetches attestation data without requiring the attestation to be complete.
13487
13683
  *
13488
- * Key features:
13489
- * - Rule-based transitions: Easy to extend with new step types and logic
13490
- * - Context-aware decisions: Considers execution history and step states
13491
- * - Actionable logic: Distinguishes between steps requiring user action vs waiting
13492
- * - Terminal states: Properly handles completion and non-actionable states
13684
+ * This function is useful for retrieving attestation data (particularly the nonce)
13685
+ * from an existing transaction, even if the attestation has expired or is pending.
13686
+ * It uses minimal retries since we're fetching existing data, not waiting for completion.
13493
13687
  *
13494
- * @param bridgeResult - The bridge result containing step execution history.
13495
- * @returns Analysis result with continuation step and actionability information.
13496
- * @throws Error when bridgeResult is invalid or contains no steps array.
13688
+ * @param sourceDomainId - The CCTP domain ID of the source chain
13689
+ * @param transactionHash - The transaction hash to fetch attestation for
13690
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
13691
+ * @param config - Optional configuration overrides
13692
+ * @returns The attestation response data (may contain incomplete/expired attestations)
13693
+ * @throws If the request fails, times out, or returns invalid data
13497
13694
  *
13498
13695
  * @example
13499
13696
  * ```typescript
13500
- * import { analyzeSteps } from './analyzeSteps'
13697
+ * // Fetch existing attestation to extract nonce for re-attestation
13698
+ * const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
13699
+ * const nonce = response.messages[0]?.eventNonce
13700
+ * ```
13701
+ */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
13702
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
13703
+ // Use minimal retries since we're just fetching existing data
13704
+ const effectiveConfig = mergeAttestationConfig(config, {
13705
+ maxRetries: 3
13706
+ });
13707
+ return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
13708
+ };
13709
+ /**
13710
+ * Type guard that validates attestation response has expirationBlock === '0'.
13501
13711
  *
13502
- * // Failed approval step (requires user action)
13503
- * const bridgeResult = {
13504
- * steps: [
13505
- * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
13506
- * ]
13507
- * }
13712
+ * This is used after requestReAttestation() to poll until the attestation
13713
+ * is fully re-processed and has a zero expiration block (never expires).
13714
+ * The expiration block transitions from non-zero to zero when Circle
13715
+ * completes processing the re-attestation request.
13508
13716
  *
13509
- * const analysis = analyzeSteps(bridgeResult)
13510
- * // Result: { continuationStep: 'Approve', isRetryable: true,
13511
- * // reason: 'Retry failed approval' }
13512
- * ```
13717
+ * @param obj - The value to check, typically a parsed JSON response
13718
+ * @returns True if the attestation has expirationBlock === '0'
13719
+ * @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
13513
13720
  *
13514
13721
  * @example
13515
13722
  * ```typescript
13516
- * // Pending transaction (requires waiting, not actionable)
13517
- * const bridgeResult = {
13518
- * steps: [
13519
- * { name: 'Approve', state: 'pending' }
13520
- * ]
13521
- * }
13522
- *
13523
- * const analysis = analyzeSteps(bridgeResult)
13524
- * // Result: { continuationStep: 'Approve', isRetryable: false,
13525
- * // reason: 'Continue pending approval' }
13723
+ * // After requesting re-attestation, use this to validate the response
13724
+ * const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
13725
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
13526
13726
  * ```
13527
13727
  *
13728
+ * @internal
13729
+ */ const isReAttestedAttestationResponse = (obj)=>{
13730
+ // First validate the basic structure and completion status
13731
+ // This will throw appropriate errors for invalid structure or incomplete attestation
13732
+ if (!isAttestationResponse(obj)) ;
13733
+ // Check if the first message has expirationBlock === '0'
13734
+ const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
13735
+ if (expirationBlock !== '0') {
13736
+ // Re-attestation not yet complete - allow retry via polling
13737
+ throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
13738
+ }
13739
+ return true;
13740
+ };
13741
+ /**
13742
+ * Fetches attestation data and polls until expirationBlock === '0'.
13743
+ *
13744
+ * This function is used after calling requestReAttestation() to wait until
13745
+ * the attestation is fully re-processed. The expirationBlock transitions
13746
+ * from non-zero to zero when Circle completes the re-attestation.
13747
+ *
13748
+ * @param sourceDomainId - The CCTP domain ID of the source chain
13749
+ * @param transactionHash - The transaction hash to fetch attestation for
13750
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
13751
+ * @param config - Optional configuration overrides
13752
+ * @returns The re-attested attestation response with expirationBlock === '0'
13753
+ * @throws If the request fails, times out, or expirationBlock never becomes 0
13754
+ *
13528
13755
  * @example
13529
13756
  * ```typescript
13530
- * // Completed bridge (nothing to do)
13531
- * const bridgeResult = {
13532
- * steps: [
13533
- * { name: 'Approve', state: 'success' },
13534
- * { name: 'Burn', state: 'success' },
13535
- * { name: 'FetchAttestation', state: 'success' },
13536
- * { name: 'Mint', state: 'success' }
13537
- * ]
13538
- * }
13757
+ * // After requesting re-attestation
13758
+ * await requestReAttestation(nonce, isTestnet)
13539
13759
  *
13540
- * const analysis = analyzeSteps(bridgeResult)
13541
- * // Result: { continuationStep: null, isRetryable: false,
13542
- * // reason: 'Bridge completed successfully' }
13760
+ * // Poll until expirationBlock becomes 0
13761
+ * const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
13762
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
13543
13763
  * ```
13544
- */ const analyzeSteps = (bridgeResult)=>{
13545
- // Input validation
13546
- if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
13547
- throw new Error('Invalid bridgeResult: must contain a steps array');
13548
- }
13549
- const { steps } = bridgeResult;
13550
- // Build execution context from step history
13551
- const context = buildFlowContext(steps);
13552
- // Determine continuation logic using rule engine
13553
- const continuation = determineContinuationFromRules(context);
13554
- return {
13555
- continuationStep: continuation.nextStep,
13556
- isActionable: continuation.isActionable,
13557
- completedSteps: Array.from(context.completedSteps),
13558
- failedSteps: Array.from(context.failedSteps),
13559
- reason: continuation.reason
13560
- };
13764
+ */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
13765
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
13766
+ const effectiveConfig = mergeAttestationConfig(config);
13767
+ return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
13561
13768
  };
13562
13769
  /**
13563
- * Build flow context from the execution history.
13564
- *
13565
- * @param steps - Array of executed bridge steps.
13566
- * @returns Flow context with execution state and history.
13567
- */ function buildFlowContext(steps) {
13568
- const completedSteps = new Set();
13569
- const failedSteps = new Set();
13570
- let lastStep;
13571
- // Process step history to build context
13572
- for (const step of steps){
13573
- if (step.state === 'success' || step.state === 'noop') {
13574
- completedSteps.add(step.name);
13575
- } else if (step.state === 'error') {
13576
- failedSteps.add(step.name);
13577
- }
13578
- // Track the last step for continuation logic
13579
- lastStep = {
13580
- name: step.name,
13581
- state: step.state
13582
- };
13583
- }
13584
- return {
13585
- completedSteps,
13586
- failedSteps,
13587
- ...lastStep && {
13588
- lastStep
13589
- }
13590
- };
13591
- }
13592
- /**
13593
- * Determine continuation step using the rule engine.
13770
+ * Builds the IRIS API URL for re-attestation requests.
13594
13771
  *
13595
- * @param context - The flow context with execution history.
13596
- * @returns Continuation decision with next step and actionability information.
13597
- */ function determineContinuationFromRules(context) {
13598
- const lastStepName = context.lastStep?.name;
13599
- // Handle initial state when no steps have been executed
13600
- if (lastStepName === undefined) {
13601
- const rules = STEP_TRANSITION_RULES[''];
13602
- const matchingRule = rules?.find((rule)=>rule.condition(context));
13603
- if (!matchingRule) {
13604
- return {
13605
- nextStep: null,
13606
- isActionable: false,
13607
- reason: 'No initial state rule found'
13608
- };
13609
- }
13610
- return {
13611
- nextStep: matchingRule.nextStep,
13612
- isActionable: matchingRule.isActionable,
13613
- reason: matchingRule.reason
13614
- };
13615
- }
13616
- // A step with an empty name is ambiguous and should be treated as an unrecoverable state.
13617
- if (lastStepName === '') {
13618
- return {
13619
- nextStep: null,
13620
- isActionable: false,
13621
- reason: 'No transition rules defined for step with empty name'
13622
- };
13623
- }
13624
- const rules = STEP_TRANSITION_RULES[lastStepName];
13625
- if (!rules) {
13626
- return {
13627
- nextStep: null,
13628
- isActionable: false,
13629
- reason: `No transition rules defined for step: ${lastStepName}`
13630
- };
13631
- }
13632
- // Find the first matching rule
13633
- const matchingRule = rules.find((rule)=>rule.condition(context));
13634
- if (!matchingRule) {
13635
- return {
13636
- nextStep: null,
13637
- isActionable: false,
13638
- reason: `No matching transition rule for current context`
13639
- };
13640
- }
13641
- return {
13642
- nextStep: matchingRule.nextStep,
13643
- isActionable: matchingRule.isActionable,
13644
- reason: matchingRule.reason
13645
- };
13646
- }
13647
-
13648
- /**
13649
- * Find a step by name in the bridge result.
13772
+ * Constructs the URL for Circle's re-attestation endpoint that allows
13773
+ * requesting a fresh attestation for an expired nonce.
13650
13774
  *
13651
- * @param result - The bridge result to search.
13652
- * @param stepName - The name of the step to find.
13653
- * @returns The step if found, undefined otherwise.
13775
+ * @param nonce - The nonce from the original attestation
13776
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
13777
+ * @returns A fully qualified URL string for the re-attestation endpoint
13654
13778
  *
13655
13779
  * @example
13656
13780
  * ```typescript
13657
- * import { findStepByName } from './findStep'
13781
+ * // Mainnet URL
13782
+ * const mainnetUrl = buildReAttestUrl('0xabc', false)
13783
+ * // => 'https://iris-api.circle.com/v2/reattest/0xabc'
13658
13784
  *
13659
- * const burnStep = findStepByName(result, 'burn')
13660
- * if (burnStep) {
13661
- * console.log('Burn tx:', burnStep.txHash)
13662
- * }
13785
+ * // Testnet URL
13786
+ * const testnetUrl = buildReAttestUrl('0xabc', true)
13787
+ * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
13663
13788
  * ```
13664
- */ function findStepByName(result, stepName) {
13665
- return result.steps.find((step)=>step.name === stepName);
13666
- }
13789
+ */ const buildReAttestUrl = (nonce, isTestnet)=>{
13790
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
13791
+ const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
13792
+ return url.toString();
13793
+ };
13667
13794
  /**
13668
- * Find a pending step by name and return it with its index.
13795
+ * Type guard that validates the re-attestation API response structure.
13669
13796
  *
13670
- * Searches for a step that matches both the step name and has a pending state.
13797
+ * @param obj - The value to check, typically a parsed JSON response
13798
+ * @returns True if the object matches the ReAttestationResponse shape
13799
+ * @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
13800
+ * @internal
13801
+ */ const isReAttestationResponse = (obj)=>{
13802
+ if (typeof obj !== 'object' || obj === null || !('message' in obj) || !('nonce' in obj) || typeof obj.message !== 'string' || typeof obj.nonce !== 'string') {
13803
+ throw new Error('Invalid re-attestation response structure');
13804
+ }
13805
+ return true;
13806
+ };
13807
+ /**
13808
+ * Requests re-attestation for an expired attestation nonce.
13671
13809
  *
13672
- * @param result - The bridge result containing steps to search through.
13673
- * @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
13674
- * @returns An object containing the step and its index in the steps array.
13675
- * @throws KitError if the specified pending step is not found.
13810
+ * This function calls Circle's re-attestation API endpoint to request a fresh
13811
+ * attestation for a previously issued nonce. After calling this function,
13812
+ * you should poll `fetchAttestation` to retrieve the new attestation.
13813
+ *
13814
+ * @param nonce - The nonce from the original (expired) attestation
13815
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
13816
+ * @param config - Optional configuration overrides for the request
13817
+ * @returns The re-attestation response confirming the request was accepted
13818
+ * @throws If the request fails, times out, or returns invalid data
13676
13819
  *
13677
13820
  * @example
13678
13821
  * ```typescript
13679
- * import { findPendingStep } from './findStep'
13822
+ * // Request re-attestation for an expired nonce
13823
+ * const response = await requestReAttestation('0xabc', true)
13824
+ * console.log(response.message) // "Re-attestation successfully requested for nonce."
13680
13825
  *
13681
- * const { step, index } = findPendingStep(result, 'burn')
13682
- * console.log('Pending step:', step.name, 'at index:', index)
13826
+ * // After requesting re-attestation, poll for the new attestation
13827
+ * const attestation = await fetchAttestation(domainId, txHash, true)
13683
13828
  * ```
13684
- */ function findPendingStep(result, stepName) {
13685
- const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
13686
- if (index === -1) {
13829
+ */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
13830
+ const url = buildReAttestUrl(nonce, isTestnet);
13831
+ // Use minimal retries since we're just submitting a request, not polling for state
13832
+ const effectiveConfig = mergeAttestationConfig(config, {
13833
+ maxRetries: 3
13834
+ });
13835
+ return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
13836
+ };
13837
+
13838
+ /**
13839
+ * Type guard that checks if the relayer has confirmed the mint transaction.
13840
+ *
13841
+ * This function validates that:
13842
+ * 1. The response has valid AttestationResponse structure
13843
+ * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
13844
+ *
13845
+ * If forwardState is 'FAILED', throws a non-retryable KitError.
13846
+ * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
13847
+ *
13848
+ * @param obj - The value to check, typically a parsed JSON response
13849
+ * @returns True if the relayer has confirmed the mint
13850
+ * @throws {KitError} With FATAL recoverability if structure is invalid
13851
+ * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
13852
+ * @throws {KitError} With RETRYABLE recoverability if still pending
13853
+ * @internal
13854
+ */ const isRelayerMintConfirmed = (obj)=>{
13855
+ // First check if the structure is valid
13856
+ if (!hasValidAttestationStructure(obj)) {
13687
13857
  throw new KitError({
13688
13858
  ...InputError.VALIDATION_FAILED,
13689
13859
  recoverability: 'FATAL',
13690
- message: `Pending step "${stepName}" not found in result`
13860
+ message: 'Invalid attestation response structure from IRIS API.'
13691
13861
  });
13692
13862
  }
13693
- const step = result.steps[index];
13694
- if (!step) {
13863
+ // Find the first message (typically there's only one)
13864
+ const message = obj.messages[0];
13865
+ if (!message) {
13695
13866
  throw new KitError({
13696
13867
  ...InputError.VALIDATION_FAILED,
13697
13868
  recoverability: 'FATAL',
13698
- message: 'Pending step is undefined'
13869
+ message: 'No attestation messages found in IRIS API response.'
13699
13870
  });
13700
13871
  }
13701
- return {
13702
- step,
13703
- index
13704
- };
13705
- }
13872
+ // Check for FAILED state - this is a permanent failure
13873
+ if (message.forwardState === 'FAILED') {
13874
+ throw new KitError({
13875
+ ...NetworkError.RELAYER_FORWARD_FAILED,
13876
+ recoverability: 'RESUMABLE',
13877
+ 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.',
13878
+ cause: {
13879
+ trace: {
13880
+ eventNonce: message.eventNonce,
13881
+ attestation: message.attestation,
13882
+ message: message.message
13883
+ }
13884
+ }
13885
+ });
13886
+ }
13887
+ // Check if mint is confirmed (or complete) with a valid transaction hash
13888
+ // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
13889
+ if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
13890
+ return true;
13891
+ }
13892
+ // Still pending or not yet processed - throw RETRYABLE error to continue polling
13893
+ throw new KitError({
13894
+ ...NetworkError.RELAYER_PENDING,
13895
+ recoverability: 'RETRYABLE',
13896
+ message: 'Relayer mint not ready. Waiting for confirmation.'
13897
+ });
13898
+ };
13706
13899
  /**
13707
- * Get the burn transaction hash from bridge result.
13900
+ * Polls the attestation API until the relayer's mint transaction is confirmed.
13708
13901
  *
13709
- * @param result - The bridge result.
13710
- * @returns The burn transaction hash, or undefined if not found.
13902
+ * This function is used when `useForwarder` is enabled. Instead of the user
13903
+ * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
13904
+ * This function polls until the relayer has submitted and confirmed the mint transaction.
13905
+ *
13906
+ * @remarks
13907
+ * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
13908
+ * - Throws immediately if `forwardState` is 'FAILED'
13909
+ * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
13910
+ * - Returns the attestation message with `forwardTxHash` populated
13911
+ *
13912
+ * @param sourceDomainId - The CCTP domain ID of the source chain
13913
+ * @param transactionHash - The transaction hash of the burn operation
13914
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
13915
+ * @param config - Optional configuration overrides for polling behavior
13916
+ * @returns The attestation message with confirmed forwardTxHash
13917
+ * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
13918
+ * @throws {KitError} If timeout is reached while still pending
13711
13919
  *
13712
13920
  * @example
13713
13921
  * ```typescript
13714
- * import { getBurnTxHash } from './findStep'
13715
- *
13716
- * const burnTxHash = getBurnTxHash(result)
13717
- * if (burnTxHash) {
13718
- * console.log('Burn tx hash:', burnTxHash)
13719
- * }
13922
+ * const attestation = await fetchRelayerMint(0, '0xabc...', false)
13923
+ * console.log('Relayer mint tx:', attestation.forwardTxHash)
13720
13924
  * ```
13721
- */ function getBurnTxHash(result) {
13722
- return findStepByName(result, CCTPv2StepName.burn)?.txHash;
13723
- }
13925
+ */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
13926
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
13927
+ const effectiveConfig = mergeAttestationConfig(config);
13928
+ let response;
13929
+ try {
13930
+ response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
13931
+ } catch (error) {
13932
+ // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
13933
+ if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
13934
+ throw new KitError({
13935
+ ...NetworkError.RELAYER_FORWARD_FAILED,
13936
+ recoverability: error.recoverability,
13937
+ message: error.message,
13938
+ cause: {
13939
+ ...error.cause,
13940
+ trace: {
13941
+ ...error.cause?.trace,
13942
+ burnTxHash: transactionHash
13943
+ }
13944
+ }
13945
+ });
13946
+ }
13947
+ throw error;
13948
+ }
13949
+ // Return the first message (which should have forwardTxHash)
13950
+ // Note: This check is needed for TypeScript type safety even though
13951
+ // isRelayerMintConfirmed validates messages[0] exists. The type guard
13952
+ // narrows the type at the call site, but TypeScript can't infer that
13953
+ // the array still has elements after pollApiGet returns.
13954
+ const message = response.messages[0];
13955
+ if (!message) {
13956
+ throw new KitError({
13957
+ ...InputError.VALIDATION_FAILED,
13958
+ recoverability: 'FATAL',
13959
+ message: 'No attestation messages found in response after polling.'
13960
+ });
13961
+ }
13962
+ return message;
13963
+ };
13964
+
13965
+ const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
13724
13966
  /**
13725
- * Get the attestation data from bridge result.
13967
+ * Asserts that the provided parameters match the CCTPv2 wallet context interface.
13968
+ * The validation includes:
13969
+ * - Basic wallet context validation (adapter, address, chain)
13970
+ * - CCTPv2-specific chain validation (must be an EVM chain)
13726
13971
  *
13727
- * @param result - The bridge result.
13728
- * @returns The attestation data, or undefined if not found.
13972
+ * @param params - The parameters to validate
13973
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
13729
13974
  *
13730
13975
  * @example
13731
13976
  * ```typescript
13732
- * import { getAttestationData } from './findStep'
13977
+ * import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
13978
+ * import { Ethereum } from '@core/chains'
13733
13979
  *
13734
- * const attestation = getAttestationData(result)
13735
- * if (attestation) {
13736
- * console.log('Attestation:', attestation.message)
13980
+ * // Prepare wallet context
13981
+ * const context = {
13982
+ * adapter: {
13983
+ * prepare: async () => ({ data: 'prepared transaction' }),
13984
+ * waitForTransaction: async () => ({ status: 'confirmed' })
13985
+ * },
13986
+ * address: '0x1234567890123456789012345678901234567890',
13987
+ * chain: {
13988
+ * ...Ethereum,
13989
+ * usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
13990
+ * cctp: {
13991
+ * domain: 1,
13992
+ * contracts: {
13993
+ * v2: {
13994
+ * tokenMessenger: '0xTokenMessenger',
13995
+ * messageTransmitter: '0xMessageTransmitter'
13996
+ * }
13997
+ * }
13998
+ * }
13999
+ * }
13737
14000
  * }
14001
+ *
14002
+ * // This will throw if validation fails
14003
+ * assertCCTPv2WalletContext(context)
14004
+ *
14005
+ * // If we get here, context is guaranteed to be valid
14006
+ * console.log('CCTPv2 wallet context is valid')
13738
14007
  * ```
13739
- */ function getAttestationData(result) {
13740
- // Prefer reAttest data (most recent attestation after expiry)
13741
- const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
13742
- if (reAttestStep?.state === 'success' && reAttestStep.data) {
13743
- return reAttestStep.data;
14008
+ */ function assertCCTPv2WalletContext(params) {
14009
+ // First validate basic wallet context
14010
+ validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
14011
+ // After validation, we know params is WalletContext
14012
+ const context = params;
14013
+ // Validate USDC support
14014
+ if (context.chain.usdcAddress === null) {
14015
+ throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
14016
+ }
14017
+ // Validate CCTPv2 support
14018
+ if (!isCCTPV2Supported(context.chain)) {
14019
+ throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
13744
14020
  }
13745
- // Fall back to fetchAttestation step
13746
- const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
13747
- return fetchStep?.data;
13748
14021
  }
13749
14022
 
14023
+ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
13750
14024
  /**
13751
- * Check if the analysis indicates a non-actionable pending state.
13752
- *
13753
- * A pending state is non-actionable when there's a continuation step but
13754
- * the analysis marks it as not actionable, typically because we need to
13755
- * wait for an ongoing operation to complete.
14025
+ * Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
14026
+ * The validation includes:
14027
+ * - Basic parameter structure and types
14028
+ * - Amount validation (non-empty numeric string \> 0)
14029
+ * - Wallet address format validation (must be valid Ethereum address)
14030
+ * - Chain definition validation (must be a valid chain with required properties)
14031
+ * - Adapter validation (must implement required methods)
14032
+ * - Optional config validation (transfer speed and max fee)
14033
+ * - Network compatibility (source and destination chains must both be testnet or both mainnet)
14034
+ * - CCTPv2-specific wallet context validations
13756
14035
  *
13757
- * @param analysis - The step analysis result from analyzeSteps.
13758
- * @param result - The bridge result to check for pending steps.
13759
- * @returns True if there is a pending step that we should wait for.
14036
+ * @param params - The parameters to validate
14037
+ * @throws {KitError} If validation fails, with details about which properties failed
13760
14038
  *
13761
14039
  * @example
13762
14040
  * ```typescript
13763
- * import { hasPendingState } from './stepUtils'
13764
- * import { analyzeSteps } from '../analyzeSteps'
14041
+ * import { assertCCTPv2BridgeParams } from '@circle-fin/provider-cctp-v2'
14042
+ * import { Ethereum, Base } from '@core/chains'
13765
14043
  *
13766
- * const analysis = analyzeSteps(bridgeResult)
13767
- * if (hasPendingState(analysis, bridgeResult)) {
13768
- * // Wait for the pending operation to complete
14044
+ * // Prepare transfer parameters
14045
+ * const params = {
14046
+ * amount: '100.50',
14047
+ * source: {
14048
+ * adapter: sourceAdapter,
14049
+ * address: '0xSourceAddress',
14050
+ * chain: {
14051
+ * ...Ethereum,
14052
+ * cctp: {
14053
+ * domain: 1,
14054
+ * contracts: {
14055
+ * v2: {
14056
+ * tokenMessenger: '0xTokenMessenger',
14057
+ * messageTransmitter: '0xMessageTransmitter'
14058
+ * }
14059
+ * }
14060
+ * }
14061
+ * }
14062
+ * },
14063
+ * destination: {
14064
+ * adapter: destAdapter,
14065
+ * address: '0xDestAddress',
14066
+ * chain: {
14067
+ * ...Base,
14068
+ * cctp: {
14069
+ * domain: 2,
14070
+ * contracts: {
14071
+ * v2: {
14072
+ * tokenMessenger: '0xTokenMessenger',
14073
+ * messageTransmitter: '0xMessageTransmitter'
14074
+ * }
14075
+ * }
14076
+ * }
14077
+ * }
14078
+ * },
14079
+ * token: 'USDC',
14080
+ * config: {
14081
+ * transferSpeed: 'FAST',
14082
+ * maxFee: '1000000'
14083
+ * }
13769
14084
  * }
13770
- * ```
13771
- */ /**
13772
- * Evaluate a transaction receipt and return the corresponding step state
13773
- * and error message. Centralises the success/revert/unconfirmed logic so
13774
- * every call-site behaves identically.
13775
14085
  *
13776
- * @param receipt - The transaction receipt containing status and block info.
13777
- * @param txHash - The transaction hash used in error messages.
13778
- * @returns An object with `state` and an optional `errorMessage`.
14086
+ * // This will throw if validation fails
14087
+ * assertCCTPv2BridgeParams(params)
13779
14088
  *
13780
- * @example
13781
- * ```typescript
13782
- * const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
13783
- * step.state = outcome.state
13784
- * if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
14089
+ * // If we get here, params is guaranteed to be valid
14090
+ * console.log('CCTPv2 transfer parameters are valid')
13785
14091
  * ```
13786
- */ function evaluateTransactionOutcome(receipt, txHash) {
13787
- if (receipt.status === 'success' && receipt.blockNumber) {
13788
- return {
13789
- state: 'success'
13790
- };
14092
+ */ function assertCCTPv2BridgeParams(params) {
14093
+ // First validate basic bridge params
14094
+ validateWithStateTracking(params, bridgeParamsSchema, 'CCTPv2 bridge parameters', assertCCTPv2BridgeParamsSymbol);
14095
+ // After validation, we know params is CCTPV2BridgeParams
14096
+ const bridgeParams = params;
14097
+ // Enforce that source and destination chains are either testnet or mainnet
14098
+ if (bridgeParams.source.chain.isTestnet !== bridgeParams.destination.chain.isTestnet) {
14099
+ throw createNetworkMismatchError(bridgeParams.source.chain, bridgeParams.destination.chain);
13791
14100
  }
13792
- return {
13793
- state: 'error',
13794
- errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
13795
- };
13796
- }
13797
- function hasPendingState(analysis, result) {
13798
- // Check if there's a continuation step that's marked as non-actionable
13799
- if (analysis.continuationStep === null || analysis.isActionable) {
13800
- return false;
14101
+ assertCCTPV2Support(bridgeParams.source.chain, bridgeParams.destination.chain);
14102
+ // Validate that the destination chain supports forwarding when forwarder is enabled
14103
+ assertForwarderRouteSupport(bridgeParams.source.chain, bridgeParams.destination.chain, bridgeParams.destination.useForwarder);
14104
+ /**
14105
+ * Enforce that if fee is defined then feeRecipient must be defined.
14106
+ * We do not do this in the validation function itself because we want to allow
14107
+ * optional properties when calling `provider.bridge()` due to the custom fee
14108
+ * configuration being possible at the kit level as well.
14109
+ */ if (bridgeParams.config?.customFee?.value !== undefined && bridgeParams.config?.customFee?.recipientAddress === undefined) {
14110
+ throw createValidationFailedError$1('recipientAddress', bridgeParams.config.customFee.value, 'Custom fee is defined but fee recipient is not. Please provide a fee recipient.');
14111
+ }
14112
+ // Check if this is a forwarder-only destination (no adapter, requires useForwarder: true)
14113
+ const isForwarderOnly = bridgeParams.destination.useForwarder === true && !('adapter' in bridgeParams.destination && bridgeParams.destination.adapter);
14114
+ // Forwarder-only destinations require recipientAddress
14115
+ if (isForwarderOnly) {
14116
+ if (!bridgeParams.destination.recipientAddress?.trim()) {
14117
+ throw createValidationFailedError$1('recipientAddress', bridgeParams.destination.recipientAddress, 'recipientAddress is required when using forwarder without a destination adapter.');
14118
+ }
14119
+ }
14120
+ // Validate CCTP v2 specific requirements for source wallet
14121
+ assertCCTPv2WalletContext(bridgeParams.source);
14122
+ // Validate that source adapter supports the chain (defense-in-depth)
14123
+ bridgeParams.source.adapter.validateChainSupport(bridgeParams.source.chain);
14124
+ // Only validate destination wallet context and adapter if not forwarder-only
14125
+ if (!isForwarderOnly) {
14126
+ assertCCTPv2WalletContext(bridgeParams.destination);
14127
+ // Validate that destination adapter supports the chain (defense-in-depth)
14128
+ bridgeParams.destination.adapter.validateChainSupport(bridgeParams.destination.chain);
13801
14129
  }
13802
- // Verify that the continuation step actually exists and is in pending state
13803
- const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
13804
- return pendingStep !== undefined;
13805
14130
  }
13806
14131
  /**
13807
- * Check if the step is the last one in the execution flow.
13808
- *
13809
- * @param step - The step object to check.
13810
- * @param stepNames - The ordered list of step names in the execution flow.
13811
- * @returns True if this is the last step in the flow.
13812
- *
13813
- * @example
13814
- * ```typescript
13815
- * import { isLastStep } from './stepUtils'
14132
+ * Validate CCTP v2 support on both chains
14133
+ */ /**
14134
+ * Throws a KitError if the given chain does not support CCTP v2.
13816
14135
  *
13817
- * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
13818
- * isLastStep({ name: 'mint' }, stepNames) // true
13819
- * isLastStep({ name: 'burn' }, stepNames) // false
13820
- * ```
13821
- */ function isLastStep(step, stepNames) {
13822
- const stepIndex = stepNames.indexOf(step.name);
13823
- return stepIndex === -1 || stepIndex >= stepNames.length - 1;
14136
+ * @param chain - The chain to check for CCTP v2 support
14137
+ * @param otherChain - The other chain in the route (for error context)
14138
+ * @param isSource - Whether this is the source chain (for error context)
14139
+ */ function assertCCTPV2Support(source, destination) {
14140
+ if (!isCCTPV2Supported(source) || !isCCTPV2Supported(destination)) {
14141
+ throw createUnsupportedRouteError(source.name, destination.name);
14142
+ }
13824
14143
  }
13825
14144
  /**
13826
- * Wait for a pending transaction to complete.
13827
- *
13828
- * Poll the adapter until the transaction is confirmed on-chain and return
13829
- * the updated step with success or error state based on the receipt.
14145
+ * Validates that the forwarder (relaying) feature is compatible with the route.
13830
14146
  *
13831
- * @param pendingStep - The full step object containing the transaction hash.
13832
- * @param adapter - The adapter to use for waiting.
13833
- * @param chain - The chain where the transaction was submitted.
13834
- * @returns The updated step object with success or error state.
14147
+ * Checks the destination chain's `cctp.forwarderSupported.destination` property
14148
+ * to determine whether the chain supports receiving forwarded transfers.
13835
14149
  *
13836
- * @throws KitError when the pending step has no transaction hash.
14150
+ * @param source - The source chain definition
14151
+ * @param destination - The destination chain definition
14152
+ * @param useForwarder - Whether the forwarder is enabled on the destination
14153
+ * @throws {KitError} If the forwarder is enabled and the destination chain does not support forwarding
14154
+ */ function assertForwarderRouteSupport(source, destination, useForwarder) {
14155
+ if (useForwarder === true && !destination.cctp?.forwarderSupported.destination) {
14156
+ throw new KitError({
14157
+ ...InputError.UNSUPPORTED_ROUTE,
14158
+ recoverability: 'FATAL',
14159
+ message: `Route from ${source.name} to ${destination.name} with forwarder is not supported (destination chain does not support forwarding).`,
14160
+ cause: {
14161
+ trace: {
14162
+ source: source.name,
14163
+ destination: destination.name
14164
+ }
14165
+ }
14166
+ });
14167
+ }
14168
+ }
14169
+
14170
+ /**
14171
+ * Checks if a decoded attestation field matches the corresponding transfer parameter.
14172
+ * If the values do not match, appends a descriptive error message to the errors array.
13837
14173
  *
13838
- * @example
13839
- * ```typescript
13840
- * import { waitForPendingTransaction } from './bridgeStepUtils'
14174
+ * @param field - The name of the field being compared (for error reporting)
14175
+ * @param decoded - The value decoded from the attestation message
14176
+ * @param param - The expected value from the transfer parameters
14177
+ * @param errors - The array to which error messages will be appended if a mismatch is found
14178
+ */ function checkFieldMismatch(field, decoded, param, errors) {
14179
+ if (decoded !== param) {
14180
+ errors.push(`${field} mismatch: decoded=${String(decoded)}, params=${String(param)}`);
14181
+ }
14182
+ }
14183
+ /**
14184
+ * Asserts that the decoded message from attestation matches the provided transfer params.
14185
+ * Throws KitError if any field mismatches, with clear error messages.
13841
14186
  *
13842
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
13843
- * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
13844
- * // updatedStep.state is now 'success' or 'error'
13845
- * ```
13846
- */ async function waitForPendingTransaction(pendingStep, adapter, chain) {
13847
- if (!pendingStep.txHash) {
14187
+ * @param attestation - The attestation message containing the decoded message
14188
+ * @param params - The transfer parameters to validate against
14189
+ * @throws {@link KitError} If any field mismatches
14190
+ */ async function assertCCTPv2AttestationParams(attestation, params) {
14191
+ const errors = [];
14192
+ const message = attestation.decodedMessage;
14193
+ const messageBody = message.decodedMessageBody;
14194
+ // Use recipientAddress if provided, otherwise use destination.address
14195
+ const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
14196
+ const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
14197
+ let sender;
14198
+ if (hasCustomContractSupport(params.source.chain, 'bridge')) {
14199
+ if (params.source.chain.type === 'solana') {
14200
+ // Solana: User → Bridge contract → CCTP (user remains sender)
14201
+ sender = params.source.address;
14202
+ } else {
14203
+ // Other chains (like EVM): Bridge contract → CCTP (bridge contract becomes sender)
14204
+ sender = params.source.chain.kitContracts?.bridge;
14205
+ }
14206
+ } else {
14207
+ sender = params.source.address;
14208
+ }
14209
+ checkFieldMismatch('sourceDomain', message.sourceDomain, params.source.chain.cctp.domain.toString(), errors);
14210
+ checkFieldMismatch('destinationDomain', message.destinationDomain, params.destination.chain.cctp.domain.toString(), errors);
14211
+ checkFieldMismatch('minFinalityThreshold', message.minFinalityThreshold, CCTPv2MinFinalityThreshold[params.config.transferSpeed ?? 'FAST'].toString(), errors);
14212
+ checkFieldMismatch('sender', params.source.chain.type === 'evm' ? messageBody.messageSender.toLowerCase() : messageBody.messageSender, params.source.chain.type === 'evm' ? sender?.toLowerCase() : sender, errors);
14213
+ checkFieldMismatch('recipient', params.destination.chain.type === 'evm' ? messageBody.mintRecipient.toLowerCase() : messageBody.mintRecipient, params.destination.chain.type === 'evm' ? mintRecipient.toLowerCase() : mintRecipient, errors);
14214
+ checkFieldMismatch('amount', messageBody.amount, params.amount.toString(), errors);
14215
+ checkFieldMismatch('burnToken', messageBody.burnToken.toLowerCase(), params.source.chain.usdcAddress.toLowerCase(), errors);
14216
+ if (errors.length > 0) {
14217
+ const errorMessage = 'Attestation validation failed: received attestation does not match expected transfer parameters';
14218
+ const firstError = errors[0] ?? '';
13848
14219
  throw new KitError({
13849
14220
  ...InputError.VALIDATION_FAILED,
13850
14221
  recoverability: 'FATAL',
13851
- message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
14222
+ message: `${errorMessage}: ${firstError}`,
14223
+ cause: {
14224
+ trace: {
14225
+ validationErrors: errors
14226
+ }
14227
+ }
13852
14228
  });
13853
14229
  }
13854
- const txHash = pendingStep.txHash;
13855
- const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
13856
- isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
13857
- chain: chain.name,
13858
- txHash
13859
- }))
13860
- });
13861
- const outcome = evaluateTransactionOutcome(txReceipt, txHash);
13862
- return {
13863
- ...pendingStep,
13864
- state: outcome.state,
13865
- data: txReceipt,
13866
- explorerUrl: buildExplorerUrl(chain, txHash),
13867
- ...outcome.errorMessage ? {
13868
- errorMessage: outcome.errorMessage
13869
- } : {}
13870
- };
13871
14230
  }
14231
+
13872
14232
  /**
13873
- * Wait for a pending step to complete.
14233
+ * Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
13874
14234
  *
13875
- * For transaction steps: waits for the transaction to be confirmed.
13876
- * For attestation: re-executes the attestation fetch.
14235
+ * Validates the full public-boundary input before any field destructuring,
14236
+ * `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
14237
+ * inputs always produce typed `KitError` validation failures.
13877
14238
  *
13878
- * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
13879
- * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
13880
- * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
13881
- * @param adapter - The adapter to use.
13882
- * @param chain - The chain where the step is executing.
13883
- * @param context - The retry context.
13884
- * @param result - The bridge result.
13885
- * @param provider - The CCTP v2 bridging provider.
13886
- * @returns The resolved step object with updated state.
14239
+ * Checks performed (in order):
14240
+ * - `params` must be a non-null plain object
14241
+ * - `source` valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
14242
+ * - `destinationChain` present and supports CCTP v2
14243
+ * - source and destination chains must both be testnet or both mainnet
14244
+ * - source and destination chains must differ
14245
+ * - `executor` — non-empty string
14246
+ * - `amount` — bigint or non-empty string coercible to bigint
14247
+ * - `feeTotalAmount` bigint or non-empty string coercible to bigint
14248
+ * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
14249
+ * - `claim.signedQuote` — valid `0x`-prefixed hex string
14250
+ * - `claim.refundAddress` — valid EVM address
14251
+ * - `hookData` — valid `0x`-prefixed hex string when present
13887
14252
  *
13888
- * @throws KitError when fetching attestation but burn transaction hash is not found.
14253
+ * @param params - The value to validate.
14254
+ * @throws {KitError} If any field is missing or invalid.
13889
14255
  *
13890
14256
  * @example
13891
14257
  * ```typescript
13892
- * import { waitForStepToComplete } from './bridgeStepUtils'
13893
- *
13894
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
13895
- * const updatedStep = await waitForStepToComplete(
13896
- * pendingStep,
13897
- * adapter,
13898
- * chain,
13899
- * context,
13900
- * result,
13901
- * provider,
13902
- * )
13903
- * // updatedStep.state is now 'success' or 'error'
14258
+ * assertBurnWithFeesParams(params)
14259
+ * // params is now typed as BurnWithFeesParams and safe to use
14260
+ * const { source, destinationChain, amount } = params
13904
14261
  * ```
13905
- */ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
13906
- if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
13907
- // For attestation, re-run the fetch (it has built-in polling)
13908
- const burnTxHash = getBurnTxHash(result);
13909
- if (!burnTxHash) {
13910
- throw new KitError({
13911
- ...InputError.VALIDATION_FAILED,
13912
- recoverability: 'FATAL',
13913
- message: 'Cannot fetch attestation: burn transaction hash not found'
13914
- });
13915
- }
13916
- const sourceAddress = result.source.address;
13917
- const attestation = await provider.fetchAttestation({
13918
- chain: result.source.chain,
13919
- adapter: context.from,
13920
- address: sourceAddress
13921
- }, burnTxHash);
13922
- return {
13923
- ...pendingStep,
13924
- state: 'success',
13925
- data: attestation
13926
- };
14262
+ */ function assertBurnWithFeesParams(params) {
14263
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
14264
+ throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
14265
+ }
14266
+ const p = params;
14267
+ // Source wallet context
14268
+ assertCCTPv2WalletContext(p['source']);
14269
+ const source = p['source'];
14270
+ // destinationChain
14271
+ const destinationChain = p['destinationChain'];
14272
+ if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
14273
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
14274
+ }
14275
+ if (!isCCTPV2Supported(destinationChain)) {
14276
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
14277
+ }
14278
+ const dest = destinationChain;
14279
+ // Testnet / mainnet mismatch
14280
+ if (source.chain.isTestnet !== dest.isTestnet) {
14281
+ throw createNetworkMismatchError(source.chain, dest);
14282
+ }
14283
+ // Same-chain guard
14284
+ if (source.chain.name === dest.name) {
14285
+ throw createUnsupportedRouteError(source.chain.name, dest.name);
14286
+ }
14287
+ // executor
14288
+ const executor = p['executor'];
14289
+ if (typeof executor !== 'string' || executor === '') {
14290
+ throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
14291
+ }
14292
+ // amount
14293
+ const rawAmount = p['amount'];
14294
+ if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
14295
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
13927
14296
  }
13928
- // For transaction steps, wait for the transaction to complete
13929
- return waitForPendingTransaction(pendingStep, adapter, chain);
13930
- }
13931
-
13932
- /**
13933
- * Executes a prepared chain request and returns the result as a bridge step.
13934
- *
13935
- * This function takes a prepared chain request (containing transaction data) and executes
13936
- * it using the appropriate adapter. It handles the execution details and formats
13937
- * the result as a standardized bridge step with transaction details and explorer URLs.
13938
- *
13939
- * @param params - The execution parameters containing:
13940
- * - `name`: The name of the step
13941
- * - `request`: The prepared chain request containing transaction data
13942
- * - `adapter`: The adapter that will execute the transaction
13943
- * - `confirmations`: The number of confirmations to wait for (defaults to 1)
13944
- * - `timeout`: The timeout for the request in milliseconds
13945
- * - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
13946
- * bypassing `eth_estimateGas`; ignored for non-EVM requests
13947
- * @returns The bridge step with the transaction details and explorer URL
13948
- * @throws If the transaction execution fails
13949
- *
13950
- * @example
13951
- * ```typescript
13952
- * const step = await executePreparedChainRequest({
13953
- * name: 'approve',
13954
- * request: preparedRequest,
13955
- * adapter: adapter,
13956
- * confirmations: 2,
13957
- * timeout: 30000
13958
- * })
13959
- * console.log('Transaction hash:', step.txHash)
13960
- * ```
13961
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
13962
- const step = {
13963
- name,
13964
- state: 'pending'
13965
- };
13966
14297
  try {
13967
- /**
13968
- * No-op requests are not executed.
13969
- * We return a noop step instead.
13970
- */ if (request.type === 'noop') {
13971
- step.state = 'noop';
13972
- return step;
13973
- }
13974
- const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
13975
- gasLimit
13976
- }) : await request.execute();
13977
- step.txHash = txHash;
13978
- const retryOptions = {
13979
- isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
13980
- chain: chain.name,
13981
- txHash
13982
- }))
13983
- };
13984
- if (timeout !== undefined) {
13985
- retryOptions.deadlineMs = Date.now() + timeout;
13986
- }
13987
- const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
13988
- confirmations,
13989
- timeout
13990
- }, chain), retryOptions);
13991
- const outcome = evaluateTransactionOutcome(transaction, txHash);
13992
- step.state = outcome.state;
13993
- step.data = transaction;
13994
- // Generate explorer URL for the step
13995
- step.explorerUrl = buildExplorerUrl(chain, txHash);
13996
- if (outcome.errorMessage) {
13997
- step.errorMessage = outcome.errorMessage;
13998
- // Transaction was mined but reverted on-chain.
13999
- step.errorCategory = 'chain_revert';
14000
- }
14001
- } catch (err) {
14002
- step.state = 'error';
14003
- step.error = err;
14004
- // Sequential path does not yet attempt fine-grained classification of
14005
- // pre-submission errors (user_rejected, capability errors, etc.). Mark
14006
- // as `unknown` so consumers can at least detect the category is
14007
- // populated uniformly across batched and sequential flows.
14008
- step.errorCategory = 'unknown';
14009
- // Optionally parse for common blockchain error formats
14010
- if (err instanceof Error) {
14011
- step.errorMessage = err.message;
14012
- } else if (typeof err === 'object' && err != null && 'message' in err) {
14013
- step.errorMessage = String(err.message);
14014
- } else {
14015
- step.errorMessage = `Unknown error occurred during ${name} step.`;
14016
- }
14298
+ BigInt(rawAmount);
14299
+ } catch {
14300
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
14301
+ }
14302
+ // feeTotalAmount
14303
+ const rawFee = p['feeTotalAmount'];
14304
+ if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
14305
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
14306
+ }
14307
+ try {
14308
+ BigInt(rawFee);
14309
+ } catch {
14310
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
14311
+ }
14312
+ // feeToken
14313
+ if (!evmAddressSchema.safeParse(p['feeToken']).success) {
14314
+ throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
14315
+ }
14316
+ // claim
14317
+ const rawClaim = p['claim'];
14318
+ if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
14319
+ throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
14320
+ }
14321
+ const claim = rawClaim;
14322
+ if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
14323
+ throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
14324
+ }
14325
+ if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
14326
+ throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
14327
+ }
14328
+ // hookData (optional)
14329
+ const hookData = p['hookData'];
14330
+ if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
14331
+ throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
14017
14332
  }
14018
- return step;
14019
14333
  }
14020
14334
 
14021
14335
  /**
@@ -14046,7 +14360,7 @@ function hasPendingState(analysis, result) {
14046
14360
  adapter: params.source.adapter,
14047
14361
  chain: params.source.chain,
14048
14362
  request: await provider.approve(params.source, approvalAmount),
14049
- gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
14363
+ gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.approve)
14050
14364
  });
14051
14365
  }
14052
14366
 
@@ -14074,7 +14388,7 @@ function hasPendingState(analysis, result) {
14074
14388
  adapter: params.source.adapter,
14075
14389
  chain: params.source.chain,
14076
14390
  request: await provider.burn(params),
14077
- gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
14391
+ gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.burn)
14078
14392
  });
14079
14393
  }
14080
14394
 
@@ -14168,10 +14482,9 @@ function hasPendingState(analysis, result) {
14168
14482
  request: mintRequest,
14169
14483
  // Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
14170
14484
  // eth_estimateGas does not account for, returning a below-floor value
14171
- // without reverting. Pinning to a value above the observed execution max
14172
- // (310_839) bypasses re-estimation and guarantees we clear both the floor
14173
- // and the actual execution cost.
14174
- gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
14485
+ // without reverting. The floor covers those; chains that cost more than the
14486
+ // floor are covered by their own estimate.
14487
+ gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.mint)
14175
14488
  });
14176
14489
  // Add forwarded: false for non-relayer mints
14177
14490
  return {
@@ -14694,7 +15007,7 @@ const mockAttestationMessage = {
14694
15007
  return step;
14695
15008
  }
14696
15009
 
14697
- var version$2 = "1.10.1";
15010
+ var version$2 = "1.11.0";
14698
15011
  var pkg$2 = {
14699
15012
  version: version$2};
14700
15013
 
@@ -15653,10 +15966,39 @@ function assertCCTPV2Config(config) {
15653
15966
  // CCTP-specific transfer params validation (includes base validation)
15654
15967
  assertCCTPv2BridgeParams(params);
15655
15968
  const { source, destination, amount } = params;
15656
- const estimateBurn = async ()=>{
15657
- const burn = await this.burn(params);
15658
- 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));
15969
+ /**
15970
+ * Price the gas a step will RESERVE on-chain, not what it will spend.
15971
+ *
15972
+ * A transaction is only admitted when the sender holds
15973
+ * `gasLimit * gasPrice`, and `executePreparedChainRequest` submits
15974
+ * `max(estimate * buffer, floor)`. Quoting a bare `estimate()` therefore
15975
+ * under-reports what a wallet must hold to send at all: the floor governs
15976
+ * on virtually every chain, which put the burn quote ~2.5x below the real
15977
+ * requirement and left anyone funding from it unable to submit.
15978
+ *
15979
+ * Delegates to the same `resolveGasLimit` the execute path calls, so a
15980
+ * quote and the limit later submitted for that step cannot drift apart.
15981
+ * That also means an estimation failure degrades to the floor here exactly
15982
+ * as it does on execution, rather than surfacing as a failed quote — the
15983
+ * floor is what would be submitted, so it is the honest number to quote.
15984
+ *
15985
+ * Non-EVM requests are unchanged: `executePreparedChainRequest` only
15986
+ * applies a floor when `request.type === 'evm'`, so there is no reserved
15987
+ * limit to quote on other chains.
15988
+ */ const quoteReservedGas = async (request, gasFloor, priceGas, // Ignored on EVM — only evaluated and used on non-EVM paths.
15989
+ nonEvmFallbackGasEstimate)=>{
15990
+ if (request.type !== 'evm') {
15991
+ return nonEvmFallbackGasEstimate === undefined ? await request.estimate() : await request.estimate(undefined, await priceGas(nonEvmFallbackGasEstimate()));
15992
+ }
15993
+ // resolveGasLimit returns number; gas units are well below Number.MAX_SAFE_INTEGER,
15994
+ // so the Number() → resolveGasLimit → BigInt() round-trip is lossless.
15995
+ return await priceGas(BigInt(await resolveGasLimit(request, Number(gasFloor))));
15659
15996
  };
15997
+ const priceGasFor = (ctx)=>async (gasUnits)=>await ctx.adapter.calculateTransactionFee(gasUnits, undefined, ctx.chain);
15998
+ const priceSourceGas = priceGasFor(source);
15999
+ const priceDestinationGas = priceGasFor(destination);
16000
+ const estimateApprove = async ()=>await quoteReservedGas(await this.approve(source, amount), BRIDGE_STEP_GAS_FLOORS_EVM.approve, priceSourceGas);
16001
+ 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);
15660
16002
  // Only estimate Mint gas when not using forwarder (user pays gas)
15661
16003
  // When useForwarder=true, Circle's Orbit relayer handles and pays for the mint
15662
16004
  const useForwarder = destination.useForwarder === true;
@@ -15665,12 +16007,11 @@ function assertCCTPV2Config(config) {
15665
16007
  return null // Skip mint estimation when forwarder handles it
15666
16008
  ;
15667
16009
  }
15668
- const mint = await this.mint(source, destination, mockAttestationMessage);
15669
- return await mint.estimate(undefined, await destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, undefined, destination.chain));
16010
+ return await quoteReservedGas(await this.mint(source, destination, mockAttestationMessage), BRIDGE_STEP_GAS_FLOORS_EVM.mint, priceDestinationGas, ()=>RECEIVE_MESSAGE_GAS_ESTIMATE_EVM);
15670
16011
  };
15671
16012
  // Parallelize all independent async operations
15672
16013
  const [approveEstimate, depositForBurnFee, receiveMessageFee, feeEstimates] = await Promise.allSettled([
15673
- this.approve(source, amount).then(async (approve)=>approve.estimate()),
16014
+ estimateApprove(),
15674
16015
  estimateBurn(),
15675
16016
  estimateMint(),
15676
16017
  this.getMaxFee(params)
@@ -17292,7 +17633,9 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
17292
17633
  headers: context.headers
17293
17634
  }
17294
17635
  });
17295
- if (hasBoth) {
17636
+ if (context.customFeePolicy?.bridge != null) {
17637
+ kit.setCustomFeePolicy(context.customFeePolicy.bridge);
17638
+ } else if (hasBoth) {
17296
17639
  kit.setCustomFeePolicy({
17297
17640
  calculateFee: async (params)=>{
17298
17641
  const feeStr = await getFee('bridge', params);
@@ -17308,7 +17651,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
17308
17651
  };
17309
17652
 
17310
17653
  var name$1 = "@circle-fin/swap-kit";
17311
- var version$1 = "1.5.0";
17654
+ var version$1 = "1.5.2";
17312
17655
  var pkg$1 = {
17313
17656
  name: name$1,
17314
17657
  version: version$1};
@@ -19251,7 +19594,7 @@ new Set(Object.values(Blockchain));
19251
19594
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
19252
19595
 
19253
19596
  var name = "@circle-fin/earn-kit";
19254
- var version = "1.4.0";
19597
+ var version = "1.5.1";
19255
19598
  var pkg = {
19256
19599
  name: name,
19257
19600
  version: version};
@@ -20262,11 +20605,16 @@ const sourceAdapterContextSchema = z.object({
20262
20605
  *
20263
20606
  * Validate the optional Kit Key field using the standard `apiKeySchema`
20264
20607
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
20265
- * operates in permissionless mode.
20608
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
20609
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
20610
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
20611
+ * schema must accept them or a valid config object is rejected.
20266
20612
  *
20267
20613
  * @internal
20268
20614
  */ const earnConfigSchema = z.object({
20269
- kitKey: apiKeySchema.optional()
20615
+ kitKey: apiKeySchema.optional(),
20616
+ baseUrl: z.string().optional(),
20617
+ batchTransactions: z.boolean().optional()
20270
20618
  }).strict();
20271
20619
  /**
20272
20620
  * Canonical decimal form: a leading digit with no leading zeros (a single