@circle-fin/provider-cctp-v2 1.8.5 → 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.cjs CHANGED
@@ -35,6 +35,7 @@ var units = require('@ethersproject/units');
35
35
  var bytes = require('@ethersproject/bytes');
36
36
  var address = require('@ethersproject/address');
37
37
  var bs58 = require('bs58');
38
+ require('@ethersproject/abi');
38
39
 
39
40
  function _interopDefault (e) { return e && e.__esModule ? e.default : e; }
40
41
 
@@ -84,6 +85,8 @@ var bs58__default = /*#__PURE__*/_interopDefault(bs58);
84
85
  Blockchain["Celo_Alfajores_Testnet"] = "Celo_Alfajores_Testnet";
85
86
  Blockchain["Codex"] = "Codex";
86
87
  Blockchain["Codex_Testnet"] = "Codex_Testnet";
88
+ Blockchain["Cronos"] = "Cronos";
89
+ Blockchain["Cronos_Testnet"] = "Cronos_Testnet";
87
90
  Blockchain["Edge"] = "Edge";
88
91
  Blockchain["Edge_Testnet"] = "Edge_Testnet";
89
92
  Blockchain["Ethereum"] = "Ethereum";
@@ -166,6 +169,7 @@ var BridgeChain;
166
169
  BridgeChain["Avalanche"] = "Avalanche";
167
170
  BridgeChain["Base"] = "Base";
168
171
  BridgeChain["Codex"] = "Codex";
172
+ BridgeChain["Cronos"] = "Cronos";
169
173
  BridgeChain["Edge"] = "Edge";
170
174
  BridgeChain["Ethereum"] = "Ethereum";
171
175
  BridgeChain["HyperEVM"] = "HyperEVM";
@@ -190,6 +194,7 @@ var BridgeChain;
190
194
  BridgeChain["Avalanche_Fuji"] = "Avalanche_Fuji";
191
195
  BridgeChain["Base_Sepolia"] = "Base_Sepolia";
192
196
  BridgeChain["Codex_Testnet"] = "Codex_Testnet";
197
+ BridgeChain["Cronos_Testnet"] = "Cronos_Testnet";
193
198
  BridgeChain["Edge_Testnet"] = "Edge_Testnet";
194
199
  BridgeChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
195
200
  BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
@@ -1235,6 +1240,96 @@ var EarnChain;
1235
1240
  }
1236
1241
  });
1237
1242
 
