@circle-fin/provider-cctp-v2 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @circle-fin/provider-cctp-v2
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Integrate automatic re-attestation into bridge orchestrator retry logic for CCTP v2 fast transfers:
8
+ - Automatically detect mint failures due to expired attestations and trigger re-attestation flow
9
+
3
10
  ## 1.2.0
4
11
 
5
12
  ### Minor Changes
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";
@@ -1177,6 +1181,86 @@ const LineaSepolia = defineChain({
1177
1181
  },
1178
1182
  });
1179
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
+
1180
1264
  /**
1181
1265
  * NEAR Protocol Mainnet chain definition
1182
1266
  * @remarks
@@ -2261,6 +2345,8 @@ var Chains = {
2261
2345
  InkTestnet: InkTestnet,
2262
2346
  Linea: Linea,
2263
2347
  LineaSepolia: LineaSepolia,
2348
+ Monad: Monad,
2349
+ MonadTestnet: MonadTestnet,
2264
2350
  NEAR: NEAR,
2265
2351
  NEARTestnet: NEARTestnet,
2266
2352
  Noble: Noble,
@@ -3284,6 +3370,8 @@ const ERROR_TYPES = {
3284
3370
  RPC: 'RPC',
3285
3371
  /** Internet connectivity, DNS resolution, connection issues */
3286
3372
  NETWORK: 'NETWORK',
3373
+ /** Catch-all for unrecognized errors (code 0) */
3374
+ UNKNOWN: 'UNKNOWN',
3287
3375
  };
3288
3376
  /**
3289
3377
  * Array of valid error type values for validation.
@@ -3297,6 +3385,8 @@ const ERROR_TYPE_ARRAY = [...ERROR_TYPE_VALUES];
3297
3385
  /**
3298
3386
  * Error code ranges for validation.
3299
3387
  * Single source of truth for valid error code ranges.
3388
+ *
3389
+ * Note: Code 0 is special - it's the UNKNOWN catch-all error.
3300
3390
  */
