@circle-fin/provider-cctp-v2 1.0.5 → 1.2.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 +27 -0
- package/index.cjs +688 -80
- package/index.d.ts +232 -5
- package/index.mjs +686 -81
- package/package.json +13 -1
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2026, Circle Internet Group, Inc. All rights reserved.
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*
|
|
@@ -379,8 +379,11 @@ const ArcTestnet = defineChain({
|
|
|
379
379
|
name: 'Arc Testnet',
|
|
380
380
|
title: 'ArcTestnet',
|
|
381
381
|
nativeCurrency: {
|
|
382
|
-
name: '
|
|
383
|
-
symbol: '
|
|
382
|
+
name: 'USDC',
|
|
383
|
+
symbol: 'USDC',
|
|
384
|
+
// Arc uses native USDC with 18 decimals for gas payments (EVM standard).
|
|
385
|
+
// Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
|
|
386
|
+
// See: https://docs.arc.network/arc/references/contract-addresses
|
|
384
387
|
decimals: 18,
|
|
385
388
|
},
|
|
386
389
|
chainId: 5042002,
|
|
@@ -3014,7 +3017,9 @@ const makeApiRequest = async (url, method, isValidType, config, body) => {
|
|
|
3014
3017
|
signal: controller.signal,
|
|
3015
3018
|
};
|
|
3016
3019
|
// Add body for methods that support it
|
|
3017
|
-
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(method))
|
|
3020
|
+
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
3021
|
+
requestInit.body = JSON.stringify(body);
|
|
3022
|
+
}
|
|
3018
3023
|
const response = await fetch(url, requestInit);
|
|
3019
3024
|
clearTimeout(timeoutId);
|
|
3020
3025
|
if (!response.ok) {
|
|
@@ -3192,6 +3197,30 @@ const pollApiWithValidation = async (url, method, isValidType, config = {}, body
|
|
|
3192
3197
|
const pollApiGet = async (url, isValidType, config) => {
|
|
3193
3198
|
return pollApiWithValidation(url, 'GET', isValidType, config);
|
|
3194
3199
|
};
|
|
3200
|
+
/**
|
|
3201
|
+
* Convenience function for making POST requests with validation.
|
|
3202
|
+
*
|
|
3203
|
+
* @typeParam TResponseType - The expected response type after validation
|
|
3204
|
+
* @typeParam TBody - The type of the request body
|
|
3205
|
+
* @param url - The API endpoint URL
|
|
3206
|
+
* @param body - The request body
|
|
3207
|
+
* @param isValidType - Type guard function to validate the response
|
|
3208
|
+
* @param config - Optional configuration overrides
|
|
3209
|
+
* @returns Promise resolving to the validated response
|
|
3210
|
+
*
|
|
3211
|
+
* @example
|
|
3212
|
+
* ```typescript
|
|
3213
|
+
* const result = await pollApiPost(
|
|
3214
|
+
* 'https://api.example.com/submit',
|
|
3215
|
+
* { name: 'John', email: 'john@example.com' },
|
|
3216
|
+
* isSubmissionResponse,
|
|
3217
|
+
* { maxRetries: 3 }
|
|
3218
|
+
* )
|
|
3219
|
+
* ```
|
|
3220
|
+
*/
|
|
3221
|
+
const pollApiPost = async (url, body, isValidType, config) => {
|
|
3222
|
+
return pollApiWithValidation(url, 'POST', isValidType, config, body);
|
|
3223
|
+
};
|
|
3195
3224
|
|
|
3196
3225
|
/**
|
|
3197
3226
|
* Valid recoverability values for error handling strategies.
|
|
@@ -3399,6 +3428,24 @@ function validateErrorDetails(details) {
|
|
|
3399
3428
|
* stays within KitError's constraints.
|
|
3400
3429
|
*/
|
|
3401
3430
|
const MAX_MESSAGE_LENGTH = 950;
|
|
3431
|
+
/**
|
|
3432
|
+
* Standard error message for invalid amount format.
|
|
3433
|
+
*
|
|
3434
|
+
* The SDK enforces strict dot-decimal notation for amount values. This constant
|
|
3435
|
+
* provides a consistent error message when users provide amounts with:
|
|
3436
|
+
* - Comma decimals (e.g., "1,5")
|
|
3437
|
+
* - Thousand separators (e.g., "1,000.50")
|
|
3438
|
+
* - Non-numeric characters
|
|
3439
|
+
* - Invalid format
|
|
3440
|
+
*/
|
|
3441
|
+
const AMOUNT_FORMAT_ERROR_MESSAGE = 'Amount must be a numeric string with dot (.) as decimal separator (e.g., "10.5", "100"), with no thousand separators or comma decimals';
|
|
3442
|
+
/**
|
|
3443
|
+
* Error message for invalid maxFee format.
|
|
3444
|
+
*
|
|
3445
|
+
* Used when validating the maxFee configuration parameter. The maxFee can be zero
|
|
3446
|
+
* or positive and must follow strict dot-decimal notation.
|
|
3447
|
+
*/
|
|
3448
|
+
const MAX_FEE_FORMAT_ERROR_MESSAGE = 'maxFee must be a numeric string with dot (.) as decimal separator (e.g., "1", "0.5", ".5", "1.5"), with no thousand separators or comma decimals';
|
|
3402
3449
|
|
|
3403
3450
|
/**
|
|
3404
3451
|
* Structured error class for Stablecoin Kit operations.
|
|
@@ -3594,6 +3641,12 @@ const BalanceError = {
|
|
|
3594
3641
|
code: 9001,
|
|
3595
3642
|
name: 'BALANCE_INSUFFICIENT_TOKEN',
|
|
3596
3643
|
type: 'BALANCE',
|
|
3644
|
+
},
|
|
3645
|
+
/** Insufficient native token (ETH/SOL/etc) for gas fees */
|
|
3646
|
+
INSUFFICIENT_GAS: {
|
|
3647
|
+
code: 9002,
|
|
3648
|
+
name: 'BALANCE_INSUFFICIENT_GAS',
|
|
3649
|
+
type: 'BALANCE',
|
|
3597
3650
|
}};
|
|
3598
3651
|
|
|
3599
3652
|
/**
|
|
@@ -3816,7 +3869,7 @@ function createValidationErrorFromZod(zodError, context) {
|
|
|
3816
3869
|
*
|
|
3817
3870
|
* @param chain - The blockchain network where the balance check failed
|
|
3818
3871
|
* @param token - The token symbol (e.g., 'USDC', 'ETH')
|
|
3819
|
-
* @param
|
|
3872
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3820
3873
|
* @returns KitError with insufficient token balance details
|
|
3821
3874
|
*
|
|
3822
3875
|
* @example
|
|
@@ -3829,24 +3882,71 @@ function createValidationErrorFromZod(zodError, context) {
|
|
|
3829
3882
|
*
|
|
3830
3883
|
* @example
|
|
3831
3884
|
* ```typescript
|
|
3832
|
-
* // With
|
|
3885
|
+
* // With trace context for debugging
|
|
3833
3886
|
* try {
|
|
3834
3887
|
* await transfer(...)
|
|
3835
3888
|
* } catch (error) {
|
|
3836
|
-
* throw createInsufficientTokenBalanceError('Base', 'USDC',
|
|
3889
|
+
* throw createInsufficientTokenBalanceError('Base', 'USDC', {
|
|
3890
|
+
* rawError: error,
|
|
3891
|
+
* balance: '1000000',
|
|
3892
|
+
* amount: '5000000',
|
|
3893
|
+
* })
|
|
3837
3894
|
* }
|
|
3838
3895
|
* ```
|
|
3839
3896
|
*/
|
|
3840
|
-
function createInsufficientTokenBalanceError(chain, token,
|
|
3897
|
+
function createInsufficientTokenBalanceError(chain, token, trace) {
|
|
3841
3898
|
return new KitError({
|
|
3842
3899
|
...BalanceError.INSUFFICIENT_TOKEN,
|
|
3843
3900
|
recoverability: 'FATAL',
|
|
3844
3901
|
message: `Insufficient ${token} balance on ${chain}`,
|
|
3845
3902
|
cause: {
|
|
3846
3903
|
trace: {
|
|
3904
|
+
...trace,
|
|
3847
3905
|
chain,
|
|
3848
3906
|
token,
|
|
3849
|
-
|
|
3907
|
+
},
|
|
3908
|
+
},
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Creates error for insufficient gas funds.
|
|
3913
|
+
*
|
|
3914
|
+
* This error is thrown when a wallet does not have enough native tokens
|
|
3915
|
+
* (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
|
|
3916
|
+
* as it requires user intervention to add gas funds.
|
|
3917
|
+
*
|
|
3918
|
+
* @param chain - The blockchain network where the gas check failed
|
|
3919
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3920
|
+
* @returns KitError with insufficient gas details
|
|
3921
|
+
*
|
|
3922
|
+
* @example
|
|
3923
|
+
* ```typescript
|
|
3924
|
+
* import { createInsufficientGasError } from '@core/errors'
|
|
3925
|
+
*
|
|
3926
|
+
* throw createInsufficientGasError('Ethereum')
|
|
3927
|
+
* // Message: "Insufficient gas funds on Ethereum"
|
|
3928
|
+
* ```
|
|
3929
|
+
*
|
|
3930
|
+
* @example
|
|
3931
|
+
* ```typescript
|
|
3932
|
+
* // With trace context for debugging
|
|
3933
|
+
* throw createInsufficientGasError('Ethereum', {
|
|
3934
|
+
* rawError: error,
|
|
3935
|
+
* gasRequired: '21000',
|
|
3936
|
+
* gasAvailable: '10000',
|
|
3937
|
+
* walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
3938
|
+
* })
|
|
3939
|
+
* ```
|
|
3940
|
+
*/
|
|
3941
|
+
function createInsufficientGasError(chain, trace) {
|
|
3942
|
+
return new KitError({
|
|
3943
|
+
...BalanceError.INSUFFICIENT_GAS,
|
|
3944
|
+
recoverability: 'FATAL',
|
|
3945
|
+
message: `Insufficient gas funds on ${chain}`,
|
|
3946
|
+
cause: {
|
|
3947
|
+
trace: {
|
|
3948
|
+
...trace,
|
|
3949
|
+
chain,
|
|
3850
3950
|
},
|
|
3851
3951
|
},
|
|
3852
3952
|
});
|
|
@@ -3908,6 +4008,38 @@ function isKitError(error) {
|
|
|
3908
4008
|
function isFatalError(error) {
|
|
3909
4009
|
return isKitError(error) && error.recoverability === 'FATAL';
|
|
3910
4010
|
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Safely extracts error message from any error type.
|
|
4013
|
+
*
|
|
4014
|
+
* This utility handles different error types gracefully, extracting
|
|
4015
|
+
* meaningful messages from Error instances, string errors, or providing
|
|
4016
|
+
* a fallback for unknown error types. Never throws.
|
|
4017
|
+
*
|
|
4018
|
+
* @param error - Unknown error to extract message from
|
|
4019
|
+
* @returns Error message string, or fallback message
|
|
4020
|
+
*
|
|
4021
|
+
* @example
|
|
4022
|
+
* ```typescript
|
|
4023
|
+
* import { getErrorMessage } from '@core/errors'
|
|
4024
|
+
*
|
|
4025
|
+
* try {
|
|
4026
|
+
* await riskyOperation()
|
|
4027
|
+
* } catch (error) {
|
|
4028
|
+
* const message = getErrorMessage(error)
|
|
4029
|
+
* console.log('Error occurred:', message)
|
|
4030
|
+
* // Works with Error, KitError, string, or any other type
|
|
4031
|
+
* }
|
|
4032
|
+
* ```
|
|
4033
|
+
*/
|
|
4034
|
+
function getErrorMessage(error) {
|
|
4035
|
+
if (error instanceof Error) {
|
|
4036
|
+
return error.message;
|
|
4037
|
+
}
|
|
4038
|
+
if (typeof error === 'string') {
|
|
4039
|
+
return error;
|
|
4040
|
+
}
|
|
4041
|
+
return 'An unknown error occurred';
|
|
4042
|
+
}
|
|
3911
4043
|
|
|
3912
4044
|
/**
|
|
3913
4045
|
* Validates data against a Zod schema with enhanced error reporting.
|
|
@@ -4405,41 +4537,46 @@ var TransferSpeed;
|
|
|
4405
4537
|
* - regexMessage: error message when the basic numeric format fails.
|
|
4406
4538
|
* - maxDecimals: maximum number of decimal places allowed (e.g., 6 for USDC).
|
|
4407
4539
|
*/
|
|
4408
|
-
const createDecimalStringValidator = (options) => (schema) =>
|
|
4409
|
-
|
|
4410
|
-
.
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
return;
|
|
4418
|
-
}
|
|
4419
|
-
// Check decimal precision if maxDecimals is specified
|
|
4420
|
-
if (options.maxDecimals !== undefined) {
|
|
4421
|
-
const decimalPart = val.split('.')[1];
|
|
4422
|
-
if (decimalPart && decimalPart.length > options.maxDecimals) {
|
|
4540
|
+
const createDecimalStringValidator = (options) => (schema) => {
|
|
4541
|
+
// Capitalize first letter of attribute name for error messages
|
|
4542
|
+
const capitalizedAttributeName = options.attributeName.charAt(0).toUpperCase() +
|
|
4543
|
+
options.attributeName.slice(1);
|
|
4544
|
+
return schema
|
|
4545
|
+
.regex(/^-?(?:\d+(?:\.\d+)?|\.\d+)$/, options.regexMessage)
|
|
4546
|
+
.superRefine((val, ctx) => {
|
|
4547
|
+
const amount = Number.parseFloat(val);
|
|
4548
|
+
if (Number.isNaN(amount)) {
|
|
4423
4549
|
ctx.addIssue({
|
|
4424
4550
|
code: z.ZodIssueCode.custom,
|
|
4425
|
-
message:
|
|
4551
|
+
message: options.regexMessage,
|
|
4426
4552
|
});
|
|
4427
4553
|
return;
|
|
4428
4554
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4555
|
+
// Check decimal precision if maxDecimals is specified
|
|
4556
|
+
if (options.maxDecimals !== undefined) {
|
|
4557
|
+
const decimalPart = val.split('.')[1];
|
|
4558
|
+
if (decimalPart && decimalPart.length > options.maxDecimals) {
|
|
4559
|
+
ctx.addIssue({
|
|
4560
|
+
code: z.ZodIssueCode.custom,
|
|
4561
|
+
message: `Maximum supported decimal places: ${options.maxDecimals.toString()}`,
|
|
4562
|
+
});
|
|
4563
|
+
return;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
if (options.allowZero && amount < 0) {
|
|
4567
|
+
ctx.addIssue({
|
|
4568
|
+
code: z.ZodIssueCode.custom,
|
|
4569
|
+
message: `${capitalizedAttributeName} must be non-negative`,
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
else if (!options.allowZero && amount <= 0) {
|
|
4573
|
+
ctx.addIssue({
|
|
4574
|
+
code: z.ZodIssueCode.custom,
|
|
4575
|
+
message: `${capitalizedAttributeName} must be greater than 0`,
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
});
|
|
4579
|
+
};
|
|
4443
4580
|
/**
|
|
4444
4581
|
* Schema for validating chain definitions.
|
|
4445
4582
|
* This ensures the basic structure of a chain definition is valid.
|
|
@@ -4599,7 +4736,7 @@ const bridgeParamsSchema = z.object({
|
|
|
4599
4736
|
.min(1, 'Required')
|
|
4600
4737
|
.pipe(createDecimalStringValidator({
|
|
4601
4738
|
allowZero: false,
|
|
4602
|
-
regexMessage:
|
|
4739
|
+
regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
|
|
4603
4740
|
attributeName: 'amount',
|
|
4604
4741
|
maxDecimals: 6,
|
|
4605
4742
|
})(z.string())),
|
|
@@ -4612,7 +4749,7 @@ const bridgeParamsSchema = z.object({
|
|
|
4612
4749
|
.string()
|
|
4613
4750
|
.pipe(createDecimalStringValidator({
|
|
4614
4751
|
allowZero: true,
|
|
4615
|
-
regexMessage:
|
|
4752
|
+
regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
|
|
4616
4753
|
attributeName: 'maxFee',
|
|
4617
4754
|
maxDecimals: 6,
|
|
4618
4755
|
})(z.string()))
|
|
@@ -4621,6 +4758,19 @@ const bridgeParamsSchema = z.object({
|
|
|
4621
4758
|
}),
|
|
4622
4759
|
});
|
|
4623
4760
|
|
|
4761
|
+
/**
|
|
4762
|
+
* Base URL for Circle's IRIS API (mainnet/production).
|
|
4763
|
+
*
|
|
4764
|
+
* The IRIS API provides attestation services for CCTP cross-chain transfers.
|
|
4765
|
+
*/
|
|
4766
|
+
const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
4767
|
+
/**
|
|
4768
|
+
* Base URL for Circle's IRIS API (testnet/sandbox).
|
|
4769
|
+
*
|
|
4770
|
+
* Used for development and testing on testnet chains.
|
|
4771
|
+
*/
|
|
4772
|
+
const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
4773
|
+
|
|
4624
4774
|
/**
|
|
4625
4775
|
* Type guard to validate the API response structure.
|
|
4626
4776
|
*
|
|
@@ -4669,9 +4819,7 @@ const isFastBurnFeeResponse = (data) => {
|
|
|
4669
4819
|
* @returns The complete API URL
|
|
4670
4820
|
*/
|
|
4671
4821
|
function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
4672
|
-
const baseUrl = isTestnet
|
|
4673
|
-
? 'https://iris-api-sandbox.circle.com'
|
|
4674
|
-
: 'https://iris-api.circle.com';
|
|
4822
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
4675
4823
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
|
|
4676
4824
|
}
|
|
4677
4825
|
const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
@@ -4687,15 +4835,24 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
4687
4835
|
* - Retry delays: 9 × 200 ms = 1 800 ms
|
|
4688
4836
|
* - Total max time: 2 000 ms + 1 800 ms = 3 800 ms
|
|
4689
4837
|
*
|
|
4838
|
+
* @remarks
|
|
4839
|
+
* The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps = 0.013%).
|
|
4840
|
+
* This function scales the value by 100 to preserve 2 decimal places of precision.
|
|
4841
|
+
* Callers must divide by 1,000,000 (instead of 10,000) when calculating fees.
|
|
4842
|
+
*
|
|
4690
4843
|
* @param sourceDomain - The source domain
|
|
4691
4844
|
* @param destinationDomain - The destination domain
|
|
4692
4845
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
4693
|
-
* @returns The minimum fee
|
|
4846
|
+
* @returns The minimum fee in scaled basis points (bps × 100)
|
|
4694
4847
|
* @throws Error if the input domains are invalid, the API request fails, returns invalid data, or network errors occur
|
|
4695
4848
|
* @example
|
|
4696
4849
|
* ```typescript
|
|
4697
|
-
* const
|
|
4698
|
-
* console.log(
|
|
4850
|
+
* const scaledBps = await fetchUsdcFastBurnFee(0, 6, false) // Ethereum -> Base
|
|
4851
|
+
* console.log(scaledBps) // 130n (representing 1.3 bps)
|
|
4852
|
+
*
|
|
4853
|
+
* // To calculate fee for an amount:
|
|
4854
|
+
* const amount = 1_000_000n // 1 USDC
|
|
4855
|
+
* const fee = (scaledBps * amount) / 1_000_000n // 130n (0.00013 USDC)
|
|
4699
4856
|
* ```
|
|
4700
4857
|
*/
|
|
4701
4858
|
async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet) {
|
|
@@ -4721,14 +4878,16 @@ async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet)
|
|
|
4721
4878
|
if (!fastTier) {
|
|
4722
4879
|
throw new Error(`No fast tier (finalityThreshold: ${FAST_TIER_FINALITY_THRESHOLD.toString()}) available in API response`);
|
|
4723
4880
|
}
|
|
4724
|
-
// Convert minimumFee to bigint
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
throw new Error(`Invalid minimumFee value: cannot
|
|
4881
|
+
// Convert minimumFee to scaled basis points (bigint)
|
|
4882
|
+
// The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps = 0.013%).
|
|
4883
|
+
// We scale by 100 to preserve 2 decimal places of precision.
|
|
4884
|
+
// The caller (getMaxFee) must divide by 1,000,000 instead of 10,000.
|
|
4885
|
+
const feeValue = Number.parseFloat(String(fastTier.minimumFee));
|
|
4886
|
+
if (Number.isNaN(feeValue) || !Number.isFinite(feeValue)) {
|
|
4887
|
+
throw new Error(`Invalid minimumFee value: cannot parse "${String(fastTier.minimumFee)}" as a number`);
|
|
4731
4888
|
}
|
|
4889
|
+
// Scale by 100 and round to get integer representation
|
|
4890
|
+
const minimumFee = BigInt(Math.round(feeValue * 100));
|
|
4732
4891
|
// Validate that minimumFee is non-negative
|
|
4733
4892
|
if (minimumFee < 0n) {
|
|
4734
4893
|
throw new Error('Invalid minimumFee: value must be non-negative');
|
|
@@ -4903,9 +5062,7 @@ const isAttestationResponse = (obj) => {
|
|
|
4903
5062
|
* ```
|
|
4904
5063
|
*/
|
|
4905
5064
|
const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet) => {
|
|
4906
|
-
const baseUrl = isTestnet
|
|
4907
|
-
? 'https://iris-api-sandbox.circle.com'
|
|
4908
|
-
: 'https://iris-api.circle.com';
|
|
5065
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
4909
5066
|
const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
|
|
4910
5067
|
url.searchParams.set('transactionHash', transactionHash);
|
|
4911
5068
|
return url.toString();
|
|
@@ -4950,6 +5107,133 @@ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, conf
|
|
|
4950
5107
|
const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
|
|
4951
5108
|
return await pollApiGet(url, isAttestationResponse, effectiveConfig);
|
|
4952
5109
|
};
|
|
5110
|
+
/**
|
|
5111
|
+
* Type guard that validates attestation response structure without requiring completion status.
|
|
5112
|
+
*
|
|
5113
|
+
* This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
|
|
5114
|
+
* attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
|
|
5115
|
+
* this function does not throw if no complete attestation is found.
|
|
5116
|
+
*
|
|
5117
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5118
|
+
* @returns True if the object has valid attestation structure
|
|
5119
|
+
* @throws {Error} With "Invalid attestation response structure" if structure is invalid
|
|
5120
|
+
* @internal
|
|
5121
|
+
*/
|
|
5122
|
+
const isAttestationResponseWithoutStatusCheck = (obj) => {
|
|
5123
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
5124
|
+
throw new Error('Invalid attestation response structure');
|
|
5125
|
+
}
|
|
5126
|
+
return true;
|
|
5127
|
+
};
|
|
5128
|
+
/**
|
|
5129
|
+
* Fetches attestation data without requiring the attestation to be complete.
|
|
5130
|
+
*
|
|
5131
|
+
* This function is useful for retrieving attestation data (particularly the nonce)
|
|
5132
|
+
* from an existing transaction, even if the attestation has expired or is pending.
|
|
5133
|
+
* It uses minimal retries since we're fetching existing data, not waiting for completion.
|
|
5134
|
+
*
|
|
5135
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
5136
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
5137
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5138
|
+
* @param config - Optional configuration overrides
|
|
5139
|
+
* @returns The attestation response data (may contain incomplete/expired attestations)
|
|
5140
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
5141
|
+
*
|
|
5142
|
+
* @example
|
|
5143
|
+
* ```typescript
|
|
5144
|
+
* // Fetch existing attestation to extract nonce for re-attestation
|
|
5145
|
+
* const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
|
|
5146
|
+
* const nonce = response.messages[0]?.eventNonce
|
|
5147
|
+
* ```
|
|
5148
|
+
*/
|
|
5149
|
+
const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
|
|
5150
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
5151
|
+
// Use minimal retries since we're just fetching existing data
|
|
5152
|
+
const effectiveConfig = {
|
|
5153
|
+
...DEFAULT_CONFIG,
|
|
5154
|
+
maxRetries: 3,
|
|
5155
|
+
...config,
|
|
5156
|
+
};
|
|
5157
|
+
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
5158
|
+
};
|
|
5159
|
+
/**
|
|
5160
|
+
* Builds the IRIS API URL for re-attestation requests.
|
|
5161
|
+
*
|
|
5162
|
+
* Constructs the URL for Circle's re-attestation endpoint that allows
|
|
5163
|
+
* requesting a fresh attestation for an expired nonce.
|
|
5164
|
+
*
|
|
5165
|
+
* @param nonce - The nonce from the original attestation
|
|
5166
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5167
|
+
* @returns A fully qualified URL string for the re-attestation endpoint
|
|
5168
|
+
*
|
|
5169
|
+
* @example
|
|
5170
|
+
* ```typescript
|
|
5171
|
+
* // Mainnet URL
|
|
5172
|
+
* const mainnetUrl = buildReAttestUrl('0xabc', false)
|
|
5173
|
+
* // => 'https://iris-api.circle.com/v2/reattest/0xabc'
|
|
5174
|
+
*
|
|
5175
|
+
* // Testnet URL
|
|
5176
|
+
* const testnetUrl = buildReAttestUrl('0xabc', true)
|
|
5177
|
+
* // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
|
|
5178
|
+
* ```
|
|
5179
|
+
*/
|
|
5180
|
+
const buildReAttestUrl = (nonce, isTestnet) => {
|
|
5181
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
5182
|
+
const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
|
|
5183
|
+
return url.toString();
|
|
5184
|
+
};
|
|
5185
|
+
/**
|
|
5186
|
+
* Type guard that validates the re-attestation API response structure.
|
|
5187
|
+
*
|
|
5188
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5189
|
+
* @returns True if the object matches the ReAttestationResponse shape
|
|
5190
|
+
* @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
|
|
5191
|
+
* @internal
|
|
5192
|
+
*/
|
|
5193
|
+
const isReAttestationResponse = (obj) => {
|
|
5194
|
+
if (typeof obj !== 'object' ||
|
|
5195
|
+
obj === null ||
|
|
5196
|
+
!('message' in obj) ||
|
|
5197
|
+
!('nonce' in obj) ||
|
|
5198
|
+
typeof obj.message !== 'string' ||
|
|
5199
|
+
typeof obj.nonce !== 'string') {
|
|
5200
|
+
throw new Error('Invalid re-attestation response structure');
|
|
5201
|
+
}
|
|
5202
|
+
return true;
|
|
5203
|
+
};
|
|
5204
|
+
/**
|
|
5205
|
+
* Requests re-attestation for an expired attestation nonce.
|
|
5206
|
+
*
|
|
5207
|
+
* This function calls Circle's re-attestation API endpoint to request a fresh
|
|
5208
|
+
* attestation for a previously issued nonce. After calling this function,
|
|
5209
|
+
* you should poll `fetchAttestation` to retrieve the new attestation.
|
|
5210
|
+
*
|
|
5211
|
+
* @param nonce - The nonce from the original (expired) attestation
|
|
5212
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5213
|
+
* @param config - Optional configuration overrides for the request
|
|
5214
|
+
* @returns The re-attestation response confirming the request was accepted
|
|
5215
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
5216
|
+
*
|
|
5217
|
+
* @example
|
|
5218
|
+
* ```typescript
|
|
5219
|
+
* // Request re-attestation for an expired nonce
|
|
5220
|
+
* const response = await requestReAttestation('0xabc', true)
|
|
5221
|
+
* console.log(response.message) // "Re-attestation successfully requested for nonce."
|
|
5222
|
+
*
|
|
5223
|
+
* // After requesting re-attestation, poll for the new attestation
|
|
5224
|
+
* const attestation = await fetchAttestation(domainId, txHash, true)
|
|
5225
|
+
* ```
|
|
5226
|
+
*/
|
|
5227
|
+
const requestReAttestation = async (nonce, isTestnet, config = {}) => {
|
|
5228
|
+
const url = buildReAttestUrl(nonce, isTestnet);
|
|
5229
|
+
// Use minimal retries since we're just submitting a request, not polling for state
|
|
5230
|
+
const effectiveConfig = {
|
|
5231
|
+
...DEFAULT_CONFIG,
|
|
5232
|
+
maxRetries: 3,
|
|
5233
|
+
...config,
|
|
5234
|
+
};
|
|
5235
|
+
return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
|
|
5236
|
+
};
|
|
4953
5237
|
|
|
4954
5238
|
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
4955
5239
|
/**
|
|
@@ -5187,6 +5471,170 @@ mintAddress) => {
|
|
|
5187
5471
|
}
|
|
5188
5472
|
};
|
|
5189
5473
|
|
|
5474
|
+
/**
|
|
5475
|
+
* Converts a block number to bigint with validation.
|
|
5476
|
+
*
|
|
5477
|
+
* @param value - The block number value to convert (bigint, number, or string)
|
|
5478
|
+
* @returns The validated block number as a bigint
|
|
5479
|
+
* @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
|
|
5480
|
+
* @internal
|
|
5481
|
+
*/
|
|
5482
|
+
const toBlockNumber = (value) => {
|
|
5483
|
+
if (value === null || value === undefined) {
|
|
5484
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
|
|
5485
|
+
}
|
|
5486
|
+
// Empty string edge case - BigInt('') === 0n which is misleading
|
|
5487
|
+
if (value === '') {
|
|
5488
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
|
|
5489
|
+
}
|
|
5490
|
+
// For numbers, validate before BigInt conversion
|
|
5491
|
+
if (typeof value === 'number') {
|
|
5492
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
5493
|
+
throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
let result;
|
|
5497
|
+
try {
|
|
5498
|
+
result = BigInt(value);
|
|
5499
|
+
}
|
|
5500
|
+
catch {
|
|
5501
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
|
|
5502
|
+
}
|
|
5503
|
+
if (result < 0n) {
|
|
5504
|
+
throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
|
|
5505
|
+
}
|
|
5506
|
+
return result;
|
|
5507
|
+
};
|
|
5508
|
+
/**
|
|
5509
|
+
* Determines whether an attestation has expired based on the current block number.
|
|
5510
|
+
*
|
|
5511
|
+
* An attestation expires when the destination chain's current block number is greater
|
|
5512
|
+
* than or equal to the expiration block specified in the attestation message.
|
|
5513
|
+
* Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
|
|
5514
|
+
*
|
|
5515
|
+
* @param attestation - The attestation message containing expiration block information
|
|
5516
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
5517
|
+
* @returns `true` if the attestation has expired, `false` if still valid or never expires
|
|
5518
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
5519
|
+
*
|
|
5520
|
+
* @example
|
|
5521
|
+
* ```typescript
|
|
5522
|
+
* import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
|
|
5523
|
+
*
|
|
5524
|
+
* // Check if attestation is expired on EVM chain
|
|
5525
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
5526
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
5527
|
+
* const expired = isAttestationExpired(attestation, currentBlock)
|
|
5528
|
+
*
|
|
5529
|
+
* if (expired) {
|
|
5530
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
5531
|
+
* }
|
|
5532
|
+
* ```
|
|
5533
|
+
*
|
|
5534
|
+
* @example
|
|
5535
|
+
* ```typescript
|
|
5536
|
+
* // Check on Solana
|
|
5537
|
+
* const slot = await adapter.getConnection(destinationChain).getSlot()
|
|
5538
|
+
* const expired = isAttestationExpired(attestation, slot)
|
|
5539
|
+
* ```
|
|
5540
|
+
*/
|
|
5541
|
+
const isAttestationExpired = (attestation, currentBlockNumber) => {
|
|
5542
|
+
const currentBlock = toBlockNumber(currentBlockNumber);
|
|
5543
|
+
const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
|
|
5544
|
+
// 0n means it never expires (re-attested messages and slow transfers)
|
|
5545
|
+
if (expiration === 0n) {
|
|
5546
|
+
return false;
|
|
5547
|
+
}
|
|
5548
|
+
// Attestation is expired if current block >= expiration block
|
|
5549
|
+
return currentBlock >= expiration;
|
|
5550
|
+
};
|
|
5551
|
+
/**
|
|
5552
|
+
* Calculates the number of blocks remaining until an attestation expires.
|
|
5553
|
+
*
|
|
5554
|
+
* Returns the difference between the expiration block and the current block number.
|
|
5555
|
+
* Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
|
|
5556
|
+
* Returns `0n` or a negative bigint if the attestation has already expired.
|
|
5557
|
+
*
|
|
5558
|
+
* @param attestation - The attestation message containing expiration block information
|
|
5559
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
5560
|
+
* @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
|
|
5561
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
5562
|
+
*
|
|
5563
|
+
* @example
|
|
5564
|
+
* ```typescript
|
|
5565
|
+
* import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
|
|
5566
|
+
*
|
|
5567
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
5568
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
5569
|
+
* const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
|
|
5570
|
+
*
|
|
5571
|
+
* if (blocksRemaining === null) {
|
|
5572
|
+
* console.log('Attestation never expires')
|
|
5573
|
+
* } else if (blocksRemaining <= 0n) {
|
|
5574
|
+
* console.log('Attestation has expired')
|
|
5575
|
+
* } else {
|
|
5576
|
+
* console.log(`${blocksRemaining} blocks until expiry`)
|
|
5577
|
+
* }
|
|
5578
|
+
* ```
|
|
5579
|
+
*/
|
|
5580
|
+
const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
|
|
5581
|
+
const currentBlock = toBlockNumber(currentBlockNumber);
|
|
5582
|
+
const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
|
|
5583
|
+
// 0n means it never expires (re-attested messages and slow transfers)
|
|
5584
|
+
if (expiration === 0n) {
|
|
5585
|
+
return null;
|
|
5586
|
+
}
|
|
5587
|
+
// Return the difference (can be negative if expired)
|
|
5588
|
+
return expiration - currentBlock;
|
|
5589
|
+
};
|
|
5590
|
+
/**
|
|
5591
|
+
* Determines whether a mint failure was caused by an expired attestation.
|
|
5592
|
+
*
|
|
5593
|
+
* This function inspects the error thrown during a mint operation to detect
|
|
5594
|
+
* if the failure is due to the attestation's expiration block being exceeded.
|
|
5595
|
+
* When this returns `true`, the caller should use `reAttest()` to obtain a
|
|
5596
|
+
* fresh attestation before retrying the mint.
|
|
5597
|
+
*
|
|
5598
|
+
* @param error - The error thrown during the mint operation
|
|
5599
|
+
* @returns `true` if the error indicates the attestation has expired, `false` otherwise
|
|
5600
|
+
*
|
|
5601
|
+
* @example
|
|
5602
|
+
* ```typescript
|
|
5603
|
+
* import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
|
|
5604
|
+
*
|
|
5605
|
+
* try {
|
|
5606
|
+
* await mintRequest.execute()
|
|
5607
|
+
* } catch (error) {
|
|
5608
|
+
* if (isMintFailureRelatedToAttestation(error)) {
|
|
5609
|
+
* // Attestation expired - get a fresh one
|
|
5610
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
5611
|
+
* const newMintRequest = await provider.mint(source, destination, freshAttestation)
|
|
5612
|
+
* await newMintRequest.execute()
|
|
5613
|
+
* } else {
|
|
5614
|
+
* throw error
|
|
5615
|
+
* }
|
|
5616
|
+
* }
|
|
5617
|
+
* ```
|
|
5618
|
+
*/
|
|
5619
|
+
const isMintFailureRelatedToAttestation = (error) => {
|
|
5620
|
+
if (error === null || error === undefined) {
|
|
5621
|
+
return false;
|
|
5622
|
+
}
|
|
5623
|
+
const errorString = getErrorMessage(error).toLowerCase();
|
|
5624
|
+
// Check for Solana "MessageExpired" error pattern
|
|
5625
|
+
// Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
|
|
5626
|
+
// Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
|
|
5627
|
+
if (errorString.includes('messageexpired')) {
|
|
5628
|
+
return true;
|
|
5629
|
+
}
|
|
5630
|
+
// Check for EVM attestation expiry errors
|
|
5631
|
+
// Contract reverts with: "Message expired and must be re-signed"
|
|
5632
|
+
if (errorString.includes('message') && errorString.includes('expired')) {
|
|
5633
|
+
return true;
|
|
5634
|
+
}
|
|
5635
|
+
return false;
|
|
5636
|
+
};
|
|
5637
|
+
|
|
5190
5638
|
/**
|
|
5191
5639
|
* Checks if a decoded attestation field matches the corresponding transfer parameter.
|
|
5192
5640
|
* If the values do not match, appends a descriptive error message to the errors array.
|
|
@@ -6030,21 +6478,64 @@ const validateBalanceForTransaction = async (params) => {
|
|
|
6030
6478
|
// Extract chain name from operationContext
|
|
6031
6479
|
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6032
6480
|
// Create KitError with rich context in trace
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6481
|
+
throw createInsufficientTokenBalanceError(chainName, token, {
|
|
6482
|
+
balance: balance.toString(),
|
|
6483
|
+
amount,
|
|
6484
|
+
tokenAddress,
|
|
6485
|
+
walletAddress: operationContext.address,
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
};
|
|
6489
|
+
|
|
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
|
+
});
|
|
6048
6539
|
}
|
|
6049
6540
|
};
|
|
6050
6541
|
|
|
@@ -6921,16 +7412,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
6921
7412
|
async bridge(params) {
|
|
6922
7413
|
// CCTP-specific bridge params validation (includes base validation)
|
|
6923
7414
|
assertCCTPv2BridgeParams(params);
|
|
6924
|
-
const { source, amount, token } = params;
|
|
7415
|
+
const { source, destination, amount, token } = params;
|
|
6925
7416
|
// Extract operation context from source wallet context for balance validation
|
|
6926
|
-
const
|
|
6927
|
-
// Validate balance for transaction
|
|
7417
|
+
const sourceOperationContext = this.extractOperationContext(source);
|
|
7418
|
+
// Validate USDC balance for transaction on source chain
|
|
6928
7419
|
await validateBalanceForTransaction({
|
|
6929
7420
|
adapter: source.adapter,
|
|
6930
7421
|
amount,
|
|
6931
7422
|
token,
|
|
6932
7423
|
tokenAddress: source.chain.usdcAddress,
|
|
6933
|
-
operationContext,
|
|
7424
|
+
operationContext: sourceOperationContext,
|
|
7425
|
+
});
|
|
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,
|
|
6934
7437
|
});
|
|
6935
7438
|
return bridge(params, this);
|
|
6936
7439
|
}
|
|
@@ -7011,6 +7514,19 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7011
7514
|
: Promise.resolve(0n),
|
|
7012
7515
|
]);
|
|
7013
7516
|
const estimateResult = {
|
|
7517
|
+
token: params.token,
|
|
7518
|
+
amount: params.amount,
|
|
7519
|
+
source: {
|
|
7520
|
+
address: source.address,
|
|
7521
|
+
chain: source.chain.chain,
|
|
7522
|
+
},
|
|
7523
|
+
destination: {
|
|
7524
|
+
address: destination.address,
|
|
7525
|
+
chain: destination.chain.chain,
|
|
7526
|
+
...(destination.recipientAddress && {
|
|
7527
|
+
recipientAddress: destination.recipientAddress,
|
|
7528
|
+
}),
|
|
7529
|
+
},
|
|
7014
7530
|
gasFees: [],
|
|
7015
7531
|
fees: [],
|
|
7016
7532
|
};
|
|
@@ -7296,6 +7812,93 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7296
7812
|
throw error;
|
|
7297
7813
|
}
|
|
7298
7814
|
}
|
|
7815
|
+
/**
|
|
7816
|
+
* Requests a fresh attestation for an expired attestation.
|
|
7817
|
+
*
|
|
7818
|
+
* This method is used when the original attestation has expired before the mint
|
|
7819
|
+
* transaction could be completed. It performs three steps:
|
|
7820
|
+
* 1. Fetches the existing attestation data to extract the nonce
|
|
7821
|
+
* 2. Requests re-attestation from Circle's API using the nonce
|
|
7822
|
+
* 3. Polls for the fresh attestation and returns it
|
|
7823
|
+
*
|
|
7824
|
+
* @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
|
|
7825
|
+
* @param source - The source wallet context containing the chain definition and wallet address
|
|
7826
|
+
* @param transactionHash - The transaction hash of the original burn transaction
|
|
7827
|
+
* @param config - Optional polling configuration overrides for timeout, retries, and delay
|
|
7828
|
+
* @returns A promise that resolves to the fresh attestation message
|
|
7829
|
+
* @throws {Error} With "Failed to re-attest: No nonce found for transaction" if the original
|
|
7830
|
+
* attestation cannot be found or has no nonce
|
|
7831
|
+
* @throws {Error} With "Failed to re-attest: No attestation found after re-attestation request"
|
|
7832
|
+
* if the fresh attestation cannot be retrieved
|
|
7833
|
+
* @throws {Error} With "Failed to re-attest: {details}" for other errors
|
|
7834
|
+
*
|
|
7835
|
+
* @example
|
|
7836
|
+
* ```typescript
|
|
7837
|
+
* import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
7838
|
+
* import { Chains } from '@core/chains'
|
|
7839
|
+
*
|
|
7840
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
7841
|
+
*
|
|
7842
|
+
* // After a mint fails due to expired attestation, request a fresh one
|
|
7843
|
+
* const source = {
|
|
7844
|
+
* adapter: viemAdapter,
|
|
7845
|
+
* chain: Chains.EthereumSepolia,
|
|
7846
|
+
* address: '0x1234...'
|
|
7847
|
+
* }
|
|
7848
|
+
*
|
|
7849
|
+
* try {
|
|
7850
|
+
* const freshAttestation = await provider.reAttest(
|
|
7851
|
+
* source,
|
|
7852
|
+
* '0xabc123...', // Original burn transaction hash
|
|
7853
|
+
* { timeout: 10000, maxRetries: 5 }
|
|
7854
|
+
* )
|
|
7855
|
+
*
|
|
7856
|
+
* // Use the fresh attestation to retry the mint
|
|
7857
|
+
* const mintRequest = await provider.mint(source, destination, freshAttestation)
|
|
7858
|
+
* const result = await mintRequest.execute()
|
|
7859
|
+
* } catch (error) {
|
|
7860
|
+
* console.error('Re-attestation failed:', error.message)
|
|
7861
|
+
* }
|
|
7862
|
+
* ```
|
|
7863
|
+
*/
|
|
7864
|
+
async reAttest(source, transactionHash, config) {
|
|
7865
|
+
assertCCTPv2WalletContext(source);
|
|
7866
|
+
if (!transactionHash ||
|
|
7867
|
+
typeof transactionHash !== 'string' ||
|
|
7868
|
+
transactionHash.trim() === '') {
|
|
7869
|
+
throw new Error('Failed to re-attest: Invalid transaction hash');
|
|
7870
|
+
}
|
|
7871
|
+
try {
|
|
7872
|
+
// Merge configs: defaults <- global config <- per-call config
|
|
7873
|
+
const effectiveConfig = {
|
|
7874
|
+
...this.config?.attestation,
|
|
7875
|
+
...config,
|
|
7876
|
+
};
|
|
7877
|
+
// Step 1: Get existing attestation data to extract nonce
|
|
7878
|
+
const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7879
|
+
const nonce = existingAttestation.messages[0]?.eventNonce;
|
|
7880
|
+
if (!nonce || typeof nonce !== 'string') {
|
|
7881
|
+
throw new Error('Failed to re-attest: No nonce found for transaction');
|
|
7882
|
+
}
|
|
7883
|
+
// Step 2: Request re-attestation
|
|
7884
|
+
await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
|
|
7885
|
+
// Step 3: Poll for fresh attestation
|
|
7886
|
+
const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7887
|
+
const message = response.messages[0];
|
|
7888
|
+
if (!message) {
|
|
7889
|
+
throw new Error('Failed to re-attest: No attestation found after re-attestation request');
|
|
7890
|
+
}
|
|
7891
|
+
return message;
|
|
7892
|
+
}
|
|
7893
|
+
catch (err) {
|
|
7894
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
7895
|
+
// Always prefix with 'Failed to re-attest'
|
|
7896
|
+
if (!error.message.startsWith('Failed to re-attest')) {
|
|
7897
|
+
throw new Error(`Failed to re-attest: ${error.message}`);
|
|
7898
|
+
}
|
|
7899
|
+
throw error;
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7299
7902
|
/**
|
|
7300
7903
|
* Checks if both source and destination chains support CCTP v2 transfers.
|
|
7301
7904
|
*
|
|
@@ -7375,15 +7978,17 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7375
7978
|
}
|
|
7376
7979
|
else if (config?.maxFee === undefined) {
|
|
7377
7980
|
// If the max fee is not provided and the transfer speed is fast, dynamically calculate the max fee
|
|
7378
|
-
const
|
|
7981
|
+
const scaledBps = await fetchUsdcFastBurnFee(source.chain.cctp.domain, destination.chain.cctp.domain, source.chain.isTestnet);
|
|
7379
7982
|
// Calculate fee proportional to the transfer amount
|
|
7380
7983
|
// Convert amount to minor units
|
|
7381
7984
|
const amountInMinorUnits = BigInt(amount);
|
|
7382
7985
|
// Calculate base fee proportional to the transfer amount
|
|
7383
|
-
//
|
|
7986
|
+
// The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps).
|
|
7987
|
+
// fetchUsdcFastBurnFee scales by 100 to preserve precision, so we divide by 1,000,000.
|
|
7988
|
+
// Formula: (scaledBps * amountInMinorUnits) / 1_000_000
|
|
7384
7989
|
// We use ceiling division (round up) to ensure sufficient fees and avoid failover to slow burn
|
|
7385
|
-
// Ceiling division: (a + b - 1) / b, where b =
|
|
7386
|
-
const baseFee = (
|
|
7990
|
+
// Ceiling division: (a + b - 1) / b, where b = 1_000_000, so (b - 1) = 999_999n
|
|
7991
|
+
const baseFee = (scaledBps * amountInMinorUnits + 999999n) / 1000000n;
|
|
7387
7992
|
// Add 10% buffer to account for fee fluctuations: fee + (fee * 10 / 100) = fee + (fee / 10)
|
|
7388
7993
|
maxFee = baseFee + baseFee / 10n;
|
|
7389
7994
|
}
|
|
@@ -7487,5 +8092,5 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7487
8092
|
}
|
|
7488
8093
|
}
|
|
7489
8094
|
|
|
7490
|
-
export { CCTPV2BridgingProvider, getMintRecipientAccount };
|
|
8095
|
+
export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation };
|
|
7491
8096
|
//# sourceMappingURL=index.mjs.map
|