1243
+ /**
1244
+ * Cronos Mainnet chain definition
1245
+ * @remarks
1246
+ * This represents the official production network for the Cronos blockchain.
1247
+ * Cronos is an EVM-compatible blockchain.
1248
+ */ const Cronos = defineChain({
1249
+ type: 'evm',
1250
+ chain: Blockchain.Cronos,
1251
+ name: 'Cronos',
1252
+ title: 'Cronos Mainnet',
1253
+ nativeCurrency: {
1254
+ name: 'Cronos',
1255
+ symbol: 'CRO',
1256
+ decimals: 18
1257
+ },
1258
+ chainId: 25,
1259
+ isTestnet: false,
1260
+ explorerUrl: 'https://cronoscan.com/tx/{hash}',
1261
+ rpcEndpoints: [
1262
+ 'https://evm.cronos.org'
1263
+ ],
1264
+ eurcAddress: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
1265
+ usdcAddress: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
1266
+ usdtAddress: null,
1267
+ cctp: {
1268
+ domain: 32,
1269
+ contracts: {
1270
+ v2: {
1271
+ type: 'split',
1272
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
1273
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
1274
+ confirmations: 1,
1275
+ fastConfirmations: 1
1276
+ }
1277
+ },
1278
+ forwarderSupported: {
1279
+ source: false,
1280
+ destination: false
1281
+ }
1282
+ },
1283
+ kitContracts: {
1284
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
1285
+ }
1286
+ });
1287
+
1288
+ /**
1289
+ * Cronos Testnet chain definition
1290
+ * @remarks
1291
+ * This represents the official test network for the Cronos blockchain.
1292
+ * Cronos is an EVM-compatible blockchain.
1293
+ */ const CronosTestnet = defineChain({
1294
+ type: 'evm',
1295
+ chain: Blockchain.Cronos_Testnet,
1296
+ name: 'Cronos Testnet',
1297
+ title: 'Cronos Testnet',
1298
+ nativeCurrency: {
1299
+ name: 'CRO',
1300
+ symbol: 'tCRO',
1301
+ decimals: 18
1302
+ },
1303
+ chainId: 338,
1304
+ isTestnet: true,
1305
+ explorerUrl: 'https://explorer.cronos.org/testnet/tx/{hash}',
1306
+ rpcEndpoints: [
1307
+ 'https://evm-t3.cronos.org'
1308
+ ],
1309
+ eurcAddress: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
1310
+ usdcAddress: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
1311
+ usdtAddress: null,
1312
+ cctp: {
1313
+ domain: 32,
1314
+ contracts: {
1315
+ v2: {
1316
+ type: 'split',
1317
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
1318
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
1319
+ confirmations: 1,
1320
+ fastConfirmations: 1
1321
+ }
1322
+ },
1323
+ forwarderSupported: {
1324
+ source: false,
1325
+ destination: false
1326
+ }
1327
+ },
1328
+ kitContracts: {
1329
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
1330
+ }
1331
+ });
1332
+
1238
1333
  /**
1239
1334
  * Edge Mainnet chain definition
1240
1335
  * @remarks
@@ -3586,6 +3681,8 @@ var Chains = {
3586
3681
  CeloAlfajoresTestnet: CeloAlfajoresTestnet,
3587
3682
  Codex: Codex,
3588
3683
  CodexTestnet: CodexTestnet,
3684
+ Cronos: Cronos,
3685
+ CronosTestnet: CronosTestnet,
3589
3686
  Edge: Edge,
3590
3687
  EdgeTestnet: EdgeTestnet,
3591
3688
  Ethereum: Ethereum,
@@ -7360,6 +7457,7 @@ class KitError extends Error {
7360
7457
  [Blockchain.Base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
7361
7458
  [Blockchain.Celo]: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
7362
7459
  [Blockchain.Codex]: '0xd996633a415985DBd7D6D12f4A4343E31f5037cf',
7460
+ [Blockchain.Cronos]: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
7363
7461
  [Blockchain.Edge]: '0x98d2919b9A214E6Fa5384AC81E6864bA686Ad74c',
7364
7462
  [Blockchain.Ethereum]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
7365
7463
  [Blockchain.Hedera]: '0.0.456858',
@@ -7393,6 +7491,7 @@ class KitError extends Error {
7393
7491
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
7394
7492
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
7395
7493
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
7494
+ [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
7396
7495
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
7397
7496
  [Blockchain.Ethereum_Sepolia]: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
7398
7497
  [Blockchain.Hedera_Testnet]: '0.0.429274',
@@ -7465,6 +7564,7 @@ class KitError extends Error {
7465
7564
  // =========================================================================
7466
7565
  [Blockchain.Avalanche]: '0xc891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
7467
7566
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
7567
+ [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
7468
7568
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
7469
7569
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
7470
7570
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
@@ -7473,6 +7573,7 @@ class KitError extends Error {
7473
7573
  // =========================================================================
7474
7574
  [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
7475
7575
  [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
7576
+ [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
7476
7577
  [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
7477
7578
  }
7478
7579
  };
@@ -9182,6 +9283,15 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
9182
9283
  ;
9183
9284
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
9184
9285
  ;
9286
+ // Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
9287
+ // chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
9288
+ // Kept separate from the fee-estimate averages above.
9289
+ const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
9290
+ ;
9291
+ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
9292
+ ;
9293
+ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
9294
+ ;
9185
9295
  /**
9186
9296
  * The minimum finality threshold for CCTPv2 transfers.
9187
9297
  *
@@ -9210,6 +9320,27 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9210
9320
  'Content-Type': 'application/json'
9211
9321
  }
9212
9322
  };
9323
+ /**
9324
+ * Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
9325
+ *
9326
+ * Headers are merged independently so caller-supplied headers augment the
9327
+ * defaults (such as `Content-Type`) rather than replacing them wholesale.
9328
+ *
9329
+ * @param config - Caller-provided polling configuration overrides
9330
+ * @param internalDefaults - Internal defaults applied before `config` (for example a
9331
+ * reduced `maxRetries` for one-shot requests); `config` still wins on conflict
9332
+ * @returns The effective polling configuration
9333
+ * @internal
9334
+ */ const mergeAttestationConfig = (config, internalDefaults = {})=>({
9335
+ ...DEFAULT_CONFIG,
9336
+ ...internalDefaults,
9337
+ ...config,
9338
+ headers: {
9339
+ ...DEFAULT_CONFIG.headers,
9340
+ ...internalDefaults.headers,
9341
+ ...config.headers
9342
+ }
9343
+ });
9213
9344
  /**
9214
9345
  * Type guard that verifies if an unknown value matches the AttestationMessage shape
9215
9346
  * and has all required properties.
@@ -9356,10 +9487,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9356
9487
  * ```
9357
9488
  */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9358
