@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.cjs CHANGED
@@ -81,6 +81,8 @@ var Blockchain;
81
81
  Blockchain["Ink_Testnet"] = "Ink_Testnet";
82
82
  Blockchain["Linea"] = "Linea";
83
83
  Blockchain["Linea_Sepolia"] = "Linea_Sepolia";
84
+ Blockchain["Monad"] = "Monad";
85
+ Blockchain["Monad_Testnet"] = "Monad_Testnet";
84
86
  Blockchain["NEAR"] = "NEAR";
85
87
  Blockchain["NEAR_Testnet"] = "NEAR_Testnet";
86
88
  Blockchain["Noble"] = "Noble";
@@ -172,6 +174,7 @@ var BridgeChain;
172
174
  BridgeChain["HyperEVM"] = "HyperEVM";
173
175
  BridgeChain["Ink"] = "Ink";
174
176
  BridgeChain["Linea"] = "Linea";
177
+ BridgeChain["Monad"] = "Monad";
175
178
  BridgeChain["Optimism"] = "Optimism";
176
179
  BridgeChain["Plume"] = "Plume";
177
180
  BridgeChain["Polygon"] = "Polygon";
@@ -191,6 +194,7 @@ var BridgeChain;
191
194
  BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
192
195
  BridgeChain["Ink_Testnet"] = "Ink_Testnet";
193
196
  BridgeChain["Linea_Sepolia"] = "Linea_Sepolia";
197
+ BridgeChain["Monad_Testnet"] = "Monad_Testnet";
194
198
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
195
199
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
196
200
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
@@ -385,8 +389,11 @@ const ArcTestnet = defineChain({
385
389
  name: 'Arc Testnet',
386
390
  title: 'ArcTestnet',
387
391
  nativeCurrency: {
388
- name: 'Arc',
389
- symbol: 'Arc',
392
+ name: 'USDC',
393
+ symbol: 'USDC',
394
+ // Arc uses native USDC with 18 decimals for gas payments (EVM standard).
395
+ // Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
396
+ // See: https://docs.arc.network/arc/references/contract-addresses
390
397
  decimals: 18,
391
398
  },
392
399
  chainId: 5042002,
@@ -1174,6 +1181,86 @@ const LineaSepolia = defineChain({
1174
1181
  },
1175
1182
  });
1176
1183
 
1184
+ /**
1185
+ * Monad Mainnet chain definition
1186
+ * @remarks
1187
+ * This represents the official production network for the Monad blockchain.
1188
+ * Monad is a high-performance EVM-compatible Layer-1 blockchain featuring
1189
+ * over 10,000 TPS, sub-second finality, and near-zero gas fees.
1190
+ */
1191
+ const Monad = defineChain({
1192
+ type: 'evm',
1193
+ chain: Blockchain.Monad,
1194
+ name: 'Monad',
1195
+ title: 'Monad Mainnet',
1196
+ nativeCurrency: {
1197
+ name: 'Monad',
1198
+ symbol: 'MON',
1199
+ decimals: 18,
1200
+ },
1201
+ chainId: 143,
1202
+ isTestnet: false,
1203
+ explorerUrl: 'https://monadscan.com/tx/{hash}',
1204
+ rpcEndpoints: ['https://rpc.monad.xyz'],
1205
+ eurcAddress: null,
1206
+ usdcAddress: '0x754704Bc059F8C67012fEd69BC8A327a5aafb603',
1207
+ cctp: {
1208
+ domain: 15,
1209
+ contracts: {
1210
+ v2: {
1211
+ type: 'split',
1212
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
1213
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
1214
+ confirmations: 1,
1215
+ fastConfirmations: 1,
1216
+ },
1217
+ },
1218
+ },
1219
+ kitContracts: {
1220
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET,
1221
+ },
1222
+ });
1223
+
1224
+ /**
1225
+ * Monad Testnet chain definition
1226
+ * @remarks
1227
+ * This represents the official test network for the Monad blockchain.
1228
+ * Monad is a high-performance EVM-compatible Layer-1 blockchain featuring
1229
+ * over 10,000 TPS, sub-second finality, and near-zero gas fees.
1230
+ */
1231
+ const MonadTestnet = defineChain({
1232
+ type: 'evm',
1233
+ chain: Blockchain.Monad_Testnet,
1234
+ name: 'Monad Testnet',
1235
+ title: 'Monad Testnet',
1236
+ nativeCurrency: {
1237
+ name: 'Monad',
1238
+ symbol: 'MON',
1239
+ decimals: 18,
1240
+ },
1241
+ chainId: 10143,
1242
+ isTestnet: true,
1243
+ explorerUrl: 'https://testnet.monadscan.com/tx/{hash}',
1244
+ rpcEndpoints: ['https://testnet-rpc.monad.xyz'],
1245
+ eurcAddress: null,
1246
+ usdcAddress: '0x534b2f3A21130d7a60830c2Df862319e593943A3',
1247
+ cctp: {
1248
+ domain: 15,
1249
+ contracts: {
1250
+ v2: {
1251
+ type: 'split',
1252
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
1253
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
1254
+ confirmations: 1,
1255
+ fastConfirmations: 1,
1256
+ },
1257
+ },
1258
+ },
1259
+ kitContracts: {
1260
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1261
+ },
1262
+ });
1263
+
1177
1264
  /**
1178
1265
  * NEAR Protocol Mainnet chain definition
1179
1266
  * @remarks
@@ -2258,6 +2345,8 @@ var Chains = {
2258
2345
  InkTestnet: InkTestnet,
2259
2346
  Linea: Linea,
2260
2347
  LineaSepolia: LineaSepolia,
2348
+ Monad: Monad,
2349
+ MonadTestnet: MonadTestnet,
2261
2350
  NEAR: NEAR,
2262
2351
  NEARTestnet: NEARTestnet,
2263
2352
  Noble: Noble,
@@ -3281,6 +3370,8 @@ const ERROR_TYPES = {
3281
3370
  RPC: 'RPC',
3282
3371
  /** Internet connectivity, DNS resolution, connection issues */
3283
3372
  NETWORK: 'NETWORK',
3373
+ /** Catch-all for unrecognized errors (code 0) */
3374
+ UNKNOWN: 'UNKNOWN',
3284
3375
  };
