@circle-fin/provider-cctp-v2 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @circle-fin/provider-cctp-v2
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add attestation expiry detection utilities for CCTP v2 fast transfers:
8
+ - `isAttestationExpired(attestation, currentBlockNumber)` - Check if an attestation has expired based on the destination chain's current block/slot
9
+ - `getBlocksUntilExpiry(attestation, currentBlockNumber)` - Get the number of blocks remaining until expiry (returns `null` for attestations that never expire)
10
+ - `isMintFailureRelatedToAttestation(error)` - Detect if a mint failure was caused by attestation expiry to know when to call `reAttest()`
11
+
12
+ - Add native balance validation for transaction gas fees before bridge tx.
13
+
3
14
  ## 1.1.0
4
15
 
5
16
  ### Minor Changes
package/index.cjs CHANGED
@@ -385,8 +385,11 @@ const ArcTestnet = defineChain({
385
385
  name: 'Arc Testnet',
386
386
  title: 'ArcTestnet',
387
387
  nativeCurrency: {
388
- name: 'Arc',
389
- symbol: 'Arc',
388
+ name: 'USDC',
389
+ symbol: 'USDC',
390
+ // Arc uses native USDC with 18 decimals for gas payments (EVM standard).
391
+ // Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
392
+ // See: https://docs.arc.network/arc/references/contract-addresses
390
393
  decimals: 18,
391
394
  },
392
395
  chainId: 5042002,
@@ -3644,6 +3647,12 @@ const BalanceError = {
3644
3647
  code: 9001,
3645
3648
  name: 'BALANCE_INSUFFICIENT_TOKEN',
3646
3649
  type: 'BALANCE',
3650
+ },
3651
+ /** Insufficient native token (ETH/SOL/etc) for gas fees */
3652
+ INSUFFICIENT_GAS: {
3653
+ code: 9002,
3654
+ name: 'BALANCE_INSUFFICIENT_GAS',
3655
+ type: 'BALANCE',
3647
3656
  }};
3648
3657
 
3649
3658
  /**
@@ -3866,7 +3875,7 @@ function createValidationErrorFromZod(zodError, context) {
3866
3875
  *
3867
3876
  * @param chain - The blockchain network where the balance check failed
3868
3877
  * @param token - The token symbol (e.g., 'USDC', 'ETH')
3869
- * @param rawError - The original error from the underlying system (optional)
3878
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
3870
3879
  * @returns KitError with insufficient token balance details
3871
3880
  *
3872
3881
  * @example
@@ -3879,24 +3888,71 @@ function createValidationErrorFromZod(zodError, context) {
3879
3888
  *
3880
3889
  * @example
3881
3890
  * ```typescript
3882
- * // With raw error for debugging
3891
+ * // With trace context for debugging
3883
3892
  * try {
3884
3893
  * await transfer(...)
3885
3894
  * } catch (error) {
3886
- * throw createInsufficientTokenBalanceError('Base', 'USDC', error)
3895
+ * throw createInsufficientTokenBalanceError('Base', 'USDC', {
3896
+ * rawError: error,
3897
+ * balance: '1000000',
3898
+ * amount: '5000000',
3899
+ * })
3887
3900
  * }
3888
3901
  * ```
3889
3902
  */
3890
- function createInsufficientTokenBalanceError(chain, token, rawError) {
3903
+ function createInsufficientTokenBalanceError(chain, token, trace) {
3891
3904
  return new KitError({
3892
3905
  ...BalanceError.INSUFFICIENT_TOKEN,
3893
3906
  recoverability: 'FATAL',
3894
3907
  message: `Insufficient ${token} balance on ${chain}`,
3895
3908
  cause: {
3896
3909
  trace: {
3910
+ ...trace,
3897
3911
  chain,
3898
3912
  token,
3899
- rawError,
3913
+ },
3914
+ },
3915
+ });
3916
+ }
3917
+ /**
3918
+ * Creates error for insufficient gas funds.
3919
+ *
3920
+ * This error is thrown when a wallet does not have enough native tokens
3921
+ * (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
3922
+ * as it requires user intervention to add gas funds.
3923
+ *
3924
+ * @param chain - The blockchain network where the gas check failed
3925
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
3926
+ * @returns KitError with insufficient gas details
3927
+ *
3928
+ * @example
3929
+ * ```typescript
3930
+ * import { createInsufficientGasError } from '@core/errors'
3931
+ *
3932
+ * throw createInsufficientGasError('Ethereum')
3933
+ * // Message: "Insufficient gas funds on Ethereum"
3934
+ * ```
3935
+ *
3936
+ * @example
3937
+ * ```typescript
3938
+ * // With trace context for debugging
3939
+ * throw createInsufficientGasError('Ethereum', {
3940
+ * rawError: error,
3941
+ * gasRequired: '21000',
3942
+ * gasAvailable: '10000',
3943
+ * walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
3944
+ * })
3945
+ * ```
3946
+ */
3947
+ function createInsufficientGasError(chain, trace) {
3948
+ return new KitError({
3949
+ ...BalanceError.INSUFFICIENT_GAS,
3950
+ recoverability: 'FATAL',
3951
+ message: `Insufficient gas funds on ${chain}`,
3952
+ cause: {
3953
+ trace: {
3954
+ ...trace,
3955
+ chain,
3900
3956
  },
3901
3957
  },
3902
3958
  });
@@ -3958,6 +4014,38 @@ function isKitError(error) {
3958
4014
  function isFatalError(error) {
3959
4015
  return isKitError(error) && error.recoverability === 'FATAL';
3960
4016
  }
4017
+ /**
4018
+ * Safely extracts error message from any error type.
4019
+ *
4020
+ * This utility handles different error types gracefully, extracting
4021
+ * meaningful messages from Error instances, string errors, or providing
4022
+ * a fallback for unknown error types. Never throws.
4023
+ *
4024
+ * @param error - Unknown error to extract message from
4025
+ * @returns Error message string, or fallback message
4026
+ *
4027
+ * @example
4028
+ * ```typescript
4029
+ * import { getErrorMessage } from '@core/errors'
4030
+ *
4031
+ * try {
4032
+ * await riskyOperation()
4033
+ * } catch (error) {
4034
+ * const message = getErrorMessage(error)
4035
+ * console.log('Error occurred:', message)
4036
+ * // Works with Error, KitError, string, or any other type
4037
+ * }
4038
+ * ```
4039
+ */
4040
+ function getErrorMessage(error) {
4041
+ if (error instanceof Error) {
4042
+ return error.message;
4043
+ }
4044
+ if (typeof error === 'string') {
4045
+ return error;
4046
+ }
4047
+ return 'An unknown error occurred';
4048
+ }
3961
4049
 
3962
4050
  /**
3963
4051
  * Validates data against a Zod schema with enhanced error reporting.
@@ -5389,6 +5477,170 @@ mintAddress) => {
5389
5477
  }
5390
5478
  };
5391
5479
 
5480
+ /**
5481
+ * Converts a block number to bigint with validation.
5482
+ *
5483
+ * @param value - The block number value to convert (bigint, number, or string)
5484
+ * @returns The validated block number as a bigint
5485
+ * @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
5486
+ * @internal
5487
+ */
5488
+ const toBlockNumber = (value) => {
5489
+ if (value === null || value === undefined) {
5490
+ throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
5491
+ }
5492
+ // Empty string edge case - BigInt('') === 0n which is misleading
5493
+ if (value === '') {
5494
+ throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
5495
+ }
5496
+ // For numbers, validate before BigInt conversion
5497
+ if (typeof value === 'number') {
5498
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
5499
+ throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
5500
+ }
5501
+ }
5502
+ let result;
5503
+ try {
5504
+ result = BigInt(value);
5505
+ }
5506
+ catch {
5507
+ throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
5508
+ }
5509
+ if (result < 0n) {
5510
+ throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
5511
+ }
5512
+ return result;
5513
+ };
5514
+ /**
5515
+ * Determines whether an attestation has expired based on the current block number.
5516
+ *
5517
+ * An attestation expires when the destination chain's current block number is greater
5518
+ * than or equal to the expiration block specified in the attestation message.
5519
+ * Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
5520
+ *
5521
+ * @param attestation - The attestation message containing expiration block information
5522
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5523
+ * @returns `true` if the attestation has expired, `false` if still valid or never expires
5524
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5525
+ *
5526
+ * @example
5527
+ * ```typescript
5528
+ * import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
5529
+ *
5530
+ * // Check if attestation is expired on EVM chain
5531
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5532
+ * const currentBlock = await publicClient.getBlockNumber()
5533
+ * const expired = isAttestationExpired(attestation, currentBlock)
5534
+ *
5535
+ * if (expired) {
5536
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5537
+ * }
5538
+ * ```
5539
+ *
5540
+ * @example
5541
+ * ```typescript
5542
+ * // Check on Solana
5543
+ * const slot = await adapter.getConnection(destinationChain).getSlot()
5544
+ * const expired = isAttestationExpired(attestation, slot)
5545
+ * ```
5546
+ */
5547
+ const isAttestationExpired = (attestation, currentBlockNumber) => {
5548
+ const currentBlock = toBlockNumber(currentBlockNumber);
5549
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5550
+ // 0n means it never expires (re-attested messages and slow transfers)
5551
+ if (expiration === 0n) {
5552
+ return false;
5553
+ }
5554
+ // Attestation is expired if current block >= expiration block
5555
+ return currentBlock >= expiration;
5556
+ };
5557
+ /**
5558
+ * Calculates the number of blocks remaining until an attestation expires.
5559
+ *
5560
+ * Returns the difference between the expiration block and the current block number.
5561
+ * Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
5562
+ * Returns `0n` or a negative bigint if the attestation has already expired.
5563
+ *
5564
+ * @param attestation - The attestation message containing expiration block information
5565
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5566
+ * @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
5567
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5568
+ *
5569
+ * @example
5570
+ * ```typescript
5571
+ * import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
5572
+ *
5573
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5574
+ * const currentBlock = await publicClient.getBlockNumber()
5575
+ * const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
5576
+ *
5577
+ * if (blocksRemaining === null) {
5578
+ * console.log('Attestation never expires')
5579
+ * } else if (blocksRemaining <= 0n) {
5580
+ * console.log('Attestation has expired')
5581
+ * } else {
5582
+ * console.log(`${blocksRemaining} blocks until expiry`)
5583
+ * }
5584
+ * ```
5585
+ */
5586
+ const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
5587
+ const currentBlock = toBlockNumber(currentBlockNumber);
5588
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5589
+ // 0n means it never expires (re-attested messages and slow transfers)
5590
+ if (expiration === 0n) {
5591
+ return null;
5592
+ }
5593
+ // Return the difference (can be negative if expired)
5594
+ return expiration - currentBlock;
5595
+ };
5596
+ /**
5597
+ * Determines whether a mint failure was caused by an expired attestation.
5598
+ *
5599
+ * This function inspects the error thrown during a mint operation to detect
5600
+ * if the failure is due to the attestation's expiration block being exceeded.
5601
+ * When this returns `true`, the caller should use `reAttest()` to obtain a
5602
+ * fresh attestation before retrying the mint.
5603
+ *
5604
+ * @param error - The error thrown during the mint operation
5605
+ * @returns `true` if the error indicates the attestation has expired, `false` otherwise
5606
+ *
5607
+ * @example
5608
+ * ```typescript
5609
+ * import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
5610
+ *
5611
+ * try {
5612
+ * await mintRequest.execute()
5613
+ * } catch (error) {
5614
+ * if (isMintFailureRelatedToAttestation(error)) {
5615
+ * // Attestation expired - get a fresh one
5616
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5617
+ * const newMintRequest = await provider.mint(source, destination, freshAttestation)
5618
+ * await newMintRequest.execute()
5619
+ * } else {
5620
+ * throw error
5621
+ * }
5622
+ * }
5623
+ * ```
5624
+ */
5625
+ const isMintFailureRelatedToAttestation = (error) => {
5626
+ if (error === null || error === undefined) {
5627
+ return false;
5628
+ }
5629
+ const errorString = getErrorMessage(error).toLowerCase();
5630
+ // Check for Solana "MessageExpired" error pattern
5631
+ // Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
5632
+ // Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
5633
+ if (errorString.includes('messageexpired')) {
5634
+ return true;
5635
+ }
5636
+ // Check for EVM attestation expiry errors
5637
+ // Contract reverts with: "Message expired and must be re-signed"
5638
+ if (errorString.includes('message') && errorString.includes('expired')) {
5639
+ return true;
5640
+ }
5641
+ return false;
5642
+ };
5643
+
5392
5644
  /**
5393
5645
  * Checks if a decoded attestation field matches the corresponding transfer parameter.
5394
5646
  * If the values do not match, appends a descriptive error message to the errors array.
@@ -6232,21 +6484,64 @@ const validateBalanceForTransaction = async (params) => {
6232
6484
  // Extract chain name from operationContext
6233
6485
  const chainName = extractChainInfo(operationContext.chain).name;
6234
6486
  // Create KitError with rich context in trace
6235
- const error = createInsufficientTokenBalanceError(chainName, token);
6236
- // Enhance error with additional context for debugging
6237
- if (error.cause) {
6238
- const existingTrace = typeof error.cause.trace === 'object' && error.cause.trace
6239
- ? error.cause.trace
6240
- : {};
6241
- error.cause.trace = {
6242
- ...existingTrace,
6243
- balance: balance.toString(),
6244
- amount,
6245
- tokenAddress,
6246
- walletAddress: operationContext.address,
6247
- };
6248
- }
6249
- throw error;
6487
+ throw createInsufficientTokenBalanceError(chainName, token, {
6488
+ balance: balance.toString(),
6489
+ amount,
6490
+ tokenAddress,
6491
+ walletAddress: operationContext.address,
6492
+ });
6493
+ }
6494
+ };
6495
+
6496
+ /**
6497
+ * Validate that the adapter has sufficient native token balance for transaction fees.
6498
+ *
6499
+ * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
6500
+ * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
6501
+ * if the balance is zero, indicating the wallet cannot pay for transaction fees.
6502
+ *
6503
+ * @param params - The validation parameters containing adapter and operation context.
6504
+ * @returns A promise that resolves to void if validation passes.
6505
+ * @throws {KitError} When the adapter's native balance is zero (code: 9002).
6506
+ *
6507
+ * @example
6508
+ * ```typescript
6509
+ * import { validateNativeBalanceForTransaction } from '@core/adapter'
6510
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
6511
+ * import { isKitError, ERROR_TYPES } from '@core/errors'
6512
+ *
6513
+ * const adapter = createViemAdapterFromPrivateKey({
6514
+ * privateKey: '0x...',
6515
+ * chain: 'Ethereum',
6516
+ * })
6517
+ *
6518
+ * try {
6519
+ * await validateNativeBalanceForTransaction({
6520
+ * adapter,
6521
+ * operationContext: { chain: 'Ethereum' },
6522
+ * })
6523
+ * console.log('Native balance validation passed')
6524
+ * } catch (error) {
6525
+ * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
6526
+ * console.error('Insufficient gas funds:', error.message)
6527
+ * }
6528
+ * }
6529
+ * ```
6530
+ */
6531
+ const validateNativeBalanceForTransaction = async (params) => {
6532
+ const { adapter, operationContext } = params;
6533
+ const balancePrepared = await adapter.prepareAction('native.balanceOf', {
6534
+ walletAddress: operationContext.address,
6535
+ }, operationContext);
6536
+ const balance = await balancePrepared.execute();
6537
+ if (BigInt(balance) === 0n) {
6538
+ // Extract chain name from operationContext
6539
+ const chainName = extractChainInfo(operationContext.chain).name;
6540
+ // Create KitError with rich context in trace
6541
+ throw createInsufficientGasError(chainName, {
6542
+ balance: '0',
6543
+ walletAddress: operationContext.address,
6544
+ });
6250
6545
  }
6251
6546
  };
6252
6547
 
@@ -7123,16 +7418,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7123
7418
  async bridge(params) {
7124
7419
  // CCTP-specific bridge params validation (includes base validation)
7125
7420
  assertCCTPv2BridgeParams(params);
7126
- const { source, amount, token } = params;
7421
+ const { source, destination, amount, token } = params;
7127
7422
  // Extract operation context from source wallet context for balance validation
7128
- const operationContext = this.extractOperationContext(source);
7129
- // Validate balance for transaction
7423
+ const sourceOperationContext = this.extractOperationContext(source);
7424
+ // Validate USDC balance for transaction on source chain
7130
7425
  await validateBalanceForTransaction({
7131
7426
  adapter: source.adapter,
7132
7427
  amount,
7133
7428
  token,
7134
7429
  tokenAddress: source.chain.usdcAddress,
7135
- operationContext,
7430
+ operationContext: sourceOperationContext,
7431
+ });
7432
+ // Validate native balance > 0 for gas fees on source chain
7433
+ await validateNativeBalanceForTransaction({
7434
+ adapter: source.adapter,
7435
+ operationContext: sourceOperationContext,
7436
+ });
7437
+ // Extract operation context from destination wallet context
7438
+ const destinationOperationContext = this.extractOperationContext(destination);
7439
+ // Validate native balance > 0 for gas fees on destination chain
7440
+ await validateNativeBalanceForTransaction({
7441
+ adapter: destination.adapter,
7442
+ operationContext: destinationOperationContext,
7136
7443
  });
7137
7444
  return bridge(params, this);
7138
7445
  }
@@ -7792,5 +8099,8 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7792
8099
  }
7793
8100
 
7794
8101
  exports.CCTPV2BridgingProvider = CCTPV2BridgingProvider;
8102
+ exports.getBlocksUntilExpiry = getBlocksUntilExpiry;
7795
8103
  exports.getMintRecipientAccount = getMintRecipientAccount;
8104
+ exports.isAttestationExpired = isAttestationExpired;
8105
+ exports.isMintFailureRelatedToAttestation = isMintFailureRelatedToAttestation;
7796
8106
  //# sourceMappingURL=index.cjs.map
package/index.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { Abi } from 'abitype';
20
- import { TransactionInstruction, Signer } from '@solana/web3.js';
20
+ import { TransactionInstruction, Signer, AddressLookupTableAccount } from '@solana/web3.js';
21
21
 
22
22
  /**
23
23
  * @packageDocumentation
@@ -644,14 +644,26 @@ interface SolanaPreparedChainRequestParams {
644
644
  */
645
645
  instructions: TransactionInstruction[];
646
646
  /**
647
- * Additional signers besides the Adapters wallet (e.g. program-derived authorities).
647
+ * Additional signers besides the Adapter's wallet (e.g. program-derived authorities).
648
648
  */
649
649
  signers?: Signer[];
650
650
  /**
651
651
  * Optional override for how many compute units this transaction may consume.
652
- * If omitted, the networks default compute budget applies.
652
+ * If omitted, the network's default compute budget applies.
653
653
  */
654
654
  computeUnitLimit?: number;
655
+ /**
656
+ * Optional Address Lookup Table accounts for transaction compression.
657
+ * Used to reduce transaction size by compressing frequently-used addresses.
658
+ * This is used by @solana/web3.js adapters that have already fetched the ALT data.
659
+ */
660
+ addressLookupTableAccounts?: AddressLookupTableAccount[];
661
+ /**
662
+ * Optional Address Lookup Table addresses for transaction compression.
663
+ * Used by adapters that need to fetch ALT data themselves (e.g., @solana/kit adapters).
664
+ * These are base58-encoded addresses of ALT accounts to use for compression.
665
+ */
666
+ addressLookupTableAddresses?: string[];
655
667
  }