9489
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9359
- const effectiveConfig = {
9360
- ...DEFAULT_CONFIG,
9361
- ...config
9362
- };
9490
+ const effectiveConfig = mergeAttestationConfig(config);
9363
9491
  return await pollApiGet(url, isAttestationResponse, effectiveConfig);
9364
9492
  };
9365
9493
  /**
@@ -9402,11 +9530,9 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9402
9530
  */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9403
9531
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9404
9532
  // Use minimal retries since we're just fetching existing data
9405
- const effectiveConfig = {
9406
- ...DEFAULT_CONFIG,
9407
- maxRetries: 3,
9408
- ...config
9409
- };
9533
+ const effectiveConfig = mergeAttestationConfig(config, {
9534
+ maxRetries: 3
9535
+ });
9410
9536
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
9411
9537
  };
9412
9538
  /**
@@ -9466,10 +9592,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9466
9592
  * ```
9467
9593
  */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9468
9594
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9469
- const effectiveConfig = {
9470
- ...DEFAULT_CONFIG,
9471
- ...config
9472
- };
9595
+ const effectiveConfig = mergeAttestationConfig(config);
9473
9596
  return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
9474
9597
  };
9475
9598
  /**
@@ -9535,14 +9658,139 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9535
9658
  */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
9536
9659
  const url = buildReAttestUrl(nonce, isTestnet);
9537
9660
  // Use minimal retries since we're just submitting a request, not polling for state
9538
- const effectiveConfig = {
9539
- ...DEFAULT_CONFIG,
9540
- maxRetries: 3,
9541
- ...config
9542
- };
9661
+ const effectiveConfig = mergeAttestationConfig(config, {
9662
+ maxRetries: 3
9663
+ });
9543
9664
  return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
9544
9665
  };
9545
9666
 
