@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 +7 -0
- package/index.cjs +437 -30
- package/index.d.ts +12 -0
- package/index.mjs +437 -30
- package/package.json +1 -1
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) =>
|
|
3356
|
-
|
|
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
|
-
|
|
6934
|
-
|
|
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 =
|
|
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
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
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 (
|
|
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
|
-
|
|
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) =>
|
|
3350
|
-
|
|
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
|
-
|
|
6928
|
-
|
|
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 =
|
|
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
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
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 (
|
|
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
|
-
|
|
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');
|