3301
3391
  const ERROR_CODE_RANGES = [
3302
3392
  { min: 1000, max: 1999, type: 'INPUT' },
@@ -3305,6 +3395,8 @@ const ERROR_CODE_RANGES = [
3305
3395
  { min: 5000, max: 5999, type: 'ONCHAIN' },
3306
3396
  { min: 9000, max: 9999, type: 'BALANCE' },
3307
3397
  ];
3398
+ /** Special code for UNKNOWN errors */
3399
+ const UNKNOWN_ERROR_CODE = 0;
3308
3400
  /**
3309
3401
  * Zod schema for validating ErrorDetails objects.
3310
3402
  *
@@ -3343,6 +3435,7 @@ const ERROR_CODE_RANGES = [
3343
3435
  const errorDetailsSchema = zod.z.object({
3344
3436
  /**
3345
3437
  * Numeric identifier following standardized ranges:
3438
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3346
3439
  * - 1000-1999: INPUT errors - Parameter validation
3347
3440
  * - 3000-3999: NETWORK errors - Connectivity issues
3348
3441
  * - 4000-4999: RPC errors - Provider issues, gas estimation
@@ -3352,8 +3445,9 @@ const errorDetailsSchema = zod.z.object({
3352
3445
  code: zod.z
3353
3446
  .number()
3354
3447
  .int('Error code must be an integer')
3355
- .refine((code) => ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
3356
- 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)',
3357
3451
  }),
3358
3452
  /** Human-readable ID (e.g., "INPUT_NETWORK_MISMATCH", "BALANCE_INSUFFICIENT_TOKEN") */
3359
3453
  name: zod.z
@@ -3363,7 +3457,7 @@ const errorDetailsSchema = zod.z.object({
3363
3457
  /** Error category indicating where the error originated */
3364
3458
  type: zod.z.enum(ERROR_TYPE_ARRAY, {
3365
3459
  errorMap: () => ({
3366
- 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',
3367
3461
  }),
3368
3462
  }),
3369
3463
  /** Error handling strategy */
@@ -3564,6 +3658,7 @@ class KitError extends Error {
3564
3658
  /**
3565
3659
  * Standardized error code ranges for consistent categorization:
3566
3660
  *
3661
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3567
3662
  * - 1000-1999: INPUT errors - Parameter validation, input format errors
3568
3663
  * - 3000-3999: NETWORK errors - Internet connectivity, DNS, connection issues
3569
3664
  * - 4000-4999: RPC errors - Blockchain provider issues, gas estimation, nonce errors
@@ -3654,6 +3749,31 @@ const BalanceError = {
3654
3749
  name: 'BALANCE_INSUFFICIENT_GAS',
3655
3750
  type: 'BALANCE',
3656
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',
3776
+ }};
3657
3777
 
3658
3778
  /**
3659
3779
  * Creates error for network type mismatch between source and destination.
@@ -3958,6 +4078,51 @@ function createInsufficientGasError(chain, trace) {
3958
4078
  });
3959
4079
  }
3960
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,
4121
+ },
4122
+ },
4123
+ });
4124
+ }
4125
+
3961
4126
  /**
3962
4127
  * Type guard to check if an error is a KitError instance.
3963
4128
  *
@@ -5162,6 +5327,68 @@ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHas
5162
5327
  };
5163
5328
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
5164
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
+ };
5165
5392
  /**
5166
5393
  * Builds the IRIS API URL for re-attestation requests.
5167
5394
  *
@@ -6091,6 +6318,7 @@ function dispatchStepEvent(name, step, provider) {
6091
6318
  });
6092
6319
  break;
6093
6320
  case 'fetchAttestation':
6321
+ case 'reAttest':
6094
6322
  provider.actionDispatcher.dispatch(name, {
6095
6323
  ...actionValues,
6096
6324
  method: name,
@@ -6557,6 +6785,7 @@ const CCTPv2StepName = {
6557
6785
  burn: 'burn',
6558
6786
  fetchAttestation: 'fetchAttestation',
6559
6787
  mint: 'mint',
6788
+ reAttest: 'reAttest',
6560
6789
  };
6561
6790
  /**
6562
6791
  * Conditional step transition rules for CCTP bridge flow.
@@ -6664,6 +6893,27 @@ const STEP_TRANSITION_RULES = {
6664
6893
  isActionable: false, // Waiting for pending transaction
6665
6894
  },
6666
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
+ ],
6667
6917
  };
6668
6918
  /**
6669
6919
  * Analyze bridge steps to determine retry feasibility and continuation point.
@@ -6930,8 +7180,14 @@ function getBurnTxHash(result) {
6930
7180
  * ```
6931
7181
  */
6932
7182
  function getAttestationData(result) {
6933
- const step = findStepByName(result, CCTPv2StepName.fetchAttestation);
6934
- 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;
6935
7191
  }
6936
7192
 
6937
7193
  /**
@@ -7129,7 +7385,7 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
7129
7385
  message: 'Cannot fetch attestation: burn transaction hash not found',
7130
7386
  });
7131
7387
  }
7132
- const sourceAddress = await context.from.getAddress(result.source.chain);
7388
+ const sourceAddress = result.source.address;
7133
7389
  const attestation = await provider.fetchAttestation({
7134
7390
  chain: result.source.chain,
7135
7391
  adapter: context.from,
@@ -7145,6 +7401,63 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
7145
7401
  return waitForPendingTransaction(pendingStep, adapter, chain);
7146
7402
  }
7147
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
+
7148
7461
  /**
7149
7462
  * Extract context data from completed bridge steps for retry operations.
7150
7463
  *
@@ -7160,6 +7473,116 @@ function populateContext(result) {
7160
7473
  attestationData: getAttestationData(result),
7161
7474
  };
7162
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
+ }
7163
7586
  /**
7164
7587
  * Retry a failed or incomplete CCTP v2 bridge operation from where it left off.
7165
7588
  *
@@ -7181,15 +7604,12 @@ function populateContext(result) {
7181
7604
  async function retry(result, context, provider) {
7182
7605
  const analysis = analyzeSteps(result);
7183
7606
  if (!analysis.isActionable) {
7184
- // Terminal completion - bridge already complete, return gracefully
7185
7607
  if (analysis.continuationStep === null && result.state === 'success') {
7186
7608
  return result;
7187
7609
  }
7188
- // Pending states - wait for the pending operation to complete
7189
7610
  if (hasPendingState(analysis, result)) {
7190
7611
  return handlePendingState(result, context, provider, analysis);
7191
7612
  }
7192
- // No valid continuation - cannot proceed
7193
7613
  throw new Error('Retry not supported for this result, requires user action');
7194
7614
  }
7195
7615
  if (!isCCTPV2Supported(result.source.chain)) {
@@ -7216,25 +7636,10 @@ async function retry(result, context, provider) {
7216
7636
  if (indexOfSteps === -1) {
7217
7637
  throw new Error(`Continuation step ${analysis.continuationStep ?? ''} not found`);
7218
7638
  }
7219
- let stepContext = populateContext(result);
7220
- // Execute each step in sequence
7221
- for (const { name, executor, updateContext } of stepExecutors.slice(indexOfSteps)) {
7222
- try {
7223
- const step = await executor(params, provider, stepContext);
7224
- if (step.state === 'error') {
7225
- const errorMessage = step.errorMessage ?? `${name} step returned error state`;
7226
- throw new Error(errorMessage);
7227
- }
7228
- stepContext = updateContext?.(step);
7229
- dispatchStepEvent(name, step, provider);
7230
- result.steps.push(step);
7231
- }
7232
- catch (error) {
7233
- handleStepError(name, error, result);
7234
- return result;
7235
- }
7639
+ const completed = await executeSteps(params, provider, result, indexOfSteps);
7640
+ if (completed) {
7641
+ result.state = 'success';
7236
7642
  }
7237
- result.state = 'success';
7238
7643
  return result;
7239
7644
  }
7240
7645
  /**
@@ -7254,7 +7659,7 @@ async function retry(result, context, provider) {
7254
7659
  * @returns Updated bridge result after pending operation completes.
7255
7660
  */
7256
7661
  async function handlePendingState(result, context, provider, analysis) {
7257
- if (!analysis.continuationStep) {
7662
+ if (analysis.continuationStep === null || analysis.continuationStep === '') {
7258
7663
  // This should not be reachable due to the `hasPendingState` check,
7259
7664
  // but it ensures type safety for `continuationStep`.
7260
7665
  throw new KitError({
@@ -7888,8 +8293,10 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7888
8293
  }
7889
8294
  // Step 2: Request re-attestation
7890
8295
  await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
7891
- // Step 3: Poll for fresh attestation
7892
- 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);
7893
8300
  const message = response.messages[0];
7894
8301
  if (!message) {
7895
8302
  throw new Error('Failed to re-attest: No attestation found after re-attestation request');
package/index.d.ts CHANGED
@@ -435,6 +435,8 @@ declare enum Blockchain {
435
435
  Ink_Testnet = "Ink_Testnet",
436
436
  Linea = "Linea",
437
437
  Linea_Sepolia = "Linea_Sepolia",
438
+ Monad = "Monad",
439
+ Monad_Testnet = "Monad_Testnet",
438
440
  NEAR = "NEAR",
439
441
  NEAR_Testnet = "NEAR_Testnet",
440
442
  Noble = "Noble",
@@ -3407,6 +3409,16 @@ interface CCTPV2Actions {
3407
3409
  method: 'mint';
3408
3410
  values: BridgeStep;
3409
3411
  };
3412
+ /**
3413
+ * Re-attestation action for CCTP v2 transfers.
3414
+ * Used to request a fresh attestation when the original has expired.
3415
+ */
3416
+ reAttest: {
3417
+ protocol: 'cctp';
3418
+ version: 'v2';
3419
+ method: 'reAttest';
3420
+ values: BridgeFetchAttestationStep;
3421
+ };
3410
3422
  }
3411
3423
  /**
3412
3424
  * CCTPv2 bridging provider interface.
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";
@@ -1171,6 +1175,86 @@ const LineaSepolia = defineChain({
1171
1175
  },
1172
1176
  });
1173
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
+
1174
1258
  /**
1175
1259
  * NEAR Protocol Mainnet chain definition
1176
1260
  * @remarks
@@ -2255,6 +2339,8 @@ var Chains = /*#__PURE__*/Object.freeze({
2255
2339
  InkTestnet: InkTestnet,
2256
2340
  Linea: Linea,
2257
2341
  LineaSepolia: LineaSepolia,
2342
+ Monad: Monad,
2343
+ MonadTestnet: MonadTestnet,
2258
2344
  NEAR: NEAR,
2259
2345
  NEARTestnet: NEARTestnet,
2260
2346
  Noble: Noble,
@@ -3278,6 +3364,8 @@ const ERROR_TYPES = {
3278
3364
  RPC: 'RPC',
3279
3365
  /** Internet connectivity, DNS resolution, connection issues */
3280
3366
  NETWORK: 'NETWORK',
3367
+ /** Catch-all for unrecognized errors (code 0) */
3368
+ UNKNOWN: 'UNKNOWN',
3281
3369
  };
3282
3370
  /**
3283
3371
  * Array of valid error type values for validation.
@@ -3291,6 +3379,8 @@ const ERROR_TYPE_ARRAY = [...ERROR_TYPE_VALUES];
3291
3379
  /**
3292
3380
  * Error code ranges for validation.
3293
3381
  * Single source of truth for valid error code ranges.
3382
+ *
3383
+ * Note: Code 0 is special - it's the UNKNOWN catch-all error.
3294
3384
  */
3295
3385
  const ERROR_CODE_RANGES = [
3296
3386
  { min: 1000, max: 1999, type: 'INPUT' },
@@ -3299,6 +3389,8 @@ const ERROR_CODE_RANGES = [
3299
3389
  { min: 5000, max: 5999, type: 'ONCHAIN' },
3300
3390
  { min: 9000, max: 9999, type: 'BALANCE' },
3301
3391
  ];
3392
+ /** Special code for UNKNOWN errors */
3393
+ const UNKNOWN_ERROR_CODE = 0;
3302
3394
  /**
3303
3395
  * Zod schema for validating ErrorDetails objects.
3304
3396
  *
@@ -3337,6 +3429,7 @@ const ERROR_CODE_RANGES = [
3337
3429
  const errorDetailsSchema = z.object({
3338
3430
  /**
3339
3431
  * Numeric identifier following standardized ranges:
3432
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3340
3433
  * - 1000-1999: INPUT errors - Parameter validation
3341
3434
  * - 3000-3999: NETWORK errors - Connectivity issues
3342
3435
  * - 4000-4999: RPC errors - Provider issues, gas estimation
@@ -3346,8 +3439,9 @@ const errorDetailsSchema = z.object({
3346
3439
  code: z
3347
3440
  .number()
3348
3441
  .int('Error code must be an integer')
3349
- .refine((code) => ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
3350
- 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)',
3351
3445
  }),
3352
3446
  /** Human-readable ID (e.g., "INPUT_NETWORK_MISMATCH", "BALANCE_INSUFFICIENT_TOKEN") */
3353
3447
  name: z
@@ -3357,7 +3451,7 @@ const errorDetailsSchema = z.object({
3357
3451
  /** Error category indicating where the error originated */
3358
3452
  type: z.enum(ERROR_TYPE_ARRAY, {
3359
3453
  errorMap: () => ({
3360
- 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',
3361
3455
  }),
3362
3456
  }),
3363
3457
  /** Error handling strategy */
@@ -3558,6 +3652,7 @@ class KitError extends Error {
3558
3652
  /**
3559
3653
  * Standardized error code ranges for consistent categorization:
3560
3654
  *
3655
+ * - 0: UNKNOWN - Catch-all for unrecognized errors
3561
3656
  * - 1000-1999: INPUT errors - Parameter validation, input format errors
3562
3657
  * - 3000-3999: NETWORK errors - Internet connectivity, DNS, connection issues
3563
3658
  * - 4000-4999: RPC errors - Blockchain provider issues, gas estimation, nonce errors
@@ -3648,6 +3743,31 @@ const BalanceError = {
3648
3743
  name: 'BALANCE_INSUFFICIENT_GAS',
3649
3744
  type: 'BALANCE',
3650
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',
3770
+ }};
3651
3771
 
3652
3772
  /**
3653
3773
  * Creates error for network type mismatch between source and destination.
@@ -3952,6 +4072,51 @@ function createInsufficientGasError(chain, trace) {
3952
4072
  });
3953
4073
  }
3954
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,
4115
+ },
4116
+ },
4117
+ });
4118
+ }
4119
+
3955
4120
  /**
3956
4121
  * Type guard to check if an error is a KitError instance.
3957
4122
  *
@@ -5156,6 +5321,68 @@ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHas
5156
5321
  };
5157
5322
  return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
5158
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
+ };
5159
5386
  /**
5160
5387
  * Builds the IRIS API URL for re-attestation requests.
5161
5388
  *
@@ -6085,6 +6312,7 @@ function dispatchStepEvent(name, step, provider) {
6085
6312
  });
6086
6313
  break;
6087
6314
  case 'fetchAttestation':
6315
+ case 'reAttest':
6088
6316
  provider.actionDispatcher.dispatch(name, {
6089
6317
  ...actionValues,
6090
6318
  method: name,
@@ -6551,6 +6779,7 @@ const CCTPv2StepName = {
6551
6779
  burn: 'burn',
6552
6780
  fetchAttestation: 'fetchAttestation',
6553
6781
  mint: 'mint',
6782
+ reAttest: 'reAttest',
6554
6783
  };
6555
6784
  /**
6556
6785
  * Conditional step transition rules for CCTP bridge flow.
@@ -6658,6 +6887,27 @@ const STEP_TRANSITION_RULES = {
6658
6887
  isActionable: false, // Waiting for pending transaction
6659
6888
  },
6660
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
+ ],
6661
6911
  };
6662
6912
  /**
6663
6913
  * Analyze bridge steps to determine retry feasibility and continuation point.
@@ -6924,8 +7174,14 @@ function getBurnTxHash(result) {
6924
7174
  * ```
6925
7175
  */
6926
7176
  function getAttestationData(result) {
6927
- const step = findStepByName(result, CCTPv2StepName.fetchAttestation);
6928
- 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;
6929
7185
  }
6930
7186
 
6931
7187
  /**
@@ -7123,7 +7379,7 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
7123
7379
  message: 'Cannot fetch attestation: burn transaction hash not found',
7124
7380
  });
7125
7381
  }
7126
- const sourceAddress = await context.from.getAddress(result.source.chain);
7382
+ const sourceAddress = result.source.address;
7127
7383
  const attestation = await provider.fetchAttestation({
7128
7384
  chain: result.source.chain,
7129
7385
  adapter: context.from,
@@ -7139,6 +7395,63 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
7139
7395
  return waitForPendingTransaction(pendingStep, adapter, chain);
7140
7396
  }
7141
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
+
7142
7455
  /**
7143
7456
  * Extract context data from completed bridge steps for retry operations.
7144
7457
  *
@@ -7154,6 +7467,116 @@ function populateContext(result) {
7154
7467
  attestationData: getAttestationData(result),
7155
7468
  };
7156
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
+ }
7157
7580
  /**
7158
7581
  * Retry a failed or incomplete CCTP v2 bridge operation from where it left off.
7159
7582
  *
@@ -7175,15 +7598,12 @@ function populateContext(result) {
7175
7598
  async function retry(result, context, provider) {
7176
7599
  const analysis = analyzeSteps(result);
7177
7600
  if (!analysis.isActionable) {
7178
- // Terminal completion - bridge already complete, return gracefully
7179
7601
  if (analysis.continuationStep === null && result.state === 'success') {
7180
7602
  return result;
7181
7603
  }
7182
- // Pending states - wait for the pending operation to complete
7183
7604
  if (hasPendingState(analysis, result)) {
7184
7605
  return handlePendingState(result, context, provider, analysis);
7185
7606
  }
7186
- // No valid continuation - cannot proceed
7187
7607
  throw new Error('Retry not supported for this result, requires user action');
7188
7608
  }
7189
7609
  if (!isCCTPV2Supported(result.source.chain)) {
@@ -7210,25 +7630,10 @@ async function retry(result, context, provider) {
7210
7630
  if (indexOfSteps === -1) {
7211
7631
  throw new Error(`Continuation step ${analysis.continuationStep ?? ''} not found`);
7212
7632
  }
7213
- let stepContext = populateContext(result);
7214
- // Execute each step in sequence
7215
- for (const { name, executor, updateContext } of stepExecutors.slice(indexOfSteps)) {
7216
- try {
7217
- const step = await executor(params, provider, stepContext);
7218
- if (step.state === 'error') {
7219
- const errorMessage = step.errorMessage ?? `${name} step returned error state`;
7220
- throw new Error(errorMessage);
7221
- }
7222
- stepContext = updateContext?.(step);
7223
- dispatchStepEvent(name, step, provider);
7224
- result.steps.push(step);
7225
- }
7226
- catch (error) {
7227
- handleStepError(name, error, result);
7228
- return result;
7229
- }
7633
+ const completed = await executeSteps(params, provider, result, indexOfSteps);
7634
+ if (completed) {
7635
+ result.state = 'success';
7230
7636
  }
7231
- result.state = 'success';
7232
7637
  return result;
7233
7638
  }
7234
7639
  /**
@@ -7248,7 +7653,7 @@ async function retry(result, context, provider) {
7248
7653
  * @returns Updated bridge result after pending operation completes.
7249
7654
  */
7250
7655
  async function handlePendingState(result, context, provider, analysis) {
7251
- if (!analysis.continuationStep) {
7656
+ if (analysis.continuationStep === null || analysis.continuationStep === '') {
7252
7657
  // This should not be reachable due to the `hasPendingState` check,
7253
7658
  // but it ensures type safety for `continuationStep`.
7254
7659
  throw new KitError({
@@ -7882,8 +8287,10 @@ class CCTPV2BridgingProvider extends BridgingProvider {
7882
8287
  }
7883
8288
  // Step 2: Request re-attestation
7884
8289
  await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
7885
- // Step 3: Poll for fresh attestation
7886
- 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);
7887
8294
  const message = response.messages[0];
7888
8295
  if (!message) {
7889
8296
  throw new Error('Failed to re-attest: No attestation found after re-attestation request');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circle-fin/provider-cctp-v2",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
5
5
  "keywords": [
6
6
  "circle",