@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.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',
@@ -7458,6 +7557,7 @@ class KitError extends Error {
7458
7557
  // =========================================================================
7459
7558
  [Blockchain.Avalanche]: '0xc891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
7460
7559
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
7560
+ [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
7461
7561
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
7462
7562
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
7463
7563
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
@@ -7466,6 +7566,7 @@ class KitError extends Error {
7466
7566
  // =========================================================================
7467
7567
  [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
7468
7568
  [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
7569
+ [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
7469
7570
  [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
7470
7571
  }
7471
7572
  };
@@ -9175,6 +9276,15 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
9175
9276
  ;
9176
9277
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
9177
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
+ ;
9178
9288
  /**
9179
9289
  * The minimum finality threshold for CCTPv2 transfers.
9180
9290
  *
@@ -9203,6 +9313,27 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9203
9313
  'Content-Type': 'application/json'
9204
9314
  }
9205
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
+ });
9206
9337
  /**
9207
9338
  * Type guard that verifies if an unknown value matches the AttestationMessage shape
9208
9339
  * and has all required properties.
@@ -9349,10 +9480,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9349
9480
  * ```
9350
9481
  */ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9351
9482
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9352
- const effectiveConfig = {
9353
- ...DEFAULT_CONFIG,
9354
- ...config
9355
- };
9483
+ const effectiveConfig = mergeAttestationConfig(config);
9356
9484
  return await pollApiGet(url, isAttestationResponse, effectiveConfig);
9357
9485
  };
9358
9486
  /**
@@ -9395,11 +9523,9 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9395
9523
  */ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9396
9524
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9397
9525
  // Use minimal retries since we're just fetching existing data
9398
- const effectiveConfig = {
9399
- ...DEFAULT_CONFIG,
9400
- maxRetries: 3,
9401
- ...config
9402
- };
9526
+ const effectiveConfig = mergeAttestationConfig(config, {
9527
+ maxRetries: 3
9528
+ });
9403
9529
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
9404
9530
  };
9405
9531
  /**
@@ -9459,10 +9585,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9459
9585
  * ```
9460
9586
  */ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
9461
9587
  const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
9462
- const effectiveConfig = {
9463
- ...DEFAULT_CONFIG,
9464
- ...config
9465
- };
9588
+ const effectiveConfig = mergeAttestationConfig(config);
9466
9589
  return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
9467
9590
  };
9468
9591
  /**
@@ -9528,14 +9651,139 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
9528
9651
  */ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
9529
9652
  const url = buildReAttestUrl(nonce, isTestnet);
9530
9653
  // Use minimal retries since we're just submitting a request, not polling for state
9531
- const effectiveConfig = {
9532
- ...DEFAULT_CONFIG,
9533
- maxRetries: 3,
9534
- ...config
9535
- };
9654
+ const effectiveConfig = mergeAttestationConfig(config, {
9655
+ maxRetries: 3
9656
+ });
9536
9657
  return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
9537
9658
  };
9538
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
+
9539
9787
  const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
9540
9788
  /**
9541
9789
  * Asserts that the provided parameters match the CCTPv2 wallet context interface.
@@ -10410,6 +10658,8 @@ function hasPendingState(analysis, result) {
10410
10658
  * - `adapter`: The adapter that will execute the transaction
10411
10659
  * - `confirmations`: The number of confirmations to wait for (defaults to 1)
10412
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
10413
10663
  * @returns The bridge step with the transaction details and explorer URL
10414
10664
  * @throws If the transaction execution fails
10415
10665
  *
@@ -10424,7 +10674,7 @@ function hasPendingState(analysis, result) {
10424
10674
  * })
10425
10675
  * console.log('Transaction hash:', step.txHash)
10426
10676
  * ```
10427
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout }) {
10677
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
10428
10678
  const step = {
10429
10679
  name,
10430
10680
  state: 'pending'
@@ -10437,7 +10687,9 @@ function hasPendingState(analysis, result) {
10437
10687
  step.state = 'noop';
10438
10688
  return step;
10439
10689
  }
10440
- const txHash = await request.execute();
10690
+ const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
10691
+ gasLimit
10692
+ }) : await request.execute();
10441
10693
  step.txHash = txHash;
10442
10694
  const retryOptions = {
10443
10695
  isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
@@ -10509,7 +10761,8 @@ function hasPendingState(analysis, result) {
10509
10761
  name: 'approve',
10510
10762
  adapter: params.source.adapter,
10511
10763
  chain: params.source.chain,
10512
- request: await provider.approve(params.source, approvalAmount)
10764
+ request: await provider.approve(params.source, approvalAmount),
10765
+ gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
10513
10766
  });
10514
10767
  }
10515
10768
 
@@ -10536,7 +10789,8 @@ function hasPendingState(analysis, result) {
10536
10789
  name: 'burn',
10537
10790
  adapter: params.source.adapter,
10538
10791
  chain: params.source.chain,
10539
- request: await provider.burn(params)
10792
+ request: await provider.burn(params),
10793
+ gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
10540
10794
  });
10541
10795
  }
10542
10796
 
@@ -10622,11 +10876,18 @@ function hasPendingState(analysis, result) {
10622
10876
  */ async function bridgeMint({ params, provider }, attestation) {
10623
10877
  // Validate attestation message matches transfer params
10624
10878
  await assertCCTPv2AttestationParams(attestation, params);
10879
+ const mintRequest = await provider.mint(params.source, params.destination, attestation);
10625
10880
  const step = await executePreparedChainRequest({
10626
10881
  name: 'mint',
10627
10882
  adapter: params.destination.adapter,
10628
10883
  chain: params.destination.chain,
10629
- 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)
10630
10891
  });
10631
10892
  // Add forwarded: false for non-relayer mints
10632
10893
  return {
@@ -10664,136 +10925,6 @@ const mockAttestationMessage = {
10664
10925
  delayReason: null
10665
10926
  };
10666
10927
 
10667
- /**
10668
- * Type guard that checks if the relayer has confirmed the mint transaction.
10669
- *
10670
- * This function validates that:
10671
- * 1. The response has valid AttestationResponse structure
10672
- * 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
10673
- *
10674
- * If forwardState is 'FAILED', throws a non-retryable KitError.
10675
- * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
10676
- *
10677
- * @param obj - The value to check, typically a parsed JSON response
10678
- * @returns True if the relayer has confirmed the mint
10679
- * @throws {KitError} With FATAL recoverability if structure is invalid
10680
- * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
10681
- * @throws {KitError} With RETRYABLE recoverability if still pending
10682
- * @internal
10683
- */ const isRelayerMintConfirmed = (obj)=>{
10684
- // First check if the structure is valid
10685
- if (!hasValidAttestationStructure(obj)) {
10686
- throw new KitError({
10687
- ...InputError.VALIDATION_FAILED,
10688
- recoverability: 'FATAL',
10689
- message: 'Invalid attestation response structure from IRIS API.'
10690
- });
10691
- }
10692
- // Find the first message (typically there's only one)
10693
- const message = obj.messages[0];
10694
- if (!message) {
10695
- throw new KitError({
10696
- ...InputError.VALIDATION_FAILED,
10697
- recoverability: 'FATAL',
10698
- message: 'No attestation messages found in IRIS API response.'
10699
- });
10700
- }
10701
- // Check for FAILED state - this is a permanent failure
10702
- if (message.forwardState === 'FAILED') {
10703
- throw new KitError({
10704
- ...NetworkError.RELAYER_FORWARD_FAILED,
10705
- recoverability: 'RESUMABLE',
10706
- 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.',
10707
- cause: {
10708
- trace: {
10709
- eventNonce: message.eventNonce,
10710
- attestation: message.attestation,
10711
- message: message.message
10712
- }
10713
- }
10714
- });
10715
- }
10716
- // Check if mint is confirmed (or complete) with a valid transaction hash
10717
- // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
10718
- if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
10719
- return true;
10720
- }
10721
- // Still pending or not yet processed - throw RETRYABLE error to continue polling
10722
- throw new KitError({
10723
- ...NetworkError.RELAYER_PENDING,
10724
- recoverability: 'RETRYABLE',
10725
- message: 'Relayer mint not ready. Waiting for confirmation.'
10726
- });
10727
- };
10728
- /**
10729
- * Polls the attestation API until the relayer's mint transaction is confirmed.
10730
- *
10731
- * This function is used when `useForwarder` is enabled. Instead of the user
10732
- * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
10733
- * This function polls until the relayer has submitted and confirmed the mint transaction.
10734
- *
10735
- * @remarks
10736
- * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
10737
- * - Throws immediately if `forwardState` is 'FAILED'
10738
- * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
10739
- * - Returns the attestation message with `forwardTxHash` populated
10740
- *
10741
- * @param sourceDomainId - The CCTP domain ID of the source chain
10742
- * @param transactionHash - The transaction hash of the burn operation
10743
- * @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
10744
- * @param config - Optional configuration overrides for polling behavior
10745
- * @returns The attestation message with confirmed forwardTxHash
10746
- * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
10747
- * @throws {KitError} If timeout is reached while still pending
10748
- *
10749
- * @example
10750
- * ```typescript
10751
- * const attestation = await fetchRelayerMint(0, '0xabc...', false)
10752
- * console.log('Relayer mint tx:', attestation.forwardTxHash)
10753
- * ```
10754
- */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
10755
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
10756
- const effectiveConfig = {
10757
- ...DEFAULT_CONFIG,
10758
- ...config
10759
- };
10760
- let response;
10761
- try {
10762
- response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
10763
- } catch (error) {
10764
- // Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
10765
- if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
10766
- throw new KitError({
10767
- ...NetworkError.RELAYER_FORWARD_FAILED,
10768
- recoverability: error.recoverability,
10769
- message: error.message,
10770
- cause: {
10771
- ...error.cause,
10772
- trace: {
10773
- ...error.cause?.trace,
10774
- burnTxHash: transactionHash
10775
- }
10776
- }
10777
- });
10778
- }
10779
- throw error;
10780
- }
10781
- // Return the first message (which should have forwardTxHash)
10782
- // Note: This check is needed for TypeScript type safety even though
10783
- // isRelayerMintConfirmed validates messages[0] exists. The type guard
10784
- // narrows the type at the call site, but TypeScript can't infer that
10785
- // the array still has elements after pollApiGet returns.
10786
- const message = response.messages[0];
10787
- if (!message) {
10788
- throw new KitError({
10789
- ...InputError.VALIDATION_FAILED,
10790
- recoverability: 'FATAL',
10791
- message: 'No attestation messages found in response after polling.'
10792
- });
10793
- }
10794
- return message;
10795
- };
10796
-
10797
10928
  /**
10798
10929
  * Executes the mint step for forwarding mode where Circle's relayer handles the mint.
10799
10930
  *
@@ -10807,6 +10938,7 @@ const mockAttestationMessage = {
10807
10938
  * immediately after the API confirms the mint.
10808
10939
  *
10809
10940
  * @param params - The bridge parameters containing source, destination, amount and config
10941
+ * @param provider - The CCTP v2 bridging provider
10810
10942
  * @param context - The step context containing burnTxHash from the burn step
10811
10943
  * @returns Promise resolving to the bridge step with transaction details
10812
10944
  * @throws {KitError} If burnTxHash is not available in context
@@ -10815,10 +10947,13 @@ const mockAttestationMessage = {
10815
10947
  *
10816
10948
  * @example
10817
10949
  * ```typescript
10818
- * const step = await bridgeRelayerMint(params, { burnTxHash: '0x...' })
10950
+ * const step = await bridgeRelayerMint(
10951
+ * { params, provider },
10952
+ * { burnTxHash: '0x...' }
10953
+ * )
10819
10954
  * console.log('Relayer mint tx:', step.txHash)
10820
10955
  * ```
10821
- */ async function bridgeRelayerMint(params, context) {
10956
+ */ async function bridgeRelayerMint({ params, provider }, context) {
10822
10957
  const step = {
10823
10958
  name: 'mint',
10824
10959
  state: 'pending',
@@ -10834,7 +10969,7 @@ const mockAttestationMessage = {
10834
10969
  }
10835
10970
  try {
10836
10971
  // Poll attestation API until relayer confirms the mint
10837
- 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);
10838
10973
  // Extract the relayer's mint transaction hash
10839
10974
  const forwardTxHash = attestation.forwardTxHash;
10840
10975
  if (!forwardTxHash) {
@@ -10928,7 +11063,7 @@ const mockAttestationMessage = {
10928
11063
  name: 'mint',
10929
11064
  // bridgeRelayerMint validates context.burnTxHash internally with a KitError
10930
11065
  // bridgeMint requires attestationData which is validated here
10931
- executor: useForwarder ? async (params, _provider, context)=>{
11066
+ executor: useForwarder ? async (params, provider, context)=>{
10932
11067
  if (!context) {
10933
11068
  throw new KitError({
10934
11069
  ...InputError.VALIDATION_FAILED,
@@ -10936,7 +11071,10 @@ const mockAttestationMessage = {
10936
11071
  message: 'Step context is required for relayer mint'
10937
11072
  });
10938
11073
  }
10939
- return bridgeRelayerMint(params, context);
11074
+ return bridgeRelayerMint({
11075
+ params,
11076
+ provider
11077
+ }, context);
10940
11078
  } : async (params, provider, context)=>{
10941
11079
  if (!context?.attestationData) {
10942
11080
  throw new KitError({
@@ -12145,7 +12283,7 @@ const mockAttestationMessage = {
12145
12283
  return step;
12146
12284
  }
12147
12285
 
12148
- var version = "1.8.5";
12286
+ var version = "1.9.0";
12149
12287
  var pkg = {
12150
12288
  version: version};
12151
12289
 
@@ -13171,6 +13309,39 @@ var pkg = {
13171
13309
  }
13172
13310
  }
13173
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
+ }
13174
13345
  /**
13175
13346
  * All chains that are supported by the CCTP v2 provider.
13176
13347
  *
@@ -13239,9 +13410,38 @@ var pkg = {
13239
13410
  * @param config - Optional configuration overrides for the provider
13240
13411
  */ constructor(config = {}){
13241
13412
  super();
13413
+ assertCCTPV2Config(config);
13242
13414
  this.config = config;
13243
13415
  }
13244
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
+ /**
13245
13445
  * Execute a cross-chain USDC bridge operation using the CCTP v2 protocol.
13246
13446
  *
13247
13447
  * This method orchestrates the complete CCTP v2 bridge flow including validation,
@@ -13667,11 +13867,7 @@ var pkg = {
13667
13867
  */ async fetchAttestation(source, transactionHash, config) {
13668
13868
  assertCCTPv2WalletContext(source);
13669
13869
  try {
13670
- // Merge configs: defaults <- global config <- per-call config
13671
- const effectiveConfig = {
13672
- ...this.config?.attestation,
13673
- ...config
13674
- };
13870
+ const effectiveConfig = this.resolveAttestationConfig(config);
13675
13871
  const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13676
13872
  const message = response.messages[0];
13677
13873
  if (!message) {
@@ -13688,6 +13884,49 @@ var pkg = {
13688
13884
  }
13689
13885
  }
13690
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
+ /**
13691
13930
  * Requests a fresh attestation for an expired attestation.
13692
13931
  *
13693
13932
  * This method is used when the original attestation has expired before the mint
@@ -13741,11 +13980,7 @@ var pkg = {
13741
13980
  throw new Error('Failed to re-attest: Invalid transaction hash');
13742
13981
  }
13743
13982
  try {
13744
- // Merge configs: defaults <- global config <- per-call config
13745
- const effectiveConfig = {
13746
- ...this.config?.attestation,
13747
- ...config
13748
- };
13983
+ const effectiveConfig = this.resolveAttestationConfig(config);
13749
13984
  // Step 1: Get existing attestation data to extract nonce
13750
13985
  const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
13751
13986
  const nonce = existingAttestation.messages[0]?.eventNonce;