9667
+ /**
9668
+ * Type guard that checks if the relayer has confirmed the mint transaction.
9669
+ *
9670
+ * This function validates that:
9671
+ * 1. The response has valid AttestationResponse structure
9672
+ * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
9673
+ *
9674
+ * If forwardState is 'FAILED', throws a non-retryable KitError.
9675
+ * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
9676
+ *
9677
+ * @param obj - The value to check, typically a parsed JSON response
9678
+ * @returns True if the relayer has confirmed the mint
9679
+ * @throws {KitError} With FATAL recoverability if structure is invalid
9680
+ * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
9681
+ * @throws {KitError} With RETRYABLE recoverability if still pending
9682
+ * @internal
9683
+ */ const isRelayerMintConfirmed = (obj)=>{
9684
+ // First check if the structure is valid
9685
+ if (!hasValidAttestationStructure(obj)) {
9686
+ throw new KitError({
9687
+ ...InputError.VALIDATION_FAILED,
9688
+ recoverability: 'FATAL',
9689
+ message: 'Invalid attestation response structure from IRIS API.'
9690
+ });
9691
+ }
9692
+ // Find the first message (typically there's only one)
9693
+ const message = obj.messages[0];
9694
+ if (!message) {
9695
+ throw new KitError({
9696
+ ...InputError.VALIDATION_FAILED,
9697
+ recoverability: 'FATAL',
9698
+ message: 'No attestation messages found in IRIS API response.'
9699
+ });
9700
+ }
9701
+ // Check for FAILED state - this is a permanent failure
9702
+ if (message.forwardState === 'FAILED') {
9703
+ throw new KitError({
9704
+ ...NetworkError.RELAYER_FORWARD_FAILED,
9705
+ recoverability: 'RESUMABLE',
9706
+ 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.',
9707
+ cause: {
9708
+ trace: {
9709
+ eventNonce: message.eventNonce,
9710
+ attestation: message.attestation,
9711
+ message: message.message
9712
+ }
9713
+ }
9714
+ });
9715
+ }
9716
+ // Check if mint is confirmed (or complete) with a valid transaction hash
9717
+ // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
9718
+ if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
9719
+ return true;
9720
+ }
9721
+ // Still pending or not yet processed - throw RETRYABLE error to continue polling
9722
+ throw new KitError({
9723
+ ...NetworkError.RELAYER_PENDING,
9724
+ recoverability: 'RETRYABLE',
9725
+ message: 'Relayer mint not ready. Waiting for confirmation.'
9726
+ });
9727
+ };
9728
+ /**
9729
+ * Polls the attestation API until the relayer's mint transaction is confirmed.
9730
+ *
9731
+ * This function is used when `useForwarder` is enabled. Instead of the user
9732
+ * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
9733
+ * This function polls until the relayer has submitted and confirmed the mint transaction.
9734
+ *
9735
+ * @remarks
9736
+ * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
9737
+ * - Throws immediately if `forwardState` is 'FAILED'
9738
+ * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
9739
+ * - Returns the attestation message with `forwardTxHash` populated
9740
+ *
9741
+ * @param sourceDomainId - The CCTP domain ID of the source chain
9742
+ * @param transactionHash - The transaction hash of the burn operation
9743
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
9744
+ * @param config - Optional configuration overrides for polling behavior
9745
+ * @returns The attestation message with confirmed forwardTxHash
9746
+ * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
9747
+ * @throws {KitError} If timeout is reached while still pending
9748
+ *
9749
+ * @example
9750
+ * ```typescript
9751
+ * const attestation = await fetchRelayerMint(0, '0xabc...', false)
9752
+ * console.log('Relayer mint tx:', attestation.forwardTxHash)
9753
+ * ```
9754
+ */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9755
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9756
+ const effectiveConfig = mergeAttestationConfig(config);
9757
+ let response;
9758
+ try {
9759
+ response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
9760
+ } catch (error) {
9761
+ // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
9762
+ if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
9763
+ throw new KitError({
9764
+ ...NetworkError.RELAYER_FORWARD_FAILED,
9765
+ recoverability: error.recoverability,
9766
+ message: error.message,
9767
+ cause: {
9768
+ ...error.cause,
9769
+ trace: {
9770
+ ...error.cause?.trace,
9771
+ burnTxHash: transactionHash
9772
+ }
9773
+ }
9774
+ });
9775
+ }
9776
+ throw error;
9777
+ }
9778
+ // Return the first message (which should have forwardTxHash)
9779
+ // Note: This check is needed for TypeScript type safety even though
9780
+ // isRelayerMintConfirmed validates messages[0] exists. The type guard
9781
+ // narrows the type at the call site, but TypeScript can't infer that
9782
+ // the array still has elements after pollApiGet returns.
9783
+ const message = response.messages[0];
9784
+ if (!message) {
9785
+ throw new KitError({
9786
+ ...InputError.VALIDATION_FAILED,
9787
+ recoverability: 'FATAL',
9788
+ message: 'No attestation messages found in response after polling.'
9789
+ });
9790
+ }
9791
+ return message;
9792
+ };
9793
+
9546
9794
  const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
