@circle-fin/provider-cctp-v2 1.2.0 → 1.3.1
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 +13 -0
- package/index.cjs +411 -117
- package/index.d.ts +12 -0
- package/index.mjs +411 -117
- package/package.json +1 -1
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
|
|
@@ -3647,12 +3742,31 @@ const BalanceError = {
|
|
|
3647
3742
|
code: 9001,
|
|
3648
3743
|
name: 'BALANCE_INSUFFICIENT_TOKEN',
|
|
3649
3744
|
type: 'BALANCE',
|
|
3650
|
-
}
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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',
|
|
3656
3770
|
}};
|
|
3657
3771
|
|
|
3658
3772
|
/**
|
|
@@ -3914,45 +4028,47 @@ function createInsufficientTokenBalanceError(chain, token, trace) {
|
|
|
3914
4028
|
},
|
|
3915
4029
|
});
|
|
3916
4030
|
}
|
|
4031
|
+
|
|
3917
4032
|
/**
|
|
3918
|
-
* Creates error for
|
|
4033
|
+
* Creates error for transaction simulation failures.
|
|
3919
4034
|
*
|
|
3920
|
-
* This error is thrown when a
|
|
3921
|
-
*
|
|
3922
|
-
* as it
|
|
4035
|
+
* This error is thrown when a pre-flight transaction simulation fails,
|
|
4036
|
+
* typically due to contract logic that would revert. The error is FATAL
|
|
4037
|
+
* as it indicates the transaction would fail if submitted.
|
|
3923
4038
|
*
|
|
3924
|
-
* @param chain - The blockchain network where the
|
|
4039
|
+
* @param chain - The blockchain network where the simulation failed
|
|
4040
|
+
* @param reason - The reason for simulation failure (e.g., revert message)
|
|
3925
4041
|
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3926
|
-
* @returns KitError with
|
|
4042
|
+
* @returns KitError with simulation failure details
|
|
3927
4043
|
*
|
|
3928
4044
|
* @example
|
|
3929
4045
|
* ```typescript
|
|
3930
|
-
* import {
|
|
4046
|
+
* import { createSimulationFailedError } from '@core/errors'
|
|
3931
4047
|
*
|
|
3932
|
-
* throw
|
|
3933
|
-
* // Message: "
|
|
4048
|
+
* throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance')
|
|
4049
|
+
* // Message: "Simulation failed on Ethereum: ERC20: insufficient allowance"
|
|
3934
4050
|
* ```
|
|
3935
4051
|
*
|
|
3936
4052
|
* @example
|
|
3937
4053
|
* ```typescript
|
|
3938
4054
|
* // With trace context for debugging
|
|
3939
|
-
* throw
|
|
4055
|
+
* throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance', {
|
|
3940
4056
|
* rawError: error,
|
|
3941
|
-
*
|
|
3942
|
-
*
|
|
3943
|
-
* walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
4057
|
+
* txHash: '0x1234...',
|
|
4058
|
+
* gasLimit: '21000',
|
|
3944
4059
|
* })
|
|
3945
4060
|
* ```
|
|
3946
4061
|
*/
|
|
3947
|
-
function
|
|
4062
|
+
function createSimulationFailedError(chain, reason, trace) {
|
|
3948
4063
|
return new KitError({
|
|
3949
|
-
...
|
|
4064
|
+
...OnchainError.SIMULATION_FAILED,
|
|
3950
4065
|
recoverability: 'FATAL',
|
|
3951
|
-
message: `
|
|
4066
|
+
message: `Simulation failed on ${chain}: ${reason}`,
|
|
3952
4067
|
cause: {
|
|
3953
4068
|
trace: {
|
|
3954
4069
|
...trace,
|
|
3955
4070
|
chain,
|
|
4071
|
+
reason,
|
|
3956
4072
|
},
|
|
3957
4073
|
},
|
|
3958
4074
|
});
|
|
@@ -5162,6 +5278,68 @@ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHas
|
|
|
5162
5278
|
};
|
|
5163
5279
|
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
5164
5280
|
};
|
|
5281
|
+
/**
|
|
5282
|
+
* Type guard that validates attestation response has expirationBlock === '0'.
|
|
5283
|
+
*
|
|
5284
|
+
* This is used after requestReAttestation() to poll until the attestation
|
|
5285
|
+
* is fully re-processed and has a zero expiration block (never expires).
|
|
5286
|
+
* The expiration block transitions from non-zero to zero when Circle
|
|
5287
|
+
* completes processing the re-attestation request.
|
|
5288
|
+
*
|
|
5289
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5290
|
+
* @returns True if the attestation has expirationBlock === '0'
|
|
5291
|
+
* @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
|
|
5292
|
+
*
|
|
5293
|
+
* @example
|
|
5294
|
+
* ```typescript
|
|
5295
|
+
* // After requesting re-attestation, use this to validate the response
|
|
5296
|
+
* const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
|
|
5297
|
+
* // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
|
|
5298
|
+
* ```
|
|
5299
|
+
*
|
|
5300
|
+
* @internal
|
|
5301
|
+
*/
|
|
5302
|
+
const isReAttestedAttestationResponse = (obj) => {
|
|
5303
|
+
// First validate the basic structure and completion status
|
|
5304
|
+
// This will throw appropriate errors for invalid structure or incomplete attestation
|
|
5305
|
+
if (!isAttestationResponse(obj)) ;
|
|
5306
|
+
// Check if the first message has expirationBlock === '0'
|
|
5307
|
+
const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
|
|
5308
|
+
if (expirationBlock !== '0') {
|
|
5309
|
+
// Re-attestation not yet complete - allow retry via polling
|
|
5310
|
+
throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
|
|
5311
|
+
}
|
|
5312
|
+
return true;
|
|
5313
|
+
};
|
|
5314
|
+
/**
|
|
5315
|
+
* Fetches attestation data and polls until expirationBlock === '0'.
|
|
5316
|
+
*
|
|
5317
|
+
* This function is used after calling requestReAttestation() to wait until
|
|
5318
|
+
* the attestation is fully re-processed. The expirationBlock transitions
|
|
5319
|
+
* from non-zero to zero when Circle completes the re-attestation.
|
|
5320
|
+
*
|
|
5321
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
5322
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
5323
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5324
|
+
* @param config - Optional configuration overrides
|
|
5325
|
+
* @returns The re-attested attestation response with expirationBlock === '0'
|
|
5326
|
+
* @throws If the request fails, times out, or expirationBlock never becomes 0
|
|
5327
|
+
*
|
|
5328
|
+
* @example
|
|
5329
|
+
* ```typescript
|
|
5330
|
+
* // After requesting re-attestation
|
|
5331
|
+
* await requestReAttestation(nonce, isTestnet)
|
|
5332
|
+
*
|
|
5333
|
+
* // Poll until expirationBlock becomes 0
|
|
5334
|
+
* const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
|
|
5335
|
+
* // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
|
|
5336
|
+
* ```
|
|
5337
|
+
*/
|
|
5338
|
+
const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
|
|
5339
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
5340
|
+
const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
|
|
5341
|
+
return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
|
|
5342
|
+
};
|
|
5165
5343
|
/**
|
|
5166
5344
|
* Builds the IRIS API URL for re-attestation requests.
|
|
5167
5345
|
*
|
|
@@ -6091,6 +6269,7 @@ function dispatchStepEvent(name, step, provider) {
|
|
|
6091
6269
|
});
|
|
6092
6270
|
break;
|
|
6093
6271
|
case 'fetchAttestation':
|
|
6272
|
+
case 'reAttest':
|
|
6094
6273
|
provider.actionDispatcher.dispatch(name, {
|
|
6095
6274
|
...actionValues,
|
|
6096
6275
|
method: name,
|
|
@@ -6493,58 +6672,6 @@ const validateBalanceForTransaction = async (params) => {
|
|
|
6493
6672
|
}
|
|
6494
6673
|
};
|
|
6495
6674
|
|
|
6496
|
-
/**
|
|
6497
|
-
* Validate that the adapter has sufficient native token balance for transaction fees.
|
|
6498
|
-
*
|
|
6499
|
-
* This function checks if the adapter's current native token balance (ETH, SOL, etc.)
|
|
6500
|
-
* is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
|
|
6501
|
-
* if the balance is zero, indicating the wallet cannot pay for transaction fees.
|
|
6502
|
-
*
|
|
6503
|
-
* @param params - The validation parameters containing adapter and operation context.
|
|
6504
|
-
* @returns A promise that resolves to void if validation passes.
|
|
6505
|
-
* @throws {KitError} When the adapter's native balance is zero (code: 9002).
|
|
6506
|
-
*
|
|
6507
|
-
* @example
|
|
6508
|
-
* ```typescript
|
|
6509
|
-
* import { validateNativeBalanceForTransaction } from '@core/adapter'
|
|
6510
|
-
* import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
6511
|
-
* import { isKitError, ERROR_TYPES } from '@core/errors'
|
|
6512
|
-
*
|
|
6513
|
-
* const adapter = createViemAdapterFromPrivateKey({
|
|
6514
|
-
* privateKey: '0x...',
|
|
6515
|
-
* chain: 'Ethereum',
|
|
6516
|
-
* })
|
|
6517
|
-
*
|
|
6518
|
-
* try {
|
|
6519
|
-
* await validateNativeBalanceForTransaction({
|
|
6520
|
-
* adapter,
|
|
6521
|
-
* operationContext: { chain: 'Ethereum' },
|
|
6522
|
-
* })
|
|
6523
|
-
* console.log('Native balance validation passed')
|
|
6524
|
-
* } catch (error) {
|
|
6525
|
-
* if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
|
|
6526
|
-
* console.error('Insufficient gas funds:', error.message)
|
|
6527
|
-
* }
|
|
6528
|
-
* }
|
|
6529
|
-
* ```
|
|
6530
|
-
*/
|
|
6531
|
-
const validateNativeBalanceForTransaction = async (params) => {
|
|
6532
|
-
const { adapter, operationContext } = params;
|
|
6533
|
-
const balancePrepared = await adapter.prepareAction('native.balanceOf', {
|
|
6534
|
-
walletAddress: operationContext.address,
|
|
6535
|
-
}, operationContext);
|
|
6536
|
-
const balance = await balancePrepared.execute();
|
|
6537
|
-
if (BigInt(balance) === 0n) {
|
|
6538
|
-
// Extract chain name from operationContext
|
|
6539
|
-
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6540
|
-
// Create KitError with rich context in trace
|
|
6541
|
-
throw createInsufficientGasError(chainName, {
|
|
6542
|
-
balance: '0',
|
|
6543
|
-
walletAddress: operationContext.address,
|
|
6544
|
-
});
|
|
6545
|
-
}
|
|
6546
|
-
};
|
|
6547
|
-
|
|
6548
6675
|
/**
|
|
6549
6676
|
* CCTP bridge step names that can occur in the bridging flow.
|
|
6550
6677
|
*
|
|
@@ -6557,6 +6684,7 @@ const CCTPv2StepName = {
|
|
|
6557
6684
|
burn: 'burn',
|
|
6558
6685
|
fetchAttestation: 'fetchAttestation',
|
|
6559
6686
|
mint: 'mint',
|
|
6687
|
+
reAttest: 'reAttest',
|
|
6560
6688
|
};
|
|
6561
6689
|
/**
|
|
6562
6690
|
* Conditional step transition rules for CCTP bridge flow.
|
|
@@ -6664,6 +6792,27 @@ const STEP_TRANSITION_RULES = {
|
|
|
6664
6792
|
isActionable: false, // Waiting for pending transaction
|
|
6665
6793
|
},
|
|
6666
6794
|
],
|
|
6795
|
+
// After ReAttest step
|
|
6796
|
+
[CCTPv2StepName.reAttest]: [
|
|
6797
|
+
{
|
|
6798
|
+
condition: (ctx) => ctx.lastStep?.state === 'success',
|
|
6799
|
+
nextStep: CCTPv2StepName.mint,
|
|
6800
|
+
reason: 'Re-attestation successful, proceed to mint',
|
|
6801
|
+
isActionable: true,
|
|
6802
|
+
},
|
|
6803
|
+
{
|
|
6804
|
+
condition: (ctx) => ctx.lastStep?.state === 'error',
|
|
6805
|
+
nextStep: CCTPv2StepName.mint,
|
|
6806
|
+
reason: 'Re-attestation failed, retry mint to re-initiate recovery',
|
|
6807
|
+
isActionable: true,
|
|
6808
|
+
},
|
|
6809
|
+
{
|
|
6810
|
+
condition: (ctx) => ctx.lastStep?.state === 'pending',
|
|
6811
|
+
nextStep: CCTPv2StepName.mint,
|
|
6812
|
+
reason: 'Re-attestation pending, retry mint to re-initiate recovery',
|
|
6813
|
+
isActionable: true,
|
|
6814
|
+
},
|
|
6815
|
+
],
|
|
6667
6816
|
};
|
|
6668
6817
|
/**
|
|
6669
6818
|
* Analyze bridge steps to determine retry feasibility and continuation point.
|
|
@@ -6930,8 +7079,14 @@ function getBurnTxHash(result) {
|
|
|
6930
7079
|
* ```
|
|
6931
7080
|
*/
|
|
6932
7081
|
function getAttestationData(result) {
|
|
6933
|
-
|
|
6934
|
-
|
|
7082
|
+
// Prefer reAttest data (most recent attestation after expiry)
|
|
7083
|
+
const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
|
|
7084
|
+
if (reAttestStep?.state === 'success' && reAttestStep.data) {
|
|
7085
|
+
return reAttestStep.data;
|
|
7086
|
+
}
|
|
7087
|
+
// Fall back to fetchAttestation step
|
|
7088
|
+
const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
|
|
7089
|
+
return fetchStep?.data;
|
|
6935
7090
|
}
|
|
6936
7091
|
|
|
6937
7092
|
/**
|
|
@@ -7129,7 +7284,7 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
|
|
|
7129
7284
|
message: 'Cannot fetch attestation: burn transaction hash not found',
|
|
7130
7285
|
});
|
|
7131
7286
|
}
|
|
7132
|
-
const sourceAddress =
|
|
7287
|
+
const sourceAddress = result.source.address;
|
|
7133
7288
|
const attestation = await provider.fetchAttestation({
|
|
7134
7289
|
chain: result.source.chain,
|
|
7135
7290
|
adapter: context.from,
|
|
@@ -7145,6 +7300,63 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
|
|
|
7145
7300
|
return waitForPendingTransaction(pendingStep, adapter, chain);
|
|
7146
7301
|
}
|
|
7147
7302
|
|
|
7303
|
+
/**
|
|
7304
|
+
* Executes a re-attestation operation to obtain a fresh attestation for an expired message.
|
|
7305
|
+
*
|
|
7306
|
+
* This function handles the re-attestation step of the CCTP v2 bridge process, where a fresh
|
|
7307
|
+
* attestation is requested from Circle's API when the original attestation has expired.
|
|
7308
|
+
* It first checks if the attestation has already been re-attested before making the API call.
|
|
7309
|
+
*
|
|
7310
|
+
* @param params - The bridge parameters containing source, destination, amount and optional config
|
|
7311
|
+
* @param provider - The CCTP v2 bridging provider
|
|
7312
|
+
* @param burnTxHash - The transaction hash of the original burn operation
|
|
7313
|
+
* @returns Promise resolving to the bridge step with fresh attestation data
|
|
7314
|
+
*
|
|
7315
|
+
* @example
|
|
7316
|
+
* ```typescript
|
|
7317
|
+
* const reAttestStep = await bridgeReAttest(
|
|
7318
|
+
* { params, provider },
|
|
7319
|
+
* burnTxHash
|
|
7320
|
+
* )
|
|
7321
|
+
* console.log('Fresh attestation:', reAttestStep.data)
|
|
7322
|
+
* ```
|
|
7323
|
+
*/
|
|
7324
|
+
async function bridgeReAttest({ params, provider, }, burnTxHash) {
|
|
7325
|
+
const step = {
|
|
7326
|
+
name: 'reAttest',
|
|
7327
|
+
state: 'pending',
|
|
7328
|
+
};
|
|
7329
|
+
try {
|
|
7330
|
+
// Fetch current attestation to check if already re-attested
|
|
7331
|
+
const currentAttestation = await provider.fetchAttestation(params.source, burnTxHash);
|
|
7332
|
+
// Check if already re-attested (expirationBlock === '0' means never expires)
|
|
7333
|
+
const expirationBlock = currentAttestation.decodedMessage.decodedMessageBody.expirationBlock;
|
|
7334
|
+
if (expirationBlock === '0') {
|
|
7335
|
+
// Already re-attested - return current attestation without calling reAttest API
|
|
7336
|
+
return { ...step, state: 'success', data: currentAttestation };
|
|
7337
|
+
}
|
|
7338
|
+
// Not yet re-attested - proceed with re-attestation request
|
|
7339
|
+
const reAttestedAttestation = await provider.reAttest(params.source, burnTxHash);
|
|
7340
|
+
return { ...step, state: 'success', data: reAttestedAttestation };
|
|
7341
|
+
}
|
|
7342
|
+
catch (err) {
|
|
7343
|
+
let errorMessage = 'Unknown re-attestation error';
|
|
7344
|
+
if (err instanceof Error) {
|
|
7345
|
+
errorMessage = err.message;
|
|
7346
|
+
}
|
|
7347
|
+
else if (typeof err === 'string') {
|
|
7348
|
+
errorMessage = err;
|
|
7349
|
+
}
|
|
7350
|
+
return {
|
|
7351
|
+
...step,
|
|
7352
|
+
state: 'error',
|
|
7353
|
+
error: err,
|
|
7354
|
+
errorMessage,
|
|
7355
|
+
data: undefined,
|
|
7356
|
+
};
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7359
|
+
|
|
7148
7360
|
/**
|
|
7149
7361
|
* Extract context data from completed bridge steps for retry operations.
|
|
7150
7362
|
*
|
|
@@ -7160,6 +7372,116 @@ function populateContext(result) {
|
|
|
7160
7372
|
attestationData: getAttestationData(result),
|
|
7161
7373
|
};
|
|
7162
7374
|
}
|
|
7375
|
+
/**
|
|
7376
|
+
* Handle re-attestation and mint retry when mint fails due to expired attestation.
|
|
7377
|
+
*
|
|
7378
|
+
* @internal
|
|
7379
|
+
*/
|
|
7380
|
+
async function handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result) {
|
|
7381
|
+
const burnTxHash = stepContext?.burnTxHash ?? getBurnTxHash(result);
|
|
7382
|
+
if (burnTxHash === undefined || burnTxHash === '') {
|
|
7383
|
+
handleStepError('mint', new Error('Cannot attempt re-attestation: Burn transaction hash not found in previous steps.'), result);
|
|
7384
|
+
return { success: false };
|
|
7385
|
+
}
|
|
7386
|
+
const reAttestStep = await bridgeReAttest({ params, provider }, burnTxHash);
|
|
7387
|
+
dispatchStepEvent('reAttest', reAttestStep, provider);
|
|
7388
|
+
result.steps.push(reAttestStep);
|
|
7389
|
+
if (reAttestStep.state === 'error') {
|
|
7390
|
+
result.state = 'error';
|
|
7391
|
+
return { success: false };
|
|
7392
|
+
}
|
|
7393
|
+
const freshContext = {
|
|
7394
|
+
...stepContext,
|
|
7395
|
+
burnTxHash,
|
|
7396
|
+
attestationData: reAttestStep.data,
|
|
7397
|
+
};
|
|
7398
|
+
return executeMintRetry(params, provider, executor, freshContext, updateContext, result);
|
|
7399
|
+
}
|
|
7400
|
+
/**
|
|
7401
|
+
* Handle step execution error in retry loop.
|
|
7402
|
+
*
|
|
7403
|
+
* Determines if re-attestation should be attempted for mint failures,
|
|
7404
|
+
* or records the error and signals to exit the loop.
|
|
7405
|
+
*
|
|
7406
|
+
* @internal
|
|
7407
|
+
*/
|
|
7408
|
+
async function handleStepExecutionError(name, error, context) {
|
|
7409
|
+
const { params, provider, executor, updateContext, stepContext, result } = context;
|
|
7410
|
+
const shouldAttemptReAttestation = name === 'mint' && isMintFailureRelatedToAttestation(error);
|
|
7411
|
+
if (!shouldAttemptReAttestation) {
|
|
7412
|
+
handleStepError(name, error, result);
|
|
7413
|
+
return { shouldContinue: false };
|
|
7414
|
+
}
|
|
7415
|
+
const reAttestResult = await handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result);
|
|
7416
|
+
if (!reAttestResult.success) {
|
|
7417
|
+
return { shouldContinue: false };
|
|
7418
|
+
}
|
|
7419
|
+
return { shouldContinue: true, stepContext: reAttestResult.stepContext };
|
|
7420
|
+
}
|
|
7421
|
+
/**
|
|
7422
|
+
* Execute mint retry with fresh attestation context.
|
|
7423
|
+
*
|
|
7424
|
+
* @internal
|
|
7425
|
+
*/
|
|
7426
|
+
async function executeMintRetry(params, provider, executor, freshContext, updateContext, result) {
|
|
7427
|
+
try {
|
|
7428
|
+
const retryStep = await executor(params, provider, freshContext);
|
|
7429
|
+
if (retryStep.state === 'error') {
|
|
7430
|
+
throw new Error(retryStep.errorMessage ?? 'mint step returned error state');
|
|
7431
|
+
}
|
|
7432
|
+
dispatchStepEvent('mint', retryStep, provider);
|
|
7433
|
+
result.steps.push(retryStep);
|
|
7434
|
+
return { success: true, stepContext: updateContext?.(retryStep) };
|
|
7435
|
+
}
|
|
7436
|
+
catch (retryError) {
|
|
7437
|
+
if (isMintFailureRelatedToAttestation(retryError)) {
|
|
7438
|
+
const kitError = createSimulationFailedError(result.destination.chain.name, getErrorMessage(retryError), { error: retryError });
|
|
7439
|
+
handleStepError('mint', kitError, result);
|
|
7440
|
+
}
|
|
7441
|
+
else {
|
|
7442
|
+
handleStepError('mint', retryError, result);
|
|
7443
|
+
}
|
|
7444
|
+
return { success: false };
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7447
|
+
/**
|
|
7448
|
+
* Execute remaining bridge steps starting from a specific index.
|
|
7449
|
+
*
|
|
7450
|
+
* Handles the step execution loop with error handling and re-attestation support.
|
|
7451
|
+
* Returns true if all steps completed successfully, false if stopped due to error.
|
|
7452
|
+
*
|
|
7453
|
+
* @internal
|
|
7454
|
+
*/
|
|
7455
|
+
async function executeSteps(params, provider, result, startIndex) {
|
|
7456
|
+
let stepContext = populateContext(result);
|
|
7457
|
+
for (const { name, executor, updateContext } of stepExecutors.slice(startIndex)) {
|
|
7458
|
+
try {
|
|
7459
|
+
const step = await executor(params, provider, stepContext);
|
|
7460
|
+
if (step.state === 'error') {
|
|
7461
|
+
const errorMessage = step.errorMessage ?? `${name} step returned error state`;
|
|
7462
|
+
throw new Error(errorMessage);
|
|
7463
|
+
}
|
|
7464
|
+
stepContext = updateContext?.(step);
|
|
7465
|
+
dispatchStepEvent(name, step, provider);
|
|
7466
|
+
result.steps.push(step);
|
|
7467
|
+
}
|
|
7468
|
+
catch (error) {
|
|
7469
|
+
const errorResult = await handleStepExecutionError(name, error, {
|
|
7470
|
+
params,
|
|
7471
|
+
provider,
|
|
7472
|
+
executor,
|
|
7473
|
+
updateContext,
|
|
7474
|
+
stepContext,
|
|
7475
|
+
result,
|
|
7476
|
+
});
|
|
7477
|
+
if (!errorResult.shouldContinue) {
|
|
7478
|
+
return false;
|
|
7479
|
+
}
|
|
7480
|
+
stepContext = errorResult.stepContext;
|
|
7481
|
+
}
|
|
7482
|
+
}
|
|
7483
|
+
return true;
|
|
7484
|
+
}
|
|
7163
7485
|
/**
|
|
7164
7486
|
* Retry a failed or incomplete CCTP v2 bridge operation from where it left off.
|
|
7165
7487
|
*
|
|
@@ -7181,15 +7503,12 @@ function populateContext(result) {
|
|
|
7181
7503
|
async function retry(result, context, provider) {
|
|
7182
7504
|
const analysis = analyzeSteps(result);
|
|
7183
7505
|
if (!analysis.isActionable) {
|
|
7184
|
-
// Terminal completion - bridge already complete, return gracefully
|
|
7185
7506
|
if (analysis.continuationStep === null && result.state === 'success') {
|
|
7186
7507
|
return result;
|
|
7187
7508
|
}
|
|
7188
|
-
// Pending states - wait for the pending operation to complete
|
|
7189
7509
|
if (hasPendingState(analysis, result)) {
|
|
7190
7510
|
return handlePendingState(result, context, provider, analysis);
|
|
7191
7511
|
}
|
|
7192
|
-
// No valid continuation - cannot proceed
|
|
7193
7512
|
throw new Error('Retry not supported for this result, requires user action');
|
|
7194
7513
|
}
|
|
7195
7514
|
if (!isCCTPV2Supported(result.source.chain)) {
|
|
@@ -7216,25 +7535,10 @@ async function retry(result, context, provider) {
|
|
|
7216
7535
|
if (indexOfSteps === -1) {
|
|
7217
7536
|
throw new Error(`Continuation step ${analysis.continuationStep ?? ''} not found`);
|
|
7218
7537
|
}
|
|
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
|
-
}
|
|
7538
|
+
const completed = await executeSteps(params, provider, result, indexOfSteps);
|
|
7539
|
+
if (completed) {
|
|
7540
|
+
result.state = 'success';
|
|
7236
7541
|
}
|
|
7237
|
-
result.state = 'success';
|
|
7238
7542
|
return result;
|
|
7239
7543
|
}
|
|
7240
7544
|
/**
|
|
@@ -7254,7 +7558,7 @@ async function retry(result, context, provider) {
|
|
|
7254
7558
|
* @returns Updated bridge result after pending operation completes.
|
|
7255
7559
|
*/
|
|
7256
7560
|
async function handlePendingState(result, context, provider, analysis) {
|
|
7257
|
-
if (
|
|
7561
|
+
if (analysis.continuationStep === null || analysis.continuationStep === '') {
|
|
7258
7562
|
// This should not be reachable due to the `hasPendingState` check,
|
|
7259
7563
|
// but it ensures type safety for `continuationStep`.
|
|
7260
7564
|
throw new KitError({
|
|
@@ -7418,7 +7722,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7418
7722
|
async bridge(params) {
|
|
7419
7723
|
// CCTP-specific bridge params validation (includes base validation)
|
|
7420
7724
|
assertCCTPv2BridgeParams(params);
|
|
7421
|
-
const { source,
|
|
7725
|
+
const { source, amount, token } = params;
|
|
7422
7726
|
// Extract operation context from source wallet context for balance validation
|
|
7423
7727
|
const sourceOperationContext = this.extractOperationContext(source);
|
|
7424
7728
|
// Validate USDC balance for transaction on source chain
|
|
@@ -7429,18 +7733,6 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7429
7733
|
tokenAddress: source.chain.usdcAddress,
|
|
7430
7734
|
operationContext: sourceOperationContext,
|
|
7431
7735
|
});
|
|
7432
|
-
// Validate native balance > 0 for gas fees on source chain
|
|
7433
|
-
await validateNativeBalanceForTransaction({
|
|
7434
|
-
adapter: source.adapter,
|
|
7435
|
-
operationContext: sourceOperationContext,
|
|
7436
|
-
});
|
|
7437
|
-
// Extract operation context from destination wallet context
|
|
7438
|
-
const destinationOperationContext = this.extractOperationContext(destination);
|
|
7439
|
-
// Validate native balance > 0 for gas fees on destination chain
|
|
7440
|
-
await validateNativeBalanceForTransaction({
|
|
7441
|
-
adapter: destination.adapter,
|
|
7442
|
-
operationContext: destinationOperationContext,
|
|
7443
|
-
});
|
|
7444
7736
|
return bridge(params, this);
|
|
7445
7737
|
}
|
|
7446
7738
|
/**
|
|
@@ -7888,8 +8180,10 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7888
8180
|
}
|
|
7889
8181
|
// Step 2: Request re-attestation
|
|
7890
8182
|
await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
|
|
7891
|
-
// Step 3: Poll for fresh attestation
|
|
7892
|
-
|
|
8183
|
+
// Step 3: Poll for fresh attestation until expirationBlock === '0'
|
|
8184
|
+
// The expiration block transitions from non-zero to zero when Circle
|
|
8185
|
+
// completes processing the re-attestation request.
|
|
8186
|
+
const response = await fetchReAttestedAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7893
8187
|
const message = response.messages[0];
|
|
7894
8188
|
if (!message) {
|
|
7895
8189
|
throw new Error('Failed to re-attest: No attestation found after re-attestation request');
|