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