@circle-fin/provider-cctp-v2 1.0.5 → 1.1.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 +16 -0
- package/index.cjs +352 -54
- package/index.d.ts +89 -1
- package/index.mjs +352 -54
- package/package.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @circle-fin/provider-cctp-v2
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `reAttest` method to `CCTPV2BridgingProvider` for handling expired attestations.
|
|
8
|
+
|
|
9
|
+
When a CCTP v2 attestation expires before the mint transaction can be completed, users can now call `reAttest()` to request a fresh attestation using the original burn transaction hash.
|
|
10
|
+
|
|
11
|
+
- **Enhanced bridge estimate response**: `EstimateResult` now includes `token`, `amount`, `source`, `destination` fields to provide complete transfer information alongside cost estimates.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Fix fast burn fee calculation to handle decimal basis points from IRIS API
|
|
16
|
+
|
|
17
|
+
The IRIS API returns `minimumFee` as decimal basis points (e.g., `"1.3"` for 1.3 bps), but the code was attempting to convert this directly to `BigInt`, which fails for non-integer values. This caused `"Max fee must be less than amount"` errors during FAST transfers.
|
|
18
|
+
|
|
3
19
|
## 1.0.5
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/index.cjs
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
|
*
|
|
@@ -3020,7 +3020,9 @@ const makeApiRequest = async (url, method, isValidType, config, body) => {
|
|
|
3020
3020
|
signal: controller.signal,
|
|
3021
3021
|
};
|
|
3022
3022
|
// Add body for methods that support it
|
|
3023
|
-
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(method))
|
|
3023
|
+
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
3024
|
+
requestInit.body = JSON.stringify(body);
|
|
3025
|
+
}
|
|
3024
3026
|
const response = await fetch(url, requestInit);
|
|
3025
3027
|
clearTimeout(timeoutId);
|
|
3026
3028
|
if (!response.ok) {
|
|
@@ -3198,6 +3200,30 @@ const pollApiWithValidation = async (url, method, isValidType, config = {}, body
|
|
|
3198
3200
|
const pollApiGet = async (url, isValidType, config) => {
|
|
3199
3201
|
return pollApiWithValidation(url, 'GET', isValidType, config);
|
|
3200
3202
|
};
|
|
3203
|
+
/**
|
|
3204
|
+
* Convenience function for making POST requests with validation.
|
|
3205
|
+
*
|
|
3206
|
+
* @typeParam TResponseType - The expected response type after validation
|
|
3207
|
+
* @typeParam TBody - The type of the request body
|
|
3208
|
+
* @param url - The API endpoint URL
|
|
3209
|
+
* @param body - The request body
|
|
3210
|
+
* @param isValidType - Type guard function to validate the response
|
|
3211
|
+
* @param config - Optional configuration overrides
|
|
3212
|
+
* @returns Promise resolving to the validated response
|
|
3213
|
+
*
|
|
3214
|
+
* @example
|
|
3215
|
+
* ```typescript
|
|
3216
|
+
* const result = await pollApiPost(
|
|
3217
|
+
* 'https://api.example.com/submit',
|
|
3218
|
+
* { name: 'John', email: 'john@example.com' },
|
|
3219
|
+
* isSubmissionResponse,
|
|
3220
|
+
* { maxRetries: 3 }
|
|
3221
|
+
* )
|
|
3222
|
+
* ```
|
|
3223
|
+
*/
|
|
3224
|
+
const pollApiPost = async (url, body, isValidType, config) => {
|
|
3225
|
+
return pollApiWithValidation(url, 'POST', isValidType, config, body);
|
|
3226
|
+
};
|
|
3201
3227
|
|
|
3202
3228
|
/**
|
|
3203
3229
|
* Valid recoverability values for error handling strategies.
|
|
@@ -3405,6 +3431,24 @@ function validateErrorDetails(details) {
|
|
|
3405
3431
|
* stays within KitError's constraints.
|
|
3406
3432
|
*/
|
|
3407
3433
|
const MAX_MESSAGE_LENGTH = 950;
|
|
3434
|
+
/**
|
|
3435
|
+
* Standard error message for invalid amount format.
|
|
3436
|
+
*
|
|
3437
|
+
* The SDK enforces strict dot-decimal notation for amount values. This constant
|
|
3438
|
+
* provides a consistent error message when users provide amounts with:
|
|
3439
|
+
* - Comma decimals (e.g., "1,5")
|
|
3440
|
+
* - Thousand separators (e.g., "1,000.50")
|
|
3441
|
+
* - Non-numeric characters
|
|
3442
|
+
* - Invalid format
|
|
3443
|
+
*/
|
|
3444
|
+
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';
|
|
3445
|
+
/**
|
|
3446
|
+
* Error message for invalid maxFee format.
|
|
3447
|
+
*
|
|
3448
|
+
* Used when validating the maxFee configuration parameter. The maxFee can be zero
|
|
3449
|
+
* or positive and must follow strict dot-decimal notation.
|
|
3450
|
+
*/
|
|
3451
|
+
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';
|
|
3408
3452
|
|
|
3409
3453
|
/**
|
|
3410
3454
|
* Structured error class for Stablecoin Kit operations.
|
|
@@ -4411,41 +4455,46 @@ var TransferSpeed;
|
|
|
4411
4455
|
* - regexMessage: error message when the basic numeric format fails.
|
|
4412
4456
|
* - maxDecimals: maximum number of decimal places allowed (e.g., 6 for USDC).
|
|
4413
4457
|
*/
|
|
4414
|
-
const createDecimalStringValidator = (options) => (schema) =>
|
|
4415
|
-
|
|
4416
|
-
.
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4425
|
-
// Check decimal precision if maxDecimals is specified
|
|
4426
|
-
if (options.maxDecimals !== undefined) {
|
|
4427
|
-
const decimalPart = val.split('.')[1];
|
|
4428
|
-
if (decimalPart && decimalPart.length > options.maxDecimals) {
|
|
4458
|
+
const createDecimalStringValidator = (options) => (schema) => {
|
|
4459
|
+
// Capitalize first letter of attribute name for error messages
|
|
4460
|
+
const capitalizedAttributeName = options.attributeName.charAt(0).toUpperCase() +
|
|
4461
|
+
options.attributeName.slice(1);
|
|
4462
|
+
return schema
|
|
4463
|
+
.regex(/^-?(?:\d+(?:\.\d+)?|\.\d+)$/, options.regexMessage)
|
|
4464
|
+
.superRefine((val, ctx) => {
|
|
4465
|
+
const amount = Number.parseFloat(val);
|
|
4466
|
+
if (Number.isNaN(amount)) {
|
|
4429
4467
|
ctx.addIssue({
|
|
4430
4468
|
code: zod.z.ZodIssueCode.custom,
|
|
4431
|
-
message:
|
|
4469
|
+
message: options.regexMessage,
|
|
4432
4470
|
});
|
|
4433
4471
|
return;
|
|
4434
4472
|
}
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4473
|
+
// Check decimal precision if maxDecimals is specified
|
|
4474
|
+
if (options.maxDecimals !== undefined) {
|
|
4475
|
+
const decimalPart = val.split('.')[1];
|
|
4476
|
+
if (decimalPart && decimalPart.length > options.maxDecimals) {
|
|
4477
|
+
ctx.addIssue({
|
|
4478
|
+
code: zod.z.ZodIssueCode.custom,
|
|
4479
|
+
message: `Maximum supported decimal places: ${options.maxDecimals.toString()}`,
|
|
4480
|
+
});
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
if (options.allowZero && amount < 0) {
|
|
4485
|
+
ctx.addIssue({
|
|
4486
|
+
code: zod.z.ZodIssueCode.custom,
|
|
4487
|
+
message: `${capitalizedAttributeName} must be non-negative`,
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
else if (!options.allowZero && amount <= 0) {
|
|
4491
|
+
ctx.addIssue({
|
|
4492
|
+
code: zod.z.ZodIssueCode.custom,
|
|
4493
|
+
message: `${capitalizedAttributeName} must be greater than 0`,
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
};
|
|
4449
4498
|
/**
|
|
4450
4499
|
* Schema for validating chain definitions.
|
|
4451
4500
|
* This ensures the basic structure of a chain definition is valid.
|
|
@@ -4605,7 +4654,7 @@ const bridgeParamsSchema = zod.z.object({
|
|
|
4605
4654
|
.min(1, 'Required')
|
|
4606
4655
|
.pipe(createDecimalStringValidator({
|
|
4607
4656
|
allowZero: false,
|
|
4608
|
-
regexMessage:
|
|
4657
|
+
regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
|
|
4609
4658
|
attributeName: 'amount',
|
|
4610
4659
|
maxDecimals: 6,
|
|
4611
4660
|
})(zod.z.string())),
|
|
@@ -4618,7 +4667,7 @@ const bridgeParamsSchema = zod.z.object({
|
|
|
4618
4667
|
.string()
|
|
4619
4668
|
.pipe(createDecimalStringValidator({
|
|
4620
4669
|
allowZero: true,
|
|
4621
|
-
regexMessage:
|
|
4670
|
+
regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
|
|
4622
4671
|
attributeName: 'maxFee',
|
|
4623
4672
|
maxDecimals: 6,
|
|
4624
4673
|
})(zod.z.string()))
|
|
@@ -4627,6 +4676,19 @@ const bridgeParamsSchema = zod.z.object({
|
|
|
4627
4676
|
}),
|
|
4628
4677
|
});
|
|
4629
4678
|
|
|
4679
|
+
/**
|
|
4680
|
+
* Base URL for Circle's IRIS API (mainnet/production).
|
|
4681
|
+
*
|
|
4682
|
+
* The IRIS API provides attestation services for CCTP cross-chain transfers.
|
|
4683
|
+
*/
|
|
4684
|
+
const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
4685
|
+
/**
|
|
4686
|
+
* Base URL for Circle's IRIS API (testnet/sandbox).
|
|
4687
|
+
*
|
|
4688
|
+
* Used for development and testing on testnet chains.
|
|
4689
|
+
*/
|
|
4690
|
+
const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
4691
|
+
|
|
4630
4692
|
/**
|
|
4631
4693
|
* Type guard to validate the API response structure.
|
|
4632
4694
|
*
|
|
@@ -4675,9 +4737,7 @@ const isFastBurnFeeResponse = (data) => {
|
|
|
4675
4737
|
* @returns The complete API URL
|
|
4676
4738
|
*/
|
|
4677
4739
|
function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
4678
|
-
const baseUrl = isTestnet
|
|
4679
|
-
? 'https://iris-api-sandbox.circle.com'
|
|
4680
|
-
: 'https://iris-api.circle.com';
|
|
4740
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
4681
4741
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
|
|
4682
4742
|
}
|
|
4683
4743
|
const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
@@ -4693,15 +4753,24 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
4693
4753
|
* - Retry delays: 9 × 200 ms = 1 800 ms
|
|
4694
4754
|
* - Total max time: 2 000 ms + 1 800 ms = 3 800 ms
|
|
4695
4755
|
*
|
|
4756
|
+
* @remarks
|
|
4757
|
+
* The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps = 0.013%).
|
|
4758
|
+
* This function scales the value by 100 to preserve 2 decimal places of precision.
|
|
4759
|
+
* Callers must divide by 1,000,000 (instead of 10,000) when calculating fees.
|
|
4760
|
+
*
|
|
4696
4761
|
* @param sourceDomain - The source domain
|
|
4697
4762
|
* @param destinationDomain - The destination domain
|
|
4698
4763
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
4699
|
-
* @returns The minimum fee
|
|
4764
|
+
* @returns The minimum fee in scaled basis points (bps × 100)
|
|
4700
4765
|
* @throws Error if the input domains are invalid, the API request fails, returns invalid data, or network errors occur
|
|
4701
4766
|
* @example
|
|
4702
4767
|
* ```typescript
|
|
4703
|
-
* const
|
|
4704
|
-
* console.log(
|
|
4768
|
+
* const scaledBps = await fetchUsdcFastBurnFee(0, 6, false) // Ethereum -> Base
|
|
4769
|
+
* console.log(scaledBps) // 130n (representing 1.3 bps)
|
|
4770
|
+
*
|
|
4771
|
+
* // To calculate fee for an amount:
|
|
4772
|
+
* const amount = 1_000_000n // 1 USDC
|
|
4773
|
+
* const fee = (scaledBps * amount) / 1_000_000n // 130n (0.00013 USDC)
|
|
4705
4774
|
* ```
|
|
4706
4775
|
*/
|
|
4707
4776
|
async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet) {
|
|
@@ -4727,14 +4796,16 @@ async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet)
|
|
|
4727
4796
|
if (!fastTier) {
|
|
4728
4797
|
throw new Error(`No fast tier (finalityThreshold: ${FAST_TIER_FINALITY_THRESHOLD.toString()}) available in API response`);
|
|
4729
4798
|
}
|
|
4730
|
-
// Convert minimumFee to bigint
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
throw new Error(`Invalid minimumFee value: cannot
|
|
4799
|
+
// Convert minimumFee to scaled basis points (bigint)
|
|
4800
|
+
// The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps = 0.013%).
|
|
4801
|
+
// We scale by 100 to preserve 2 decimal places of precision.
|
|
4802
|
+
// The caller (getMaxFee) must divide by 1,000,000 instead of 10,000.
|
|
4803
|
+
const feeValue = Number.parseFloat(String(fastTier.minimumFee));
|
|
4804
|
+
if (Number.isNaN(feeValue) || !Number.isFinite(feeValue)) {
|
|
4805
|
+
throw new Error(`Invalid minimumFee value: cannot parse "${String(fastTier.minimumFee)}" as a number`);
|
|
4737
4806
|
}
|
|
4807
|
+
// Scale by 100 and round to get integer representation
|
|
4808
|
+
const minimumFee = BigInt(Math.round(feeValue * 100));
|
|
4738
4809
|
// Validate that minimumFee is non-negative
|
|
4739
4810
|
if (minimumFee < 0n) {
|
|
4740
4811
|
throw new Error('Invalid minimumFee: value must be non-negative');
|
|
@@ -4909,9 +4980,7 @@ const isAttestationResponse = (obj) => {
|
|
|
4909
4980
|
* ```
|
|
4910
4981
|
*/
|
|
4911
4982
|
const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet) => {
|
|
4912
|
-
const baseUrl = isTestnet
|
|
4913
|
-
? 'https://iris-api-sandbox.circle.com'
|
|
4914
|
-
: 'https://iris-api.circle.com';
|
|
4983
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
4915
4984
|
const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
|
|
4916
4985
|
url.searchParams.set('transactionHash', transactionHash);
|
|
4917
4986
|
return url.toString();
|
|
@@ -4956,6 +5025,133 @@ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, conf
|
|
|
4956
5025
|
const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
|
|
4957
5026
|
return await pollApiGet(url, isAttestationResponse, effectiveConfig);
|
|
4958
5027
|
};
|
|
5028
|
+
/**
|
|
5029
|
+
* Type guard that validates attestation response structure without requiring completion status.
|
|
5030
|
+
*
|
|
5031
|
+
* This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
|
|
5032
|
+
* attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
|
|
5033
|
+
* this function does not throw if no complete attestation is found.
|
|
5034
|
+
*
|
|
5035
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5036
|
+
* @returns True if the object has valid attestation structure
|
|
5037
|
+
* @throws {Error} With "Invalid attestation response structure" if structure is invalid
|
|
5038
|
+
* @internal
|
|
5039
|
+
*/
|
|
5040
|
+
const isAttestationResponseWithoutStatusCheck = (obj) => {
|
|
5041
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
5042
|
+
throw new Error('Invalid attestation response structure');
|
|
5043
|
+
}
|
|
5044
|
+
return true;
|
|
5045
|
+
};
|
|
5046
|
+
/**
|
|
5047
|
+
* Fetches attestation data without requiring the attestation to be complete.
|
|
5048
|
+
*
|
|
5049
|
+
* This function is useful for retrieving attestation data (particularly the nonce)
|
|
5050
|
+
* from an existing transaction, even if the attestation has expired or is pending.
|
|
5051
|
+
* It uses minimal retries since we're fetching existing data, not waiting for completion.
|
|
5052
|
+
*
|
|
5053
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
5054
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
5055
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5056
|
+
* @param config - Optional configuration overrides
|
|
5057
|
+
* @returns The attestation response data (may contain incomplete/expired attestations)
|
|
5058
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
5059
|
+
*
|
|
5060
|
+
* @example
|
|
5061
|
+
* ```typescript
|
|
5062
|
+
* // Fetch existing attestation to extract nonce for re-attestation
|
|
5063
|
+
* const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
|
|
5064
|
+
* const nonce = response.messages[0]?.eventNonce
|
|
5065
|
+
* ```
|
|
5066
|
+
*/
|
|
5067
|
+
const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
|
|
5068
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
5069
|
+
// Use minimal retries since we're just fetching existing data
|
|
5070
|
+
const effectiveConfig = {
|
|
5071
|
+
...DEFAULT_CONFIG,
|
|
5072
|
+
maxRetries: 3,
|
|
5073
|
+
...config,
|
|
5074
|
+
};
|
|
5075
|
+
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
5076
|
+
};
|
|
5077
|
+
/**
|
|
5078
|
+
* Builds the IRIS API URL for re-attestation requests.
|
|
5079
|
+
*
|
|
5080
|
+
* Constructs the URL for Circle's re-attestation endpoint that allows
|
|
5081
|
+
* requesting a fresh attestation for an expired nonce.
|
|
5082
|
+
*
|
|
5083
|
+
* @param nonce - The nonce from the original attestation
|
|
5084
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5085
|
+
* @returns A fully qualified URL string for the re-attestation endpoint
|
|
5086
|
+
*
|
|
5087
|
+
* @example
|
|
5088
|
+
* ```typescript
|
|
5089
|
+
* // Mainnet URL
|
|
5090
|
+
* const mainnetUrl = buildReAttestUrl('0xabc', false)
|
|
5091
|
+
* // => 'https://iris-api.circle.com/v2/reattest/0xabc'
|
|
5092
|
+
*
|
|
5093
|
+
* // Testnet URL
|
|
5094
|
+
* const testnetUrl = buildReAttestUrl('0xabc', true)
|
|
5095
|
+
* // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
|
|
5096
|
+
* ```
|
|
5097
|
+
*/
|
|
5098
|
+
const buildReAttestUrl = (nonce, isTestnet) => {
|
|
5099
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
5100
|
+
const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
|
|
5101
|
+
return url.toString();
|
|
5102
|
+
};
|
|
5103
|
+
/**
|
|
5104
|
+
* Type guard that validates the re-attestation API response structure.
|
|
5105
|
+
*
|
|
5106
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5107
|
+
* @returns True if the object matches the ReAttestationResponse shape
|
|
5108
|
+
* @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
|
|
5109
|
+
* @internal
|
|
5110
|
+
*/
|
|
5111
|
+
const isReAttestationResponse = (obj) => {
|
|
5112
|
+
if (typeof obj !== 'object' ||
|
|
5113
|
+
obj === null ||
|
|
5114
|
+
!('message' in obj) ||
|
|
5115
|
+
!('nonce' in obj) ||
|
|
5116
|
+
typeof obj.message !== 'string' ||
|
|
5117
|
+
typeof obj.nonce !== 'string') {
|
|
5118
|
+
throw new Error('Invalid re-attestation response structure');
|
|
5119
|
+
}
|
|
5120
|
+
return true;
|
|
5121
|
+
};
|
|
5122
|
+
/**
|
|
5123
|
+
* Requests re-attestation for an expired attestation nonce.
|
|
5124
|
+
*
|
|
5125
|
+
* This function calls Circle's re-attestation API endpoint to request a fresh
|
|
5126
|
+
* attestation for a previously issued nonce. After calling this function,
|
|
5127
|
+
* you should poll `fetchAttestation` to retrieve the new attestation.
|
|
5128
|
+
*
|
|
5129
|
+
* @param nonce - The nonce from the original (expired) attestation
|
|
5130
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5131
|
+
* @param config - Optional configuration overrides for the request
|
|
5132
|
+
* @returns The re-attestation response confirming the request was accepted
|
|
5133
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
5134
|
+
*
|
|
5135
|
+
* @example
|
|
5136
|
+
* ```typescript
|
|
5137
|
+
* // Request re-attestation for an expired nonce
|
|
5138
|
+
* const response = await requestReAttestation('0xabc', true)
|
|
5139
|
+
* console.log(response.message) // "Re-attestation successfully requested for nonce."
|
|
5140
|
+
*
|
|
5141
|
+
* // After requesting re-attestation, poll for the new attestation
|
|
5142
|
+
* const attestation = await fetchAttestation(domainId, txHash, true)
|
|
5143
|
+
* ```
|
|
5144
|
+
*/
|
|
5145
|
+
const requestReAttestation = async (nonce, isTestnet, config = {}) => {
|
|
5146
|
+
const url = buildReAttestUrl(nonce, isTestnet);
|
|
5147
|
+
// Use minimal retries since we're just submitting a request, not polling for state
|
|
5148
|
+
const effectiveConfig = {
|
|
5149
|
+
...DEFAULT_CONFIG,
|
|
5150
|
+
maxRetries: 3,
|
|
5151
|
+
...config,
|
|
5152
|
+
};
|
|
5153
|
+
return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
|
|
5154
|
+
};
|
|
4959
5155
|
|
|
4960
5156
|
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
4961
5157
|
/**
|
|
@@ -7017,6 +7213,19 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7017
7213
|
: Promise.resolve(0n),
|
|
7018
7214
|
]);
|
|
7019
7215
|
const estimateResult = {
|
|
7216
|
+
token: params.token,
|
|
7217
|
+
amount: params.amount,
|
|
7218
|
+
source: {
|
|
7219
|
+
address: source.address,
|
|
7220
|
+
chain: source.chain.chain,
|
|
7221
|
+
},
|
|
7222
|
+
destination: {
|
|
7223
|
+
address: destination.address,
|
|
7224
|
+
chain: destination.chain.chain,
|
|
7225
|
+
...(destination.recipientAddress && {
|
|
7226
|
+
recipientAddress: destination.recipientAddress,
|
|
7227
|
+
}),
|
|
7228
|
+
},
|
|
7020
7229
|
gasFees: [],
|
|
7021
7230
|
fees: [],
|
|
7022
7231
|
};
|
|
@@ -7302,6 +7511,93 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7302
7511
|
throw error;
|
|
7303
7512
|
}
|
|
7304
7513
|
}
|
|
7514
|
+
/**
|
|
7515
|
+
* Requests a fresh attestation for an expired attestation.
|
|
7516
|
+
*
|
|
7517
|
+
* This method is used when the original attestation has expired before the mint
|
|
7518
|
+
* transaction could be completed. It performs three steps:
|
|
7519
|
+
* 1. Fetches the existing attestation data to extract the nonce
|
|
7520
|
+
* 2. Requests re-attestation from Circle's API using the nonce
|
|
7521
|
+
* 3. Polls for the fresh attestation and returns it
|
|
7522
|
+
*
|
|
7523
|
+
* @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
|
|
7524
|
+
* @param source - The source wallet context containing the chain definition and wallet address
|
|
7525
|
+
* @param transactionHash - The transaction hash of the original burn transaction
|
|
7526
|
+
* @param config - Optional polling configuration overrides for timeout, retries, and delay
|
|
7527
|
+
* @returns A promise that resolves to the fresh attestation message
|
|
7528
|
+
* @throws {Error} With "Failed to re-attest: No nonce found for transaction" if the original
|
|
7529
|
+
* attestation cannot be found or has no nonce
|
|
7530
|
+
* @throws {Error} With "Failed to re-attest: No attestation found after re-attestation request"
|
|
7531
|
+
* if the fresh attestation cannot be retrieved
|
|
7532
|
+
* @throws {Error} With "Failed to re-attest: {details}" for other errors
|
|
7533
|
+
*
|
|
7534
|
+
* @example
|
|
7535
|
+
* ```typescript
|
|
7536
|
+
* import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
7537
|
+
* import { Chains } from '@core/chains'
|
|
7538
|
+
*
|
|
7539
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
7540
|
+
*
|
|
7541
|
+
* // After a mint fails due to expired attestation, request a fresh one
|
|
7542
|
+
* const source = {
|
|
7543
|
+
* adapter: viemAdapter,
|
|
7544
|
+
* chain: Chains.EthereumSepolia,
|
|
7545
|
+
* address: '0x1234...'
|
|
7546
|
+
* }
|
|
7547
|
+
*
|
|
7548
|
+
* try {
|
|
7549
|
+
* const freshAttestation = await provider.reAttest(
|
|
7550
|
+
* source,
|
|
7551
|
+
* '0xabc123...', // Original burn transaction hash
|
|
7552
|
+
* { timeout: 10000, maxRetries: 5 }
|
|
7553
|
+
* )
|
|
7554
|
+
*
|
|
7555
|
+
* // Use the fresh attestation to retry the mint
|
|
7556
|
+
* const mintRequest = await provider.mint(source, destination, freshAttestation)
|
|
7557
|
+
* const result = await mintRequest.execute()
|
|
7558
|
+
* } catch (error) {
|
|
7559
|
+
* console.error('Re-attestation failed:', error.message)
|
|
7560
|
+
* }
|
|
7561
|
+
* ```
|
|
7562
|
+
*/
|
|
7563
|
+
async reAttest(source, transactionHash, config) {
|
|
7564
|
+
assertCCTPv2WalletContext(source);
|
|
7565
|
+
if (!transactionHash ||
|
|
7566
|
+
typeof transactionHash !== 'string' ||
|
|
7567
|
+
transactionHash.trim() === '') {
|
|
7568
|
+
throw new Error('Failed to re-attest: Invalid transaction hash');
|
|
7569
|
+
}
|
|
7570
|
+
try {
|
|
7571
|
+
// Merge configs: defaults <- global config <- per-call config
|
|
7572
|
+
const effectiveConfig = {
|
|
7573
|
+
...this.config?.attestation,
|
|
7574
|
+
...config,
|
|
7575
|
+
};
|
|
7576
|
+
// Step 1: Get existing attestation data to extract nonce
|
|
7577
|
+
const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7578
|
+
const nonce = existingAttestation.messages[0]?.eventNonce;
|
|
7579
|
+
if (!nonce || typeof nonce !== 'string') {
|
|
7580
|
+
throw new Error('Failed to re-attest: No nonce found for transaction');
|
|
7581
|
+
}
|
|
7582
|
+
// Step 2: Request re-attestation
|
|
7583
|
+
await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
|
|
7584
|
+
// Step 3: Poll for fresh attestation
|
|
7585
|
+
const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7586
|
+
const message = response.messages[0];
|
|
7587
|
+
if (!message) {
|
|
7588
|
+
throw new Error('Failed to re-attest: No attestation found after re-attestation request');
|
|
7589
|
+
}
|
|
7590
|
+
return message;
|
|
7591
|
+
}
|
|
7592
|
+
catch (err) {
|
|
7593
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
7594
|
+
// Always prefix with 'Failed to re-attest'
|
|
7595
|
+
if (!error.message.startsWith('Failed to re-attest')) {
|
|
7596
|
+
throw new Error(`Failed to re-attest: ${error.message}`);
|
|
7597
|
+
}
|
|
7598
|
+
throw error;
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7305
7601
|
/**
|
|
7306
7602
|
* Checks if both source and destination chains support CCTP v2 transfers.
|
|
7307
7603
|
*
|
|
@@ -7381,15 +7677,17 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7381
7677
|
}
|
|
7382
7678
|
else if (config?.maxFee === undefined) {
|
|
7383
7679
|
// If the max fee is not provided and the transfer speed is fast, dynamically calculate the max fee
|
|
7384
|
-
const
|
|
7680
|
+
const scaledBps = await fetchUsdcFastBurnFee(source.chain.cctp.domain, destination.chain.cctp.domain, source.chain.isTestnet);
|
|
7385
7681
|
// Calculate fee proportional to the transfer amount
|
|
7386
7682
|
// Convert amount to minor units
|
|
7387
7683
|
const amountInMinorUnits = BigInt(amount);
|
|
7388
7684
|
// Calculate base fee proportional to the transfer amount
|
|
7389
|
-
//
|
|
7685
|
+
// The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps).
|
|
7686
|
+
// fetchUsdcFastBurnFee scales by 100 to preserve precision, so we divide by 1,000,000.
|
|
7687
|
+
// Formula: (scaledBps * amountInMinorUnits) / 1_000_000
|
|
7390
7688
|
// We use ceiling division (round up) to ensure sufficient fees and avoid failover to slow burn
|
|
7391
|
-
// Ceiling division: (a + b - 1) / b, where b =
|
|
7392
|
-
const baseFee = (
|
|
7689
|
+
// Ceiling division: (a + b - 1) / b, where b = 1_000_000, so (b - 1) = 999_999n
|
|
7690
|
+
const baseFee = (scaledBps * amountInMinorUnits + 999999n) / 1000000n;
|
|
7393
7691
|
// Add 10% buffer to account for fee fluctuations: fee + (fee * 10 / 100) = fee + (fee / 10)
|
|
7394
7692
|
maxFee = baseFee + baseFee / 10n;
|
|
7395
7693
|
}
|
package/index.d.ts
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
|
*
|
|
@@ -2614,15 +2614,39 @@ interface BridgeResult {
|
|
|
2614
2614
|
*
|
|
2615
2615
|
* This interface provides detailed information about the expected costs
|
|
2616
2616
|
* for a transfer, including gas fees on different chains and protocol fees.
|
|
2617
|
+
* It also includes the input context (token, amount, source, destination) to
|
|
2618
|
+
* provide a complete view of the transfer being estimated.
|
|
2617
2619
|
*
|
|
2618
2620
|
* @example
|
|
2619
2621
|
* ```typescript
|
|
2620
2622
|
* const estimate: EstimateResult = await provider.estimate(source, dest, '100')
|
|
2623
|
+
* console.log('Estimating transfer of', estimate.amount, estimate.token)
|
|
2624
|
+
* console.log('From', estimate.source.chain.name, 'to', estimate.destination.chain.name)
|
|
2621
2625
|
* console.log('Total gas fees:', estimate.gasFees.length)
|
|
2622
2626
|
* console.log('Protocol fees:', estimate.fees.length)
|
|
2623
2627
|
* ```
|
|
2624
2628
|
*/
|
|
2625
2629
|
interface EstimateResult {
|
|
2630
|
+
/** The token being transferred */
|
|
2631
|
+
token: 'USDC';
|
|
2632
|
+
/** The amount being transferred */
|
|
2633
|
+
amount: string;
|
|
2634
|
+
/** Information about the source chain and address */
|
|
2635
|
+
source: {
|
|
2636
|
+
/** The source wallet/contract address */
|
|
2637
|
+
address: string;
|
|
2638
|
+
/** The source blockchain network */
|
|
2639
|
+
chain: Blockchain;
|
|
2640
|
+
};
|
|
2641
|
+
/** Information about the destination chain and address */
|
|
2642
|
+
destination: {
|
|
2643
|
+
/** The destination wallet/contract address */
|
|
2644
|
+
address: string;
|
|
2645
|
+
/** The destination blockchain network */
|
|
2646
|
+
chain: Blockchain;
|
|
2647
|
+
/** Optional custom recipient address for minted funds. */
|
|
2648
|
+
recipientAddress?: string;
|
|
2649
|
+
};
|
|
2626
2650
|
/** Array of gas fees required for the transfer on different blockchains */
|
|
2627
2651
|
gasFees: {
|
|
2628
2652
|
/** The name of the step */
|
|
@@ -3373,6 +3397,20 @@ interface ICCTPV2BridgingProvider extends BridgingProvider<CCTPV2Actions> {
|
|
|
3373
3397
|
* @returns A promise that resolves to the attestation message.
|
|
3374
3398
|
*/
|
|
3375
3399
|
fetchAttestation<TFromAdapterCapabilities extends AdapterCapabilities, TToAdapterCapabilities extends AdapterCapabilities>(source: BridgeParams<TFromAdapterCapabilities, TToAdapterCapabilities>['source'], txHash: string, config?: Partial<ApiPollingConfig>): Promise<AttestationMessage>;
|
|
3400
|
+
/**
|
|
3401
|
+
* Requests a fresh attestation for an expired attestation.
|
|
3402
|
+
*
|
|
3403
|
+
* This method is used when the original attestation has expired before the mint
|
|
3404
|
+
* transaction could be completed. It fetches the existing attestation data to
|
|
3405
|
+
* extract the nonce, requests re-attestation from Circle's API, and then polls
|
|
3406
|
+
* for the fresh attestation.
|
|
3407
|
+
*
|
|
3408
|
+
* @param source - The source wallet context containing the chain definition and wallet address.
|
|
3409
|
+
* @param txHash - The transaction hash of the original burn transaction.
|
|
3410
|
+
* @param config - Optional polling configuration overrides.
|
|
3411
|
+
* @returns A promise that resolves to the fresh attestation message.
|
|
3412
|
+
*/
|
|
3413
|
+
reAttest<TFromAdapterCapabilities extends AdapterCapabilities, TToAdapterCapabilities extends AdapterCapabilities>(source: BridgeParams<TFromAdapterCapabilities, TToAdapterCapabilities>['source'], txHash: string, config?: Partial<ApiPollingConfig>): Promise<AttestationMessage>;
|
|
3376
3414
|
/**
|
|
3377
3415
|
* Mints the amount of tokens for the destination wallet.
|
|
3378
3416
|
*
|
|
@@ -3685,6 +3723,56 @@ declare class CCTPV2BridgingProvider extends BridgingProvider<CCTPV2Actions> imp
|
|
|
3685
3723
|
* ```
|
|
3686
3724
|
*/
|
|
3687
3725
|
fetchAttestation<TFromAdapterCapabilities extends AdapterCapabilities>(source: WalletContext<TFromAdapterCapabilities, ChainDefinitionWithCCTPv2>, transactionHash: string, config?: Partial<ApiPollingConfig>): Promise<AttestationMessage>;
|
|
3726
|
+
/**
|
|
3727
|
+
* Requests a fresh attestation for an expired attestation.
|
|
3728
|
+
*
|
|
3729
|
+
* This method is used when the original attestation has expired before the mint
|
|
3730
|
+
* transaction could be completed. It performs three steps:
|
|
3731
|
+
* 1. Fetches the existing attestation data to extract the nonce
|
|
3732
|
+
* 2. Requests re-attestation from Circle's API using the nonce
|
|
3733
|
+
* 3. Polls for the fresh attestation and returns it
|
|
3734
|
+
*
|
|
3735
|
+
* @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
|
|
3736
|
+
* @param source - The source wallet context containing the chain definition and wallet address
|
|
3737
|
+
* @param transactionHash - The transaction hash of the original burn transaction
|
|
3738
|
+
* @param config - Optional polling configuration overrides for timeout, retries, and delay
|
|
3739
|
+
* @returns A promise that resolves to the fresh attestation message
|
|
3740
|
+
* @throws {Error} With "Failed to re-attest: No nonce found for transaction" if the original
|
|
3741
|
+
* attestation cannot be found or has no nonce
|
|
3742
|
+
* @throws {Error} With "Failed to re-attest: No attestation found after re-attestation request"
|
|
3743
|
+
* if the fresh attestation cannot be retrieved
|
|
3744
|
+
* @throws {Error} With "Failed to re-attest: {details}" for other errors
|
|
3745
|
+
*
|
|
3746
|
+
* @example
|
|
3747
|
+
* ```typescript
|
|
3748
|
+
* import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
3749
|
+
* import { Chains } from '@core/chains'
|
|
3750
|
+
*
|
|
3751
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
3752
|
+
*
|
|
3753
|
+
* // After a mint fails due to expired attestation, request a fresh one
|
|
3754
|
+
* const source = {
|
|
3755
|
+
* adapter: viemAdapter,
|
|
3756
|
+
* chain: Chains.EthereumSepolia,
|
|
3757
|
+
* address: '0x1234...'
|
|
3758
|
+
* }
|
|
3759
|
+
*
|
|
3760
|
+
* try {
|
|
3761
|
+
* const freshAttestation = await provider.reAttest(
|
|
3762
|
+
* source,
|
|
3763
|
+
* '0xabc123...', // Original burn transaction hash
|
|
3764
|
+
* { timeout: 10000, maxRetries: 5 }
|
|
3765
|
+
* )
|
|
3766
|
+
*
|
|
3767
|
+
* // Use the fresh attestation to retry the mint
|
|
3768
|
+
* const mintRequest = await provider.mint(source, destination, freshAttestation)
|
|
3769
|
+
* const result = await mintRequest.execute()
|
|
3770
|
+
* } catch (error) {
|
|
3771
|
+
* console.error('Re-attestation failed:', error.message)
|
|
3772
|
+
* }
|
|
3773
|
+
* ```
|
|
3774
|
+
*/
|
|
3775
|
+
reAttest<TFromAdapterCapabilities extends AdapterCapabilities>(source: WalletContext<TFromAdapterCapabilities, ChainDefinitionWithCCTPv2>, transactionHash: string, config?: Partial<ApiPollingConfig>): Promise<AttestationMessage>;
|
|
3688
3776
|
/**
|
|
3689
3777
|
* Checks if both source and destination chains support CCTP v2 transfers.
|
|
3690
3778
|
*
|
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
|
*
|
|
@@ -3014,7 +3014,9 @@ const makeApiRequest = async (url, method, isValidType, config, body) => {
|
|
|
3014
3014
|
signal: controller.signal,
|
|
3015
3015
|
};
|
|
3016
3016
|
// Add body for methods that support it
|
|
3017
|
-
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(method))
|
|
3017
|
+
if (body !== undefined && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
3018
|
+
requestInit.body = JSON.stringify(body);
|
|
3019
|
+
}
|
|
3018
3020
|
const response = await fetch(url, requestInit);
|
|
3019
3021
|
clearTimeout(timeoutId);
|
|
3020
3022
|
if (!response.ok) {
|
|
@@ -3192,6 +3194,30 @@ const pollApiWithValidation = async (url, method, isValidType, config = {}, body
|
|
|
3192
3194
|
const pollApiGet = async (url, isValidType, config) => {
|
|
3193
3195
|
return pollApiWithValidation(url, 'GET', isValidType, config);
|
|
3194
3196
|
};
|
|
3197
|
+
/**
|
|
3198
|
+
* Convenience function for making POST requests with validation.
|
|
3199
|
+
*
|
|
3200
|
+
* @typeParam TResponseType - The expected response type after validation
|
|
3201
|
+
* @typeParam TBody - The type of the request body
|
|
3202
|
+
* @param url - The API endpoint URL
|
|
3203
|
+
* @param body - The request body
|
|
3204
|
+
* @param isValidType - Type guard function to validate the response
|
|
3205
|
+
* @param config - Optional configuration overrides
|
|
3206
|
+
* @returns Promise resolving to the validated response
|
|
3207
|
+
*
|
|
3208
|
+
* @example
|
|
3209
|
+
* ```typescript
|
|
3210
|
+
* const result = await pollApiPost(
|
|
3211
|
+
* 'https://api.example.com/submit',
|
|
3212
|
+
* { name: 'John', email: 'john@example.com' },
|
|
3213
|
+
* isSubmissionResponse,
|
|
3214
|
+
* { maxRetries: 3 }
|
|
3215
|
+
* )
|
|
3216
|
+
* ```
|
|
3217
|
+
*/
|
|
3218
|
+
const pollApiPost = async (url, body, isValidType, config) => {
|
|
3219
|
+
return pollApiWithValidation(url, 'POST', isValidType, config, body);
|
|
3220
|
+
};
|
|
3195
3221
|
|
|
3196
3222
|
/**
|
|
3197
3223
|
* Valid recoverability values for error handling strategies.
|
|
@@ -3399,6 +3425,24 @@ function validateErrorDetails(details) {
|
|
|
3399
3425
|
* stays within KitError's constraints.
|
|
3400
3426
|
*/
|
|
3401
3427
|
const MAX_MESSAGE_LENGTH = 950;
|
|
3428
|
+
/**
|
|
3429
|
+
* Standard error message for invalid amount format.
|
|
3430
|
+
*
|
|
3431
|
+
* The SDK enforces strict dot-decimal notation for amount values. This constant
|
|
3432
|
+
* provides a consistent error message when users provide amounts with:
|
|
3433
|
+
* - Comma decimals (e.g., "1,5")
|
|
3434
|
+
* - Thousand separators (e.g., "1,000.50")
|
|
3435
|
+
* - Non-numeric characters
|
|
3436
|
+
* - Invalid format
|
|
3437
|
+
*/
|
|
3438
|
+
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';
|
|
3439
|
+
/**
|
|
3440
|
+
* Error message for invalid maxFee format.
|
|
3441
|
+
*
|
|
3442
|
+
* Used when validating the maxFee configuration parameter. The maxFee can be zero
|
|
3443
|
+
* or positive and must follow strict dot-decimal notation.
|
|
3444
|
+
*/
|
|
3445
|
+
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
3446
|
|
|
3403
3447
|
/**
|
|
3404
3448
|
* Structured error class for Stablecoin Kit operations.
|
|
@@ -4405,41 +4449,46 @@ var TransferSpeed;
|
|
|
4405
4449
|
* - regexMessage: error message when the basic numeric format fails.
|
|
4406
4450
|
* - maxDecimals: maximum number of decimal places allowed (e.g., 6 for USDC).
|
|
4407
4451
|
*/
|
|
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) {
|
|
4452
|
+
const createDecimalStringValidator = (options) => (schema) => {
|
|
4453
|
+
// Capitalize first letter of attribute name for error messages
|
|
4454
|
+
const capitalizedAttributeName = options.attributeName.charAt(0).toUpperCase() +
|
|
4455
|
+
options.attributeName.slice(1);
|
|
4456
|
+
return schema
|
|
4457
|
+
.regex(/^-?(?:\d+(?:\.\d+)?|\.\d+)$/, options.regexMessage)
|
|
4458
|
+
.superRefine((val, ctx) => {
|
|
4459
|
+
const amount = Number.parseFloat(val);
|
|
4460
|
+
if (Number.isNaN(amount)) {
|
|
4423
4461
|
ctx.addIssue({
|
|
4424
4462
|
code: z.ZodIssueCode.custom,
|
|
4425
|
-
message:
|
|
4463
|
+
message: options.regexMessage,
|
|
4426
4464
|
});
|
|
4427
4465
|
return;
|
|
4428
4466
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4467
|
+
// Check decimal precision if maxDecimals is specified
|
|
4468
|
+
if (options.maxDecimals !== undefined) {
|
|
4469
|
+
const decimalPart = val.split('.')[1];
|
|
4470
|
+
if (decimalPart && decimalPart.length > options.maxDecimals) {
|
|
4471
|
+
ctx.addIssue({
|
|
4472
|
+
code: z.ZodIssueCode.custom,
|
|
4473
|
+
message: `Maximum supported decimal places: ${options.maxDecimals.toString()}`,
|
|
4474
|
+
});
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
if (options.allowZero && amount < 0) {
|
|
4479
|
+
ctx.addIssue({
|
|
4480
|
+
code: z.ZodIssueCode.custom,
|
|
4481
|
+
message: `${capitalizedAttributeName} must be non-negative`,
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
else if (!options.allowZero && amount <= 0) {
|
|
4485
|
+
ctx.addIssue({
|
|
4486
|
+
code: z.ZodIssueCode.custom,
|
|
4487
|
+
message: `${capitalizedAttributeName} must be greater than 0`,
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
});
|
|
4491
|
+
};
|
|
4443
4492
|
/**
|
|
4444
4493
|
* Schema for validating chain definitions.
|
|
4445
4494
|
* This ensures the basic structure of a chain definition is valid.
|
|
@@ -4599,7 +4648,7 @@ const bridgeParamsSchema = z.object({
|
|
|
4599
4648
|
.min(1, 'Required')
|
|
4600
4649
|
.pipe(createDecimalStringValidator({
|
|
4601
4650
|
allowZero: false,
|
|
4602
|
-
regexMessage:
|
|
4651
|
+
regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
|
|
4603
4652
|
attributeName: 'amount',
|
|
4604
4653
|
maxDecimals: 6,
|
|
4605
4654
|
})(z.string())),
|
|
@@ -4612,7 +4661,7 @@ const bridgeParamsSchema = z.object({
|
|
|
4612
4661
|
.string()
|
|
4613
4662
|
.pipe(createDecimalStringValidator({
|
|
4614
4663
|
allowZero: true,
|
|
4615
|
-
regexMessage:
|
|
4664
|
+
regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
|
|
4616
4665
|
attributeName: 'maxFee',
|
|
4617
4666
|
maxDecimals: 6,
|
|
4618
4667
|
})(z.string()))
|
|
@@ -4621,6 +4670,19 @@ const bridgeParamsSchema = z.object({
|
|
|
4621
4670
|
}),
|
|
4622
4671
|
});
|
|
4623
4672
|
|
|
4673
|
+
/**
|
|
4674
|
+
* Base URL for Circle's IRIS API (mainnet/production).
|
|
4675
|
+
*
|
|
4676
|
+
* The IRIS API provides attestation services for CCTP cross-chain transfers.
|
|
4677
|
+
*/
|
|
4678
|
+
const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
4679
|
+
/**
|
|
4680
|
+
* Base URL for Circle's IRIS API (testnet/sandbox).
|
|
4681
|
+
*
|
|
4682
|
+
* Used for development and testing on testnet chains.
|
|
4683
|
+
*/
|
|
4684
|
+
const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
4685
|
+
|
|
4624
4686
|
/**
|
|
4625
4687
|
* Type guard to validate the API response structure.
|
|
4626
4688
|
*
|
|
@@ -4669,9 +4731,7 @@ const isFastBurnFeeResponse = (data) => {
|
|
|
4669
4731
|
* @returns The complete API URL
|
|
4670
4732
|
*/
|
|
4671
4733
|
function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
4672
|
-
const baseUrl = isTestnet
|
|
4673
|
-
? 'https://iris-api-sandbox.circle.com'
|
|
4674
|
-
: 'https://iris-api.circle.com';
|
|
4734
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
4675
4735
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
|
|
4676
4736
|
}
|
|
4677
4737
|
const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
@@ -4687,15 +4747,24 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
4687
4747
|
* - Retry delays: 9 × 200 ms = 1 800 ms
|
|
4688
4748
|
* - Total max time: 2 000 ms + 1 800 ms = 3 800 ms
|
|
4689
4749
|
*
|
|
4750
|
+
* @remarks
|
|
4751
|
+
* The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps = 0.013%).
|
|
4752
|
+
* This function scales the value by 100 to preserve 2 decimal places of precision.
|
|
4753
|
+
* Callers must divide by 1,000,000 (instead of 10,000) when calculating fees.
|
|
4754
|
+
*
|
|
4690
4755
|
* @param sourceDomain - The source domain
|
|
4691
4756
|
* @param destinationDomain - The destination domain
|
|
4692
4757
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
4693
|
-
* @returns The minimum fee
|
|
4758
|
+
* @returns The minimum fee in scaled basis points (bps × 100)
|
|
4694
4759
|
* @throws Error if the input domains are invalid, the API request fails, returns invalid data, or network errors occur
|
|
4695
4760
|
* @example
|
|
4696
4761
|
* ```typescript
|
|
4697
|
-
* const
|
|
4698
|
-
* console.log(
|
|
4762
|
+
* const scaledBps = await fetchUsdcFastBurnFee(0, 6, false) // Ethereum -> Base
|
|
4763
|
+
* console.log(scaledBps) // 130n (representing 1.3 bps)
|
|
4764
|
+
*
|
|
4765
|
+
* // To calculate fee for an amount:
|
|
4766
|
+
* const amount = 1_000_000n // 1 USDC
|
|
4767
|
+
* const fee = (scaledBps * amount) / 1_000_000n // 130n (0.00013 USDC)
|
|
4699
4768
|
* ```
|
|
4700
4769
|
*/
|
|
4701
4770
|
async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet) {
|
|
@@ -4721,14 +4790,16 @@ async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet)
|
|
|
4721
4790
|
if (!fastTier) {
|
|
4722
4791
|
throw new Error(`No fast tier (finalityThreshold: ${FAST_TIER_FINALITY_THRESHOLD.toString()}) available in API response`);
|
|
4723
4792
|
}
|
|
4724
|
-
// Convert minimumFee to bigint
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
throw new Error(`Invalid minimumFee value: cannot
|
|
4793
|
+
// Convert minimumFee to scaled basis points (bigint)
|
|
4794
|
+
// The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps = 0.013%).
|
|
4795
|
+
// We scale by 100 to preserve 2 decimal places of precision.
|
|
4796
|
+
// The caller (getMaxFee) must divide by 1,000,000 instead of 10,000.
|
|
4797
|
+
const feeValue = Number.parseFloat(String(fastTier.minimumFee));
|
|
4798
|
+
if (Number.isNaN(feeValue) || !Number.isFinite(feeValue)) {
|
|
4799
|
+
throw new Error(`Invalid minimumFee value: cannot parse "${String(fastTier.minimumFee)}" as a number`);
|
|
4731
4800
|
}
|
|
4801
|
+
// Scale by 100 and round to get integer representation
|
|
4802
|
+
const minimumFee = BigInt(Math.round(feeValue * 100));
|
|
4732
4803
|
// Validate that minimumFee is non-negative
|
|
4733
4804
|
if (minimumFee < 0n) {
|
|
4734
4805
|
throw new Error('Invalid minimumFee: value must be non-negative');
|
|
@@ -4903,9 +4974,7 @@ const isAttestationResponse = (obj) => {
|
|
|
4903
4974
|
* ```
|
|
4904
4975
|
*/
|
|
4905
4976
|
const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet) => {
|
|
4906
|
-
const baseUrl = isTestnet
|
|
4907
|
-
? 'https://iris-api-sandbox.circle.com'
|
|
4908
|
-
: 'https://iris-api.circle.com';
|
|
4977
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
4909
4978
|
const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
|
|
4910
4979
|
url.searchParams.set('transactionHash', transactionHash);
|
|
4911
4980
|
return url.toString();
|
|
@@ -4950,6 +5019,133 @@ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, conf
|
|
|
4950
5019
|
const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
|
|
4951
5020
|
return await pollApiGet(url, isAttestationResponse, effectiveConfig);
|
|
4952
5021
|
};
|
|
5022
|
+
/**
|
|
5023
|
+
* Type guard that validates attestation response structure without requiring completion status.
|
|
5024
|
+
*
|
|
5025
|
+
* This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
|
|
5026
|
+
* attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
|
|
5027
|
+
* this function does not throw if no complete attestation is found.
|
|
5028
|
+
*
|
|
5029
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5030
|
+
* @returns True if the object has valid attestation structure
|
|
5031
|
+
* @throws {Error} With "Invalid attestation response structure" if structure is invalid
|
|
5032
|
+
* @internal
|
|
5033
|
+
*/
|
|
5034
|
+
const isAttestationResponseWithoutStatusCheck = (obj) => {
|
|
5035
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
5036
|
+
throw new Error('Invalid attestation response structure');
|
|
5037
|
+
}
|
|
5038
|
+
return true;
|
|
5039
|
+
};
|
|
5040
|
+
/**
|
|
5041
|
+
* Fetches attestation data without requiring the attestation to be complete.
|
|
5042
|
+
*
|
|
5043
|
+
* This function is useful for retrieving attestation data (particularly the nonce)
|
|
5044
|
+
* from an existing transaction, even if the attestation has expired or is pending.
|
|
5045
|
+
* It uses minimal retries since we're fetching existing data, not waiting for completion.
|
|
5046
|
+
*
|
|
5047
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
5048
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
5049
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5050
|
+
* @param config - Optional configuration overrides
|
|
5051
|
+
* @returns The attestation response data (may contain incomplete/expired attestations)
|
|
5052
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
5053
|
+
*
|
|
5054
|
+
* @example
|
|
5055
|
+
* ```typescript
|
|
5056
|
+
* // Fetch existing attestation to extract nonce for re-attestation
|
|
5057
|
+
* const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
|
|
5058
|
+
* const nonce = response.messages[0]?.eventNonce
|
|
5059
|
+
* ```
|
|
5060
|
+
*/
|
|
5061
|
+
const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {}) => {
|
|
5062
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
5063
|
+
// Use minimal retries since we're just fetching existing data
|
|
5064
|
+
const effectiveConfig = {
|
|
5065
|
+
...DEFAULT_CONFIG,
|
|
5066
|
+
maxRetries: 3,
|
|
5067
|
+
...config,
|
|
5068
|
+
};
|
|
5069
|
+
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
5070
|
+
};
|
|
5071
|
+
/**
|
|
5072
|
+
* Builds the IRIS API URL for re-attestation requests.
|
|
5073
|
+
*
|
|
5074
|
+
* Constructs the URL for Circle's re-attestation endpoint that allows
|
|
5075
|
+
* requesting a fresh attestation for an expired nonce.
|
|
5076
|
+
*
|
|
5077
|
+
* @param nonce - The nonce from the original attestation
|
|
5078
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5079
|
+
* @returns A fully qualified URL string for the re-attestation endpoint
|
|
5080
|
+
*
|
|
5081
|
+
* @example
|
|
5082
|
+
* ```typescript
|
|
5083
|
+
* // Mainnet URL
|
|
5084
|
+
* const mainnetUrl = buildReAttestUrl('0xabc', false)
|
|
5085
|
+
* // => 'https://iris-api.circle.com/v2/reattest/0xabc'
|
|
5086
|
+
*
|
|
5087
|
+
* // Testnet URL
|
|
5088
|
+
* const testnetUrl = buildReAttestUrl('0xabc', true)
|
|
5089
|
+
* // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
|
|
5090
|
+
* ```
|
|
5091
|
+
*/
|
|
5092
|
+
const buildReAttestUrl = (nonce, isTestnet) => {
|
|
5093
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
5094
|
+
const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
|
|
5095
|
+
return url.toString();
|
|
5096
|
+
};
|
|
5097
|
+
/**
|
|
5098
|
+
* Type guard that validates the re-attestation API response structure.
|
|
5099
|
+
*
|
|
5100
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
5101
|
+
* @returns True if the object matches the ReAttestationResponse shape
|
|
5102
|
+
* @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
|
|
5103
|
+
* @internal
|
|
5104
|
+
*/
|
|
5105
|
+
const isReAttestationResponse = (obj) => {
|
|
5106
|
+
if (typeof obj !== 'object' ||
|
|
5107
|
+
obj === null ||
|
|
5108
|
+
!('message' in obj) ||
|
|
5109
|
+
!('nonce' in obj) ||
|
|
5110
|
+
typeof obj.message !== 'string' ||
|
|
5111
|
+
typeof obj.nonce !== 'string') {
|
|
5112
|
+
throw new Error('Invalid re-attestation response structure');
|
|
5113
|
+
}
|
|
5114
|
+
return true;
|
|
5115
|
+
};
|
|
5116
|
+
/**
|
|
5117
|
+
* Requests re-attestation for an expired attestation nonce.
|
|
5118
|
+
*
|
|
5119
|
+
* This function calls Circle's re-attestation API endpoint to request a fresh
|
|
5120
|
+
* attestation for a previously issued nonce. After calling this function,
|
|
5121
|
+
* you should poll `fetchAttestation` to retrieve the new attestation.
|
|
5122
|
+
*
|
|
5123
|
+
* @param nonce - The nonce from the original (expired) attestation
|
|
5124
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
5125
|
+
* @param config - Optional configuration overrides for the request
|
|
5126
|
+
* @returns The re-attestation response confirming the request was accepted
|
|
5127
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
5128
|
+
*
|
|
5129
|
+
* @example
|
|
5130
|
+
* ```typescript
|
|
5131
|
+
* // Request re-attestation for an expired nonce
|
|
5132
|
+
* const response = await requestReAttestation('0xabc', true)
|
|
5133
|
+
* console.log(response.message) // "Re-attestation successfully requested for nonce."
|
|
5134
|
+
*
|
|
5135
|
+
* // After requesting re-attestation, poll for the new attestation
|
|
5136
|
+
* const attestation = await fetchAttestation(domainId, txHash, true)
|
|
5137
|
+
* ```
|
|
5138
|
+
*/
|
|
5139
|
+
const requestReAttestation = async (nonce, isTestnet, config = {}) => {
|
|
5140
|
+
const url = buildReAttestUrl(nonce, isTestnet);
|
|
5141
|
+
// Use minimal retries since we're just submitting a request, not polling for state
|
|
5142
|
+
const effectiveConfig = {
|
|
5143
|
+
...DEFAULT_CONFIG,
|
|
5144
|
+
maxRetries: 3,
|
|
5145
|
+
...config,
|
|
5146
|
+
};
|
|
5147
|
+
return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
|
|
5148
|
+
};
|
|
4953
5149
|
|
|
4954
5150
|
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
4955
5151
|
/**
|
|
@@ -7011,6 +7207,19 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7011
7207
|
: Promise.resolve(0n),
|
|
7012
7208
|
]);
|
|
7013
7209
|
const estimateResult = {
|
|
7210
|
+
token: params.token,
|
|
7211
|
+
amount: params.amount,
|
|
7212
|
+
source: {
|
|
7213
|
+
address: source.address,
|
|
7214
|
+
chain: source.chain.chain,
|
|
7215
|
+
},
|
|
7216
|
+
destination: {
|
|
7217
|
+
address: destination.address,
|
|
7218
|
+
chain: destination.chain.chain,
|
|
7219
|
+
...(destination.recipientAddress && {
|
|
7220
|
+
recipientAddress: destination.recipientAddress,
|
|
7221
|
+
}),
|
|
7222
|
+
},
|
|
7014
7223
|
gasFees: [],
|
|
7015
7224
|
fees: [],
|
|
7016
7225
|
};
|
|
@@ -7296,6 +7505,93 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7296
7505
|
throw error;
|
|
7297
7506
|
}
|
|
7298
7507
|
}
|
|
7508
|
+
/**
|
|
7509
|
+
* Requests a fresh attestation for an expired attestation.
|
|
7510
|
+
*
|
|
7511
|
+
* This method is used when the original attestation has expired before the mint
|
|
7512
|
+
* transaction could be completed. It performs three steps:
|
|
7513
|
+
* 1. Fetches the existing attestation data to extract the nonce
|
|
7514
|
+
* 2. Requests re-attestation from Circle's API using the nonce
|
|
7515
|
+
* 3. Polls for the fresh attestation and returns it
|
|
7516
|
+
*
|
|
7517
|
+
* @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
|
|
7518
|
+
* @param source - The source wallet context containing the chain definition and wallet address
|
|
7519
|
+
* @param transactionHash - The transaction hash of the original burn transaction
|
|
7520
|
+
* @param config - Optional polling configuration overrides for timeout, retries, and delay
|
|
7521
|
+
* @returns A promise that resolves to the fresh attestation message
|
|
7522
|
+
* @throws {Error} With "Failed to re-attest: No nonce found for transaction" if the original
|
|
7523
|
+
* attestation cannot be found or has no nonce
|
|
7524
|
+
* @throws {Error} With "Failed to re-attest: No attestation found after re-attestation request"
|
|
7525
|
+
* if the fresh attestation cannot be retrieved
|
|
7526
|
+
* @throws {Error} With "Failed to re-attest: {details}" for other errors
|
|
7527
|
+
*
|
|
7528
|
+
* @example
|
|
7529
|
+
* ```typescript
|
|
7530
|
+
* import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
7531
|
+
* import { Chains } from '@core/chains'
|
|
7532
|
+
*
|
|
7533
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
7534
|
+
*
|
|
7535
|
+
* // After a mint fails due to expired attestation, request a fresh one
|
|
7536
|
+
* const source = {
|
|
7537
|
+
* adapter: viemAdapter,
|
|
7538
|
+
* chain: Chains.EthereumSepolia,
|
|
7539
|
+
* address: '0x1234...'
|
|
7540
|
+
* }
|
|
7541
|
+
*
|
|
7542
|
+
* try {
|
|
7543
|
+
* const freshAttestation = await provider.reAttest(
|
|
7544
|
+
* source,
|
|
7545
|
+
* '0xabc123...', // Original burn transaction hash
|
|
7546
|
+
* { timeout: 10000, maxRetries: 5 }
|
|
7547
|
+
* )
|
|
7548
|
+
*
|
|
7549
|
+
* // Use the fresh attestation to retry the mint
|
|
7550
|
+
* const mintRequest = await provider.mint(source, destination, freshAttestation)
|
|
7551
|
+
* const result = await mintRequest.execute()
|
|
7552
|
+
* } catch (error) {
|
|
7553
|
+
* console.error('Re-attestation failed:', error.message)
|
|
7554
|
+
* }
|
|
7555
|
+
* ```
|
|
7556
|
+
*/
|
|
7557
|
+
async reAttest(source, transactionHash, config) {
|
|
7558
|
+
assertCCTPv2WalletContext(source);
|
|
7559
|
+
if (!transactionHash ||
|
|
7560
|
+
typeof transactionHash !== 'string' ||
|
|
7561
|
+
transactionHash.trim() === '') {
|
|
7562
|
+
throw new Error('Failed to re-attest: Invalid transaction hash');
|
|
7563
|
+
}
|
|
7564
|
+
try {
|
|
7565
|
+
// Merge configs: defaults <- global config <- per-call config
|
|
7566
|
+
const effectiveConfig = {
|
|
7567
|
+
...this.config?.attestation,
|
|
7568
|
+
...config,
|
|
7569
|
+
};
|
|
7570
|
+
// Step 1: Get existing attestation data to extract nonce
|
|
7571
|
+
const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7572
|
+
const nonce = existingAttestation.messages[0]?.eventNonce;
|
|
7573
|
+
if (!nonce || typeof nonce !== 'string') {
|
|
7574
|
+
throw new Error('Failed to re-attest: No nonce found for transaction');
|
|
7575
|
+
}
|
|
7576
|
+
// Step 2: Request re-attestation
|
|
7577
|
+
await requestReAttestation(nonce, source.chain.isTestnet, effectiveConfig);
|
|
7578
|
+
// Step 3: Poll for fresh attestation
|
|
7579
|
+
const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
7580
|
+
const message = response.messages[0];
|
|
7581
|
+
if (!message) {
|
|
7582
|
+
throw new Error('Failed to re-attest: No attestation found after re-attestation request');
|
|
7583
|
+
}
|
|
7584
|
+
return message;
|
|
7585
|
+
}
|
|
7586
|
+
catch (err) {
|
|
7587
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
7588
|
+
// Always prefix with 'Failed to re-attest'
|
|
7589
|
+
if (!error.message.startsWith('Failed to re-attest')) {
|
|
7590
|
+
throw new Error(`Failed to re-attest: ${error.message}`);
|
|
7591
|
+
}
|
|
7592
|
+
throw error;
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7299
7595
|
/**
|
|
7300
7596
|
* Checks if both source and destination chains support CCTP v2 transfers.
|
|
7301
7597
|
*
|
|
@@ -7375,15 +7671,17 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7375
7671
|
}
|
|
7376
7672
|
else if (config?.maxFee === undefined) {
|
|
7377
7673
|
// If the max fee is not provided and the transfer speed is fast, dynamically calculate the max fee
|
|
7378
|
-
const
|
|
7674
|
+
const scaledBps = await fetchUsdcFastBurnFee(source.chain.cctp.domain, destination.chain.cctp.domain, source.chain.isTestnet);
|
|
7379
7675
|
// Calculate fee proportional to the transfer amount
|
|
7380
7676
|
// Convert amount to minor units
|
|
7381
7677
|
const amountInMinorUnits = BigInt(amount);
|
|
7382
7678
|
// Calculate base fee proportional to the transfer amount
|
|
7383
|
-
//
|
|
7679
|
+
// The API returns basis points as a decimal (e.g., "1.3" for 1.3 bps).
|
|
7680
|
+
// fetchUsdcFastBurnFee scales by 100 to preserve precision, so we divide by 1,000,000.
|
|
7681
|
+
// Formula: (scaledBps * amountInMinorUnits) / 1_000_000
|
|
7384
7682
|
// 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 = (
|
|
7683
|
+
// Ceiling division: (a + b - 1) / b, where b = 1_000_000, so (b - 1) = 999_999n
|
|
7684
|
+
const baseFee = (scaledBps * amountInMinorUnits + 999999n) / 1000000n;
|
|
7387
7685
|
// Add 10% buffer to account for fee fluctuations: fee + (fee * 10 / 100) = fee + (fee / 10)
|
|
7388
7686
|
maxFee = baseFee + baseFee / 10n;
|
|
7389
7687
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@circle-fin/provider-cctp-v2",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"circle",
|
|
7
|
+
"cctp",
|
|
8
|
+
"usdc",
|
|
9
|
+
"stablecoin",
|
|
10
|
+
"provider",
|
|
11
|
+
"typescript",
|
|
12
|
+
"cross-chain",
|
|
13
|
+
"bridge",
|
|
14
|
+
"bridging",
|
|
15
|
+
"bridge-kit"
|
|
16
|
+
],
|
|
5
17
|
"main": "./index.cjs",
|
|
6
18
|
"module": "./index.mjs",
|
|
7
19
|
"types": "./index.d.ts",
|