@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.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
|
|
@@ -3641,12 +3736,31 @@ const BalanceError = {
|
|
|
3641
3736
|
code: 9001,
|
|
3642
3737
|
name: 'BALANCE_INSUFFICIENT_TOKEN',
|
|
3643
3738
|
type: 'BALANCE',
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3739
|
+
}};
|
|
3740
|
+
/**
|
|
3741
|
+
* Standardized error definitions for ONCHAIN type errors.
|
|
3742
|
+
*
|
|
3743
|
+
* ONCHAIN errors occur during transaction execution, simulation,
|
|
3744
|
+
* or interaction with smart contracts on the blockchain.
|
|
3745
|
+
*
|
|
3746
|
+
* @example
|
|
3747
|
+
* ```typescript
|
|
3748
|
+
* import { OnchainError } from '@core/errors'
|
|
3749
|
+
*
|
|
3750
|
+
* const error = new KitError({
|
|
3751
|
+
* ...OnchainError.SIMULATION_FAILED,
|
|
3752
|
+
* recoverability: 'FATAL',
|
|
3753
|
+
* message: 'Simulation failed: ERC20 transfer amount exceeds balance',
|
|
3754
|
+
* cause: { trace: { reason: 'ERC20: transfer amount exceeds balance' } }
|
|
3755
|
+
* })
|
|
3756
|
+
* ```
|
|
3757
|
+
*/
|
|
3758
|
+
const OnchainError = {
|
|
3759
|
+
/** Pre-flight transaction simulation failed */
|
|
3760
|
+
SIMULATION_FAILED: {
|
|
3761
|
+
code: 5002,
|
|
3762
|
+
name: 'ONCHAIN_SIMULATION_FAILED',
|
|
3763
|
+
type: 'ONCHAIN',
|
|
3650
3764
|
}};
|
|
3651
3765
|
|
|
3652
3766
|
/**
|
|
@@ -3908,45 +4022,47 @@ function createInsufficientTokenBalanceError(chain, token, trace) {
|
|
|
3908
4022
|
},
|
|
3909
4023
|
});
|
|
3910
4024
|
}
|
|
4025
|
+
|
|
3911
4026
|
/**
|
|
3912
|
-
* Creates error for
|
|
4027
|
+
* Creates error for transaction simulation failures.
|
|
3913
4028
|
*
|
|
3914
|
-
* This error is thrown when a
|
|
3915
|
-
*
|
|
3916
|
-
* as it
|
|
4029
|
+
* This error is thrown when a pre-flight transaction simulation fails,
|
|
4030
|
+
* typically due to contract logic that would revert. The error is FATAL
|
|
4031
|
+
* as it indicates the transaction would fail if submitted.
|
|
3917
4032
|
*
|
|
3918
|
-
* @param chain - The blockchain network where the
|
|
4033
|
+
* @param chain - The blockchain network where the simulation failed
|
|
4034
|
+
* @param reason - The reason for simulation failure (e.g., revert message)
|
|
3919
4035
|
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3920
|
-
* @returns KitError with
|
|
4036
|
+
* @returns KitError with simulation failure details
|
|
3921
4037
|
*
|
|
3922
4038
|
* @example
|
|
3923
4039
|
* ```typescript
|
|
3924
|
-
* import {
|
|
4040
|
+
* import { createSimulationFailedError } from '@core/errors'
|
|
3925
4041
|
*
|
|
3926
|
-
* throw
|
|
3927
|
-
* // Message: "
|
|
4042
|
+
* throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance')
|
|
4043
|
+
* // Message: "Simulation failed on Ethereum: ERC20: insufficient allowance"
|
|
3928
4044
|
* ```
|
|
3929
4045
|
*
|
|
3930
4046
|
* @example
|
|
3931
4047
|
* ```typescript
|
|
3932
4048
|
* // With trace context for debugging
|
|
3933
|
-
* throw
|
|
4049
|
+
* throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance', {
|
|
3934
4050
|
* rawError: error,
|
|
3935
|
-
*
|
|
3936
|
-
*
|
|
3937
|
-
* walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
4051
|
+
* txHash: '0x1234...',
|
|
4052
|
+
* gasLimit: '21000',
|
|
3938
4053
|
* })
|
|
3939
4054
|
* ```
|
|
3940
4055
|
*/
|
|
3941
|
-
function
|
|
4056
|
+
function createSimulationFailedError(chain, reason, trace) {
|
|
3942
4057
|
return new KitError({
|
|
3943
|
-
...
|
|
4058
|
+
...OnchainError.SIMULATION_FAILED,
|
|
3944
4059
|
recoverability: 'FATAL',
|
|
3945
|
-
message: `
|
|
4060
|
+
message: `Simulation failed on ${chain}: ${reason}`,
|
|
3946
4061
|
cause: {
|
|
3947
4062
|
trace: {
|
|
3948
4063
|
...trace,
|
|
3949
4064
|
chain,
|
|
4065
|
+
reason,
|
|
3950
4066
|
},
|
|
3951
4067
|
},
|
|
3952
4068
|
});
|
|
@@ -5156,6 +5272,68 @@ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHas
|
|
|
5156
5272
|
};
|
|
5157
5273
|
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
5158
5274
|
};
|
|
5275
|
+
/**
|
|
5276
|
+
* Type guard that validates attestation response has expirationBlock === '0'.
|
|
5277
|
+
*
|
|
5278
|
+
* This is used after requestReAttestation() to poll until the attestation
|
|
5279
|
+
* is fully re-processed and has a zero expiration block (never expires).
|
|
5280
|
+
* The expiration block transitions from non-zero to zero when Circle
|
|
5281
|
+
* completes processing the re-attestation request.
|
|
5282
|
+
*
|
|
5283
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5284
|
+
* @returns True if the attestation has expirationBlock === '0'
|
|
5285
|
+
* @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
|
|
5286
|
+
*
|
|
5287
|
+
* @example
|
|
5288
|
+
* ```typescript
|
|
5289
|
+
* // After requesting re-attestation, use this to validate the response
|
|
5290
|
+
* const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
|
|
5291
|
+
* // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
|
|
5292
|
+
* ```
|
|
5293
|
+
*
|
|
5294
|
+
* @internal
|
|
5295
|
+
*/
|
|
5296
|
+
const isReAttestedAttestationResponse = (obj) => {
|
|
5297
|
+
// First validate the basic structure and completion status
|
|
5298
|
+
// This will throw appropriate errors for invalid structure or incomplete attestation
|
|
5299
|
+
if (!isAttestationResponse(obj)) ;
|
|
5300
|
+
// Check if the first message has expirationBlock === '0'
|
|
5301
|
+
const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
|
|
5302
|
+
if (expirationBlock !== '0') {
|
|
5303
|
+
// Re-attestation not yet complete - allow retry via polling
|
|
5304
|
+
throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
|
|
5305
|
+
}
|
|
5306
|
+
return true;
|
|
5307
|
+
};
|
|
5308
|
+
/**
|
|
5309
|
+
* Fetches attestation data and polls until expirationBlock === '0'.
|
|
5310
|
+
*
|
|
5311
|
+
* This function is used after calling requestReAttestation() to wait until
|
|
5312
|
+
* the attestation is fully re-processed. The expirationBlock transitions
|
|
5313
|
+
* from non-zero to zero when Circle completes the re-attestation.
|
|
5314
|
+
*
|
|
5315
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
5316
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
5317
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5318
|
+
* @param config - Optional configuration overrides
|
|
5319
|
+
* @returns The re-attested attestation response with expirationBlock === '0'
|
|
5320
|
+
* @throws If the request fails, times out, or expirationBlock never becomes 0
|
|
5321
|
+
*
|
|
5322
|
+
* @example
|
|
5323
|
+
* ```typescript
|
|
5324
|
+
* // After requesting re-attestation
|
|
5325
|
+
* await requestReAttestation(nonce, isTestnet)
|
|
5326
|
+
*
|
|
5327
|
+
* // Poll until expirationBlock becomes 0
|
|
5328
|
+
* const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
|
|
5329
|
+
* // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
|
|
5330
|
+
* ```
|
|
5331
|
+
*/
|
|
5332
|
+
const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
|
|
5333
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
5334
|
+
const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
|
|
5335
|
+
return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
|
|
5336
|
+
};
|
|
5159
5337
|
/**
|
|
5160
5338
|
* Builds the IRIS API URL for re-attestation requests.
|
|
5161
5339
|
*
|
|
@@ -6085,6 +6263,7 @@ function dispatchStepEvent(name, step, provider) {
|
|
|
6085
6263
|
});
|
|
6086
6264
|
break;
|
|
6087
6265
|
case 'fetchAttestation':
|
|
6266
|
+
case 'reAttest':
|
|
6088
6267
|
provider.actionDispatcher.dispatch(name, {
|
|
6089
6268
|
...actionValues,
|
|
6090
6269
|
method: name,
|
|
@@ -6487,58 +6666,6 @@ const validateBalanceForTransaction = async (params) => {
|
|
|
6487
6666
|
}
|
|
6488
6667
|
};
|
|
6489
6668
|
|
|
6490
|
-
/**
|
|
6491
|
-
* Validate that the adapter has sufficient native token balance for transaction fees.
|
|
6492
|
-
*
|
|
6493
|
-
* This function checks if the adapter's current native token balance (ETH, SOL, etc.)
|
|
6494
|
-
* is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
|
|
6495
|
-
* if the balance is zero, indicating the wallet cannot pay for transaction fees.
|
|
6496
|
-
*
|
|
6497
|
-
* @param params - The validation parameters containing adapter and operation context.
|
|
6498
|
-
* @returns A promise that resolves to void if validation passes.
|
|
6499
|
-
* @throws {KitError} When the adapter's native balance is zero (code: 9002).
|
|
6500
|
-
*
|
|
6501
|
-
* @example
|
|
6502
|
-
* ```typescript
|
|
6503
|
-
* import { validateNativeBalanceForTransaction } from '@core/adapter'
|
|
6504
|
-
* import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
6505
|
-
* import { isKitError, ERROR_TYPES } from '@core/errors'
|
|
6506
|
-
*
|
|
6507
|
-
* const adapter = createViemAdapterFromPrivateKey({
|
|
6508
|
-
* privateKey: '0x...',
|
|
6509
|
-
* chain: 'Ethereum',
|
|
6510
|
-
* })
|
|
6511
|
-
*
|
|
6512
|
-
* try {
|
|
6513
|
-
* await validateNativeBalanceForTransaction({
|
|
6514
|
-
* adapter,
|
|
6515
|
-
* operationContext: { chain: 'Ethereum' },
|
|
6516
|
-
* })
|
|
6517
|
-
* console.log('Native balance validation passed')
|
|
6518
|
-
* } catch (error) {
|
|
6519
|
-
* if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
|
|
6520
|
-
* console.error('Insufficient gas funds:', error.message)
|
|
6521
|
-
* }
|
|
6522
|
-
* }
|
|
6523
|
-
* ```
|
|
6524
|
-
*/
|
|
6525
|
-
const validateNativeBalanceForTransaction = async (params) => {
|
|
6526
|
-
const { adapter, operationContext } = params;
|
|
6527
|
-
const balancePrepared = await adapter.prepareAction('native.balanceOf', {
|
|
6528
|
-
walletAddress: operationContext.address,
|
|
6529
|
-
}, operationContext);
|
|
6530
|
-
const balance = await balancePrepared.execute();
|
|
6531
|
-
if (BigInt(balance) === 0n) {
|
|
6532
|
-
// Extract chain name from operationContext
|
|
6533
|
-
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6534
|
-
// Create KitError with rich context in trace
|
|
6535
|
-
throw createInsufficientGasError(chainName, {
|
|
6536
|
-
balance: '0',
|
|
6537
|
-
walletAddress: operationContext.address,
|
|
6538
|
-
});
|
|
6539
|
-
}
|
|
6540
|
-
};
|
|
6541
|
-
|
|
6542
6669
|
/**
|
|
6543
6670
|
* CCTP bridge step names that can occur in the bridging flow.
|
|
6544
6671
|
*
|
|
@@ -6551,6 +6678,7 @@ const CCTPv2StepName = {
|
|
|
6551
6678
|
burn: 'burn',
|
|
6552
6679
|
fetchAttestation: 'fetchAttestation',
|
|
6553
6680
|
mint: 'mint',
|
|
6681
|
+
reAttest: 'reAttest',
|
|
6554
6682
|
};
|
|
6555
6683
|
/**
|
|
6556
6684
|
* Conditional step transition rules for CCTP bridge flow.
|
|
@@ -6658,6 +6786,27 @@ const STEP_TRANSITION_RULES = {
|
|
|
6658
6786
|
isActionable: false, // Waiting for pending transaction
|
|
6659
6787
|
},
|
|
6660
6788
|
],
|
|
6789
|
+
// After ReAttest step
|
|
6790
|
+
[CCTPv2StepName.reAttest]: [
|
|
6791
|
+
{
|
|
6792
|
+
condition: (ctx) => ctx.lastStep?.state === 'success',
|
|
6793
|
+
nextStep: CCTPv2StepName.mint,
|
|
6794
|
+
reason: 'Re-attestation successful, proceed to mint',
|
|
6795
|
+
isActionable: true,
|
|
6796
|
+
},
|
|
6797
|
+
{
|
|
6798
|
+
condition: (ctx) => ctx.lastStep?.state === 'error',
|
|
6799
|
+
nextStep: CCTPv2StepName.mint,
|
|
6800
|
+
reason: 'Re-attestation failed, retry mint to re-initiate recovery',
|
|
6801
|
+
isActionable: true,
|
|
6802
|
+
},
|
|
6803
|
+
{
|
|
6804
|
+
condition: (ctx) => ctx.lastStep?.state === 'pending',
|
|
6805
|
+
nextStep: CCTPv2StepName.mint,
|
|
6806
|
+
reason: 'Re-attestation pending, retry mint to re-initiate recovery',
|
|
6807
|
+
isActionable: true,
|
|
6808
|
+
},
|
|
6809
|
+
],
|
|
6661
6810
|
};
|
|
6662
6811
|
/**
|
|
6663
6812
|
* Analyze bridge steps to determine retry feasibility and continuation point.
|
|
@@ -6924,8 +7073,14 @@ function getBurnTxHash(result) {
|
|
|
6924
7073
|
* ```
|
|
6925
7074
|
*/
|
|
6926
7075
|
function getAttestationData(result) {
|
|
6927
|
-
|
|
6928
|
-
|
|
7076
|
+
// Prefer reAttest data (most recent attestation after expiry)
|
|
7077
|
+
const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
|
|
7078
|
+
if (reAttestStep?.state === 'success' && reAttestStep.data) {
|
|
7079
|
+
return reAttestStep.data;
|
|
7080
|
+
}
|
|
7081
|
+
// Fall back to fetchAttestation step
|
|
7082
|
+
const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
|
|
7083
|
+
return fetchStep?.data;
|
|
6929
7084
|
}
|
|
6930
7085
|
|
|
6931
7086
|
/**
|
|
@@ -7123,7 +7278,7 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
|
|
|
7123
7278
|
message: 'Cannot fetch attestation: burn transaction hash not found',
|
|
7124
7279
|
});
|
|
7125
7280
|
}
|
|
7126
|
-
const sourceAddress =
|
|
7281
|
+
const sourceAddress = result.source.address;
|
|
7127
7282
|
const attestation = await provider.fetchAttestation({
|
|
7128
7283
|
chain: result.source.chain,
|
|
7129
7284
|
adapter: context.from,
|
|
@@ -7139,6 +7294,63 @@ async function waitForStepToComplete(pendingStep, adapter, chain, context, resul
|
|
|
7139
7294
|
return waitForPendingTransaction(pendingStep, adapter, chain);
|
|
7140
7295
|
}
|
|
7141
7296
|
|
|
7297
|
+
/**
|
|
7298
|
+
* Executes a re-attestation operation to obtain a fresh attestation for an expired message.
|
|
7299
|
+
*
|
|
7300
|
+
* This function handles the re-attestation step of the CCTP v2 bridge process, where a fresh
|
|
7301
|
+
* attestation is requested from Circle's API when the original attestation has expired.
|
|
7302
|
+
* It first checks if the attestation has already been re-attested before making the API call.
|
|
7303
|
+
*
|
|
7304
|
+
* @param params - The bridge parameters containing source, destination, amount and optional config
|
|
7305
|
+
* @param provider - The CCTP v2 bridging provider
|
|
7306
|
+
* @param burnTxHash - The transaction hash of the original burn operation
|
|
7307
|
+
* @returns Promise resolving to the bridge step with fresh attestation data
|
|
7308
|
+
*
|
|
7309
|
+
* @example
|
|
7310
|
+
* ```typescript
|
|
7311
|
+
* const reAttestStep = await bridgeReAttest(
|
|
7312
|
+
* { params, provider },
|
|
7313
|
+
* burnTxHash
|
|
7314
|
+
* )
|
|
7315
|
+
* console.log('Fresh attestation:', reAttestStep.data)
|
|
7316
|
+
* ```
|
|
7317
|
+
*/
|
|
7318
|
+
async function bridgeReAttest({ params, provider, }, burnTxHash) {
|
|
7319
|
+
const step = {
|
|
7320
|
+
name: 'reAttest',
|
|
7321
|
+
state: 'pending',
|
|
7322
|
+
};
|
|
7323
|
+
try {
|
|
7324
|
+
// Fetch current attestation to check if already re-attested
|
|
7325
|
+
const currentAttestation = await provider.fetchAttestation(params.source, burnTxHash);
|
|
7326
|
+
// Check if already re-attested (expirationBlock === '0' means never expires)
|
|
7327
|
+
const expirationBlock = currentAttestation.decodedMessage.decodedMessageBody.expirationBlock;
|
|
7328
|
+
if (expirationBlock === '0') {
|
|
7329
|
+
// Already re-attested - return current attestation without calling reAttest API
|
|
7330
|
+
return { ...step, state: 'success', data: currentAttestation };
|
|
7331
|
+
}
|
|
7332
|
+
// Not yet re-attested - proceed with re-attestation request
|
|
7333
|
+
const reAttestedAttestation = await provider.reAttest(params.source, burnTxHash);
|
|
7334
|
+
return { ...step, state: 'success', data: reAttestedAttestation };
|
|
7335
|
+
}
|
|
7336
|
+
catch (err) {
|
|
7337
|
+
let errorMessage = 'Unknown re-attestation error';
|
|
7338
|
+
if (err instanceof Error) {
|
|
7339
|
+
errorMessage = err.message;
|
|
7340
|
+
}
|
|
7341
|
+
else if (typeof err === 'string') {
|
|
7342
|
+
errorMessage = err;
|
|
7343
|
+
}
|
|
7344
|
+
return {
|
|
7345
|
+
...step,
|
|
7346
|
+
state: 'error',
|
|
7347
|
+
error: err,
|
|
7348
|
+
errorMessage,
|
|
7349
|
+
data: undefined,
|
|
7350
|
+
};
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7142
7354
|
/**
|
|
7143
7355
|
* Extract context data from completed bridge steps for retry operations.
|
|
7144
7356
|
*
|
|
@@ -7154,6 +7366,116 @@ function populateContext(result) {
|
|
|
7154
7366
|
attestationData: getAttestationData(result),
|
|
7155
7367
|
};
|
|
7156
7368
|
}
|
|
7369
|
+
/**
|
|
7370
|
+
* Handle re-attestation and mint retry when mint fails due to expired attestation.
|
|
7371
|
+
*
|
|
7372
|
+
* @internal
|
|
7373
|
+
*/
|
|
7374
|
+
async function handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result) {
|
|
7375
|
+
const burnTxHash = stepContext?.burnTxHash ?? getBurnTxHash(result);
|
|
7376
|
+
if (burnTxHash === undefined || burnTxHash === '') {
|
|
7377
|
+
handleStepError('mint', new Error('Cannot attempt re-attestation: Burn transaction hash not found in previous steps.'), result);
|
|
7378
|
+
return { success: false };
|
|
7379
|
+
}
|
|
7380
|
+
const reAttestStep = await bridgeReAttest({ params, provider }, burnTxHash);
|
|
7381
|
+
dispatchStepEvent('reAttest', reAttestStep, provider);
|
|
7382
|
+
result.steps.push(reAttestStep);
|
|
7383
|
+
if (reAttestStep.state === 'error') {
|
|
7384
|
+
result.state = 'error';
|
|
7385
|
+
return { success: false };
|
|
7386
|
+
}
|
|
7387
|
+
const freshContext = {
|
|
7388
|
+
...stepContext,
|
|
7389
|
+
burnTxHash,
|
|
7390
|
+
attestationData: reAttestStep.data,
|
|
7391
|
+
};
|
|
7392
|
+
return executeMintRetry(params, provider, executor, freshContext, updateContext, result);
|
|
7393
|
+
}
|
|
7394
|
+
/**
|
|
7395
|
+
* Handle step execution error in retry loop.
|
|
7396
|
+
*
|
|
7397
|
+
* Determines if re-attestation should be attempted for mint failures,
|
|
7398
|
+
* or records the error and signals to exit the loop.
|
|
7399
|
+
*
|
|
7400
|
+
* @internal
|
|
7401
|
+
*/
|
|
7402
|
+
async function handleStepExecutionError(name, error, context) {
|
|
7403
|
+
const { params, provider, executor, updateContext, stepContext, result } = context;
|
|
7404
|
+
const shouldAttemptReAttestation = name === 'mint' && isMintFailureRelatedToAttestation(error);
|
|
7405
|
+
if (!shouldAttemptReAttestation) {
|
|
7406
|
+
handleStepError(name, error, result);
|
|
7407
|
+
return { shouldContinue: false };
|
|
7408
|
+
}
|
|
7409
|
+
const reAttestResult = await handleReAttestationAndRetry(params, provider, executor, updateContext, stepContext, result);
|
|
7410
|
+
if (!reAttestResult.success) {
|
|
7411
|
+
return { shouldContinue: false };
|
|
7412
|
+
}
|
|
7413
|
+
return { shouldContinue: true, stepContext: reAttestResult.stepContext };
|
|
7414
|
+
}
|
|
7415
|
+
/**
|
|
7416
|
+
* Execute mint retry with fresh attestation context.
|
|
7417
|
+
*
|
|
7418
|
+
* @internal
|
|
7419
|
+
*/
|
|
7420
|
+
async function executeMintRetry(params, provider, executor, freshContext, updateContext, result) {
|
|
7421
|
+
try {
|
|
7422
|
+
const retryStep = await executor(params, provider, freshContext);
|
|
7423
|
+
if (retryStep.state === 'error') {
|
|
7424
|
+
throw new Error(retryStep.errorMessage ?? 'mint step returned error state');
|
|
7425
|
+
}
|
|
7426
|
+
dispatchStepEvent('mint', retryStep, provider);
|
|
7427
|
+
result.steps.push(retryStep);
|
|
7428
|
+
return { success: true, stepContext: updateContext?.(retryStep) };
|
|
7429
|
+
}
|
|
7430
|
+
catch (retryError) {
|
|
7431
|
+
if (isMintFailureRelatedToAttestation(retryError)) {
|
|
7432
|
+
const kitError = createSimulationFailedError(result.destination.chain.name, getErrorMessage(retryError), { error: retryError });
|
|
7433
|
+
handleStepError('mint', kitError, result);
|
|
7434
|
+
}
|
|
7435
|
+
else {
|
|
7436
|
+
handleStepError('mint', retryError, result);
|
|
7437
|
+
}
|
|
7438
|
+
return { success: false };
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
/**
|
|
7442
|
+
* Execute remaining bridge steps starting from a specific index.
|
|
7443
|
+
*
|
|
7444
|
+
* Handles the step execution loop with error handling and re-attestation support.
|
|
7445
|
+
* Returns true if all steps completed successfully, false if stopped due to error.
|
|
7446
|
+
*
|
|
7447
|
+
* @internal
|
|
7448
|
+
*/
|
|
7449
|
+
async function executeSteps(params, provider, result, startIndex) {
|
|
7450
|
+
let stepContext = populateContext(result);
|
|
7451
|
+
for (const { name, executor, updateContext } of stepExecutors.slice(startIndex)) {
|
|
7452
|
+
try {
|
|
7453
|
+
const step = await executor(params, provider, stepContext);
|
|
7454
|
+
if (step.state === 'error') {
|
|
7455
|
+
const errorMessage = step.errorMessage ?? `${name} step returned error state`;
|
|
7456
|
+
throw new Error(errorMessage);
|
|
7457
|
+
}
|
|
7458
|
+
stepContext = updateContext?.(step);
|
|
7459
|
+
dispatchStepEvent(name, step, provider);
|
|
7460
|
+
result.steps.push(step);
|
|
7461
|
+
}
|
|
7462
|
+
catch (error) {
|
|
7463
|
+
const errorResult = await handleStepExecutionError(name, error, {
|
|
7464
|
+
params,
|
|
7465
|
+
provider,
|
|
7466
|
+
executor,
|
|
7467
|
+
updateContext,
|
|
7468
|
+
stepContext,
|
|
7469
|
+
result,
|
|
7470
|
+
});
|
|
7471
|
+
if (!errorResult.shouldContinue) {
|
|
7472
|
+
return false;
|
|
7473
|
+
}
|
|
7474
|
+
stepContext = errorResult.stepContext;
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
return true;
|
|
7478
|
+
}
|
|
7157
7479
|
/**
|
|
7158
7480
|
* Retry a failed or incomplete CCTP v2 bridge operation from where it left off.
|
|
7159
7481
|
*
|
|
@@ -7175,15 +7497,12 @@ function populateContext(result) {
|
|
|
7175
7497
|
async function retry(result, context, provider) {
|
|
7176
7498
|
const analysis = analyzeSteps(result);
|
|
7177
7499
|
if (!analysis.isActionable) {
|
|
7178
|
-
// Terminal completion - bridge already complete, return gracefully
|
|
7179
7500
|
if (analysis.continuationStep === null && result.state === 'success') {
|
|
7180
7501
|
return result;
|
|
7181
7502
|
}
|
|
7182
|
-
// Pending states - wait for the pending operation to complete
|
|
7183
7503
|
if (hasPendingState(analysis, result)) {
|
|
7184
7504
|
return handlePendingState(result, context, provider, analysis);
|
|
7185
7505
|
}
|
|
7186
|
-
// No valid continuation - cannot proceed
|
|
7187
7506
|
throw new Error('Retry not supported for this result, requires user action');
|
|
7188
7507
|
}
|
|
7189
7508
|
if (!isCCTPV2Supported(result.source.chain)) {
|
|
@@ -7210,25 +7529,10 @@ async function retry(result, context, provider) {
|
|
|
7210
7529
|
if (indexOfSteps === -1) {
|
|
7211
7530
|
throw new Error(`Continuation step ${analysis.continuationStep ?? ''} not found`);
|
|
7212
7531
|
}
|
|
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
|
-
}
|
|
7532
|
+
const completed = await executeSteps(params, provider, result, indexOfSteps);
|
|
7533
|
+
if (completed) {
|
|
7534
|
+
result.state = 'success';
|
|
7230
7535
|
}
|
|
7231
|
-
result.state = 'success';
|
|
7232
7536
|
return result;
|
|
7233
7537
|
}
|
|
7234
7538
|
/**
|
|
@@ -7248,7 +7552,7 @@ async function retry(result, context, provider) {
|
|
|
7248
7552
|
* @returns Updated bridge result after pending operation completes.
|
|
7249
7553
|
*/
|
|
7250
7554
|
async function handlePendingState(result, context, provider, analysis) {
|
|
7251
|
-
if (
|
|
7555
|
+
if (analysis.continuationStep === null || analysis.continuationStep === '') {
|
|
7252
7556
|
// This should not be reachable due to the `hasPendingState` check,
|
|
7253
7557
|
// but it ensures type safety for `continuationStep`.
|
|
7254
7558
|
throw new KitError({
|
|
@@ -7412,7 +7716,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7412
7716
|
async bridge(params) {
|
|
7413
7717
|
// CCTP-specific bridge params validation (includes base validation)
|
|
7414
7718
|
assertCCTPv2BridgeParams(params);
|
|
7415
|
-
const { source,
|
|
7719
|
+
const { source, amount, token } = params;
|
|
7416
7720
|
// Extract operation context from source wallet context for balance validation
|
|
7417
7721
|
const sourceOperationContext = this.extractOperationContext(source);
|
|
7418
7722
|
// Validate USDC balance for transaction on source chain
|
|
@@ -7423,18 +7727,6 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7423
7727
|
tokenAddress: source.chain.usdcAddress,
|
|
7424
7728
|
operationContext: sourceOperationContext,
|
|
7425
7729
|
});
|
|
7426
|
-
// Validate native balance > 0 for gas fees on source chain
|
|
7427
|
-
await validateNativeBalanceForTransaction({
|
|
7428
|
-
adapter: source.adapter,
|
|
7429
|
-
operationContext: sourceOperationContext,
|
|
7430
|
-
});
|
|
7431
|
-
// Extract operation context from destination wallet context
|
|
7432
|
-
const destinationOperationContext = this.extractOperationContext(destination);
|
|
7433
|
-
// Validate native balance > 0 for gas fees on destination chain
|
|
7434
|
-
await validateNativeBalanceForTransaction({
|
|
7435
|
-
adapter: destination.adapter,
|
|
7436
|
-
operationContext: destinationOperationContext,
|
|
7437
|
-
});
|
|
7438
7730
|
return bridge(params, this);
|
|
7439
7731
|
}
|
|
7440
7732
|
/**
|
|
@@ -7882,8 +8174,10 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7882
8174
|
}
|
|
7883
8175
|
// Step 2: Request re-attestation
|
|
7884
8176
|
await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
|
|
7885
|
-
// Step 3: Poll for fresh attestation
|
|
7886
|
-
|
|
8177
|
+
// Step 3: Poll for fresh attestation until expirationBlock === '0'
|
|
8178
|
+
// The expiration block transitions from non-zero to zero when Circle
|
|
8179
|
+
// completes processing the re-attestation request.
|
|
8180
|
+
const response = await fetchReAttestedAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7887
8181
|
const message = response.messages[0];
|
|
7888
8182
|
if (!message) {
|
|
7889
8183
|
throw new Error('Failed to re-attest: No attestation found after re-attestation request');
|