656
668
  /**
657
669
  * Solana-specific configuration for transaction estimation.
@@ -1333,6 +1345,34 @@ interface CCTPActionMap {
1333
1345
  readonly v2: CCTPv2ActionMap;
1334
1346
  }
1335
1347
 
1348
+ /**
1349
+ * Action map for native token operations (ETH, SOL, MATIC, etc.).
1350
+ *
1351
+ * Native tokens are the primary currency of each blockchain network,
1352
+ * used for paying transaction fees and as a store of value.
1353
+ * These actions operate on the native token without requiring
1354
+ * a separate token contract address.
1355
+ *
1356
+ * @remarks
1357
+ * Native token operations differ from ERC-20/SPL token operations
1358
+ * in that they don't require contract interactions for basic transfers
1359
+ * and balance checks.
1360
+ *
1361
+ * @see {@link ActionMap} for the complete action structure
1362
+ */
1363
+ interface NativeActionMap {
1364
+ /**
1365
+ * Get the native token balance (SOL, ETH, etc.) for a wallet address.
1366
+ */
1367
+ balanceOf: ActionParameters & {
1368
+ /**
1369
+ * The address to check the native balance for. If not provided, it will be
1370
+ * automatically derived from the adapter context.
1371
+ */
1372
+ walletAddress?: string | undefined;
1373
+ };
1374
+ }
1375
+
1336
1376
  interface TokenActionMap {
1337
1377
  /**
1338
1378
  * Set an allowance for a delegate to spend tokens on behalf of the wallet.
@@ -1564,6 +1604,8 @@ interface USDCActionMap {
1564
1604
  interface ActionMap {
1565
1605
  /** CCTP-specific operations with automatic address resolution. */
1566
1606
  readonly cctp: CCTPActionMap;
1607
+ /** Native token operations (ETH, SOL, MATIC, etc.). */
1608
+ readonly native: NativeActionMap;
1567
1609
  /** General token operations requiring explicit token addresses. */
1568
1610
  readonly token: TokenActionMap;
1569
1611
  /** USDC-specific operations with automatic address resolution. */
@@ -3917,5 +3959,102 @@ rawAddress: string,
3917
3959
  /** The USDC mint address (ignored for EVM chains, required base58 address for Solana) */
3918
3960
  mintAddress: string) => Promise<string>;
