@circle-fin/provider-cctp-v2 1.8.4 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -33,6 +33,7 @@ import { formatUnits as formatUnits$1 } from '@ethersproject/units';
33
33
  import { hexlify, hexZeroPad } from '@ethersproject/bytes';
34
34
  import { getAddress } from '@ethersproject/address';
35
35
  import bs58 from 'bs58';
36
+ import '@ethersproject/abi';
36
37
 
37
38
  /**
38
39
  * @packageDocumentation
@@ -77,6 +78,8 @@ import bs58 from 'bs58';
77
78
  Blockchain["Celo_Alfajores_Testnet"] = "Celo_Alfajores_Testnet";
78
79
  Blockchain["Codex"] = "Codex";
79
80
  Blockchain["Codex_Testnet"] = "Codex_Testnet";
81
+ Blockchain["Cronos"] = "Cronos";
82
+ Blockchain["Cronos_Testnet"] = "Cronos_Testnet";
80
83
  Blockchain["Edge"] = "Edge";
81
84
  Blockchain["Edge_Testnet"] = "Edge_Testnet";
82
85
  Blockchain["Ethereum"] = "Ethereum";
@@ -159,6 +162,7 @@ var BridgeChain;
159
162
  BridgeChain["Avalanche"] = "Avalanche";
160
163
  BridgeChain["Base"] = "Base";
161
164
  BridgeChain["Codex"] = "Codex";
165
+ BridgeChain["Cronos"] = "Cronos";
162
166
  BridgeChain["Edge"] = "Edge";
163
167
  BridgeChain["Ethereum"] = "Ethereum";
164
168
  BridgeChain["HyperEVM"] = "HyperEVM";
@@ -183,6 +187,7 @@ var BridgeChain;
183
187
  BridgeChain["Avalanche_Fuji"] = "Avalanche_Fuji";
184
188
  BridgeChain["Base_Sepolia"] = "Base_Sepolia";
185
189
  BridgeChain["Codex_Testnet"] = "Codex_Testnet";
190
+ BridgeChain["Cronos_Testnet"] = "Cronos_Testnet";
186
191
  BridgeChain["Edge_Testnet"] = "Edge_Testnet";
187
192
  BridgeChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
188
193
  BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
@@ -1228,6 +1233,96 @@ var EarnChain;
1228
1233
  }
1229
1234
  });
1230
1235
 
1236
+ /**
1237
+ * Cronos Mainnet chain definition
1238
+ * @remarks
1239
+ * This represents the official production network for the Cronos blockchain.
1240
+ * Cronos is an EVM-compatible blockchain.
1241
+ */ const Cronos = defineChain({
1242
+ type: 'evm',
1243
+ chain: Blockchain.Cronos,
1244
+ name: 'Cronos',
1245
+ title: 'Cronos Mainnet',
1246
+ nativeCurrency: {
1247
+ name: 'Cronos',
1248
+ symbol: 'CRO',
1249
+ decimals: 18
1250
+ },
1251
+ chainId: 25,
1252
+ isTestnet: false,
1253
+ explorerUrl: 'https://cronoscan.com/tx/{hash}',
1254
+ rpcEndpoints: [
1255
+ 'https://evm.cronos.org'
1256
+ ],
1257
+ eurcAddress: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
1258
+ usdcAddress: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
1259
+ usdtAddress: null,
1260
+ cctp: {
1261
+ domain: 32,
1262
+ contracts: {
1263
+ v2: {
1264
+ type: 'split',
1265
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
1266
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
1267
+ confirmations: 1,
1268
+ fastConfirmations: 1
1269
+ }
1270
+ },
1271
+ forwarderSupported: {
1272
+ source: false,
1273
+ destination: false
1274
+ }
1275
+ },
1276
+ kitContracts: {
1277
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
1278
+ }
1279
+ });
1280
+
1281
+ /**
1282
+ * Cronos Testnet chain definition
1283
+ * @remarks
1284
+ * This represents the official test network for the Cronos blockchain.
1285
+ * Cronos is an EVM-compatible blockchain.
1286
+ */ const CronosTestnet = defineChain({
1287
+ type: 'evm',
1288
+ chain: Blockchain.Cronos_Testnet,
1289
+ name: 'Cronos Testnet',
1290
+ title: 'Cronos Testnet',
1291
+ nativeCurrency: {
1292
+ name: 'CRO',
1293
+ symbol: 'tCRO',
1294
+ decimals: 18
1295
+ },
1296
+ chainId: 338,
1297
+ isTestnet: true,
1298
+ explorerUrl: 'https://explorer.cronos.org/testnet/tx/{hash}',
1299
+ rpcEndpoints: [
1300
+ 'https://evm-t3.cronos.org'
1301
+ ],
1302
+ eurcAddress: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
1303
+ usdcAddress: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
1304
+ usdtAddress: null,
1305
+ cctp: {
1306
+ domain: 32,
1307
+ contracts: {
1308
+ v2: {
1309
+ type: 'split',
1310
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
1311
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
1312
+ confirmations: 1,
1313
+ fastConfirmations: 1
1314
+ }
1315
+ },
1316
+ forwarderSupported: {
1317
+ source: false,
1318
+ destination: false
1319
+ }
1320
+ },
1321
+ kitContracts: {
1322
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
1323
+ }
1324
+ });
1325
+
1231
1326
  /**
1232
1327
  * Edge Mainnet chain definition
1233
1328
  * @remarks
@@ -3579,6 +3674,8 @@ var Chains = /*#__PURE__*/Object.freeze({
3579
3674
  CeloAlfajoresTestnet: CeloAlfajoresTestnet,
3580
3675
  Codex: Codex,
3581
3676
  CodexTestnet: CodexTestnet,
3677
+ Cronos: Cronos,
3678
+ CronosTestnet: CronosTestnet,
3582
3679
  Edge: Edge,
3583
3680
  EdgeTestnet: EdgeTestnet,
3584
3681
  Ethereum: Ethereum,
@@ -7353,6 +7450,7 @@ class KitError extends Error {
7353
7450
  [Blockchain.Base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
7354
7451
  [Blockchain.Celo]: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
7355
7452
  [Blockchain.Codex]: '0xd996633a415985DBd7D6D12f4A4343E31f5037cf',
7453
+ [Blockchain.Cronos]: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
7356
7454
  [Blockchain.Edge]: '0x98d2919b9A214E6Fa5384AC81E6864bA686Ad74c',
7357
7455
  [Blockchain.Ethereum]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
7358
7456
  [Blockchain.Hedera]: '0.0.456858',
@@ -7386,6 +7484,7 @@ class KitError extends Error {
7386
7484
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
7387
7485
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
7388
7486
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
7487
+ [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
7389
7488
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
7390
7489
  [Blockchain.Ethereum_Sepolia]: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
7391
7490
  [Blockchain.Hedera_Testnet]: '0.0.429274',
@@ -7453,13 +7552,22 @@ class KitError extends Error {
7453
7552
  symbol: 'EURC',
7454
7553
  decimals: 6,
7455
7554
  locators: {
7555
+ // =========================================================================
7556
+ // Mainnets
7557
+ // =========================================================================
7456
7558
  [Blockchain.Avalanche]: '0xc891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
7457
7559
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
7560
+ [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
7458
7561
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
7459
7562
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
7460
7563
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
7564
+ // =========================================================================
7461
7565
  // Testnets
7462
- [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a'
7566
+ // =========================================================================
7567
+ [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
7568
+ [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
7569
+ [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
7570
+ [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
7463
7571
  }
7464
7572
  };
7465
7573
 
@@ -7669,8 +7777,7 @@ class KitError extends Error {
7669
7777
  * cirBTC (Circle Bitcoin) token definition with addresses and metadata.
7670
7778
  *
7671
7779
  * @remarks
7672
- * Built-in cirBTC definition for the TokenRegistry. Currently deployed
7673
- * on Arc Testnet.
7780
+ * Built-in cirBTC definition for the TokenRegistry.
7674
7781
  *
7675
7782
  * @example
7676
7783
  * ```typescript
@@ -7686,7 +7793,15 @@ class KitError extends Error {
7686
7793
  symbol: 'cirBTC',
7687
7794
  decimals: 8,
7688
7795
  locators: {
7689
- [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF'
7796
+ // =========================================================================
7797
+ // Mainnets
7798
+ // =========================================================================
7799
+ [Blockchain.Ethereum]: '0x72DFB2E44f59C5AD2bAFE84314E5b99a7cd5075E',
7800
+ // =========================================================================
7801
+ // Testnets
7802
+ // =========================================================================
7803
+ [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF',
7804
+ [Blockchain.Ethereum_Sepolia]: '0x3a3fe695F684Bf9b9e43CF43C2b895Ea5e392bB3'
7690
7805
  }
7691
7806
  };
7692
7807
 
@@ -9161,6 +9276,15 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
9161
9276
  ;
9162
9277
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
9163
9278
  ;
9279
+ // Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
9280
+ // chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
9281
+ // Kept separate from the fee-estimate averages above.
9282
+ const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
9283
+ ;
9284
+ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
9285
+ ;
9286
+ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
9287
+ ;
9164
9288
  /**
9165
9289
  * The minimum finality threshold for CCTPv2 transfers.
9166
9290
  *
@@ -9189,6 +9313,27 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9189
9313
  'Content-Type': 'application/json'
9190
9314
  }
9191
9315
  };
9316
+ /**
9317
+ * Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
9318
+ *
9319
+ * Headers are merged independently so caller-supplied headers augment the
9320
+ * defaults (such as `Content-Type`) rather than replacing them wholesale.
9321
+ *
9322
+ * @param config - Caller-provided polling configuration overrides
9323
+ * @param internalDefaults - Internal defaults applied before `config` (for example a
9324
+ * reduced `maxRetries` for one-shot requests); `config` still wins on conflict
9325
+ * @returns The effective polling configuration
9326
+ * @internal
9327
+ */ const mergeAttestationConfig = (config, internalDefaults = {})=>({
9328
+ ...DEFAULT_CONFIG,
9329
+ ...internalDefaults,
9330
+ ...config,
9331
+ headers: {
9332
+ ...DEFAULT_CONFIG.headers,
9333
+ ...internalDefaults.headers,
9334
+ ...config.headers
9335
+ }
9336
+ });
9192
9337
  /**
9193
9338
  * Type guard that verifies if an unknown value matches the AttestationMessage shape
9194
9339
  * and has all required properties.
@@ -9335,10 +9480,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9335
9480
  * ```
9336
9481
  */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9337
9482
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9338
- const effectiveConfig = {
9339
- ...DEFAULT_CONFIG,
9340
- ...config
9341
- };
9483
+ const effectiveConfig = mergeAttestationConfig(config);
9342
9484
  return await pollApiGet(url, isAttestationResponse, effectiveConfig);
9343
9485
  };
9344
9486
  /**
@@ -9381,11 +9523,9 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9381
9523
  */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9382
9524
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9383
9525
  // Use minimal retries since we're just fetching existing data
9384
- const effectiveConfig = {
9385
- ...DEFAULT_CONFIG,
9386
- maxRetries: 3,
9387
- ...config
9388
- };
9526
+ const effectiveConfig = mergeAttestationConfig(config, {
9527
+ maxRetries: 3
9528
+ });
9389
9529
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
9390
9530
  };
9391
9531
  /**
@@ -9445,10 +9585,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9445
9585
  * ```
9446
9586
  */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9447
9587
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9448
- const effectiveConfig = {
9449
- ...DEFAULT_CONFIG,
9450
- ...config
9451
- };
9588
+ const effectiveConfig = mergeAttestationConfig(config);
9452
9589
  return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
9453
9590
  };
9454
9591
  /**
@@ -9514,14 +9651,139 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9514
9651
  */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
9515
9652
  const url = buildReAttestUrl(nonce, isTestnet);
9516
9653
  // Use minimal retries since we're just submitting a request, not polling for state
9517
- const effectiveConfig = {
9518
- ...DEFAULT_CONFIG,
9519
- maxRetries: 3,
9520
- ...config
9521
- };
9654
+ const effectiveConfig = mergeAttestationConfig(config, {
9655
+ maxRetries: 3
9656
+ });
9522
9657
  return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
9523
9658
  };
9524
9659
 
9660
+ /**
9661
+ * Type guard that checks if the relayer has confirmed the mint transaction.
9662
+ *
9663
+ * This function validates that:
9664
+ * 1. The response has valid AttestationResponse structure
9665
+ * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
9666
+ *
9667
+ * If forwardState is 'FAILED', throws a non-retryable KitError.
9668
+ * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
9669
+ *
9670
+ * @param obj - The value to check, typically a parsed JSON response
9671
+ * @returns True if the relayer has confirmed the mint
9672
+ * @throws {KitError} With FATAL recoverability if structure is invalid
9673
+ * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
9674
+ * @throws {KitError} With RETRYABLE recoverability if still pending
9675
+ * @internal
9676
+ */ const isRelayerMintConfirmed = (obj)=>{
9677
+ // First check if the structure is valid
9678
+ if (!hasValidAttestationStructure(obj)) {
9679
+ throw new KitError({
9680
+ ...InputError.VALIDATION_FAILED,
9681
+ recoverability: 'FATAL',
9682
+ message: 'Invalid attestation response structure from IRIS API.'
9683
+ });
9684
+ }
9685
+ // Find the first message (typically there's only one)
9686
+ const message = obj.messages[0];
9687
+ if (!message) {
9688
+ throw new KitError({
9689
+ ...InputError.VALIDATION_FAILED,
9690
+ recoverability: 'FATAL',
9691
+ message: 'No attestation messages found in IRIS API response.'
9692
+ });
9693
+ }
9694
+ // Check for FAILED state - this is a permanent failure
9695
+ if (message.forwardState === 'FAILED') {
9696
+ throw new KitError({
9697
+ ...NetworkError.RELAYER_FORWARD_FAILED,
9698
+ recoverability: 'RESUMABLE',
9699
+ 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.',
9700
+ cause: {
9701
+ trace: {
9702
+ eventNonce: message.eventNonce,
9703
+ attestation: message.attestation,
9704
+ message: message.message
9705
+ }
9706
+ }
9707
+ });
9708
+ }
9709
+ // Check if mint is confirmed (or complete) with a valid transaction hash
9710
+ // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
9711
+ if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
9712
+ return true;
9713
+ }
9714
+ // Still pending or not yet processed - throw RETRYABLE error to continue polling
9715
+ throw new KitError({
9716
+ ...NetworkError.RELAYER_PENDING,
9717
+ recoverability: 'RETRYABLE',
9718
+ message: 'Relayer mint not ready. Waiting for confirmation.'
9719
+ });
9720
+ };
9721
+ /**
9722
+ * Polls the attestation API until the relayer's mint transaction is confirmed.
9723
+ *
9724
+ * This function is used when `useForwarder` is enabled. Instead of the user
9725
+ * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
9726
+ * This function polls until the relayer has submitted and confirmed the mint transaction.
9727
+ *
9728
+ * @remarks
9729
+ * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
9730
+ * - Throws immediately if `forwardState` is 'FAILED'
9731
+ * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
9732
+ * - Returns the attestation message with `forwardTxHash` populated
9733
+ *
9734
+ * @param sourceDomainId - The CCTP domain ID of the source chain
9735
+ * @param transactionHash - The transaction hash of the burn operation
9736
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
9737
+ * @param config - Optional configuration overrides for polling behavior
9738
+ * @returns The attestation message with confirmed forwardTxHash
9739
+ * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
9740
+ * @throws {KitError} If timeout is reached while still pending
9741
+ *
9742
+ * @example
9743
+ * ```typescript
9744
+ * const attestation = await fetchRelayerMint(0, '0xabc...', false)
9745
+ * console.log('Relayer mint tx:', attestation.forwardTxHash)
9746
+ * ```
9747
+ */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9748
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9749
+ const effectiveConfig = mergeAttestationConfig(config);
9750
+ let response;
9751
+ try {
9752
+ response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
9753
+ } catch (error) {
9754
+ // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
9755
+ if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
9756
+ throw new KitError({
9757
+ ...NetworkError.RELAYER_FORWARD_FAILED,
9758
+ recoverability: error.recoverability,
9759
+ message: error.message,
9760
+ cause: {
9761
+ ...error.cause,
9762
+ trace: {
9763
+ ...error.cause?.trace,
9764
+ burnTxHash: transactionHash
9765
+ }
9766
+ }
9767
+ });
9768
+ }
9769
+ throw error;
9770
+ }
9771
+ // Return the first message (which should have forwardTxHash)
9772
+ // Note: This check is needed for TypeScript type safety even though
9773
+ // isRelayerMintConfirmed validates messages[0] exists. The type guard
9774
+ // narrows the type at the call site, but TypeScript can't infer that
9775
+ // the array still has elements after pollApiGet returns.
9776
+ const message = response.messages[0];
9777
+ if (!message) {
9778
+ throw new KitError({
9779
+ ...InputError.VALIDATION_FAILED,
9780
+ recoverability: 'FATAL',
9781
+ message: 'No attestation messages found in response after polling.'
9782
+ });
9783
+ }
9784
+ return message;
9785
+ };
9786
+
9525
9787
  const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
9526
9788
  /**
9527
9789
  * Asserts that the provided parameters match the CCTPv2 wallet context interface.
@@ -10396,6 +10658,8 @@ function hasPendingState(analysis, result) {
10396
10658
  * - `adapter`: The adapter that will execute the transaction
10397
10659
  * - `confirmations`: The number of confirmations to wait for (defaults to 1)
10398
10660
  * - `timeout`: The timeout for the request in milliseconds
10661
+ * - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
10662
+ * bypassing `eth_estimateGas`; ignored for non-EVM requests
10399
10663
  * @returns The bridge step with the transaction details and explorer URL
10400
10664
  * @throws If the transaction execution fails
10401
10665
  *
@@ -10410,7 +10674,7 @@ function hasPendingState(analysis, result) {
10410
10674
  * })
10411
10675
  * console.log('Transaction hash:', step.txHash)
10412
10676
  * ```
10413
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout }) {
10677
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
10414
10678
  const step = {
10415
10679
  name,
10416
10680
  state: 'pending'
@@ -10423,7 +10687,9 @@ function hasPendingState(analysis, result) {
10423
10687
  step.state = 'noop';
10424
10688
  return step;
10425
10689
  }
10426
- const txHash = await request.execute();
10690
+ const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
10691
+ gasLimit
10692
+ }) : await request.execute();
10427
10693
  step.txHash = txHash;
10428
10694
  const retryOptions = {
10429
10695
  isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
@@ -10495,7 +10761,8 @@ function hasPendingState(analysis, result) {
10495
10761
  name: 'approve',
10496
10762
  adapter: params.source.adapter,
10497
10763
  chain: params.source.chain,
10498
- request: await provider.approve(params.source, approvalAmount)
10764
+ request: await provider.approve(params.source, approvalAmount),
10765
+ gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
10499
10766
  });
10500
10767
  }
10501
10768
 
@@ -10522,7 +10789,8 @@ function hasPendingState(analysis, result) {
10522
10789
  name: 'burn',
10523
10790
  adapter: params.source.adapter,
10524
10791
  chain: params.source.chain,
10525
- request: await provider.burn(params)
10792
+ request: await provider.burn(params),
10793
+ gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
10526
10794
  });
10527
10795
  }
10528
10796
 
@@ -10608,11 +10876,18 @@ function hasPendingState(analysis, result) {
10608
10876
  */ async function bridgeMint({ params, provider }, attestation) {
10609
10877
  // Validate attestation message matches transfer params
10610
10878
  await assertCCTPv2AttestationParams(attestation, params);
10879
+ const mintRequest = await provider.mint(params.source, params.destination, attestation);
10611
10880
  const step = await executePreparedChainRequest({
10612
10881
  name: 'mint',
10613
10882
  adapter: params.destination.adapter,
10614
10883
  chain: params.destination.chain,
10615
- request: await provider.mint(params.source, params.destination, attestation)
10884
+ request: mintRequest,
10885
+ // Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
10886
+ // eth_estimateGas does not account for, returning a below-floor value
10887
+ // without reverting. Pinning to a value above the observed execution max
10888
+ // (310_839) bypasses re-estimation and guarantees we clear both the floor
10889
+ // and the actual execution cost.
10890
+ gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
10616
10891
  });
10617
10892
  // Add forwarded: false for non-relayer mints
10618
10893
  return {
@@ -10650,136 +10925,6 @@ const mockAttestationMessage = {
10650
10925
  delayReason: null
10651
10926
  };
10652
10927
 
10653
- /**
10654
- * Type guard that checks if the relayer has confirmed the mint transaction.
10655
- *
10656
- * This function validates that:
10657
- * 1. The response has valid AttestationResponse structure
10658
- * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
10659
- *
10660
- * If forwardState is 'FAILED', throws a non-retryable KitError.
10661
- * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
10662
- *
10663
- * @param obj - The value to check, typically a parsed JSON response
10664
- * @returns True if the relayer has confirmed the mint
10665
- * @throws {KitError} With FATAL recoverability if structure is invalid
10666
- * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
10667
- * @throws {KitError} With RETRYABLE recoverability if still pending
10668
- * @internal
10669
- */ const isRelayerMintConfirmed = (obj)=>{
10670
- // First check if the structure is valid
10671
- if (!hasValidAttestationStructure(obj)) {
10672
- throw new KitError({
10673
- ...InputError.VALIDATION_FAILED,
10674
- recoverability: 'FATAL',
10675
- message: 'Invalid attestation response structure from IRIS API.'
10676
- });
10677
- }
10678
- // Find the first message (typically there's only one)
10679
- const message = obj.messages[0];
10680
- if (!message) {
10681
- throw new KitError({
10682
- ...InputError.VALIDATION_FAILED,
10683
- recoverability: 'FATAL',
10684
- message: 'No attestation messages found in IRIS API response.'
10685
- });
10686
- }
10687
- // Check for FAILED state - this is a permanent failure
10688
- if (message.forwardState === 'FAILED') {
10689
- throw new KitError({
10690
- ...NetworkError.RELAYER_FORWARD_FAILED,
10691
- recoverability: 'RESUMABLE',
10692
- 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.',
10693
- cause: {
10694
- trace: {
10695
- eventNonce: message.eventNonce,
10696
- attestation: message.attestation,
10697
- message: message.message
10698
- }
10699
- }
10700
- });
10701
- }
10702
- // Check if mint is confirmed (or complete) with a valid transaction hash
10703
- // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
10704
- if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
10705
- return true;
10706
- }
10707
- // Still pending or not yet processed - throw RETRYABLE error to continue polling
10708
- throw new KitError({
10709
- ...NetworkError.RELAYER_PENDING,
10710
- recoverability: 'RETRYABLE',
10711
- message: 'Relayer mint not ready. Waiting for confirmation.'
10712
- });
10713
- };
10714
- /**
10715
- * Polls the attestation API until the relayer's mint transaction is confirmed.
10716
- *
10717
- * This function is used when `useForwarder` is enabled. Instead of the user
10718
- * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
10719
- * This function polls until the relayer has submitted and confirmed the mint transaction.
10720
- *
10721
- * @remarks
10722
- * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
10723
- * - Throws immediately if `forwardState` is 'FAILED'
10724
- * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
10725
- * - Returns the attestation message with `forwardTxHash` populated
10726
- *
10727
- * @param sourceDomainId - The CCTP domain ID of the source chain
10728
- * @param transactionHash - The transaction hash of the burn operation
10729
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
10730
- * @param config - Optional configuration overrides for polling behavior
10731
- * @returns The attestation message with confirmed forwardTxHash
10732
- * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
10733
- * @throws {KitError} If timeout is reached while still pending
10734
- *
10735
- * @example
10736
- * ```typescript
10737
- * const attestation = await fetchRelayerMint(0, '0xabc...', false)
10738
- * console.log('Relayer mint tx:', attestation.forwardTxHash)
10739
- * ```
10740
- */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
10741
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
10742
- const effectiveConfig = {
10743
- ...DEFAULT_CONFIG,
10744
- ...config
10745
- };
10746
- let response;
10747
- try {
10748
- response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
10749
- } catch (error) {
10750
- // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
10751
- if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
10752
- throw new KitError({
10753
- ...NetworkError.RELAYER_FORWARD_FAILED,
10754
- recoverability: error.recoverability,
10755
- message: error.message,
10756
- cause: {
10757
- ...error.cause,
10758
- trace: {
10759
- ...error.cause?.trace,
10760
- burnTxHash: transactionHash
10761
- }
10762
- }
10763
- });
10764
- }
10765
- throw error;
10766
- }
10767
- // Return the first message (which should have forwardTxHash)
10768
- // Note: This check is needed for TypeScript type safety even though
10769
- // isRelayerMintConfirmed validates messages[0] exists. The type guard
10770
- // narrows the type at the call site, but TypeScript can't infer that
10771
- // the array still has elements after pollApiGet returns.
10772
- const message = response.messages[0];
10773
- if (!message) {
10774
- throw new KitError({
10775
- ...InputError.VALIDATION_FAILED,
10776
- recoverability: 'FATAL',
10777
- message: 'No attestation messages found in response after polling.'
10778
- });
10779
- }
10780
- return message;
10781
- };
10782
-
10783
10928
  /**
10784
10929
  * Executes the mint step for forwarding mode where Circle's relayer handles the mint.
10785
10930
  *
@@ -10793,6 +10938,7 @@ const mockAttestationMessage = {
10793
10938
  * immediately after the API confirms the mint.
10794
10939
  *
10795
10940
  * @param params - The bridge parameters containing source, destination, amount and config
10941
+ * @param provider - The CCTP v2 bridging provider
10796
10942
  * @param context - The step context containing burnTxHash from the burn step
10797
10943
  * @returns Promise resolving to the bridge step with transaction details
10798
10944
  * @throws {KitError} If burnTxHash is not available in context
@@ -10801,10 +10947,13 @@ const mockAttestationMessage = {
10801
10947
  *
10802
10948
  * @example
10803
10949
  * ```typescript
10804
- * const step = await bridgeRelayerMint(params, { burnTxHash: '0x...' })
10950
+ * const step = await bridgeRelayerMint(
10951
+ * { params, provider },
10952
+ * { burnTxHash: '0x...' }
10953
+ * )
10805
10954
  * console.log('Relayer mint tx:', step.txHash)
10806
10955
  * ```
10807
- */ async function bridgeRelayerMint(params, context) {
10956
+ */ async function bridgeRelayerMint({ params, provider }, context) {
10808
10957
  const step = {
10809
10958
  name: 'mint',
10810
10959
  state: 'pending',
@@ -10820,7 +10969,7 @@ const mockAttestationMessage = {
10820
10969
  }
10821
10970
  try {
10822
10971
  // Poll attestation API until relayer confirms the mint
10823
- const attestation = await fetchRelayerMint(params.source.chain.cctp.domain, context.burnTxHash, params.source.chain.isTestnet ?? false);
10972
+ const attestation = await provider.fetchRelayerMint(params.source, context.burnTxHash);
10824
10973
  // Extract the relayer's mint transaction hash
10825
10974
  const forwardTxHash = attestation.forwardTxHash;
10826
10975
  if (!forwardTxHash) {
@@ -10914,7 +11063,7 @@ const mockAttestationMessage = {
10914
11063
  name: 'mint',
10915
11064
  // bridgeRelayerMint validates context.burnTxHash internally with a KitError
10916
11065
  // bridgeMint requires attestationData which is validated here
10917
- executor: useForwarder ? async (params, _provider, context)=>{
11066
+ executor: useForwarder ? async (params, provider, context)=>{
10918
11067
  if (!context) {
10919
11068
  throw new KitError({
10920
11069
  ...InputError.VALIDATION_FAILED,
@@ -10922,7 +11071,10 @@ const mockAttestationMessage = {
10922
11071
  message: 'Step context is required for relayer mint'
10923
11072
  });
10924
11073
  }
10925
- return bridgeRelayerMint(params, context);
11074
+ return bridgeRelayerMint({
11075
+ params,
11076
+ provider
11077
+ }, context);
10926
11078
  } : async (params, provider, context)=>{
10927
11079
  if (!context?.attestationData) {
10928
11080
  throw new KitError({
@@ -12131,7 +12283,7 @@ const mockAttestationMessage = {
12131
12283
  return step;
12132
12284
  }
12133
12285
 
12134
- var version = "1.8.4";
12286
+ var version = "1.9.0";
12135
12287
  var pkg = {
12136
12288
  version: version};
12137
12289
 
@@ -13157,6 +13309,39 @@ var pkg = {
13157
13309
  }
13158
13310
  }
13159
13311
 
13312
+ function isPlainObject(value) {
13313
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
13314
+ return false;
13315
+ }
13316
+ const prototype = Object.getPrototypeOf(value);
13317
+ return prototype === Object.prototype || prototype === null;
13318
+ }
13319
+ function assertHeadersConfig(headers, field) {
13320
+ if (headers === undefined) {
13321
+ return;
13322
+ }
13323
+ if (!isPlainObject(headers)) {
13324
+ throw createValidationFailedError(field, headers, `${field} must be a plain object with string header values when provided`);
13325
+ }
13326
+ for (const [name, value] of Object.entries(headers)){
13327
+ if (typeof value !== 'string') {
13328
+ throw createValidationFailedError(`${field}.${name}`, value, 'header values must be strings');
13329
+ }
13330
+ }
13331
+ }
13332
+ function assertCCTPV2Config(config) {
13333
+ if (!isPlainObject(config)) {
13334
+ throw createValidationFailedError('config', config, 'config must be a plain object when provided');
13335
+ }
13336
+ assertHeadersConfig(config['headers'], 'config.headers');
13337
+ const attestation = config['attestation'];
13338
+ if (attestation !== undefined) {
13339
+ if (!isPlainObject(attestation)) {
13340
+ throw createValidationFailedError('config.attestation', attestation, 'config.attestation must be a plain object when provided');
13341
+ }
13342
+ assertHeadersConfig(attestation['headers'], 'config.attestation.headers');
13343
+ }
13344
+ }
13160
13345
  /**
13161
13346
  * All chains that are supported by the CCTP v2 provider.
13162
13347
  *
@@ -13225,9 +13410,38 @@ var pkg = {
13225
13410
  * @param config - Optional configuration overrides for the provider
13226
13411
  */ constructor(config = {}){
13227
13412
  super();
13413
+ assertCCTPV2Config(config);
13228
13414
  this.config = config;
13229
13415
  }
13230
13416
  /**
13417
+ * Resolves the effective polling configuration for an attestation request.
13418
+ *
13419
+ * Precedence (lowest to highest): provider `config.attestation`, then the
13420
+ * per-call `config`. Headers merge independently across
13421
+ * `config.attestation.headers`, the provider-level `config.headers`, and any
13422
+ * per-call `config.headers`, so a more specific header augments rather than
13423
+ * replaces the broader ones. The `headers` key is omitted entirely when no
13424
+ * headers are configured, leaving the attestation fetchers' defaults intact.
13425
+ *
13426
+ * @param config - Optional per-call polling configuration overrides
13427
+ * @returns The merged polling configuration passed to the attestation fetchers
13428
+ */ resolveAttestationConfig(config) {
13429
+ const headers = {
13430
+ ...this.config?.attestation?.headers,
13431
+ ...this.config?.headers,
13432
+ ...config?.headers
13433
+ };
13434
+ // Polling fields follow normal precedence; headers are merged separately
13435
+ // below so narrower config layers augment rather than replace broader ones.
13436
+ return {
13437
+ ...this.config?.attestation,
13438
+ ...config,
13439
+ ...Object.keys(headers).length > 0 ? {
13440
+ headers
13441
+ } : {}
13442
+ };
13443
+ }
13444
+ /**
13231
13445
  * Execute a cross-chain USDC bridge operation using the CCTP v2 protocol.
13232
13446
  *
13233
13447
  * This method orchestrates the complete CCTP v2 bridge flow including validation,
@@ -13653,11 +13867,7 @@ var pkg = {
13653
13867
  */ async fetchAttestation(source, transactionHash, config) {
13654
13868
  assertCCTPv2WalletContext(source);
13655
13869
  try {
13656
- // Merge configs: defaults <- global config <- per-call config
13657
- const effectiveConfig = {
13658
- ...this.config?.attestation,
13659
- ...config
13660
- };
13870
+ const effectiveConfig = this.resolveAttestationConfig(config);
13661
13871
  const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13662
13872
  const message = response.messages[0];
13663
13873
  if (!message) {
@@ -13674,6 +13884,49 @@ var pkg = {
13674
13884
  }
13675
13885
  }
13676
13886
  /**
13887
+ * Polls attestation data until Circle's relayer mint transaction is confirmed.
13888
+ *
13889
+ * This method is used by forwarded transfers. It polls the same Iris
13890
+ * attestation endpoint as {@link CCTPV2BridgingProvider.fetchAttestation},
13891
+ * but waits for a completed relayer forward state and returns the attestation
13892
+ * message containing `forwardTxHash`.
13893
+ *
13894
+ * @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
13895
+ * @param source - The source wallet context containing the chain definition and wallet address
13896
+ * @param transactionHash - The transaction hash of the burn operation
13897
+ * @param config - Optional polling configuration overrides for timeout, retries, delay, and headers
13898
+ * @returns A promise that resolves to the attestation message with `forwardTxHash`
13899
+ * @throws KitError If the relayer forward fails, the response is invalid, or polling times out
13900
+ *
13901
+ * @example
13902
+ * ```typescript
13903
+ * import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
13904
+ * import { Chains } from '@core/chains'
13905
+ *
13906
+ * const provider = new CCTPV2BridgingProvider({
13907
+ * headers: { 'X-Partner-UUID': '00000000-0000-0000-0000-000000000000' },
13908
+ * })
13909
+ *
13910
+ * const attestation = await provider.fetchRelayerMint(
13911
+ * {
13912
+ * adapter,
13913
+ * chain: Chains.EthereumSepolia,
13914
+ * address: '0x1234...',
13915
+ * },
13916
+ * '0xabc123...',
13917
+ * )
13918
+ *
13919
+ * console.log('Relayer mint tx:', attestation.forwardTxHash)
13920
+ * ```
13921
+ */ async fetchRelayerMint(source, transactionHash, config) {
13922
+ assertCCTPv2WalletContext(source);
13923
+ if (typeof transactionHash !== 'string' || transactionHash.trim() === '') {
13924
+ throw createValidationFailedError('transactionHash', transactionHash, 'transactionHash must be a non-empty string');
13925
+ }
13926
+ const effectiveConfig = this.resolveAttestationConfig(config);
13927
+ return await fetchRelayerMint(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13928
+ }
13929
+ /**
13677
13930
  * Requests a fresh attestation for an expired attestation.
13678
13931
  *
13679
13932
  * This method is used when the original attestation has expired before the mint
@@ -13727,11 +13980,7 @@ var pkg = {
13727
13980
  throw new Error('Failed to re-attest: Invalid transaction hash');
13728
13981
  }
13729
13982
  try {
13730
- // Merge configs: defaults <- global config <- per-call config
13731
- const effectiveConfig = {
13732
- ...this.config?.attestation,
13733
- ...config
13734
- };
13983
+ const effectiveConfig = this.resolveAttestationConfig(config);
13735
13984
  // Step 1: Get existing attestation data to extract nonce
13736
13985
  const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13737
13986
  const nonce = existingAttestation.messages[0]?.eventNonce;