@circle-fin/provider-cctp-v2 1.1.0 → 1.3.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
@@ -75,6 +75,8 @@ var Blockchain;
75
75
  Blockchain["Ink_Testnet"] = "Ink_Testnet";
76
76
  Blockchain["Linea"] = "Linea";
77
77
  Blockchain["Linea_Sepolia"] = "Linea_Sepolia";
78
+ Blockchain["Monad"] = "Monad";
79
+ Blockchain["Monad_Testnet"] = "Monad_Testnet";
78
80
  Blockchain["NEAR"] = "NEAR";
79
81
  Blockchain["NEAR_Testnet"] = "NEAR_Testnet";
80
82
  Blockchain["Noble"] = "Noble";
@@ -166,6 +168,7 @@ var BridgeChain;
166
168
  BridgeChain["HyperEVM"] = "HyperEVM";
167
169
  BridgeChain["Ink"] = "Ink";
168
170
  BridgeChain["Linea"] = "Linea";
171
+ BridgeChain["Monad"] = "Monad";
169
172
  BridgeChain["Optimism"] = "Optimism";
170
173
  BridgeChain["Plume"] = "Plume";
171
174
  BridgeChain["Polygon"] = "Polygon";
@@ -185,6 +188,7 @@ var BridgeChain;
185
188
  BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
186
189
  BridgeChain["Ink_Testnet"] = "Ink_Testnet";
187
190
  BridgeChain["Linea_Sepolia"] = "Linea_Sepolia";