3919
3961
 
3920
- export { CCTPV2BridgingProvider, getMintRecipientAccount };
3962
+ /** A block number value that can be provided as bigint, number, or string. */
3963
+ type BlockNumberInput = bigint | number | string | undefined;
3964
+ /**
3965
+ * Determines whether an attestation has expired based on the current block number.
3966
+ *
3967
+ * An attestation expires when the destination chain's current block number is greater
3968
+ * than or equal to the expiration block specified in the attestation message.
3969
+ * Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
3970
+ *
3971
+ * @param attestation - The attestation message containing expiration block information
3972
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
3973
+ * @returns `true` if the attestation has expired, `false` if still valid or never expires
3974
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
3975
+ *
3976
+ * @example
3977
+ * ```typescript
3978
+ * import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
3979
+ *
3980
+ * // Check if attestation is expired on EVM chain
3981
+ * const publicClient = await adapter.getPublicClient(destinationChain)
3982
+ * const currentBlock = await publicClient.getBlockNumber()
3983
+ * const expired = isAttestationExpired(attestation, currentBlock)
3984
+ *
3985
+ * if (expired) {
3986
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
3987
+ * }
3988
+ * ```
3989
+ *
3990
+ * @example
3991
+ * ```typescript
3992
+ * // Check on Solana
3993
+ * const slot = await adapter.getConnection(destinationChain).getSlot()
3994
+ * const expired = isAttestationExpired(attestation, slot)
3995
+ * ```
3996
+ */
3997
+ declare const isAttestationExpired: (attestation: AttestationMessage, currentBlockNumber: BlockNumberInput) => boolean;
3998
+ /**
3999
+ * Calculates the number of blocks remaining until an attestation expires.
4000
+ *
4001
+ * Returns the difference between the expiration block and the current block number.
4002
+ * Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
4003
+ * Returns `0n` or a negative bigint if the attestation has already expired.
4004
+ *
4005
+ * @param attestation - The attestation message containing expiration block information
4006
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
4007
+ * @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
4008
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
4009
+ *
4010
+ * @example
4011
+ * ```typescript
4012
+ * import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
4013
+ *
4014
+ * const publicClient = await adapter.getPublicClient(destinationChain)
4015
+ * const currentBlock = await publicClient.getBlockNumber()
4016
+ * const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
4017
+ *
4018
+ * if (blocksRemaining === null) {
4019
+ * console.log('Attestation never expires')
4020
+ * } else if (blocksRemaining <= 0n) {
4021
+ * console.log('Attestation has expired')
4022
+ * } else {
4023
+ * console.log(`${blocksRemaining} blocks until expiry`)
4024
+ * }
4025
+ * ```
4026
+ */
4027
+ declare const getBlocksUntilExpiry: (attestation: AttestationMessage, currentBlockNumber: BlockNumberInput) => bigint | null;
4028
+ /**
4029
+ * Determines whether a mint failure was caused by an expired attestation.
4030
+ *
4031
+ * This function inspects the error thrown during a mint operation to detect
4032
+ * if the failure is due to the attestation's expiration block being exceeded.
4033
+ * When this returns `true`, the caller should use `reAttest()` to obtain a
4034
+ * fresh attestation before retrying the mint.
4035
+ *
4036
+ * @param error - The error thrown during the mint operation
4037
+ * @returns `true` if the error indicates the attestation has expired, `false` otherwise
4038
+ *
4039
+ * @example
4040
+ * ```typescript
4041
+ * import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
4042
+ *
4043
+ * try {
4044
+ * await mintRequest.execute()
4045
+ * } catch (error) {
4046
+ * if (isMintFailureRelatedToAttestation(error)) {
4047
+ * // Attestation expired - get a fresh one
4048
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
4049
+ * const newMintRequest = await provider.mint(source, destination, freshAttestation)
4050
+ * await newMintRequest.execute()
4051
+ * } else {
4052
+ * throw error
4053
+ * }
4054
+ * }
4055
+ * ```
4056
+ */
4057
+ declare const isMintFailureRelatedToAttestation: (error: unknown) => boolean;
4058
+
4059
+ export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation };
3921
4060
  export type { CCTPV2Config };