3285
3376
  /**
3286
3377
  * Array of valid error type values for validation.
@@ -3294,6 +3385,8 @@ const ERROR_TYPE_ARRAY = [...ERROR_TYPE_VALUES];
3294
3385
  /**
3295
3386
  * Error code ranges for validation.
3296
3387
  * Single source of truth for valid error code ranges.
3388
+ *
3389
+ * Note: Code 0 is special - it's the UNKNOWN catch-all error.
3297
3390
  */
3298
3391
  const ERROR_CODE_RANGES = [
3299
3392
  { min: 1000, max: 1999, type: 'INPUT' },
@@ -3302,6 +3395,8 @@ const ERROR_CODE_RANGES = [
3302
3395
  { min: 5000, max: 5999, type: 'ONCHAIN' },
3303
3396
  { min: 9000, max: 9999, type: 'BALANCE' },
3304
3397
  ];
3398
+ /** Special code for UNKNOWN errors */
3399
+ const UNKNOWN_ERROR_CODE = 0;
3305
3400
  /**
3306
3401
  * Zod schema for validating ErrorDetails objects.
3307
3402
  *
@@ -3340,6 +3435,7 @@ const ERROR_CODE_RANGES = [
3340
3435
  const errorDetailsSchema = zod.z.object({
3341
3436
  /**
3342
3437
  * Numeric identifier following standardized ranges:
3438
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3343
3439
  * - 1000-1999: INPUT errors - Parameter validation
3344
3440
  * - 3000-3999: NETWORK errors - Connectivity issues
3345
3441
  * - 4000-4999: RPC errors - Provider issues, gas estimation
@@ -3349,8 +3445,9 @@ const errorDetailsSchema = zod.z.object({
3349
3445
  code: zod.z
3350
3446
  .number()
3351
3447
  .int('Error code must be an integer')
3352
- .refine((code) => ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
3353
- message: 'Error code must be in valid ranges: 1000-1999 (INPUT), 3000-3999 (NETWORK), 4000-4999 (RPC), 5000-5999 (ONCHAIN), 9000-9999 (BALANCE)',
3448
+ .refine((code) => code === UNKNOWN_ERROR_CODE ||
3449
+ ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
3450
+ 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)',
3354
3451
  }),
3355
3452
  /** Human-readable ID (e.g., "INPUT_NETWORK_MISMATCH", "BALANCE_INSUFFICIENT_TOKEN") */
3356
3453
  name: zod.z
@@ -3360,7 +3457,7 @@ const errorDetailsSchema = zod.z.object({
3360
3457
  /** Error category indicating where the error originated */
3361
3458
  type: zod.z.enum(ERROR_TYPE_ARRAY, {
3362
3459
  errorMap: () => ({
3363
- message: 'Error type must be one of: INPUT, BALANCE, ONCHAIN, RPC, NETWORK',
3460
+ message: 'Error type must be one of: INPUT, BALANCE, ONCHAIN, RPC, NETWORK, UNKNOWN',
3364
3461
  }),
3365
3462
  }),
3366
3463
  /** Error handling strategy */
@@ -3561,6 +3658,7 @@ class KitError extends Error {
3561
3658
  /**
3562
3659
  * Standardized error code ranges for consistent categorization:
3563
3660
  *
3661
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3564
3662
  * - 1000-1999: INPUT errors - Parameter validation, input format errors
3565
3663
  * - 3000-3999: NETWORK errors - Internet connectivity, DNS, connection issues
3566
3664
  * - 4000-4999: RPC errors - Blockchain provider issues, gas estimation, nonce errors
@@ -3644,6 +3742,37 @@ const BalanceError = {
3644
3742
  code: 9001,
3645
3743
  name: 'BALANCE_INSUFFICIENT_TOKEN',
3646
3744
  type: 'BALANCE',
3745
+ },
3746
+ /** Insufficient native token (ETH/SOL/etc) for gas fees */
3747
+ INSUFFICIENT_GAS: {
3748
+ code: 9002,
3749
+ name: 'BALANCE_INSUFFICIENT_GAS',
3750
+ type: 'BALANCE',
3751
+ }};
3752
+ /**
3753
+ * Standardized error definitions for ONCHAIN type errors.
3754
+ *
3755
+ * ONCHAIN errors occur during transaction execution, simulation,
3756
+ * or interaction with smart contracts on the blockchain.
3757
+ *
3758
+ * @example
3759
+ * ```typescript
3760
+ * import { OnchainError } from '@core/errors'
3761
+ *
3762
+ * const error = new KitError({
3763
+ * ...OnchainError.SIMULATION_FAILED,
3764
+ * recoverability: 'FATAL',
3765
+ * message: 'Simulation failed: ERC20 transfer amount exceeds balance',
3766
+ * cause: { trace: { reason: 'ERC20: transfer amount exceeds balance' } }
3767
+ * })
3768
+ * ```
3769
+ */
3770
+ const OnchainError = {
3771
+ /** Pre-flight transaction simulation failed */
3772
+ SIMULATION_FAILED: {
3773
+ code: 5002,
3774
+ name: 'ONCHAIN_SIMULATION_FAILED',
3775
+ type: 'ONCHAIN',
3647
3776
  }};
3648
3777
 
3649
3778
  /**
@@ -3866,7 +3995,7 @@ function createValidationErrorFromZod(zodError, context) {
3866
3995
  *
3867
3996
  * @param chain - The blockchain network where the balance check failed
3868
3997
  * @param token - The token symbol (e.g., 'USDC', 'ETH')
3869
- * @param rawError - The original error from the underlying system (optional)
3998
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
3870
3999
  * @returns KitError with insufficient token balance details
3871
4000
  *
3872
4001
  * @example
@@ -3879,24 +4008,116 @@ function createValidationErrorFromZod(zodError, context) {
3879
4008
  *
3880
4009
  * @example
3881
4010
  * ```typescript
3882
- * // With raw error for debugging
4011
+ * // With trace context for debugging
3883
4012
  * try {
3884
4013
  * await transfer(...)
3885
4014
  * } catch (error) {
3886
- * throw createInsufficientTokenBalanceError('Base', 'USDC', error)
4015
+ * throw createInsufficientTokenBalanceError('Base', 'USDC', {
4016
+ * rawError: error,
4017
+ * balance: '1000000',
4018
+ * amount: '5000000',
4019
+ * })
3887
4020
  * }
3888
4021
  * ```
3889
4022
  */
3890
- function createInsufficientTokenBalanceError(chain, token, rawError) {
4023
+ function createInsufficientTokenBalanceError(chain, token, trace) {
3891
4024
  return new KitError({
3892
4025
  ...BalanceError.INSUFFICIENT_TOKEN,
3893
4026
  recoverability: 'FATAL',
3894
4027
  message: `Insufficient ${token} balance on ${chain}`,
3895
4028
  cause: {
3896
4029
  trace: {
4030
+ ...trace,
3897
4031
  chain,
3898
4032
  token,
3899
- rawError,
4033
+ },
4034
+ },
4035
+ });
4036
+ }
4037
+ /**
4038
+ * Creates error for insufficient gas funds.
4039
+ *
4040
+ * This error is thrown when a wallet does not have enough native tokens
4041
+ * (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
4042
+ * as it requires user intervention to add gas funds.
4043
+ *
4044
+ * @param chain - The blockchain network where the gas check failed
4045
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
4046
+ * @returns KitError with insufficient gas details
4047
+ *
4048
+ * @example
4049
+ * ```typescript
4050
+ * import { createInsufficientGasError } from '@core/errors'
4051
+ *
4052
+ * throw createInsufficientGasError('Ethereum')
4053
+ * // Message: "Insufficient gas funds on Ethereum"
4054
+ * ```
4055
+ *
4056
+ * @example
4057
+ * ```typescript
4058
+ * // With trace context for debugging
4059
+ * throw createInsufficientGasError('Ethereum', {
4060
+ * rawError: error,
4061
+ * gasRequired: '21000',
4062
+ * gasAvailable: '10000',
4063
+ * walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
4064
+ * })
4065
+ * ```
4066
+ */
4067
+ function createInsufficientGasError(chain, trace) {
4068
+ return new KitError({
4069
+ ...BalanceError.INSUFFICIENT_GAS,
4070
+ recoverability: 'FATAL',
4071
+ message: `Insufficient gas funds on ${chain}`,
4072
+ cause: {
4073
+ trace: {
4074
+ ...trace,
4075
+ chain,
4076
+ },
4077
+ },
4078
+ });
4079
+ }
4080
+
4081
+ /**
4082
+ * Creates error for transaction simulation failures.
4083
+ *
4084
+ * This error is thrown when a pre-flight transaction simulation fails,
4085
+ * typically due to contract logic that would revert. The error is FATAL
4086
+ * as it indicates the transaction would fail if submitted.
4087
+ *
4088
+ * @param chain - The blockchain network where the simulation failed
4089
+ * @param reason - The reason for simulation failure (e.g., revert message)
4090
+ * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
4091
+ * @returns KitError with simulation failure details
4092
+ *
4093
+ * @example
4094
+ * ```typescript
4095
+ * import { createSimulationFailedError } from '@core/errors'
4096
+ *
4097
+ * throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance')
4098
+ * // Message: "Simulation failed on Ethereum: ERC20: insufficient allowance"
4099
+ * ```
4100
+ *
4101
+ * @example
4102
+ * ```typescript
4103
+ * // With trace context for debugging
4104
+ * throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance', {
4105
+ * rawError: error,
4106
+ * txHash: '0x1234...',
4107
+ * gasLimit: '21000',
4108
+ * })
4109
+ * ```
4110
+ */
4111
+ function createSimulationFailedError(chain, reason, trace) {
4112
+ return new KitError({
4113
+ ...OnchainError.SIMULATION_FAILED,
4114
+ recoverability: 'FATAL',
4115
+ message: `Simulation failed on ${chain}: ${reason}`,
4116
+ cause: {
4117
+ trace: {
4118
+ ...trace,
4119
+ chain,
4120
+ reason,
3900
4121
  },
3901
4122
  },
3902
4123
  });
@@ -3958,6 +4179,38 @@ function isKitError(error) {
3958
4179
  function isFatalError(error) {
3959
4180
  return isKitError(error) && error.recoverability === 'FATAL';
3960
4181
  }
4182
+ /**
4183
+ * Safely extracts error message from any error type.
4184
+ *
4185
+ * This utility handles different error types gracefully, extracting
4186
+ * meaningful messages from Error instances, string errors, or providing
4187
+ * a fallback for unknown error types. Never throws.
4188
+ *
4189
+ * @param error - Unknown error to extract message from
4190
+ * @returns Error message string, or fallback message
4191
+ *
4192
+ * @example
4193
+ * ```typescript
4194
+ * import { getErrorMessage } from '@core/errors'
4195
+ *
4196
+ * try {
4197
+ * await riskyOperation()
4198
+ * } catch (error) {
4199
+ * const message = getErrorMessage(error)
4200
+ * console.log('Error occurred:', message)
4201
+ * // Works with Error, KitError, string, or any other type
4202
+ * }
4203
+ * ```
4204
+ */
4205
+ function getErrorMessage(error) {
4206
+ if (error instanceof Error) {
4207
+ return error.message;
4208
+ }
4209
+ if (typeof error === 'string') {
4210
+ return error;
4211
+ }
4212
+ return 'An unknown error occurred';
4213
+ }
3961
4214
 
3962
4215
  /**
3963
4216
  * Validates data against a Zod schema with enhanced error reporting.
@@ -5074,6 +5327,68 @@ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHas
5074
5327
  };
5075
5328
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
5076
5329
  };
5330
+ /**
5331
+ * Type guard that validates attestation response has expirationBlock === '0'.
5332
+ *
5333
+ * This is used after requestReAttestation() to poll until the attestation
5334
+ * is fully re-processed and has a zero expiration block (never expires).
5335
+ * The expiration block transitions from non-zero to zero when Circle
5336
+ * completes processing the re-attestation request.
5337
+ *
5338
+ * @param obj - The value to check, typically a parsed JSON response
5339
+ * @returns True if the attestation has expirationBlock === '0'
5340
+ * @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
5341
+ *
5342
+ * @example
5343
+ * ```typescript
5344
+ * // After requesting re-attestation, use this to validate the response
5345
+ * const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
5346
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
5347
+ * ```
5348
+ *
5349
+ * @internal
5350
+ */
5351
+ const isReAttestedAttestationResponse = (obj) => {
5352
+ // First validate the basic structure and completion status
5353
+ // This will throw appropriate errors for invalid structure or incomplete attestation
5354
+ if (!isAttestationResponse(obj)) ;
5355
+ // Check if the first message has expirationBlock === '0'
5356
+ const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
5357
+ if (expirationBlock !== '0') {
5358
+ // Re-attestation not yet complete - allow retry via polling
5359
+ throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
5360
+ }
5361
+ return true;
5362
+ };
5363
+ /**
5364
+ * Fetches attestation data and polls until expirationBlock === '0'.
5365
+ *
5366
+ * This function is used after calling requestReAttestation() to wait until
5367
+ * the attestation is fully re-processed. The expirationBlock transitions
5368
+ * from non-zero to zero when Circle completes the re-attestation.
5369
+ *
5370
+ * @param sourceDomainId - The CCTP domain ID of the source chain
5371
+ * @param transactionHash - The transaction hash to fetch attestation for
5372
+ * @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
5373
+ * @param config - Optional configuration overrides
5374
+ * @returns The re-attested attestation response with expirationBlock === '0'
5375
+ * @throws If the request fails, times out, or expirationBlock never becomes 0
5376
+ *
5377
+ * @example
5378
+ * ```typescript
5379
+ * // After requesting re-attestation
5380
+ * await requestReAttestation(nonce, isTestnet)
5381
+ *
5382
+ * // Poll until expirationBlock becomes 0
5383
+ * const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
5384
+ * // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
5385
+ * ```
5386
+ */
5387
+ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
5388
+ const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
5389
+ const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
5390
+ return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
5391
+ };
5077
5392
  /**
5078
5393
  * Builds the IRIS API URL for re-attestation requests.
5079
5394
  *
@@ -5389,6 +5704,170 @@ mintAddress) => {
5389
5704
  }
5390
5705
  };
5391
5706
 
5707
+ /**
5708
+ * Converts a block number to bigint with validation.
5709
+ *
5710
+ * @param value - The block number value to convert (bigint, number, or string)
5711
+ * @returns The validated block number as a bigint
5712
+ * @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
5713
+ * @internal
5714
+ */
5715
+ const toBlockNumber = (value) => {
5716
+ if (value === null || value === undefined) {
5717
+ throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
5718
+ }
5719
+ // Empty string edge case - BigInt('') === 0n which is misleading
5720
+ if (value === '') {
5721
+ throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
5722
+ }
5723
+ // For numbers, validate before BigInt conversion
5724
+ if (typeof value === 'number') {
5725
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
5726
+ throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
5727
+ }
5728
+ }
5729
+ let result;
5730
+ try {
5731
+ result = BigInt(value);
5732
+ }
5733
+ catch {
5734
+ throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
5735
+ }
5736
+ if (result < 0n) {
5737
+ throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
5738
+ }
5739
+ return result;
5740
+ };
5741
+ /**
5742
+ * Determines whether an attestation has expired based on the current block number.
5743
+ *
5744
+ * An attestation expires when the destination chain's current block number is greater
5745
+ * than or equal to the expiration block specified in the attestation message.
5746
+ * Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
5747
+ *
5748
+ * @param attestation - The attestation message containing expiration block information
5749
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5750
+ * @returns `true` if the attestation has expired, `false` if still valid or never expires
5751
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5752
+ *
5753
+ * @example
5754
+ * ```typescript
5755
+ * import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
5756
+ *
5757
+ * // Check if attestation is expired on EVM chain
5758
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5759
+ * const currentBlock = await publicClient.getBlockNumber()
5760
+ * const expired = isAttestationExpired(attestation, currentBlock)
5761
+ *
5762
+ * if (expired) {
5763
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5764
+ * }
5765
+ * ```
5766
+ *
5767
+ * @example
5768
+ * ```typescript
5769
+ * // Check on Solana
5770
+ * const slot = await adapter.getConnection(destinationChain).getSlot()
5771
+ * const expired = isAttestationExpired(attestation, slot)
5772
+ * ```
5773
+ */
5774
+ const isAttestationExpired = (attestation, currentBlockNumber) => {
5775
+ const currentBlock = toBlockNumber(currentBlockNumber);
5776
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5777
+ // 0n means it never expires (re-attested messages and slow transfers)
5778
+ if (expiration === 0n) {
5779
+ return false;
5780
+ }
5781
+ // Attestation is expired if current block >= expiration block
5782
+ return currentBlock >= expiration;
5783
+ };
5784
+ /**
5785
+ * Calculates the number of blocks remaining until an attestation expires.
5786
+ *
5787
+ * Returns the difference between the expiration block and the current block number.
5788
+ * Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
5789
+ * Returns `0n` or a negative bigint if the attestation has already expired.
5790
+ *
5791
+ * @param attestation - The attestation message containing expiration block information
5792
+ * @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
5793
+ * @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
5794
+ * @throws KitError If currentBlockNumber or expirationBlock is invalid
5795
+ *
5796
+ * @example
5797
+ * ```typescript
5798
+ * import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
5799
+ *
5800
+ * const publicClient = await adapter.getPublicClient(destinationChain)
5801
+ * const currentBlock = await publicClient.getBlockNumber()
5802
+ * const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
5803
+ *
5804
+ * if (blocksRemaining === null) {
5805
+ * console.log('Attestation never expires')
5806
+ * } else if (blocksRemaining <= 0n) {
5807
+ * console.log('Attestation has expired')
5808
+ * } else {
5809
+ * console.log(`${blocksRemaining} blocks until expiry`)
5810
+ * }
5811
+ * ```
5812
+ */
5813
+ const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
5814
+ const currentBlock = toBlockNumber(currentBlockNumber);
5815
+ const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
5816
+ // 0n means it never expires (re-attested messages and slow transfers)
5817
+ if (expiration === 0n) {
5818
+ return null;
5819
+ }
5820
+ // Return the difference (can be negative if expired)
5821
+ return expiration - currentBlock;
5822
+ };
5823
+ /**
5824
+ * Determines whether a mint failure was caused by an expired attestation.
5825
+ *
5826
+ * This function inspects the error thrown during a mint operation to detect
5827
+ * if the failure is due to the attestation's expiration block being exceeded.
5828
+ * When this returns `true`, the caller should use `reAttest()` to obtain a
5829
+ * fresh attestation before retrying the mint.
5830
+ *
5831
+ * @param error - The error thrown during the mint operation
5832
+ * @returns `true` if the error indicates the attestation has expired, `false` otherwise
5833
+ *
5834
+ * @example
5835
+ * ```typescript
5836
+ * import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
5837
+ *
5838
+ * try {
5839
+ * await mintRequest.execute()
5840
+ * } catch (error) {
5841
+ * if (isMintFailureRelatedToAttestation(error)) {
5842
+ * // Attestation expired - get a fresh one
5843
+ * const freshAttestation = await provider.reAttest(source, burnTxHash)
5844
+ * const newMintRequest = await provider.mint(source, destination, freshAttestation)
5845
+ * await newMintRequest.execute()
5846
+ * } else {
5847
+ * throw error
5848
+ * }
5849
+ * }
5850
+ * ```
5851
+ */
5852
+ const isMintFailureRelatedToAttestation = (error) => {
5853
+ if (error === null || error === undefined) {
5854
+ return false;
5855
+ }
5856
+ const errorString = getErrorMessage(error).toLowerCase();
5857
+ // Check for Solana "MessageExpired" error pattern
5858
+ // Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
5859
+ // Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
5860
+ if (errorString.includes('messageexpired')) {
5861
+ return true;
5862
+ }
5863
+ // Check for EVM attestation expiry errors
5864
+ // Contract reverts with: "Message expired and must be re-signed"
5865
+ if (errorString.includes('message') && errorString.includes('expired')) {
5866
+ return true;
5867
+ }
5868
+ return false;
5869
+ };
5870
+
5392
5871
  /**
5393
5872
  * Checks if a decoded attestation field matches the corresponding transfer parameter.
5394
5873
  * If the values do not match, appends a descriptive error message to the errors array.
@@ -5839,6 +6318,7 @@ function dispatchStepEvent(name, step, provider) {
5839
6318
  });
5840
6319
  break;
5841
6320
  case 'fetchAttestation':
6321
+ case 'reAttest':
5842
6322
  provider.actionDispatcher.dispatch(name, {
5843
6323
  ...actionValues,
5844
6324
  method: name,
@@ -6232,21 +6712,64 @@ const validateBalanceForTransaction = async (params) => {
6232
6712
  // Extract chain name from operationContext
6233
6713
  const chainName = extractChainInfo(operationContext.chain).name;
6234
6714
  // 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;
6715
+ throw createInsufficientTokenBalanceError(chainName, token, {
6716
+ balance: balance.toString(),
6717
+ amount,
6718
+ tokenAddress,
6719
+ walletAddress: operationContext.address,
6720
+ });
6721
+ }
6722
+ };
6723
+
6724
+ /**
6725
+ * Validate that the adapter has sufficient native token balance for transaction fees.
6726
+ *
6727
+ * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
6728
+ * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
6729
+ * if the balance is zero, indicating the wallet cannot pay for transaction fees.
6730
+ *
6731
+ * @param params - The validation parameters containing adapter and operation context.
6732
+ * @returns A promise that resolves to void if validation passes.
6733
+ * @throws {KitError} When the adapter's native balance is zero (code: 9002).
6734
+ *
6735
+ * @example
6736
+ * ```typescript
6737
+ * import { validateNativeBalanceForTransaction } from '@core/adapter'
6738
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
6739
+ * import { isKitError, ERROR_TYPES } from '@core/errors'
6740
+ *
6741
+ * const adapter = createViemAdapterFromPrivateKey({
6742
+ * privateKey: '0x...',
6743
+ * chain: 'Ethereum',
6744
+ * })
6745
+ *
6746
+ * try {
6747
+ * await validateNativeBalanceForTransaction({
6748
+ * adapter,
6749
+ * operationContext: { chain: 'Ethereum' },
6750
+ * })
6751
+ * console.log('Native balance validation passed')
6752
+ * } catch (error) {
6753
+ * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
6754
+ * console.error('Insufficient gas funds:', error.message)
6755
+ * }
6756
+ * }
6757
+ * ```
6758
+ */
6759
+ const validateNativeBalanceForTransaction = async (params) => {
6760
+ const { adapter, operationContext } = params;
6761
+ const balancePrepared = await adapter.prepareAction('native.balanceOf', {
6762
+ walletAddress: operationContext.address,
6763
+ }, operationContext);
6764
+ const balance = await balancePrepared.execute();
6765
+ if (BigInt(balance) === 0n) {
6766
+ // Extract chain name from operationContext
6767
+ const chainName = extractChainInfo(operationContext.chain).name;
6768
+ // Create KitError with rich context in trace
6769
+ throw createInsufficientGasError(chainName, {
6770
+ balance: '0',
6771
+ walletAddress: operationContext.address,
6772
+ });
6250
6773
  }
6251
6774
  };
6252
6775
 
@@ -6262,6 +6785,7 @@ const CCTPv2StepName = {
6262
6785
  burn: 'burn',
6263
6786
  fetchAttestation: 'fetchAttestation',
6264
6787
  mint: 'mint',
6788
+ reAttest: 'reAttest',
6265
6789
  };
6266
6790
  /**
6267
6791
  * Conditional step transition rules for CCTP bridge flow.
@@ -6369,6 +6893,27 @@ const STEP_TRANSITION_RULES = {
6369
6893
  isActionable: false, // Waiting for pending transaction
6370
6894
  },
6371
6895
  ],
6896
+ // After ReAttest step
6897
+ [CCTPv2StepName.reAttest]: [
6898
+ {
6899
+ condition: (ctx) => ctx.lastStep?.state === 'success',
6900
+ nextStep: CCTPv2StepName.mint,
6901
+ reason: 'Re-attestation successful, proceed to mint',
6902
+ isActionable: true,
6903
+ },
6904
+ {
6905
+ condition: (ctx) => ctx.lastStep?.state === 'error',
6906
+ nextStep: CCTPv2StepName.mint,
6907
+ reason: 'Re-attestation failed, retry mint to re-initiate recovery',
6908
+ isActionable: true,
6909
+ },
6910
+ {
6911
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
6912
+ nextStep: CCTPv2StepName.mint,
6913
+ reason: 'Re-attestation pending, retry mint to re-initiate recovery',
6914
+ isActionable: true,
6915
+ },
6916
+ ],
6372
6917
  };
6373
6918
  /**
6374
6919
  * Analyze bridge steps to determine retry feasibility and continuation point.
@@ -6635,8 +7180,14 @@ function getBurnTxHash(result) {
6635
7180
  * ```
6636
7181
  */
6637
7182
  function getAttestationData(result) {
6638
- const step = findStepByName(result, CCTPv2StepName.fetchAttestation);
6639
- return step?.data;
7183
+ // Prefer reAttest data (most recent attestation after expiry)
7184
+ const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
7185
+ if (reAttestStep?.state === 'success' && reAttestStep.data) {
7186
+ return reAttestStep.data;
7187
+ }
7188
+ // Fall back to fetchAttestation step
7189
+ const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
7190
+ return fetchStep?.data;
6640
7191
  }
6641
7192
 
6642
7193
  /**
@@ -6834,7 +7385,7 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
6834
7385
  message: 'Cannot fetch attestation: burn transaction hash not found',
6835
7386
  });
6836
7387
  }
6837
- const sourceAddress = await context.from.getAddress(result.source.chain);
7388
+ const sourceAddress = result.source.address;
6838
7389
  const attestation = await provider.fetchAttestation({
6839
7390
  chain: result.source.chain,
6840
7391
  adapter: context.from,
@@ -6850,6 +7401,63 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
6850
7401
  return waitForPendingTransaction(pendingStep, adapter, chain);
6851
7402
  }
6852
7403
 
7404
+ /**
7405
+ * Executes a re-attestation operation to obtain a fresh attestation for an expired message.
7406
+ *
7407
+ * This function handles the re-attestation step of the CCTP v2 bridge process, where a fresh
7408
+ * attestation is requested from Circle's API when the original attestation has expired.
7409
+ * It first checks if the attestation has already been re-attested before making the API call.
7410
+ *
7411
+ * @param params - The bridge parameters containing source, destination, amount and optional config
7412
+ * @param provider - The CCTP v2 bridging provider
7413
+ * @param burnTxHash - The transaction hash of the original burn operation
7414
+ * @returns Promise resolving to the bridge step with fresh attestation data
7415
+ *
7416
+ * @example
7417
+ * ```typescript
7418
+ * const reAttestStep = await bridgeReAttest(
7419
+ * { params, provider },
7420
+ * burnTxHash
7421
+ * )
7422
+ * console.log('Fresh attestation:', reAttestStep.data)
7423
+ * ```
7424
+ */
7425
+ async function bridgeReAttest({ params, provider, }, burnTxHash) {
7426
+ const step = {
7427
+ name: 'reAttest',
7428
+ state: 'pending',
7429
+ };
7430
+ try {
7431
+ // Fetch current attestation to check if already re-attested
7432
+ const currentAttestation = await provider.fetchAttestation(params.source, burnTxHash);
7433
+ // Check if already re-attested (expirationBlock === '0' means never expires)
7434
+ const expirationBlock = currentAttestation.decodedMessage.decodedMessageBody.expirationBlock;
7435
+ if (expirationBlock === '0') {
7436
+ // Already re-attested - return current attestation without calling reAttest API
7437
+ return { ...step, state: 'success', data: currentAttestation };
7438
+ }
7439
+ // Not yet re-attested - proceed with re-attestation request
7440
+ const reAttestedAttestation = await provider.reAttest(params.source, burnTxHash);
7441
+ return { ...step, state: 'success', data: reAttestedAttestation };
7442
+ }
7443
+ catch (err) {
7444
+ let errorMessage = 'Unknown re-attestation error';
7445
+ if (err instanceof Error) {
7446
+ errorMessage = err.message;
7447
+ }
7448
+ else if (typeof err === 'string') {
7449
+ errorMessage = err;
7450
+ }
7451
+ return {
7452
+ ...step,
7453
+ state: 'error',
7454
+ error: err,
7455
+ errorMessage,
7456
+ data: undefined,
7457
+ };
7458
+ }
7459
+ }
7460
+
6853
7461
  /**
6854
7462
  * Extract context data from completed bridge steps for retry operations.
6855
7463
  *
@@ -6865,6 +7473,116 @@ function populateContext(result) {
6865
7473
  attestationData: getAttestationData(result),
6866
7474
  };
6867
7475
  }
7476
+ /**
7477
+ * Handle re-attestation and mint retry when mint fails due to expired attestation.
7478
+ *
7479
+ * @internal
7480
+ */
7481
+ async function handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result) {
7482
+ const burnTxHash = stepContext?.burnTxHash ?? getBurnTxHash(result);
7483
+ if (burnTxHash === undefined || burnTxHash === '') {
7484
+ handleStepError('mint', new Error('Cannot attempt re-attestation: Burn transaction hash not found in previous steps.'), result);
7485
+ return { success: false };
7486
+ }
7487
+ const reAttestStep = await bridgeReAttest({ params, provider }, burnTxHash);
7488
+ dispatchStepEvent('reAttest', reAttestStep, provider);
7489
+ result.steps.push(reAttestStep);
7490
+ if (reAttestStep.state === 'error') {
7491
+ result.state = 'error';
7492
+ return { success: false };
7493
+ }
7494
+ const freshContext = {
7495
+ ...stepContext,
7496
+ burnTxHash,
7497
+ attestationData: reAttestStep.data,
7498
+ };
7499
+ return executeMintRetry(params, provider, executor, freshContext, updateContext, result);
7500
+ }
7501
+ /**
7502
+ * Handle step execution error in retry loop.
7503
+ *
7504
+ * Determines if re-attestation should be attempted for mint failures,
7505
+ * or records the error and signals to exit the loop.
7506
+ *
7507
+ * @internal
7508
+ */
7509
+ async function handleStepExecutionError(name, error, context) {
7510
+ const { params, provider, executor, updateContext, stepContext, result } = context;
7511
+ const shouldAttemptReAttestation = name === 'mint' && isMintFailureRelatedToAttestation(error);
7512
+ if (!shouldAttemptReAttestation) {
7513
+ handleStepError(name, error, result);
7514
+ return { shouldContinue: false };
7515
+ }
7516
+ const reAttestResult = await handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result);
7517
+ if (!reAttestResult.success) {
7518
+ return { shouldContinue: false };
7519
+ }
7520
+ return { shouldContinue: true, stepContext: reAttestResult.stepContext };
7521
+ }
7522
+ /**
7523
+ * Execute mint retry with fresh attestation context.
7524
+ *
7525
+ * @internal
7526
+ */
7527
+ async function executeMintRetry(params, provider, executor, freshContext, updateContext, result) {
7528
+ try {
7529
+ const retryStep = await executor(params, provider, freshContext);
7530
+ if (retryStep.state === 'error') {
7531
+ throw new Error(retryStep.errorMessage ?? 'mint step returned error state');
7532
+ }
7533
+ dispatchStepEvent('mint', retryStep, provider);
7534
+ result.steps.push(retryStep);
7535
+ return { success: true, stepContext: updateContext?.(retryStep) };
7536
+ }
7537
+ catch (retryError) {
7538
+ if (isMintFailureRelatedToAttestation(retryError)) {
7539
+ const kitError = createSimulationFailedError(result.destination.chain.name, getErrorMessage(retryError), { error: retryError });
7540
+ handleStepError('mint', kitError, result);
7541
+ }
7542
+ else {
7543
+ handleStepError('mint', retryError, result);
7544
+ }
7545
+ return { success: false };
7546
+ }
7547
+ }
7548
+ /**
7549
+ * Execute remaining bridge steps starting from a specific index.
7550
+ *
7551
+ * Handles the step execution loop with error handling and re-attestation support.
7552
+ * Returns true if all steps completed successfully, false if stopped due to error.
7553
+ *
7554
+ * @internal
7555
+ */
7556
+ async function executeSteps(params, provider, result, startIndex) {
7557
+ let stepContext = populateContext(result);
7558
+ for (const { name, executor, updateContext } of stepExecutors.slice(startIndex)) {
7559
+ try {
7560
+ const step = await executor(params, provider, stepContext);
7561
+ if (step.state === 'error') {
7562
+ const errorMessage = step.errorMessage ?? `${name} step returned error state`;
7563
+ throw new Error(errorMessage);
7564
+ }
7565
+ stepContext = updateContext?.(step);
7566
+ dispatchStepEvent(name, step, provider);
7567
+ result.steps.push(step);
7568
+ }
7569
+ catch (error) {
7570
+ const errorResult = await handleStepExecutionError(name, error, {
7571
+ params,
7572
+ provider,
7573
+ executor,
7574
+ updateContext,
7575
+ stepContext,
7576
+ result,
7577
+ });
7578
+ if (!errorResult.shouldContinue) {
7579
+ return false;
7580
+ }
7581
+ stepContext = errorResult.stepContext;
7582
+ }
7583
+ }
7584
+ return true;
7585
+ }
6868
7586
  /**
6869
7587
  * Retry a failed or incomplete CCTP v2 bridge operation from where it left off.
6870
7588
  *
@@ -6886,15 +7604,12 @@ function populateContext(result) {
6886
7604
  async function retry(result, context, provider) {
6887
7605
  const analysis = analyzeSteps(result);
6888
7606
  if (!analysis.isActionable) {
6889
- // Terminal completion - bridge already complete, return gracefully
6890
7607
  if (analysis.continuationStep === null && result.state === 'success') {
6891
7608
  return result;
6892
7609
  }
6893
- // Pending states - wait for the pending operation to complete
6894
7610
  if (hasPendingState(analysis, result)) {
6895
7611
  return handlePendingState(result, context, provider, analysis);
6896
7612
  }
6897
- // No valid continuation - cannot proceed
6898
7613
  throw new Error('Retry not supported for this result, requires user action');
6899
7614
  }
6900
7615
  if (!isCCTPV2Supported(result.source.chain)) {
@@ -6921,25 +7636,10 @@ async function retry(result, context, provider) {
6921
7636
  if (indexOfSteps === -1) {
6922
7637
  throw new Error(`Continuation step ${analysis.continuationStep ?? ''} not found`);
6923
7638
  }
6924
- let stepContext = populateContext(result);
6925
- // Execute each step in sequence
6926
- for (const { name, executor, updateContext } of stepExecutors.slice(indexOfSteps)) {
6927
- try {
6928
- const step = await executor(params, provider, stepContext);
6929
- if (step.state === 'error') {
6930
- const errorMessage = step.errorMessage ?? `${name} step returned error state`;
6931
- throw new Error(errorMessage);
6932
- }
6933
- stepContext = updateContext?.(step);
6934
- dispatchStepEvent(name, step, provider);
6935
- result.steps.push(step);
6936
- }
6937
- catch (error) {
6938
- handleStepError(name, error, result);
6939
- return result;
6940
- }
7639
+ const completed = await executeSteps(params, provider, result, indexOfSteps);
7640
+ if (completed) {
7641
+ result.state = 'success';
6941
7642
  }
6942
- result.state = 'success';
6943
7643
  return result;
6944
7644
  }
6945
7645
  /**
@@ -6959,7 +7659,7 @@ async function retry(result, context, provider) {
6959
7659
  * @returns Updated bridge result after pending operation completes.
6960
7660
  */
6961
7661
  async function handlePendingState(result, context, provider, analysis) {
6962
- if (!analysis.continuationStep) {
7662
+ if (analysis.continuationStep === null || analysis.continuationStep === '') {
6963
7663
  // This should not be reachable due to the `hasPendingState` check,
6964
7664
  // but it ensures type safety for `continuationStep`.
6965
7665
  throw new KitError({
@@ -7123,16 +7823,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7123
7823
  async bridge(params) {
7124
7824
  // CCTP-specific bridge params validation (includes base validation)
7125
7825
  assertCCTPv2BridgeParams(params);
7126
- const { source, amount, token } = params;
7826
+ const { source, destination, amount, token } = params;
7127
7827
  // Extract operation context from source wallet context for balance validation
7128
- const operationContext = this.extractOperationContext(source);
7129
- // Validate balance for transaction
7828
+ const sourceOperationContext = this.extractOperationContext(source);
7829
+ // Validate USDC balance for transaction on source chain
7130
7830
  await validateBalanceForTransaction({
7131
7831
  adapter: source.adapter,
7132
7832
  amount,
7133
7833
  token,
7134
7834
  tokenAddress: source.chain.usdcAddress,
7135
- operationContext,
7835
+ operationContext: sourceOperationContext,
7836
+ });
7837
+ // Validate native balance > 0 for gas fees on source chain
7838
+ await validateNativeBalanceForTransaction({
7839
+ adapter: source.adapter,
7840
+ operationContext: sourceOperationContext,
7841
+ });
7842
+ // Extract operation context from destination wallet context
7843
+ const destinationOperationContext = this.extractOperationContext(destination);
7844
+ // Validate native balance > 0 for gas fees on destination chain
7845
+ await validateNativeBalanceForTransaction({
7846
+ adapter: destination.adapter,
7847
+ operationContext: destinationOperationContext,
7136
7848
  });
7137
7849
  return bridge(params, this);
7138
7850
  }
@@ -7581,8 +8293,10 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7581
8293
  }
7582
8294
  // Step 2: Request re-attestation
7583
8295
  await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
7584
- // Step 3: Poll for fresh attestation
7585
- const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
8296
+ // Step 3: Poll for fresh attestation until expirationBlock === '0'
8297
+ // The expiration block transitions from non-zero to zero when Circle
8298
+ // completes processing the re-attestation request.
8299
+ const response = await fetchReAttestedAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
7586
8300
  const message = response.messages[0];
7587
8301
  if (!message) {
7588
8302
  throw new Error('Failed to re-attest: No attestation found after re-attestation request');
@@ -7792,5 +8506,8 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7792
8506
  }
7793
8507
 
7794
8508
  exports.CCTPV2BridgingProvider = CCTPV2BridgingProvider;
8509
+ exports.getBlocksUntilExpiry = getBlocksUntilExpiry;
7795
8510
  exports.getMintRecipientAccount = getMintRecipientAccount;
8511
+ exports.isAttestationExpired = isAttestationExpired;
8512
+ exports.isMintFailureRelatedToAttestation = isMintFailureRelatedToAttestation;
7796
8513
  //# sourceMappingURL=index.cjs.map