191
+ BridgeChain["Monad_Testnet"] = "Monad_Testnet";
188
192
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
189
193
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
190
194
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
@@ -379,8 +383,11 @@ const ArcTestnet = defineChain({
379
383
  name: 'Arc Testnet',
380
384
  title: 'ArcTestnet',
381
385
  nativeCurrency: {
382
- name: 'Arc',
383
- symbol: 'Arc',
386
+ name: 'USDC',
387
+ symbol: 'USDC',
388
+ // Arc uses native USDC with 18 decimals for gas payments (EVM standard).
389
+ // Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
390
+ // See: https://docs.arc.network/arc/references/contract-addresses
384
391
  decimals: 18,
385
392
  },
386
393
  chainId: 5042002,
@@ -1168,6 +1175,86 @@ const LineaSepolia = defineChain({
1168
1175
  },
1169
1176
  });
1170
1177
 
1178
+ /**
1179
+ * Monad Mainnet chain definition
1180
+ * @remarks
1181
+ * This represents the official production network for the Monad blockchain.
1182
+ * Monad is a high-performance EVM-compatible Layer-1 blockchain featuring
1183
+ * over 10,000 TPS, sub-second finality, and near-zero gas fees.
1184
+ */
1185
+ const Monad = defineChain({
1186
+ type: 'evm',
1187
+ chain: Blockchain.Monad,
1188
+ name: 'Monad',
1189
+ title: 'Monad Mainnet',
1190
+ nativeCurrency: {
1191
+ name: 'Monad',
1192
+ symbol: 'MON',
1193
+ decimals: 18,
1194
+ },
1195
+ chainId: 143,
1196
+ isTestnet: false,
1197
+ explorerUrl: 'https://monadscan.com/tx/{hash}',
1198
+ rpcEndpoints: ['https://rpc.monad.xyz'],
1199
+ eurcAddress: null,
1200
+ usdcAddress: '0x754704Bc059F8C67012fEd69BC8A327a5aafb603',
1201
+ cctp: {
1202
+ domain: 15,
1203
+ contracts: {
1204
+ v2: {
1205
+ type: 'split',
1206
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
1207
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
1208
+ confirmations: 1,
1209
+ fastConfirmations: 1,
1210
+ },
1211
+ },
1212
+ },
1213
+ kitContracts: {
1214
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET,
1215
+ },
1216
+ });
1217
+
1218
+ /**
1219
+ * Monad Testnet chain definition
1220
+ * @remarks
1221
+ * This represents the official test network for the Monad blockchain.
1222
+ * Monad is a high-performance EVM-compatible Layer-1 blockchain featuring
1223
+ * over 10,000 TPS, sub-second finality, and near-zero gas fees.
1224
+ */
1225
+ const MonadTestnet = defineChain({
1226
+ type: 'evm',
1227
+ chain: Blockchain.Monad_Testnet,
1228
+ name: 'Monad Testnet',
1229
+ title: 'Monad Testnet',
1230
+ nativeCurrency: {
1231
+ name: 'Monad',
1232
+ symbol: 'MON',
1233
+ decimals: 18,
1234
+ },
1235
+ chainId: 10143,
1236
+ isTestnet: true,
1237
+ explorerUrl: 'https://testnet.monadscan.com/tx/{hash}',
1238
+ rpcEndpoints: ['https://testnet-rpc.monad.xyz'],
1239
+ eurcAddress: null,
1240
+ usdcAddress: '0x534b2f3A21130d7a60830c2Df862319e593943A3',
1241
+ cctp: {
1242
+ domain: 15,
1243
+ contracts: {
1244
+ v2: {
1245
+ type: 'split',
1246
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
1247
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
1248
+ confirmations: 1,
1249
+ fastConfirmations: 1,
1250
+ },
1251
+ },
1252
+ },
1253
+ kitContracts: {
1254
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1255
+ },
1256
+ });
1257
+
1171
1258
  /**
1172
1259
  * NEAR Protocol Mainnet chain definition
1173
1260
  * @remarks
@@ -2252,6 +2339,8 @@ var Chains = /*#__PURE__*/Object.freeze({
2252
2339
  InkTestnet: InkTestnet,
2253
2340
  Linea: Linea,
2254
2341
  LineaSepolia: LineaSepolia,
2342
+ Monad: Monad,
2343
+ MonadTestnet: MonadTestnet,
2255
2344
  NEAR: NEAR,
2256
2345
  NEARTestnet: NEARTestnet,
2257
2346
  Noble: Noble,
@@ -3275,6 +3364,8 @@ const ERROR_TYPES = {
3275
3364
  RPC: 'RPC',
3276
3365
  /** Internet connectivity, DNS resolution, connection issues */
3277
3366
  NETWORK: 'NETWORK',
3367
+ /** Catch-all for unrecognized errors (code 0) */
3368
+ UNKNOWN: 'UNKNOWN',
3278
3369
  };
3279
3370
  /**
3280
3371
  * Array of valid error type values for validation.
@@ -3288,6 +3379,8 @@ const ERROR_TYPE_ARRAY = [...ERROR_TYPE_VALUES];
3288
3379
  /**
3289
3380
  * Error code ranges for validation.
3290
3381
  * Single source of truth for valid error code ranges.
3382
+ *
3383
+ * Note: Code 0 is special - it's the UNKNOWN catch-all error.
3291
3384
  */
3292
3385
  const ERROR_CODE_RANGES = [
3293
3386
  { min: 1000, max: 1999, type: 'INPUT' },
@@ -3296,6 +3389,8 @@ const ERROR_CODE_RANGES = [
3296
3389
  { min: 5000, max: 5999, type: 'ONCHAIN' },
3297
3390
  { min: 9000, max: 9999, type: 'BALANCE' },
3298
3391
  ];
3392
+ /** Special code for UNKNOWN errors */
3393
+ const UNKNOWN_ERROR_CODE = 0;
3299
3394
  /**
3300
3395
  * Zod schema for validating ErrorDetails objects.
3301
3396
  *
@@ -3334,6 +3429,7 @@ const ERROR_CODE_RANGES = [
3334
3429
  const errorDetailsSchema = z.object({
3335
3430
  /**
3336
3431
  * Numeric identifier following standardized ranges:
3432
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3337
3433
  * - 1000-1999: INPUT errors - Parameter validation
3338
3434
  * - 3000-3999: NETWORK errors - Connectivity issues
3339
3435
  * - 4000-4999: RPC errors - Provider issues, gas estimation
@@ -3343,8 +3439,9 @@ const errorDetailsSchema = z.object({
3343
3439
  code: z
3344
3440
  .number()
3345
3441
  .int('Error code must be an integer')
3346
- .refine((code) => ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
3347
- message: 'Error code must be in valid ranges: 1000-1999 (INPUT), 3000-3999 (NETWORK), 4000-4999 (RPC), 5000-5999 (ONCHAIN), 9000-9999 (BALANCE)',
3442
+ .refine((code) => code === UNKNOWN_ERROR_CODE ||
3443
+ ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
3444
+ message: 'Error code must be 0 (UNKNOWN) or in valid ranges: 1000-1999 (INPUT), 3000-3999 (NETWORK), 4000-4999 (RPC), 5000-5999 (ONCHAIN), 9000-9999 (BALANCE)',
3348
3445
  }),
3349
3446
  /** Human-readable ID (e.g., "INPUT_NETWORK_MISMATCH", "BALANCE_INSUFFICIENT_TOKEN") */
3350
3447
  name: z
@@ -3354,7 +3451,7 @@ const errorDetailsSchema = z.object({
3354
3451
  /** Error category indicating where the error originated */
3355
3452
  type: z.enum(ERROR_TYPE_ARRAY, {
3356
3453
  errorMap: () => ({
3357
- message: 'Error type must be one of: INPUT, BALANCE, ONCHAIN, RPC, NETWORK',
3454
+ message: 'Error type must be one of: INPUT, BALANCE, ONCHAIN, RPC, NETWORK, UNKNOWN',
3358
3455
  }),
3359
3456
  }),
3360
3457
  /** Error handling strategy */
@@ -3555,6 +3652,7 @@ class KitError extends Error {
3555
3652
  /**
3556
3653
  * Standardized error code ranges for consistent categorization:
3557
3654
  *
3655
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3558
3656
  * - 1000-1999: INPUT errors - Parameter validation, input format errors
3559
3657
  * - 3000-3999: NETWORK errors - Internet connectivity, DNS, connection issues
3560
3658
  * - 4000-4999: RPC errors - Blockchain provider issues, gas estimation, nonce errors
@@ -3638,6 +3736,37 @@ const BalanceError = {
3638
3736
  code: 9001,
3639
3737
  name: 'BALANCE_INSUFFICIENT_TOKEN',
3640
3738
  type: 'BALANCE',
3739
+ },
3740
+ /** Insufficient native token (ETH/SOL/etc) for gas fees */
3741
+ INSUFFICIENT_GAS: {
3742
+ code: 9002,
3743
+ name: 'BALANCE_INSUFFICIENT_GAS',
3744
+ type: 'BALANCE',
3745
+ }};
3746
+ /**
3747
+ * Standardized error definitions for ONCHAIN type errors.
3748
+ *
3749
+ * ONCHAIN errors occur during transaction execution, simulation,
3750
+ * or interaction with smart contracts on the blockchain.
3751
+ *
3752
+ * @example
3753
+ * ```typescript
3754
+ * import { OnchainError } from '@core/errors'
3755
+ *
3756
+ * const error = new KitError({
3757
+ * ...OnchainError.SIMULATION_FAILED,
3758
+ * recoverability: 'FATAL',
3759
+ * message: 'Simulation failed: ERC20 transfer amount exceeds balance',
3760
+ * cause: { trace: { reason: 'ERC20: transfer amount exceeds balance' } }
3761
+ * })
3762
+ * ```
3763
+ */
3764
+ const OnchainError = {
3765
+ /** Pre-flight transaction simulation failed */
3766
+ SIMULATION_FAILED: {
3767
+ code: 5002,
3768
+ name: 'ONCHAIN_SIMULATION_FAILED',
3769
+ type: 'ONCHAIN',
3641
3770
  }};
3642
3771
 
3643
3772
  /**
@@ -3860,7 +3989,7 @@ function createValidationErrorFromZod(zodError, context) {
3860
3989
  *
3861
3990
  * @param chain - The blockchain network where the balance check failed
3862
3991
  * @param token - The token symbol (e.g., 'USDC', 'ETH')
3863
- * @param rawError - The original error from the underlying system (optional)
3992
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
3864
3993
  * @returns KitError with insufficient token balance details
3865
3994
  *
3866
3995
  * @example
@@ -3873,24 +4002,116 @@ function createValidationErrorFromZod(zodError, context) {
3873
4002
  *
3874
4003
  * @example
3875
4004
  * ```typescript
3876
- * // With raw error for debugging
4005
+ * // With trace context for debugging
3877
4006
  * try {
3878
4007
  * await transfer(...)
3879
4008
  * } catch (error) {
3880
- * throw createInsufficientTokenBalanceError('Base', 'USDC', error)
4009
+ * throw createInsufficientTokenBalanceError('Base', 'USDC', {
4010
+ * rawError: error,
4011
+ * balance: '1000000',
4012
+ * amount: '5000000',
4013
+ * })
3881
4014
  * }
3882
4015
  * ```
3883
4016
  */
3884
- function createInsufficientTokenBalanceError(chain, token, rawError) {
4017
+ function createInsufficientTokenBalanceError(chain, token, trace) {
3885
4018
  return new KitError({
3886
4019
  ...BalanceError.INSUFFICIENT_TOKEN,
3887
4020
  recoverability: 'FATAL',
3888
4021
  message: `Insufficient ${token} balance on ${chain}`,
3889
4022
  cause: {
3890
4023
  trace: {
4024
+ ...trace,
3891
4025
  chain,
3892
4026
  token,
3893
- rawError,
4027
+ },
4028
+ },
4029
+ });
4030
+ }
4031
+ /**
4032
+ * Creates error for insufficient gas funds.
4033
+ *
4034
+ * This error is thrown when a wallet does not have enough native tokens
4035
+ * (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
4036
+ * as it requires user intervention to add gas funds.
4037
+ *
4038
+ * @param chain - The blockchain network where the gas check failed
4039
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
4040
+ * @returns KitError with insufficient gas details
4041
+ *
4042
+ * @example
4043
+ * ```typescript
4044
+ * import { createInsufficientGasError } from '@core/errors'
4045
+ *
4046
+ * throw createInsufficientGasError('Ethereum')
4047
+ * // Message: "Insufficient gas funds on Ethereum"
4048
+ * ```
4049
+ *
4050
+ * @example
4051
+ * ```typescript
4052
+ * // With trace context for debugging
4053
+ * throw createInsufficientGasError('Ethereum', {
4054
+ * rawError: error,
4055
+ * gasRequired: '21000',
4056
+ * gasAvailable: '10000',
4057
+ * walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
4058
+ * })
4059
+ * ```
4060
+ */
4061
+ function createInsufficientGasError(chain, trace) {
4062
+ return new KitError({
4063
+ ...BalanceError.INSUFFICIENT_GAS,
4064
+ recoverability: 'FATAL',
4065
+ message: `Insufficient gas funds on ${chain}`,
4066
+ cause: {
4067
+ trace: {
4068
+ ...trace,
4069
+ chain,
4070
+ },
4071
+ },
4072
+ });
4073
+ }
4074
+
4075
+ /**
4076
+ * Creates error for transaction simulation failures.
4077
+ *
4078
+ * This error is thrown when a pre-flight transaction simulation fails,
4079
+ * typically due to contract logic that would revert. The error is FATAL
4080
+ * as it indicates the transaction would fail if submitted.
4081
+ *
4082
+ * @param chain - The blockchain network where the simulation failed
4083
+ * @param reason - The reason for simulation failure (e.g., revert message)
4084
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
4085
+ * @returns KitError with simulation failure details
4086
+ *
4087
+ * @example
4088
+ * ```typescript
4089
+ * import { createSimulationFailedError } from '@core/errors'
4090
+ *
4091
+ * throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance')
4092
+ * // Message: "Simulation failed on Ethereum: ERC20: insufficient allowance"
4093
+ * ```
4094
+ *
4095
+ * @example
4096
+ * ```typescript
4097
+ * // With trace context for debugging
4098
+ * throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance', {
4099
+ * rawError: error,
4100
+ * txHash: '0x1234...',
4101
+ * gasLimit: '21000',
4102
+ * })
4103
+ * ```
4104
+ */
4105
+ function createSimulationFailedError(chain, reason, trace) {
4106
+ return new KitError({
4107
+ ...OnchainError.SIMULATION_FAILED,
4108
+ recoverability: 'FATAL',
4109
+ message: `Simulation failed on ${chain}: ${reason}`,
4110
+ cause: {
4111
+ trace: {
4112
+ ...trace,
4113
+ chain,
4114
+ reason,
3894
4115
  },
3895
4116
  },
3896
4117
  });
@@ -3952,6 +4173,38 @@ function isKitError(error) {
3952
4173
  function isFatalError(error) {
3953
4174
  return isKitError(error) && error.recoverability === 'FATAL';
3954
4175
  }
4176
+ /**
4177
+ * Safely extracts error message from any error type.
4178
+ *
4179
+ * This utility handles different error types gracefully, extracting
4180
+ * meaningful messages from Error instances, string errors, or providing
4181
+ * a fallback for unknown error types. Never throws.
4182
+ *
4183
+ * @param error - Unknown error to extract message from
4184
+ * @returns Error message string, or fallback message
4185
+ *
4186
+ * @example
4187
+ * ```typescript
4188
+ * import { getErrorMessage } from '@core/errors'
4189
+ *
4190
+ * try {
4191
+ * await riskyOperation()
4192
+ * } catch (error) {
4193
+ * const message = getErrorMessage(error)
4194
+ * console.log('Error occurred:', message)
4195
+ * // Works with Error, KitError, string, or any other type
4196
+ * }
4197
+ * ```
4198
+ */
4199
+ function getErrorMessage(error) {
4200
+ if (error instanceof Error) {
4201
+ return error.message;
4202
+ }
4203
+ if (typeof error === 'string') {
4204
+ return error;
4205
+ }
4206
+ return 'An unknown error occurred';
4207
+ }
3955
4208
 
3956
4209
  /**
3957
4210
  * Validates data against a Zod schema with enhanced error reporting.
@@ -5068,6 +5321,68 @@ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHas
5068
5321
  };
5069
5322
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
5070
5323
  };
5324
+ /**
5325
+ * Type guard that validates attestation response has expirationBlock === '0'.
5326
+ *
5327
+ * This is used after requestReAttestation() to poll until the attestation
5328
+ * is fully re-processed and has a zero expiration block (never expires).
5329
+ * The expiration block transitions from non-zero to zero when Circle
5330
+ * completes processing the re-attestation request.
5331
+ *
5332
+ * @param obj - The value to check, typically a parsed JSON response
5333
+ * @returns True if the attestation has expirationBlock === '0'
5334
+ * @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
5335
+ *
5336
+ * @example
5337
+ * ```typescript
5338
+ * // After requesting re-attestation, use this to validate the response
5339
+ * const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
5340
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
5341
+ * ```
5342
+ *
5343
+ * @internal
5344
+ */
5345
+ const isReAttestedAttestationResponse = (obj) => {
5346
+ // First validate the basic structure and completion status
5347
+ // This will throw appropriate errors for invalid structure or incomplete attestation
5348
+ if (!isAttestationResponse(obj)) ;
5349
+ // Check if the first message has expirationBlock === '0'
5350
+ const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
5351
+ if (expirationBlock !== '0') {
5352
+ // Re-attestation not yet complete - allow retry via polling
5353
+ throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
5354
+ }
5355
+ return true;
5356
+ };
5357
+ /**
5358
+ * Fetches attestation data and polls until expirationBlock === '0'.
5359
+ *
5360
+ * This function is used after calling requestReAttestation() to wait until
5361
+ * the attestation is fully re-processed. The expirationBlock transitions
5362
+ * from non-zero to zero when Circle completes the re-attestation.
5363
+ *
5364
+ * @param sourceDomainId - The CCTP domain ID of the source chain
5365
+ * @param transactionHash - The transaction hash to fetch attestation for
5366
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
5367
+ * @param config - Optional configuration overrides
5368
+ * @returns The re-attested attestation response with expirationBlock === '0'
5369
+ * @throws If the request fails, times out, or expirationBlock never becomes 0
5370
+ *
5371
+ * @example
5372
+ * ```typescript
5373
+ * // After requesting re-attestation
5374
+ * await requestReAttestation(nonce, isTestnet)
5375
+ *
5376
+ * // Poll until expirationBlock becomes 0
5377
+ * const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
5378
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
5379
+ * ```
5380
+ */
5381
+ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
5382
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
5383
+ const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
5384
+ return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
5385
+ };
5071
5386
  /**
5072
5387
  * Builds the IRIS API URL for re-attestation requests.
5073
5388
  *
@@ -5383,6 +5698,170 @@ mintAddress) => {
5383
5698
  }
5384
5699
  };
5385
5700
 
5701
+ /**
5702
+ * Converts a block number to bigint with validation.
5703
+ *
5704
+ * @param value - The block number value to convert (bigint, number, or string)
5705
+ * @returns The validated block number as a bigint
5706
+ * @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
5707
+ * @internal
5708
+ */
5709
+ const toBlockNumber = (value) => {
5710
+ if (value === null || value === undefined) {
5711
+ throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
5712
+ }
5713
+ // Empty string edge case - BigInt('') === 0n which is misleading
5714
+ if (value === '') {
5715
+ throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
5716
+ }
5717
+ // For numbers, validate before BigInt conversion
5718
+ if (typeof value === 'number') {
5719
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
5720
+ throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
5721
+ }
5722
+ }
5723
+ let result;
5724
+ try {
5725
+ result = BigInt(value);
5726
+ }
5727
+ catch {
5728
+ throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
5729
+ }
5730
+ if (result < 0n) {
5731
+ throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
5732
+ }
5733
+ return result;
5734
+ };
5735
+ /**
5736
+ * Determines whether an attestation has expired based on the current block number.
5737
+ *
5738
+ * An attestation expires when the destination chain's current block number is greater
5739
+ * than or equal to the expiration block specified in the attestation message.
5740
+ * Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
5741
+ *
5742
+ * @param attestation - The attestation message containing expiration block information
5743
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5744
+ * @returns `true` if the attestation has expired, `false` if still valid or never expires
5745
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5746
+ *
5747
+ * @example
5748
+ * ```typescript
5749
+ * import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
5750
+ *
5751
+ * // Check if attestation is expired on EVM chain
5752
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5753
+ * const currentBlock = await publicClient.getBlockNumber()
5754
+ * const expired = isAttestationExpired(attestation, currentBlock)
5755
+ *
5756
+ * if (expired) {
5757
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5758
+ * }
5759
+ * ```
5760
+ *
5761
+ * @example
5762
+ * ```typescript
5763
+ * // Check on Solana
5764
+ * const slot = await adapter.getConnection(destinationChain).getSlot()
5765
+ * const expired = isAttestationExpired(attestation, slot)
5766
+ * ```
5767
+ */
5768
+ const isAttestationExpired = (attestation, currentBlockNumber) => {
5769
+ const currentBlock = toBlockNumber(currentBlockNumber);
5770
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5771
+ // 0n means it never expires (re-attested messages and slow transfers)
5772
+ if (expiration === 0n) {
5773
+ return false;
5774
+ }
5775
+ // Attestation is expired if current block >= expiration block
5776
+ return currentBlock >= expiration;
5777
+ };
5778
+ /**
5779
+ * Calculates the number of blocks remaining until an attestation expires.
5780
+ *
5781
+ * Returns the difference between the expiration block and the current block number.
5782
+ * Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
5783
+ * Returns `0n` or a negative bigint if the attestation has already expired.
5784
+ *
5785
+ * @param attestation - The attestation message containing expiration block information
5786
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5787
+ * @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
5788
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5789
+ *
5790
+ * @example
5791
+ * ```typescript
5792
+ * import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
5793
+ *
5794
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5795
+ * const currentBlock = await publicClient.getBlockNumber()
5796
+ * const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
5797
+ *
5798
+ * if (blocksRemaining === null) {
5799
+ * console.log('Attestation never expires')
5800
+ * } else if (blocksRemaining <= 0n) {
5801
+ * console.log('Attestation has expired')
5802
+ * } else {
5803
+ * console.log(`${blocksRemaining} blocks until expiry`)
5804
+ * }
5805
+ * ```
5806
+ */
5807
+ const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
5808
+ const currentBlock = toBlockNumber(currentBlockNumber);
5809
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5810
+ // 0n means it never expires (re-attested messages and slow transfers)
5811
+ if (expiration === 0n) {
5812
+ return null;
5813
+ }
5814
+ // Return the difference (can be negative if expired)
5815
+ return expiration - currentBlock;
5816
+ };
5817
+ /**
5818
+ * Determines whether a mint failure was caused by an expired attestation.
5819
+ *
5820
+ * This function inspects the error thrown during a mint operation to detect
5821
+ * if the failure is due to the attestation's expiration block being exceeded.
5822
+ * When this returns `true`, the caller should use `reAttest()` to obtain a
5823
+ * fresh attestation before retrying the mint.
5824
+ *
5825
+ * @param error - The error thrown during the mint operation
5826
+ * @returns `true` if the error indicates the attestation has expired, `false` otherwise
5827
+ *
5828
+ * @example
5829
+ * ```typescript
5830
+ * import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
5831
+ *
5832
+ * try {
5833
+ * await mintRequest.execute()
5834
+ * } catch (error) {
5835
+ * if (isMintFailureRelatedToAttestation(error)) {
5836
+ * // Attestation expired - get a fresh one
5837
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5838
+ * const newMintRequest = await provider.mint(source, destination, freshAttestation)
5839
+ * await newMintRequest.execute()
5840
+ * } else {
5841
+ * throw error
5842
+ * }
5843
+ * }
5844
+ * ```
5845
+ */
5846
+ const isMintFailureRelatedToAttestation = (error) => {
5847
+ if (error === null || error === undefined) {
5848
+ return false;
5849
+ }
5850
+ const errorString = getErrorMessage(error).toLowerCase();
5851
+ // Check for Solana "MessageExpired" error pattern
5852
+ // Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
5853
+ // Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
5854
+ if (errorString.includes('messageexpired')) {
5855
+ return true;
5856
+ }
5857
+ // Check for EVM attestation expiry errors
5858
+ // Contract reverts with: "Message expired and must be re-signed"
5859
+ if (errorString.includes('message') && errorString.includes('expired')) {
5860
+ return true;
5861
+ }
5862
+ return false;
5863
+ };
5864
+
5386
5865
  /**
5387
5866
  * Checks if a decoded attestation field matches the corresponding transfer parameter.
5388
5867
  * If the values do not match, appends a descriptive error message to the errors array.
@@ -5833,6 +6312,7 @@ function dispatchStepEvent(name, step, provider) {
5833
6312
  });
5834
6313
  break;
5835
6314
  case 'fetchAttestation':
6315
+ case 'reAttest':
5836
6316
  provider.actionDispatcher.dispatch(name, {
5837
6317
  ...actionValues,
5838
6318
  method: name,
@@ -6226,21 +6706,64 @@ const validateBalanceForTransaction = async (params) => {
6226
6706
  // Extract chain name from operationContext
6227
6707
  const chainName = extractChainInfo(operationContext.chain).name;
6228
6708
  // 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;
6709
+ throw createInsufficientTokenBalanceError(chainName, token, {
6710
+ balance: balance.toString(),
6711
+ amount,
6712
+ tokenAddress,
6713
+ walletAddress: operationContext.address,
6714
+ });
6715
+ }
6716
+ };
6717
+
6718
+ /**
6719
+ * Validate that the adapter has sufficient native token balance for transaction fees.
6720
+ *
6721
+ * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
6722
+ * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
6723
+ * if the balance is zero, indicating the wallet cannot pay for transaction fees.
6724
+ *
6725
+ * @param params - The validation parameters containing adapter and operation context.
6726
+ * @returns A promise that resolves to void if validation passes.
6727
+ * @throws {KitError} When the adapter's native balance is zero (code: 9002).
6728
+ *
6729
+ * @example
6730
+ * ```typescript
6731
+ * import { validateNativeBalanceForTransaction } from '@core/adapter'
6732
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
6733
+ * import { isKitError, ERROR_TYPES } from '@core/errors'
6734
+ *
6735
+ * const adapter = createViemAdapterFromPrivateKey({
6736
+ * privateKey: '0x...',
6737
+ * chain: 'Ethereum',
6738
+ * })
6739
+ *
6740
+ * try {
6741
+ * await validateNativeBalanceForTransaction({
6742
+ * adapter,
6743
+ * operationContext: { chain: 'Ethereum' },
6744
+ * })
6745
+ * console.log('Native balance validation passed')
6746
+ * } catch (error) {
6747
+ * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
6748
+ * console.error('Insufficient gas funds:', error.message)
6749
+ * }
6750
+ * }
6751
+ * ```
6752
+ */
6753
+ const validateNativeBalanceForTransaction = async (params) => {
6754
+ const { adapter, operationContext } = params;
6755
+ const balancePrepared = await adapter.prepareAction('native.balanceOf', {
6756
+ walletAddress: operationContext.address,
6757
+ }, operationContext);
6758
+ const balance = await balancePrepared.execute();
6759
+ if (BigInt(balance) === 0n) {
6760
+ // Extract chain name from operationContext
6761
+ const chainName = extractChainInfo(operationContext.chain).name;
6762
+ // Create KitError with rich context in trace
6763
+ throw createInsufficientGasError(chainName, {
6764
+ balance: '0',
6765
+ walletAddress: operationContext.address,
6766
+ });
6244
6767
  }
6245
6768
  };
6246
6769
 
@@ -6256,6 +6779,7 @@ const CCTPv2StepName = {
6256
6779
  burn: 'burn',
6257
6780
  fetchAttestation: 'fetchAttestation',
6258
6781
  mint: 'mint',
6782
+ reAttest: 'reAttest',
6259
6783
  };
6260
6784
  /**
6261
6785
  * Conditional step transition rules for CCTP bridge flow.
@@ -6363,6 +6887,27 @@ const STEP_TRANSITION_RULES = {
6363
6887
  isActionable: false, // Waiting for pending transaction
6364
6888
  },
6365
6889
  ],
6890
+ // After ReAttest step
6891
+ [CCTPv2StepName.reAttest]: [
6892
+ {
6893
+ condition: (ctx) => ctx.lastStep?.state === 'success',
6894
+ nextStep: CCTPv2StepName.mint,
6895
+ reason: 'Re-attestation successful, proceed to mint',
6896
+ isActionable: true,
6897
+ },
6898
+ {
6899
+ condition: (ctx) => ctx.lastStep?.state === 'error',
6900
+ nextStep: CCTPv2StepName.mint,
6901
+ reason: 'Re-attestation failed, retry mint to re-initiate recovery',
6902
+ isActionable: true,
6903
+ },
6904
+ {
6905
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
6906
+ nextStep: CCTPv2StepName.mint,
6907
+ reason: 'Re-attestation pending, retry mint to re-initiate recovery',
6908
+ isActionable: true,
6909
+ },
6910
+ ],
6366
6911
  };
6367
6912
  /**
6368
6913
  * Analyze bridge steps to determine retry feasibility and continuation point.
@@ -6629,8 +7174,14 @@ function getBurnTxHash(result) {
6629
7174
  * ```
6630
7175
  */
6631
7176
  function getAttestationData(result) {
6632
- const step = findStepByName(result, CCTPv2StepName.fetchAttestation);
6633
- return step?.data;
7177
+ // Prefer reAttest data (most recent attestation after expiry)
7178
+ const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
7179
+ if (reAttestStep?.state === 'success' && reAttestStep.data) {
7180
+ return reAttestStep.data;
7181
+ }
7182
+ // Fall back to fetchAttestation step
7183
+ const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
7184
+ return fetchStep?.data;
6634
7185
  }
6635
7186
 
6636
7187
  /**
@@ -6828,7 +7379,7 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
6828
7379
  message: 'Cannot fetch attestation: burn transaction hash not found',
6829
7380
  });
6830
7381
  }
6831
- const sourceAddress = await context.from.getAddress(result.source.chain);
7382
+ const sourceAddress = result.source.address;
6832
7383
  const attestation = await provider.fetchAttestation({
6833
7384
  chain: result.source.chain,
6834
7385
  adapter: context.from,
@@ -6844,6 +7395,63 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
6844
7395
  return waitForPendingTransaction(pendingStep, adapter, chain);
6845
7396
  }
6846
7397
 
7398
+ /**
7399
+ * Executes a re-attestation operation to obtain a fresh attestation for an expired message.
7400
+ *
7401
+ * This function handles the re-attestation step of the CCTP v2 bridge process, where a fresh
7402
+ * attestation is requested from Circle's API when the original attestation has expired.
7403
+ * It first checks if the attestation has already been re-attested before making the API call.
7404
+ *
7405
+ * @param params - The bridge parameters containing source, destination, amount and optional config
7406
+ * @param provider - The CCTP v2 bridging provider
7407
+ * @param burnTxHash - The transaction hash of the original burn operation
7408
+ * @returns Promise resolving to the bridge step with fresh attestation data
7409
+ *
7410
+ * @example
7411
+ * ```typescript
7412
+ * const reAttestStep = await bridgeReAttest(
7413
+ * { params, provider },
7414
+ * burnTxHash
7415
+ * )
7416
+ * console.log('Fresh attestation:', reAttestStep.data)
7417
+ * ```
7418
+ */
7419
+ async function bridgeReAttest({ params, provider, }, burnTxHash) {
7420
+ const step = {
7421
+ name: 'reAttest',
7422
+ state: 'pending',
7423
+ };
7424
+ try {
7425
+ // Fetch current attestation to check if already re-attested
7426
+ const currentAttestation = await provider.fetchAttestation(params.source, burnTxHash);
7427
+ // Check if already re-attested (expirationBlock === '0' means never expires)
7428
+ const expirationBlock = currentAttestation.decodedMessage.decodedMessageBody.expirationBlock;
7429
+ if (expirationBlock === '0') {
7430
+ // Already re-attested - return current attestation without calling reAttest API
7431
+ return { ...step, state: 'success', data: currentAttestation };
7432
+ }
7433
+ // Not yet re-attested - proceed with re-attestation request
7434
+ const reAttestedAttestation = await provider.reAttest(params.source, burnTxHash);
7435
+ return { ...step, state: 'success', data: reAttestedAttestation };
7436
+ }
7437
+ catch (err) {
7438
+ let errorMessage = 'Unknown re-attestation error';
7439
+ if (err instanceof Error) {
7440
+ errorMessage = err.message;
7441
+ }
7442
+ else if (typeof err === 'string') {
7443
+ errorMessage = err;
7444
+ }
7445
+ return {
7446
+ ...step,
7447
+ state: 'error',
7448
+ error: err,
7449
+ errorMessage,
7450
+ data: undefined,
7451
+ };
7452
+ }
7453
+ }
7454
+
6847
7455
  /**
6848
7456
  * Extract context data from completed bridge steps for retry operations.
6849
7457
  *
@@ -6859,6 +7467,116 @@ function populateContext(result) {
6859
7467
  attestationData: getAttestationData(result),
6860
7468
  };
6861
7469
  }
7470
+ /**
7471
+ * Handle re-attestation and mint retry when mint fails due to expired attestation.
7472
+ *
7473
+ * @internal
7474
+ */
7475
+ async function handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result) {
7476
+ const burnTxHash = stepContext?.burnTxHash ?? getBurnTxHash(result);
7477
+ if (burnTxHash === undefined || burnTxHash === '') {
7478
+ handleStepError('mint', new Error('Cannot attempt re-attestation: Burn transaction hash not found in previous steps.'), result);
7479
+ return { success: false };
7480
+ }
7481
+ const reAttestStep = await bridgeReAttest({ params, provider }, burnTxHash);
7482
+ dispatchStepEvent('reAttest', reAttestStep, provider);
7483
+ result.steps.push(reAttestStep);
7484
+ if (reAttestStep.state === 'error') {
7485
+ result.state = 'error';
7486
+ return { success: false };
7487
+ }
7488
+ const freshContext = {
7489
+ ...stepContext,
7490
+ burnTxHash,
7491
+ attestationData: reAttestStep.data,
7492
+ };
7493
+ return executeMintRetry(params, provider, executor, freshContext, updateContext, result);
7494
+ }
7495
+ /**
7496
+ * Handle step execution error in retry loop.
7497
+ *
7498
+ * Determines if re-attestation should be attempted for mint failures,
7499
+ * or records the error and signals to exit the loop.
7500
+ *
7501
+ * @internal
7502
+ */
7503
+ async function handleStepExecutionError(name, error, context) {
7504
+ const { params, provider, executor, updateContext, stepContext, result } = context;
7505
+ const shouldAttemptReAttestation = name === 'mint' && isMintFailureRelatedToAttestation(error);
7506
+ if (!shouldAttemptReAttestation) {
7507
+ handleStepError(name, error, result);
7508
+ return { shouldContinue: false };
7509
+ }
7510
+ const reAttestResult = await handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result);
7511
+ if (!reAttestResult.success) {
7512
+ return { shouldContinue: false };
7513
+ }
7514
+ return { shouldContinue: true, stepContext: reAttestResult.stepContext };
7515
+ }
7516
+ /**
7517
+ * Execute mint retry with fresh attestation context.
7518
+ *
7519
+ * @internal
7520
+ */
7521
+ async function executeMintRetry(params, provider, executor, freshContext, updateContext, result) {
7522
+ try {
7523
+ const retryStep = await executor(params, provider, freshContext);
7524
+ if (retryStep.state === 'error') {
7525
+ throw new Error(retryStep.errorMessage ?? 'mint step returned error state');
7526
+ }
7527
+ dispatchStepEvent('mint', retryStep, provider);
7528
+ result.steps.push(retryStep);
7529
+ return { success: true, stepContext: updateContext?.(retryStep) };
7530
+ }
7531
+ catch (retryError) {
7532
+ if (isMintFailureRelatedToAttestation(retryError)) {
7533
+ const kitError = createSimulationFailedError(result.destination.chain.name, getErrorMessage(retryError), { error: retryError });
7534
+ handleStepError('mint', kitError, result);
7535
+ }
7536
+ else {
7537
+ handleStepError('mint', retryError, result);
7538
+ }
7539
+ return { success: false };
7540
+ }
7541
+ }
7542
+ /**
7543
+ * Execute remaining bridge steps starting from a specific index.
7544
+ *
7545
+ * Handles the step execution loop with error handling and re-attestation support.
7546
+ * Returns true if all steps completed successfully, false if stopped due to error.
7547
+ *
7548
+ * @internal
7549
+ */
7550
+ async function executeSteps(params, provider, result, startIndex) {
7551
+ let stepContext = populateContext(result);
7552
+ for (const { name, executor, updateContext } of stepExecutors.slice(startIndex)) {
7553
+ try {
7554
+ const step = await executor(params, provider, stepContext);
7555
+ if (step.state === 'error') {
7556
+ const errorMessage = step.errorMessage ?? `${name} step returned error state`;
7557
+ throw new Error(errorMessage);
7558
+ }
7559
+ stepContext = updateContext?.(step);
7560
+ dispatchStepEvent(name, step, provider);
7561
+ result.steps.push(step);
7562
+ }
7563
+ catch (error) {
7564
+ const errorResult = await handleStepExecutionError(name, error, {
7565
+ params,
7566
+ provider,
7567
+ executor,
7568
+ updateContext,
7569
+ stepContext,
7570
+ result,
7571
+ });
7572
+ if (!errorResult.shouldContinue) {
7573
+ return false;
7574
+ }
7575
+ stepContext = errorResult.stepContext;
7576
+ }
7577
+ }
7578
+ return true;
7579
+ }
6862
7580
  /**
6863
7581
  * Retry a failed or incomplete CCTP v2 bridge operation from where it left off.
6864
7582
  *
@@ -6880,15 +7598,12 @@ function populateContext(result) {
6880
7598
  async function retry(result, context, provider) {
6881
7599
  const analysis = analyzeSteps(result);
6882
7600
  if (!analysis.isActionable) {
6883
- // Terminal completion - bridge already complete, return gracefully
6884
7601
  if (analysis.continuationStep === null && result.state === 'success') {
6885
7602
  return result;
6886
7603
  }
6887
- // Pending states - wait for the pending operation to complete
6888
7604
  if (hasPendingState(analysis, result)) {
6889
7605
  return handlePendingState(result, context, provider, analysis);
6890
7606
  }
6891
- // No valid continuation - cannot proceed
6892
7607
  throw new Error('Retry not supported for this result, requires user action');
6893
7608
  }
6894
7609
  if (!isCCTPV2Supported(result.source.chain)) {
@@ -6915,25 +7630,10 @@ async function retry(result, context, provider) {
6915
7630
  if (indexOfSteps === -1) {
6916
7631
  throw new Error(`Continuation step ${analysis.continuationStep ?? ''} not found`);
6917
7632
  }
6918
- let stepContext = populateContext(result);
6919
- // Execute each step in sequence
6920
- for (const { name, executor, updateContext } of stepExecutors.slice(indexOfSteps)) {
6921
- try {
6922
- const step = await executor(params, provider, stepContext);
6923
- if (step.state === 'error') {
6924
- const errorMessage = step.errorMessage ?? `${name} step returned error state`;
6925
- throw new Error(errorMessage);
6926
- }
6927
- stepContext = updateContext?.(step);
6928
- dispatchStepEvent(name, step, provider);
6929
- result.steps.push(step);
6930
- }
6931
- catch (error) {
6932
- handleStepError(name, error, result);
6933
- return result;
6934
- }
7633
+ const completed = await executeSteps(params, provider, result, indexOfSteps);
7634
+ if (completed) {
7635
+ result.state = 'success';
6935
7636
  }
6936
- result.state = 'success';
6937
7637
  return result;
6938
7638
  }
6939
7639
  /**
@@ -6953,7 +7653,7 @@ async function retry(result, context, provider) {
6953
7653
  * @returns Updated bridge result after pending operation completes.
6954
7654
  */
6955
7655
  async function handlePendingState(result, context, provider, analysis) {
6956
- if (!analysis.continuationStep) {
7656
+ if (analysis.continuationStep === null || analysis.continuationStep === '') {
6957
7657
  // This should not be reachable due to the `hasPendingState` check,
6958
7658
  // but it ensures type safety for `continuationStep`.
6959
7659
  throw new KitError({
@@ -7117,16 +7817,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7117
7817
  async bridge(params) {
7118
7818
  // CCTP-specific bridge params validation (includes base validation)
7119
7819
  assertCCTPv2BridgeParams(params);
7120
- const { source, amount, token } = params;
7820
+ const { source, destination, amount, token } = params;
7121
7821
  // Extract operation context from source wallet context for balance validation
7122
- const operationContext = this.extractOperationContext(source);
7123
- // Validate balance for transaction
7822
+ const sourceOperationContext = this.extractOperationContext(source);
7823
+ // Validate USDC balance for transaction on source chain
7124
7824
  await validateBalanceForTransaction({
7125
7825
  adapter: source.adapter,
7126
7826
  amount,
7127
7827
  token,
7128
7828
  tokenAddress: source.chain.usdcAddress,
7129
- operationContext,
7829
+ operationContext: sourceOperationContext,
7830
+ });
7831
+ // Validate native balance > 0 for gas fees on source chain
7832
+ await validateNativeBalanceForTransaction({
7833
+ adapter: source.adapter,
7834
+ operationContext: sourceOperationContext,
7835
+ });
7836
+ // Extract operation context from destination wallet context
7837
+ const destinationOperationContext = this.extractOperationContext(destination);
7838
+ // Validate native balance > 0 for gas fees on destination chain
7839
+ await validateNativeBalanceForTransaction({
7840
+ adapter: destination.adapter,
7841
+ operationContext: destinationOperationContext,
7130
7842
  });
7131
7843
  return bridge(params, this);
7132
7844
  }
@@ -7575,8 +8287,10 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7575
8287
  }
7576
8288
  // Step 2: Request re-attestation
7577
8289
  await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
7578
- // Step 3: Poll for fresh attestation
7579
- const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
8290
+ // Step 3: Poll for fresh attestation until expirationBlock === '0'
8291
+ // The expiration block transitions from non-zero to zero when Circle
8292
+ // completes processing the re-attestation request.
8293
+ const response = await fetchReAttestedAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
7580
8294
  const message = response.messages[0];
7581
8295
  if (!message) {
7582
8296
  throw new Error('Failed to re-attest: No attestation found after re-attestation request');
@@ -7785,5 +8499,5 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7785
8499
  }
7786
8500
  }
7787
8501
 
7788
- export { CCTPV2BridgingProvider, getMintRecipientAccount };
8502
+ export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation };
7789
8503
  //# sourceMappingURL=index.mjs.map