9547
9795
  /**
9548
9796
  * Asserts that the provided parameters match the CCTPv2 wallet context interface.
@@ -10417,6 +10665,8 @@ function hasPendingState(analysis, result) {
10417
10665
  * - `adapter`: The adapter that will execute the transaction
10418
10666
  * - `confirmations`: The number of confirmations to wait for (defaults to 1)
10419
10667
  * - `timeout`: The timeout for the request in milliseconds
10668
+ * - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
10669
+ * bypassing `eth_estimateGas`; ignored for non-EVM requests
10420
10670
  * @returns The bridge step with the transaction details and explorer URL
10421
10671
  * @throws If the transaction execution fails
10422
10672
  *
@@ -10431,7 +10681,7 @@ function hasPendingState(analysis, result) {
10431
10681
  * })
10432
10682
  * console.log('Transaction hash:', step.txHash)
10433
10683
  * ```
10434
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout }) {
10684
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
10435
10685
  const step = {
10436
10686
  name,
10437
10687
  state: 'pending'
@@ -10444,7 +10694,9 @@ function hasPendingState(analysis, result) {
10444
10694
  step.state = 'noop';
10445
10695
  return step;
10446
10696
  }
10447
- const txHash = await request.execute();
10697
+ const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
10698
+ gasLimit
10699
+ }) : await request.execute();
10448
10700
  step.txHash = txHash;
10449
10701
  const retryOptions = {
10450
10702
  isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
@@ -10516,7 +10768,8 @@ function hasPendingState(analysis, result) {
10516
10768
  name: 'approve',
10517
10769
  adapter: params.source.adapter,
10518
10770
  chain: params.source.chain,
10519
- request: await provider.approve(params.source, approvalAmount)
10771
+ request: await provider.approve(params.source, approvalAmount),
10772
+ gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
10520
10773
  });
10521
10774
  }
10522
10775
 
@@ -10543,7 +10796,8 @@ function hasPendingState(analysis, result) {
10543
10796
  name: 'burn',
10544
10797
  adapter: params.source.adapter,
10545
10798
  chain: params.source.chain,
10546
- request: await provider.burn(params)
10799
+ request: await provider.burn(params),
10800
+ gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
10547
10801
  });
10548
10802
  }
10549
10803
 
@@ -10629,11 +10883,18 @@ function hasPendingState(analysis, result) {
10629
10883
  */ async function bridgeMint({ params, provider }, attestation) {
10630
10884
  // Validate attestation message matches transfer params
10631
10885
  await assertCCTPv2AttestationParams(attestation, params);
10886
+ const mintRequest = await provider.mint(params.source, params.destination, attestation);
10632
10887
  const step = await executePreparedChainRequest({
10633
10888
  name: 'mint',
10634
10889
  adapter: params.destination.adapter,
10635
10890
  chain: params.destination.chain,
10636
- request: await provider.mint(params.source, params.destination, attestation)
10891
+ request: mintRequest,
10892
+ // Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
10893
+ // eth_estimateGas does not account for, returning a below-floor value
10894
+ // without reverting. Pinning to a value above the observed execution max
10895
+ // (310_839) bypasses re-estimation and guarantees we clear both the floor
10896
+ // and the actual execution cost.
10897
+ gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
10637
10898
  });
10638
10899
  // Add forwarded: false for non-relayer mints
10639
10900
  return {
@@ -10671,136 +10932,6 @@ const mockAttestationMessage = {
10671
10932
  delayReason: null
10672
10933
  };
10673
10934
 
10674
- /**
10675
- * Type guard that checks if the relayer has confirmed the mint transaction.
10676
- *
10677
- * This function validates that:
10678
- * 1. The response has valid AttestationResponse structure
10679
- * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
10680
- *
10681
- * If forwardState is 'FAILED', throws a non-retryable KitError.
10682
- * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
10683
- *
10684
- * @param obj - The value to check, typically a parsed JSON response
10685
- * @returns True if the relayer has confirmed the mint
10686
- * @throws {KitError} With FATAL recoverability if structure is invalid
10687
- * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
10688
- * @throws {KitError} With RETRYABLE recoverability if still pending
10689
- * @internal
10690
- */ const isRelayerMintConfirmed = (obj)=>{
10691
- // First check if the structure is valid
10692
- if (!hasValidAttestationStructure(obj)) {
10693
- throw new KitError({
10694
- ...InputError.VALIDATION_FAILED,
10695
- recoverability: 'FATAL',
10696
- message: 'Invalid attestation response structure from IRIS API.'
10697
- });
10698
- }
10699
- // Find the first message (typically there's only one)
10700
- const message = obj.messages[0];
10701
- if (!message) {
10702
- throw new KitError({
10703
- ...InputError.VALIDATION_FAILED,
10704
- recoverability: 'FATAL',
10705
- message: 'No attestation messages found in IRIS API response.'
10706
- });
10707
- }
10708
- // Check for FAILED state - this is a permanent failure
10709
- if (message.forwardState === 'FAILED') {
10710
- throw new KitError({
10711
- ...NetworkError.RELAYER_FORWARD_FAILED,
10712
- recoverability: 'RESUMABLE',
10713
- 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.',
10714
- cause: {
10715
- trace: {
10716
- eventNonce: message.eventNonce,
10717
- attestation: message.attestation,
10718
- message: message.message
10719
- }
10720
- }
10721
- });
10722
- }
10723
- // Check if mint is confirmed (or complete) with a valid transaction hash
10724
- // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
10725
- if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
10726
- return true;
10727
- }
10728
- // Still pending or not yet processed - throw RETRYABLE error to continue polling
10729
- throw new KitError({
10730
- ...NetworkError.RELAYER_PENDING,
10731
- recoverability: 'RETRYABLE',
10732
- message: 'Relayer mint not ready. Waiting for confirmation.'
10733
- });
10734
- };
10735
- /**
10736
- * Polls the attestation API until the relayer's mint transaction is confirmed.
10737
- *
10738
- * This function is used when `useForwarder` is enabled. Instead of the user
10739
- * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
10740
- * This function polls until the relayer has submitted and confirmed the mint transaction.
10741
- *
10742
- * @remarks
10743
- * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
10744
- * - Throws immediately if `forwardState` is 'FAILED'
10745
- * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
10746
- * - Returns the attestation message with `forwardTxHash` populated
10747
- *
10748
- * @param sourceDomainId - The CCTP domain ID of the source chain
10749
- * @param transactionHash - The transaction hash of the burn operation
10750
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
10751
- * @param config - Optional configuration overrides for polling behavior
10752
- * @returns The attestation message with confirmed forwardTxHash
10753
- * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
10754
- * @throws {KitError} If timeout is reached while still pending
10755
- *
10756
- * @example
10757
- * ```typescript
10758
- * const attestation = await fetchRelayerMint(0, '0xabc...', false)
10759
- * console.log('Relayer mint tx:', attestation.forwardTxHash)
10760
- * ```
10761
- */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
10762
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
10763
- const effectiveConfig = {
10764
- ...DEFAULT_CONFIG,
10765
- ...config
10766
- };
10767
- let response;
10768
- try {
10769
- response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
10770
- } catch (error) {
10771
- // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
10772
- if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
10773
- throw new KitError({
10774
- ...NetworkError.RELAYER_FORWARD_FAILED,
10775
- recoverability: error.recoverability,
10776
- message: error.message,
10777
- cause: {
10778
- ...error.cause,
10779
- trace: {
10780
- ...error.cause?.trace,
10781
- burnTxHash: transactionHash
10782
- }
10783
- }
10784
- });
10785
- }
10786
- throw error;
10787
- }
10788
- // Return the first message (which should have forwardTxHash)
10789
- // Note: This check is needed for TypeScript type safety even though
10790
- // isRelayerMintConfirmed validates messages[0] exists. The type guard
10791
- // narrows the type at the call site, but TypeScript can't infer that
10792
- // the array still has elements after pollApiGet returns.
10793
- const message = response.messages[0];
10794
- if (!message) {
10795
- throw new KitError({
10796
- ...InputError.VALIDATION_FAILED,
10797
- recoverability: 'FATAL',
10798
- message: 'No attestation messages found in response after polling.'
10799
- });
10800
- }
10801
- return message;
10802
- };
10803
-
10804
10935
  /**
10805
10936
  * Executes the mint step for forwarding mode where Circle's relayer handles the mint.
10806
10937
  *
@@ -10814,6 +10945,7 @@ const mockAttestationMessage = {
10814
10945
  * immediately after the API confirms the mint.
10815
10946
  *
10816
10947
  * @param params - The bridge parameters containing source, destination, amount and config
10948
+ * @param provider - The CCTP v2 bridging provider
10817
10949
  * @param context - The step context containing burnTxHash from the burn step
10818
10950
  * @returns Promise resolving to the bridge step with transaction details
10819
10951
  * @throws {KitError} If burnTxHash is not available in context
@@ -10822,10 +10954,13 @@ const mockAttestationMessage = {
10822
10954
  *
10823
10955
  * @example
10824
10956
  * ```typescript
10825
- * const step = await bridgeRelayerMint(params, { burnTxHash: '0x...' })
10957
+ * const step = await bridgeRelayerMint(
10958
+ * { params, provider },
10959
+ * { burnTxHash: '0x...' }
10960
+ * )
10826
10961
  * console.log('Relayer mint tx:', step.txHash)
10827
10962
  * ```
10828
- */ async function bridgeRelayerMint(params, context) {
10963
+ */ async function bridgeRelayerMint({ params, provider }, context) {
10829
10964
  const step = {
10830
10965
  name: 'mint',
10831
10966
  state: 'pending',
@@ -10841,7 +10976,7 @@ const mockAttestationMessage = {
10841
10976
  }
10842
10977
  try {
10843
10978
  // Poll attestation API until relayer confirms the mint
10844
- const attestation = await fetchRelayerMint(params.source.chain.cctp.domain, context.burnTxHash, params.source.chain.isTestnet ?? false);
10979
+ const attestation = await provider.fetchRelayerMint(params.source, context.burnTxHash);
10845
10980
  // Extract the relayer's mint transaction hash
10846
10981
  const forwardTxHash = attestation.forwardTxHash;
10847
10982
  if (!forwardTxHash) {
@@ -10935,7 +11070,7 @@ const mockAttestationMessage = {
10935
11070
  name: 'mint',
10936
11071
  // bridgeRelayerMint validates context.burnTxHash internally with a KitError
10937
11072
  // bridgeMint requires attestationData which is validated here
10938
- executor: useForwarder ? async (params, _provider, context)=>{
11073
+ executor: useForwarder ? async (params, provider, context)=>{
10939
11074
  if (!context) {
10940
11075
  throw new KitError({
10941
11076
  ...InputError.VALIDATION_FAILED,
@@ -10943,7 +11078,10 @@ const mockAttestationMessage = {
10943
11078
  message: 'Step context is required for relayer mint'
10944
11079
  });
10945
11080
  }
10946
- return bridgeRelayerMint(params, context);
11081
+ return bridgeRelayerMint({
11082
+ params,
11083
+ provider
11084
+ }, context);
10947
11085
  } : async (params, provider, context)=>{
10948
11086
  if (!context?.attestationData) {
10949
11087
  throw new KitError({
@@ -12152,7 +12290,7 @@ const mockAttestationMessage = {
12152
12290
  return step;
12153
12291
  }
12154
12292
 
12155
- var version = "1.8.5";
12293
+ var version = "1.9.0";
12156
12294
  var pkg = {
12157
12295
  version: version};
12158
12296
 
@@ -13178,6 +13316,39 @@ var pkg = {
13178
13316
  }
13179
13317
  }
13180
13318
 
13319
+ function isPlainObject(value) {
13320
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
13321
+ return false;
13322
+ }
13323
+ const prototype = Object.getPrototypeOf(value);
13324
+ return prototype === Object.prototype || prototype === null;
13325
+ }
13326
+ function assertHeadersConfig(headers, field) {
13327
+ if (headers === undefined) {
13328
+ return;
13329
+ }
13330
+ if (!isPlainObject(headers)) {
13331
+ throw createValidationFailedError(field, headers, `${field} must be a plain object with string header values when provided`);
13332
+ }
13333
+ for (const [name, value] of Object.entries(headers)){
13334
+ if (typeof value !== 'string') {
13335
+ throw createValidationFailedError(`${field}.${name}`, value, 'header values must be strings');
13336
+ }
13337
+ }
13338
+ }
13339
+ function assertCCTPV2Config(config) {
13340
+ if (!isPlainObject(config)) {
13341
+ throw createValidationFailedError('config', config, 'config must be a plain object when provided');
13342
+ }
13343
+ assertHeadersConfig(config['headers'], 'config.headers');
13344
+ const attestation = config['attestation'];
13345
+ if (attestation !== undefined) {
13346
+ if (!isPlainObject(attestation)) {
13347
+ throw createValidationFailedError('config.attestation', attestation, 'config.attestation must be a plain object when provided');
13348
+ }
13349
+ assertHeadersConfig(attestation['headers'], 'config.attestation.headers');
13350
+ }
13351
+ }
13181
13352
  /**
13182
13353
  * All chains that are supported by the CCTP v2 provider.
13183
13354
  *
@@ -13246,9 +13417,38 @@ var pkg = {
13246
13417
  * @param config - Optional configuration overrides for the provider
13247
13418
  */ constructor(config = {}){
13248
13419
  super();
13420
+ assertCCTPV2Config(config);
13249
13421
  this.config = config;
13250
13422
  }
13251
13423
  /**
13424
+ * Resolves the effective polling configuration for an attestation request.
13425
+ *
13426
+ * Precedence (lowest to highest): provider `config.attestation`, then the
13427
+ * per-call `config`. Headers merge independently across
13428
+ * `config.attestation.headers`, the provider-level `config.headers`, and any
13429
+ * per-call `config.headers`, so a more specific header augments rather than
13430
+ * replaces the broader ones. The `headers` key is omitted entirely when no
13431
+ * headers are configured, leaving the attestation fetchers' defaults intact.
13432
+ *
13433
+ * @param config - Optional per-call polling configuration overrides
13434
+ * @returns The merged polling configuration passed to the attestation fetchers
13435
+ */ resolveAttestationConfig(config) {
13436
+ const headers = {
13437
+ ...this.config?.attestation?.headers,
13438
+ ...this.config?.headers,
13439
+ ...config?.headers
13440
+ };
13441
+ // Polling fields follow normal precedence; headers are merged separately
13442
+ // below so narrower config layers augment rather than replace broader ones.
13443
+ return {
13444
+ ...this.config?.attestation,
13445
+ ...config,
13446
+ ...Object.keys(headers).length > 0 ? {
13447
+ headers
13448
+ } : {}
13449
+ };
13450
+ }
13451
+ /**
13252
13452
  * Execute a cross-chain USDC bridge operation using the CCTP v2 protocol.
13253
13453
  *
13254
13454
  * This method orchestrates the complete CCTP v2 bridge flow including validation,
@@ -13674,11 +13874,7 @@ var pkg = {
13674
13874
  */ async fetchAttestation(source, transactionHash, config) {
13675
13875
  assertCCTPv2WalletContext(source);
13676
13876
  try {
13677
- // Merge configs: defaults <- global config <- per-call config
13678
- const effectiveConfig = {
13679
- ...this.config?.attestation,
13680
- ...config
13681
- };
13877
+ const effectiveConfig = this.resolveAttestationConfig(config);
13682
13878
  const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13683
13879
  const message = response.messages[0];
13684
13880
  if (!message) {
@@ -13695,6 +13891,49 @@ var pkg = {
13695
13891
  }
13696
13892
  }
13697
13893
  /**
13894
+ * Polls attestation data until Circle's relayer mint transaction is confirmed.
13895
+ *
13896
+ * This method is used by forwarded transfers. It polls the same Iris
13897
+ * attestation endpoint as {@link CCTPV2BridgingProvider.fetchAttestation},
13898
+ * but waits for a completed relayer forward state and returns the attestation
13899
+ * message containing `forwardTxHash`.
13900
+ *
13901
+ * @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
13902
+ * @param source - The source wallet context containing the chain definition and wallet address
13903
+ * @param transactionHash - The transaction hash of the burn operation
13904
+ * @param config - Optional polling configuration overrides for timeout, retries, delay, and headers
13905
+ * @returns A promise that resolves to the attestation message with `forwardTxHash`
13906
+ * @throws KitError If the relayer forward fails, the response is invalid, or polling times out
13907
+ *
13908
+ * @example
13909
+ * ```typescript
13910
+ * import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
13911
+ * import { Chains } from '@core/chains'
13912
+ *
13913
+ * const provider = new CCTPV2BridgingProvider({
13914
+ * headers: { 'X-Partner-UUID': '00000000-0000-0000-0000-000000000000' },
13915
+ * })
13916
+ *
13917
+ * const attestation = await provider.fetchRelayerMint(
13918
+ * {
13919
+ * adapter,
13920
+ * chain: Chains.EthereumSepolia,
13921
+ * address: '0x1234...',
13922
+ * },
13923
+ * '0xabc123...',
13924
+ * )
13925
+ *
13926
+ * console.log('Relayer mint tx:', attestation.forwardTxHash)
13927
+ * ```
13928
+ */ async fetchRelayerMint(source, transactionHash, config) {
13929
+ assertCCTPv2WalletContext(source);
13930
+ if (typeof transactionHash !== 'string' || transactionHash.trim() === '') {
13931
+ throw createValidationFailedError('transactionHash', transactionHash, 'transactionHash must be a non-empty string');
13932
+ }
13933
+ const effectiveConfig = this.resolveAttestationConfig(config);
13934
+ return await fetchRelayerMint(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13935
+ }
13936
+ /**
13698
13937
  * Requests a fresh attestation for an expired attestation.
13699
13938
  *
13700
13939
  * This method is used when the original attestation has expired before the mint
@@ -13748,11 +13987,7 @@ var pkg = {
13748
13987
  throw new Error('Failed to re-attest: Invalid transaction hash');
13749
13988
  }
13750
13989
  try {
13751
- // Merge configs: defaults <- global config <- per-call config
13752
- const effectiveConfig = {
13753
- ...this.config?.attestation,
13754
- ...config
13755
- };
13990
+ const effectiveConfig = this.resolveAttestationConfig(config);
13756
13991
  // Step 1: Get existing attestation data to extract nonce
13757
13992
  const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13758
13993
  const nonce = existingAttestation.messages[0]?.eventNonce;