package/index.mjs CHANGED
@@ -379,8 +379,11 @@ const ArcTestnet = defineChain({
379
379
  name: 'Arc Testnet',
380
380
  title: 'ArcTestnet',
381
381
  nativeCurrency: {
382
- name: 'Arc',
383
- symbol: 'Arc',
382
+ name: 'USDC',
383
+ symbol: 'USDC',
384
+ // Arc uses native USDC with 18 decimals for gas payments (EVM standard).
385
+ // Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
386
+ // See: https://docs.arc.network/arc/references/contract-addresses
384
387
  decimals: 18,
385
388
  },
386
389
  chainId: 5042002,
@@ -3638,6 +3641,12 @@ const BalanceError = {
3638
3641
  code: 9001,
3639
3642
  name: 'BALANCE_INSUFFICIENT_TOKEN',
3640
3643
  type: 'BALANCE',
3644
+ },
3645
+ /** Insufficient native token (ETH/SOL/etc) for gas fees */
3646
+ INSUFFICIENT_GAS: {
3647
+ code: 9002,
3648
+ name: 'BALANCE_INSUFFICIENT_GAS',
3649
+ type: 'BALANCE',
3641
3650
  }};
3642
3651
 
3643
3652
  /**
@@ -3860,7 +3869,7 @@ function createValidationErrorFromZod(zodError, context) {
3860
3869
  *
3861
3870
  * @param chain - The blockchain network where the balance check failed
3862
3871
  * @param token - The token symbol (e.g., 'USDC', 'ETH')
3863
- * @param rawError - The original error from the underlying system (optional)
3872
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
3864
3873
  * @returns KitError with insufficient token balance details
3865
3874
  *
3866
3875
  * @example
@@ -3873,24 +3882,71 @@ function createValidationErrorFromZod(zodError, context) {
3873
3882
  *
3874
3883
  * @example
3875
3884
  * ```typescript
3876
- * // With raw error for debugging
3885
+ * // With trace context for debugging
3877
3886
  * try {
3878
3887
  * await transfer(...)
3879
3888
  * } catch (error) {
3880
- * throw createInsufficientTokenBalanceError('Base', 'USDC', error)
3889
+ * throw createInsufficientTokenBalanceError('Base', 'USDC', {
3890
+ * rawError: error,
3891
+ * balance: '1000000',
3892
+ * amount: '5000000',
3893
+ * })
3881
3894
  * }
3882
3895
  * ```
3883
3896
  */
3884
- function createInsufficientTokenBalanceError(chain, token, rawError) {
3897
+ function createInsufficientTokenBalanceError(chain, token, trace) {
3885
3898
  return new KitError({
3886
3899
  ...BalanceError.INSUFFICIENT_TOKEN,
3887
3900
  recoverability: 'FATAL',
3888
3901
  message: `Insufficient ${token} balance on ${chain}`,
3889
3902
  cause: {
3890
3903
  trace: {
3904
+ ...trace,
3891
3905
  chain,
3892
3906
  token,
3893
- rawError,
3907
+ },
3908
+ },
3909
+ });
3910
+ }
3911
+ /**
3912
+ * Creates error for insufficient gas funds.
3913
+ *
3914
+ * This error is thrown when a wallet does not have enough native tokens
3915
+ * (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
3916
+ * as it requires user intervention to add gas funds.
3917
+ *
3918
+ * @param chain - The blockchain network where the gas check failed
3919
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
3920
+ * @returns KitError with insufficient gas details
3921
+ *
3922
+ * @example
3923
+ * ```typescript
3924
+ * import { createInsufficientGasError } from '@core/errors'
3925
+ *
3926
+ * throw createInsufficientGasError('Ethereum')
3927
+ * // Message: "Insufficient gas funds on Ethereum"
3928
+ * ```
3929
+ *
3930
+ * @example
3931
+ * ```typescript
3932
+ * // With trace context for debugging
3933
+ * throw createInsufficientGasError('Ethereum', {
3934
+ * rawError: error,
3935
+ * gasRequired: '21000',
3936
+ * gasAvailable: '10000',
3937
+ * walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
3938
+ * })
3939
+ * ```
3940
+ */
3941
+ function createInsufficientGasError(chain, trace) {
3942
+ return new KitError({
3943
+ ...BalanceError.INSUFFICIENT_GAS,
3944
+ recoverability: 'FATAL',
3945
+ message: `Insufficient gas funds on ${chain}`,
3946
+ cause: {
3947
+ trace: {
3948
+ ...trace,
3949
+ chain,
3894
3950
  },
3895
3951
  },
3896
3952
  });
@@ -3952,6 +4008,38 @@ function isKitError(error) {
3952
4008
  function isFatalError(error) {
3953
4009
  return isKitError(error) && error.recoverability === 'FATAL';
3954
4010
  }
4011
+ /**
4012
+ * Safely extracts error message from any error type.
4013
+ *
4014
+ * This utility handles different error types gracefully, extracting
4015
+ * meaningful messages from Error instances, string errors, or providing
4016
+ * a fallback for unknown error types. Never throws.
4017
+ *
4018
+ * @param error - Unknown error to extract message from
4019
+ * @returns Error message string, or fallback message
4020
+ *
4021
+ * @example
4022
+ * ```typescript
4023
+ * import { getErrorMessage } from '@core/errors'
4024
+ *
4025
+ * try {
4026
+ * await riskyOperation()
4027
+ * } catch (error) {
4028
+ * const message = getErrorMessage(error)
4029
+ * console.log('Error occurred:', message)
4030
+ * // Works with Error, KitError, string, or any other type
4031
+ * }
4032
+ * ```
4033
+ */
4034
+ function getErrorMessage(error) {
4035
+ if (error instanceof Error) {
4036
+ return error.message;
4037
+ }
4038
+ if (typeof error === 'string') {
4039
+ return error;
4040
+ }
4041
+ return 'An unknown error occurred';
4042
+ }
3955
4043
 
3956
4044
  /**
3957
4045
  * Validates data against a Zod schema with enhanced error reporting.
@@ -5383,6 +5471,170 @@ mintAddress) => {
5383
5471
  }
5384
5472
  };
5385
5473
 
5474
+ /**
5475
+ * Converts a block number to bigint with validation.
5476
+ *
5477
+ * @param value - The block number value to convert (bigint, number, or string)
5478
+ * @returns The validated block number as a bigint
5479
+ * @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
5480
+ * @internal
5481
+ */
5482
+ const toBlockNumber = (value) => {
5483
+ if (value === null || value === undefined) {
5484
+ throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
5485
+ }
5486
+ // Empty string edge case - BigInt('') === 0n which is misleading
5487
+ if (value === '') {
5488
+ throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
5489
+ }
5490
+ // For numbers, validate before BigInt conversion
5491
+ if (typeof value === 'number') {
5492
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
5493
+ throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
5494
+ }
5495
+ }
5496
+ let result;
5497
+ try {
5498
+ result = BigInt(value);
5499
+ }
5500
+ catch {
5501
+ throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
5502
+ }
5503
+ if (result < 0n) {
5504
+ throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
5505
+ }
5506
+ return result;
5507
+ };
5508
+ /**
5509
+ * Determines whether an attestation has expired based on the current block number.
5510
+ *
5511
+ * An attestation expires when the destination chain's current block number is greater
5512
+ * than or equal to the expiration block specified in the attestation message.
5513
+ * Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
5514
+ *
5515
+ * @param attestation - The attestation message containing expiration block information
5516
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5517
+ * @returns `true` if the attestation has expired, `false` if still valid or never expires
5518
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5519
+ *
5520
+ * @example
5521
+ * ```typescript
5522
+ * import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
5523
+ *
5524
+ * // Check if attestation is expired on EVM chain
5525
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5526
+ * const currentBlock = await publicClient.getBlockNumber()
5527
+ * const expired = isAttestationExpired(attestation, currentBlock)
5528
+ *
5529
+ * if (expired) {
5530
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5531
+ * }
5532
+ * ```
5533
+ *
5534
+ * @example
5535
+ * ```typescript
5536
+ * // Check on Solana
5537
+ * const slot = await adapter.getConnection(destinationChain).getSlot()
5538
+ * const expired = isAttestationExpired(attestation, slot)
5539
+ * ```
5540
+ */
5541
+ const isAttestationExpired = (attestation, currentBlockNumber) => {
5542
+ const currentBlock = toBlockNumber(currentBlockNumber);
5543
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5544
+ // 0n means it never expires (re-attested messages and slow transfers)
5545
+ if (expiration === 0n) {
5546
+ return false;
5547
+ }
5548
+ // Attestation is expired if current block >= expiration block
5549
+ return currentBlock >= expiration;
5550
+ };
5551
+ /**
5552
+ * Calculates the number of blocks remaining until an attestation expires.
5553
+ *
5554
+ * Returns the difference between the expiration block and the current block number.
5555
+ * Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
5556
+ * Returns `0n` or a negative bigint if the attestation has already expired.
5557
+ *
5558
+ * @param attestation - The attestation message containing expiration block information
5559
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5560
+ * @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
5561
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5562
+ *
5563
+ * @example
5564
+ * ```typescript
5565
+ * import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
5566
+ *
5567
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5568
+ * const currentBlock = await publicClient.getBlockNumber()
5569
+ * const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
5570
+ *
5571
+ * if (blocksRemaining === null) {
5572
+ * console.log('Attestation never expires')
5573
+ * } else if (blocksRemaining <= 0n) {
5574
+ * console.log('Attestation has expired')
5575
+ * } else {
5576
+ * console.log(`${blocksRemaining} blocks until expiry`)
5577
+ * }
5578
+ * ```
5579
+ */
5580
+ const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
5581
+ const currentBlock = toBlockNumber(currentBlockNumber);
5582
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5583
+ // 0n means it never expires (re-attested messages and slow transfers)
5584
+ if (expiration === 0n) {
5585
+ return null;
5586
+ }
5587
+ // Return the difference (can be negative if expired)
5588
+ return expiration - currentBlock;
5589
+ };
5590
+ /**
5591
+ * Determines whether a mint failure was caused by an expired attestation.
5592
+ *
5593
+ * This function inspects the error thrown during a mint operation to detect
5594
+ * if the failure is due to the attestation's expiration block being exceeded.
5595
+ * When this returns `true`, the caller should use `reAttest()` to obtain a
5596
+ * fresh attestation before retrying the mint.
5597
+ *
5598
+ * @param error - The error thrown during the mint operation
5599
+ * @returns `true` if the error indicates the attestation has expired, `false` otherwise
5600
+ *
5601
+ * @example
5602
+ * ```typescript
5603
+ * import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
5604
+ *
5605
+ * try {
5606
+ * await mintRequest.execute()
5607
+ * } catch (error) {
5608
+ * if (isMintFailureRelatedToAttestation(error)) {
5609
+ * // Attestation expired - get a fresh one
5610
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5611
+ * const newMintRequest = await provider.mint(source, destination, freshAttestation)
5612
+ * await newMintRequest.execute()
5613
+ * } else {
5614
+ * throw error
5615
+ * }
5616
+ * }
5617
+ * ```
5618
+ */
5619
+ const isMintFailureRelatedToAttestation = (error) => {
5620
+ if (error === null || error === undefined) {
5621
+ return false;
5622
+ }
5623
+ const errorString = getErrorMessage(error).toLowerCase();
5624
+ // Check for Solana "MessageExpired" error pattern
5625
+ // Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
5626
+ // Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
5627
+ if (errorString.includes('messageexpired')) {
5628
+ return true;
5629
+ }
5630
+ // Check for EVM attestation expiry errors
5631
+ // Contract reverts with: "Message expired and must be re-signed"
5632
+ if (errorString.includes('message') && errorString.includes('expired')) {
5633
+ return true;
5634
+ }
5635
+ return false;
5636
+ };
5637
+
5386
5638
  /**
5387
5639
  * Checks if a decoded attestation field matches the corresponding transfer parameter.
5388
5640
  * If the values do not match, appends a descriptive error message to the errors array.
@@ -6226,21 +6478,64 @@ const validateBalanceForTransaction = async (params) => {
6226
6478
  // Extract chain name from operationContext
6227
6479
  const chainName = extractChainInfo(operationContext.chain).name;
6228
6480
  // Create KitError with rich context in trace
6229
- const error = createInsufficientTokenBalanceError(chainName, token);
6230
- // Enhance error with additional context for debugging
6231
- if (error.cause) {
6232
- const existingTrace = typeof error.cause.trace === 'object' && error.cause.trace
6233
- ? error.cause.trace
6234
- : {};
6235
- error.cause.trace = {
6236
- ...existingTrace,
6237
- balance: balance.toString(),
6238
- amount,
6239
- tokenAddress,
6240
- walletAddress: operationContext.address,
6241
- };
6242
- }
6243
- throw error;
6481
+ throw createInsufficientTokenBalanceError(chainName, token, {
6482
+ balance: balance.toString(),
6483
+ amount,
6484
+ tokenAddress,
6485
+ walletAddress: operationContext.address,
6486
+ });
6487
+ }
6488
+ };
6489
+
6490
+ /**
6491
+ * Validate that the adapter has sufficient native token balance for transaction fees.
6492
+ *
6493
+ * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
6494
+ * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
6495
+ * if the balance is zero, indicating the wallet cannot pay for transaction fees.
6496
+ *
6497
+ * @param params - The validation parameters containing adapter and operation context.
6498
+ * @returns A promise that resolves to void if validation passes.
6499
+ * @throws {KitError} When the adapter's native balance is zero (code: 9002).
6500
+ *
6501
+ * @example
6502
+ * ```typescript
6503
+ * import { validateNativeBalanceForTransaction } from '@core/adapter'
6504
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
6505
+ * import { isKitError, ERROR_TYPES } from '@core/errors'
6506
+ *
6507
+ * const adapter = createViemAdapterFromPrivateKey({
6508
+ * privateKey: '0x...',
6509
+ * chain: 'Ethereum',
6510
+ * })
6511
+ *
6512
+ * try {
6513
+ * await validateNativeBalanceForTransaction({
6514
+ * adapter,
6515
+ * operationContext: { chain: 'Ethereum' },
6516
+ * })
6517
+ * console.log('Native balance validation passed')
6518
+ * } catch (error) {
6519
+ * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
6520
+ * console.error('Insufficient gas funds:', error.message)
6521
+ * }
6522
+ * }
6523
+ * ```
6524
+ */
6525
+ const validateNativeBalanceForTransaction = async (params) => {
6526
+ const { adapter, operationContext } = params;
6527
+ const balancePrepared = await adapter.prepareAction('native.balanceOf', {
6528
+ walletAddress: operationContext.address,
6529
+ }, operationContext);
6530
+ const balance = await balancePrepared.execute();
6531
+ if (BigInt(balance) === 0n) {
6532
+ // Extract chain name from operationContext
6533
+ const chainName = extractChainInfo(operationContext.chain).name;
6534
+ // Create KitError with rich context in trace
6535
+ throw createInsufficientGasError(chainName, {
6536
+ balance: '0',
6537
+ walletAddress: operationContext.address,
6538
+ });
6244
6539
  }
6245
6540
  };
6246
6541
 
@@ -7117,16 +7412,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7117
7412
  async bridge(params) {
7118
7413
  // CCTP-specific bridge params validation (includes base validation)
7119
7414
  assertCCTPv2BridgeParams(params);
7120
- const { source, amount, token } = params;
7415
+ const { source, destination, amount, token } = params;
7121
7416
  // Extract operation context from source wallet context for balance validation
7122
- const operationContext = this.extractOperationContext(source);
7123
- // Validate balance for transaction
7417
+ const sourceOperationContext = this.extractOperationContext(source);
7418
+ // Validate USDC balance for transaction on source chain
7124
7419
  await validateBalanceForTransaction({
7125
7420
  adapter: source.adapter,
7126
7421
  amount,
7127
7422
  token,
7128
7423
  tokenAddress: source.chain.usdcAddress,
7129
- operationContext,
7424
+ operationContext: sourceOperationContext,
7425
+ });
7426
+ // Validate native balance > 0 for gas fees on source chain
7427
+ await validateNativeBalanceForTransaction({
7428
+ adapter: source.adapter,
7429
+ operationContext: sourceOperationContext,
7430
+ });
7431
+ // Extract operation context from destination wallet context
7432
+ const destinationOperationContext = this.extractOperationContext(destination);
7433
+ // Validate native balance > 0 for gas fees on destination chain
7434
+ await validateNativeBalanceForTransaction({
7435
+ adapter: destination.adapter,
7436
+ operationContext: destinationOperationContext,
7130
7437
  });
7131
7438
  return bridge(params, this);
7132
7439
  }
@@ -7785,5 +8092,5 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7785
8092
  }
7786
8093
  }
7787
8094
 
7788
- export { CCTPV2BridgingProvider, getMintRecipientAccount };
8095
+ export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation };
7789
8096
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circle-fin/provider-cctp-v2",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
5
5
  "keywords": [
6
6
  "circle",