@circle-fin/app-kit 1.12.1 → 1.13.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 +59 -0
- package/README.md +31 -12
- package/bridge.cjs +2214 -186
- package/bridge.d.cts +86 -17
- package/bridge.d.mts +86 -17
- package/bridge.d.ts +86 -17
- package/bridge.mjs +2216 -188
- package/chains.cjs +134 -0
- package/chains.d.cts +126 -1
- package/chains.d.mts +126 -1
- package/chains.d.ts +126 -1
- package/chains.mjs +133 -1
- package/context.d.cts +86 -17
- package/context.d.mts +86 -17
- package/context.d.ts +86 -17
- package/earn.cjs +467 -48
- package/earn.d.cts +86 -17
- package/earn.d.mts +86 -17
- package/earn.d.ts +86 -17
- package/earn.mjs +467 -48
- package/estimateBridge.cjs +2214 -186
- package/estimateBridge.d.cts +196 -18
- package/estimateBridge.d.mts +196 -18
- package/estimateBridge.d.ts +196 -18
- package/estimateBridge.mjs +2216 -188
- package/estimateSwap.cjs +571 -131
- package/estimateSwap.d.cts +86 -17
- package/estimateSwap.d.mts +86 -17
- package/estimateSwap.d.ts +86 -17
- package/estimateSwap.mjs +571 -131
- package/index.cjs +2101 -434
- package/index.d.cts +949 -147
- package/index.d.mts +949 -147
- package/index.d.ts +949 -147
- package/index.mjs +2103 -436
- package/package.json +6 -6
- package/swap.cjs +571 -131
- package/swap.d.cts +93 -18
- package/swap.d.mts +93 -18
- package/swap.d.ts +93 -18
- package/swap.mjs +571 -131
- package/unifiedBalance.cjs +384 -67
- package/unifiedBalance.d.cts +76 -2
- package/unifiedBalance.d.mts +76 -2
- package/unifiedBalance.d.ts +76 -2
- package/unifiedBalance.mjs +384 -67
package/estimateBridge.mjs
CHANGED
|
@@ -30,9 +30,9 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
|
|
|
30
30
|
import { z } from 'zod';
|
|
31
31
|
import pino from 'pino';
|
|
32
32
|
import { formatUnits as formatUnits$1, parseUnits as parseUnits$1 } from '@ethersproject/units';
|
|
33
|
-
import { hexlify, hexZeroPad } from '@ethersproject/bytes';
|
|
33
|
+
import { hexlify, hexZeroPad, concat } from '@ethersproject/bytes';
|
|
34
34
|
import '@ethersproject/abi';
|
|
35
|
-
import { getAddress } from '@ethersproject/address';
|
|
35
|
+
import { getAddress, isAddress } from '@ethersproject/address';
|
|
36
36
|
import bs58 from 'bs58';
|
|
37
37
|
import { PublicKey } from '@solana/web3.js';
|
|
38
38
|
import 'bn.js';
|
|
@@ -785,6 +785,32 @@ class KitError extends Error {
|
|
|
785
785
|
type: 'ONCHAIN'
|
|
786
786
|
}
|
|
787
787
|
};
|
|
788
|
+
/**
|
|
789
|
+
* Standardized error definitions for LIQUIDITY type errors.
|
|
790
|
+
*
|
|
791
|
+
* LIQUIDITY errors indicate that an upstream provider or AMM cannot fulfill
|
|
792
|
+
* the requested swap size due to insufficient liquidity at the moment.
|
|
793
|
+
* These are typically transient — retrying later or reducing the amount
|
|
794
|
+
* may succeed once liquidity replenishes.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```typescript
|
|
798
|
+
* import { LiquidityError } from '@core/errors'
|
|
799
|
+
*
|
|
800
|
+
* const error = new KitError({
|
|
801
|
+
* ...LiquidityError.INSUFFICIENT_LIQUIDITY,
|
|
802
|
+
* recoverability: 'RETRYABLE',
|
|
803
|
+
* message: 'Insufficient liquidity for the requested swap',
|
|
804
|
+
* cause: { trace: { token: '0xA0b86991...' } }
|
|
805
|
+
* })
|
|
806
|
+
* ```
|
|
807
|
+
*/ const LiquidityError = {
|
|
808
|
+
/** Upstream provider has a route but cannot fulfill the requested size right now */ INSUFFICIENT_LIQUIDITY: {
|
|
809
|
+
code: 6001,
|
|
810
|
+
name: 'LIQUIDITY_INSUFFICIENT',
|
|
811
|
+
type: 'LIQUIDITY'
|
|
812
|
+
}
|
|
813
|
+
};
|
|
788
814
|
/**
|
|
789
815
|
* Standardized error definitions for RPC type errors.
|
|
790
816
|
*
|
|
@@ -832,7 +858,10 @@ class KitError extends Error {
|
|
|
832
858
|
type: 'NETWORK'
|
|
833
859
|
},
|
|
834
860
|
/** Network request timeout */ TIMEOUT: {
|
|
835
|
-
code: 3002
|
|
861
|
+
code: 3002,
|
|
862
|
+
name: 'NETWORK_TIMEOUT',
|
|
863
|
+
type: 'NETWORK'
|
|
864
|
+
},
|
|
836
865
|
/** Circle relayer failed to process the forwarding/mint transaction */ RELAYER_FORWARD_FAILED: {
|
|
837
866
|
code: 3003,
|
|
838
867
|
name: 'NETWORK_RELAYER_FORWARD_FAILED',
|
|
@@ -843,6 +872,58 @@ class KitError extends Error {
|
|
|
843
872
|
name: 'NETWORK_RELAYER_PENDING',
|
|
844
873
|
type: 'NETWORK'
|
|
845
874
|
}};
|
|
875
|
+
/**
|
|
876
|
+
* Standardized error definitions for RATE_LIMIT type errors.
|
|
877
|
+
*
|
|
878
|
+
* RATE_LIMIT errors indicate API throttling, request frequency limits errors.
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* ```typescript
|
|
882
|
+
* import { RateLimitError } from '@core/errors'
|
|
883
|
+
*
|
|
884
|
+
* const error = new KitError({
|
|
885
|
+
* ...RateLimitError.RATE_LIMIT_EXCEEDED,
|
|
886
|
+
* recoverability: 'RETRYABLE',
|
|
887
|
+
* message: 'Rate limit exceeded, please retry later',
|
|
888
|
+
* cause: { trace: { error: '429 Too Many Requests' } }
|
|
889
|
+
* })
|
|
890
|
+
* ```
|
|
891
|
+
*/ const RateLimitError = {
|
|
892
|
+
/** Rate limit exceeded */ RATE_LIMIT_EXCEEDED: {
|
|
893
|
+
code: 7001,
|
|
894
|
+
name: 'RATE_LIMIT_EXCEEDED',
|
|
895
|
+
type: 'RATE_LIMIT'
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
/**
|
|
899
|
+
* Standardized error definitions for SERVICE type errors.
|
|
900
|
+
*
|
|
901
|
+
* SERVICE errors indicate internal service failures, HTTP 5xx errors,
|
|
902
|
+
* or backend processing issues that are retryable.
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* ```typescript
|
|
906
|
+
* import { ServiceError } from '@core/errors'
|
|
907
|
+
*
|
|
908
|
+
* const error = new KitError({
|
|
909
|
+
* ...ServiceError.INTERNAL_ERROR,
|
|
910
|
+
* recoverability: 'RETRYABLE',
|
|
911
|
+
* message: 'Service encountered an internal error (500)',
|
|
912
|
+
* cause: { trace: { statusCode: 500 } }
|
|
913
|
+
* })
|
|
914
|
+
* ```
|
|
915
|
+
*/ const ServiceError = {
|
|
916
|
+
/** Internal server error (HTTP 5xx) */ INTERNAL_ERROR: {
|
|
917
|
+
code: 8001,
|
|
918
|
+
name: 'SERVICE_INTERNAL_ERROR',
|
|
919
|
+
type: 'SERVICE'
|
|
920
|
+
},
|
|
921
|
+
/** Unknown or unclassified error that cannot be categorized */ UNKNOWN_ERROR: {
|
|
922
|
+
code: 8002,
|
|
923
|
+
name: 'SERVICE_UNKNOWN_ERROR',
|
|
924
|
+
type: 'SERVICE'
|
|
925
|
+
}
|
|
926
|
+
};
|
|
846
927
|
|
|
847
928
|
/**
|
|
848
929
|
* Creates error for network type mismatch between source and destination.
|
|
@@ -2214,6 +2295,32 @@ class KitError extends Error {
|
|
|
2214
2295
|
}
|
|
2215
2296
|
return false;
|
|
2216
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Type guard to check if error is KitError with RATE_LIMIT type.
|
|
2300
|
+
*
|
|
2301
|
+
* RATE_LIMIT errors indicate API throttling or request frequency limits.
|
|
2302
|
+
* These errors are typically RETRYABLE after a delay.
|
|
2303
|
+
*
|
|
2304
|
+
* @param error - Unknown error to check
|
|
2305
|
+
* @returns True if error is KitError with RATE_LIMIT type
|
|
2306
|
+
*
|
|
2307
|
+
* @example
|
|
2308
|
+
* ```typescript
|
|
2309
|
+
* import { isRateLimitError } from '@core/errors'
|
|
2310
|
+
*
|
|
2311
|
+
* try {
|
|
2312
|
+
* await kit.bridge(params)
|
|
2313
|
+
* } catch (error) {
|
|
2314
|
+
* if (isRateLimitError(error)) {
|
|
2315
|
+
* console.log('Rate limited, retrying in 60s')
|
|
2316
|
+
* await sleep(60000)
|
|
2317
|
+
* retry()
|
|
2318
|
+
* }
|
|
2319
|
+
* }
|
|
2320
|
+
* ```
|
|
2321
|
+
*/ function isRateLimitError(error) {
|
|
2322
|
+
return isKitError(error) && error.type === ERROR_TYPES.RATE_LIMIT;
|
|
2323
|
+
}
|
|
2217
2324
|
/**
|
|
2218
2325
|
* Safely extracts error message from any error type.
|
|
2219
2326
|
*
|
|
@@ -2463,6 +2570,478 @@ class KitError extends Error {
|
|
|
2463
2570
|
return chain;
|
|
2464
2571
|
}
|
|
2465
2572
|
|
|
2573
|
+
/**
|
|
2574
|
+
* Proxy-specific structured error codes carried in `responseBody.code`.
|
|
2575
|
+
*
|
|
2576
|
+
* These are NOT HTTP status codes — they are application-level identifiers
|
|
2577
|
+
* the stablecoin-kits-proxy emits inside JSON error bodies so the kit can
|
|
2578
|
+
* distinguish conditions that share the same HTTP status (e.g. a 400 caused
|
|
2579
|
+
* by an out-of-range swap amount vs. a generic validation failure).
|
|
2580
|
+
*
|
|
2581
|
+
* @internal
|
|
2582
|
+
*/ const ProxyErrorCode = {
|
|
2583
|
+
/** Swap amount outside the upstream provider's accepted bounds (HTTP 400) */ INVALID_SWAP_AMOUNT: 331017,
|
|
2584
|
+
/** Upstream liquidity insufficient for the requested size (HTTP 503) */ LOW_LIQUIDITY: 331018
|
|
2585
|
+
};
|
|
2586
|
+
/**
|
|
2587
|
+
* Parses raw HTTP API errors into structured KitError instances.
|
|
2588
|
+
*
|
|
2589
|
+
* This function uses pattern matching to identify common HTTP error types
|
|
2590
|
+
* and converts them into standardized KitError format. It handles errors
|
|
2591
|
+
* from fetch, HTTP status codes, timeouts, and network failures.
|
|
2592
|
+
*
|
|
2593
|
+
* The parser recognizes the following error patterns:
|
|
2594
|
+
* - Client errors (4xx) - validation, authentication, not found
|
|
2595
|
+
* - Server errors (5xx) - service unavailability
|
|
2596
|
+
* - Timeout errors
|
|
2597
|
+
* - Network connectivity errors
|
|
2598
|
+
* - Rate limiting
|
|
2599
|
+
*
|
|
2600
|
+
* Unrecognized errors are treated as fatal `SERVICE` errors, as their
|
|
2601
|
+
* cause and recoverability are unknown.
|
|
2602
|
+
*
|
|
2603
|
+
* @param error - The raw error from the API call
|
|
2604
|
+
* @param context - Context information including operation name
|
|
2605
|
+
* @returns A structured KitError instance
|
|
2606
|
+
*
|
|
2607
|
+
* @example
|
|
2608
|
+
* ```typescript
|
|
2609
|
+
* try {
|
|
2610
|
+
* const response = await fetch(url)
|
|
2611
|
+
* } catch (error) {
|
|
2612
|
+
* throw parseApiError(error, { operation: 'getQuote' })
|
|
2613
|
+
* }
|
|
2614
|
+
* ```
|
|
2615
|
+
*/ function parseApiError(error, context) {
|
|
2616
|
+
// If it's already a KitError, return it as-is
|
|
2617
|
+
if (error instanceof KitError) {
|
|
2618
|
+
return error;
|
|
2619
|
+
}
|
|
2620
|
+
const msg = getErrorMessage(error);
|
|
2621
|
+
const statusCode = extractHttpStatusCode(msg);
|
|
2622
|
+
const serviceName = context.service ?? 'Stablecoin Service';
|
|
2623
|
+
const operation = context.operation ?? 'API';
|
|
2624
|
+
const responseBody = extractResponseBody(error);
|
|
2625
|
+
// Rate limit errors (429)
|
|
2626
|
+
if (statusCode === 429 || /too many requests|rate limit exceeded/i.test(msg)) {
|
|
2627
|
+
return handleRateLimitError(serviceName, operation, error);
|
|
2628
|
+
}
|
|
2629
|
+
// HTTP 4xx Client Errors
|
|
2630
|
+
if (statusCode !== null && statusCode >= 400 && statusCode < 500) {
|
|
2631
|
+
return handleClientError(statusCode, serviceName, operation, error, msg, responseBody);
|
|
2632
|
+
}
|
|
2633
|
+
// HTTP 5xx Server Errors
|
|
2634
|
+
if (statusCode !== null && statusCode >= 500 && statusCode < 600) {
|
|
2635
|
+
return handleServerError(statusCode, serviceName, operation, error, responseBody);
|
|
2636
|
+
}
|
|
2637
|
+
// Timeout errors
|
|
2638
|
+
if (/timeout|timed out/i.test(msg)) {
|
|
2639
|
+
return handleTimeoutError(serviceName, operation, error);
|
|
2640
|
+
}
|
|
2641
|
+
// Network connectivity errors
|
|
2642
|
+
if (/connection (refused|failed)|network|ENOTFOUND|ECONNREFUSED|ETIMEDOUT/i.test(msg)) {
|
|
2643
|
+
return handleConnectionError(serviceName, operation, error);
|
|
2644
|
+
}
|
|
2645
|
+
// Fallback: Unknown error - use UNKNOWN_ERROR with fatal recoverability since we don't know what the error is
|
|
2646
|
+
return new KitError({
|
|
2647
|
+
...ServiceError.UNKNOWN_ERROR,
|
|
2648
|
+
recoverability: 'FATAL',
|
|
2649
|
+
message: `${serviceName} ${operation} failed: ${msg.length > 0 ? msg : 'Unknown error'}`,
|
|
2650
|
+
cause: {
|
|
2651
|
+
trace: error
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Handles HTTP 4xx client errors and maps them to appropriate KitError instances.
|
|
2657
|
+
*
|
|
2658
|
+
* @param statusCode - The HTTP status code
|
|
2659
|
+
* @param serviceName - The name of the service
|
|
2660
|
+
* @param operation - The operation name
|
|
2661
|
+
* @param error - The raw error object
|
|
2662
|
+
* @param msg - The message extracted from the error
|
|
2663
|
+
* @param responseBody - The parsed JSON response body from the server, if available
|
|
2664
|
+
* @returns A KitError instance
|
|
2665
|
+
*/ function handleClientError(statusCode, serviceName, operation, error, msg, responseBody) {
|
|
2666
|
+
const detail = extractDetailFromBody(responseBody) ?? msg;
|
|
2667
|
+
switch(statusCode){
|
|
2668
|
+
// 401/403 - Authentication/Authorization
|
|
2669
|
+
case 401:
|
|
2670
|
+
case 403:
|
|
2671
|
+
return new KitError({
|
|
2672
|
+
...InputError.VALIDATION_FAILED,
|
|
2673
|
+
recoverability: 'FATAL',
|
|
2674
|
+
message: `${serviceName} ${operation} failed: Invalid or missing API key or authorization`,
|
|
2675
|
+
cause: {
|
|
2676
|
+
trace: error
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
// 404 - Not found - unsupported route OR stop-limit / slippage constraint not met
|
|
2680
|
+
case 404:
|
|
2681
|
+
if (isSlippageConstraintFailure(responseBody)) {
|
|
2682
|
+
return new KitError({
|
|
2683
|
+
...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
|
|
2684
|
+
recoverability: 'RETRYABLE',
|
|
2685
|
+
message: `${serviceName} ${operation} failed: ${detail}. ` + 'Try increasing slippageBps or adjusting stopLimit.',
|
|
2686
|
+
cause: {
|
|
2687
|
+
trace: error
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
return new KitError({
|
|
2692
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
2693
|
+
recoverability: 'FATAL',
|
|
2694
|
+
message: `${serviceName} ${operation} failed: Route or resource not found. Details: ${detail}`,
|
|
2695
|
+
cause: {
|
|
2696
|
+
trace: error
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
// 422 Unprocessable Entity
|
|
2700
|
+
// Proxy service is mapping 422 to INSUFFICIENT_SWAP_AMOUNT
|
|
2701
|
+
case 422:
|
|
2702
|
+
return new KitError({
|
|
2703
|
+
...InputError.INSUFFICIENT_SWAP_AMOUNT,
|
|
2704
|
+
recoverability: 'FATAL',
|
|
2705
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2706
|
+
cause: {
|
|
2707
|
+
trace: error
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
// 400 Bad Request - Invalid token, amount-out-of-range, or validation failed
|
|
2711
|
+
// Proxy maps 400 to UNSUPPORTED_TOKEN | AMOUNT_OUT_OF_RANGE | VALIDATION_FAILED
|
|
2712
|
+
case 400:
|
|
2713
|
+
if (responseBody?.code === ProxyErrorCode.INVALID_SWAP_AMOUNT) {
|
|
2714
|
+
const amountErr = extractAmountError(responseBody);
|
|
2715
|
+
return new KitError({
|
|
2716
|
+
...InputError.AMOUNT_OUT_OF_RANGE,
|
|
2717
|
+
recoverability: 'FATAL',
|
|
2718
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2719
|
+
cause: {
|
|
2720
|
+
trace: {
|
|
2721
|
+
rawError: error,
|
|
2722
|
+
minAmount: amountErr?.minAmount,
|
|
2723
|
+
maxAmount: amountErr?.maxAmount,
|
|
2724
|
+
token: amountErr?.token
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
return new KitError({
|
|
2730
|
+
...InputError.VALIDATION_FAILED,
|
|
2731
|
+
recoverability: 'FATAL',
|
|
2732
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2733
|
+
cause: {
|
|
2734
|
+
trace: error
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
default:
|
|
2738
|
+
// Other 4xx errors - treat as validation failures
|
|
2739
|
+
return new KitError({
|
|
2740
|
+
...InputError.VALIDATION_FAILED,
|
|
2741
|
+
recoverability: 'FATAL',
|
|
2742
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2743
|
+
cause: {
|
|
2744
|
+
trace: error
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Pattern that matches proxy response body text indicating the 404 was
|
|
2751
|
+
* caused by a slippage / price constraint rather than a truly unsupported
|
|
2752
|
+
* route. Kept case-insensitive so future proxy wording changes are tolerated.
|
|
2753
|
+
*
|
|
2754
|
+
* @internal
|
|
2755
|
+
*/ const SLIPPAGE_BODY_PATTERN = /slippage|stop.?limit|price.?impact|minimum.?output|SLIPPAGE_CONSTRAINT_NOT_MET/i;
|
|
2756
|
+
/**
|
|
2757
|
+
* Determine whether a 404 was caused by an unmet slippage or price
|
|
2758
|
+
* constraint rather than a genuinely unsupported route.
|
|
2759
|
+
*
|
|
2760
|
+
* Detection relies on the proxy response body containing slippage-related
|
|
2761
|
+
* language or a structured reason code. This avoids false positives that
|
|
2762
|
+
* would occur if we guessed based on request parameters alone (a user
|
|
2763
|
+
* can set `slippageBps` and still hit a truly unsupported route).
|
|
2764
|
+
*
|
|
2765
|
+
* @param responseBody - The parsed JSON body returned by the proxy
|
|
2766
|
+
* @returns `true` when the 404 should be treated as a slippage constraint failure
|
|
2767
|
+
* @internal
|
|
2768
|
+
*/ function isSlippageConstraintFailure(responseBody) {
|
|
2769
|
+
if (responseBody === undefined) {
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
const textsToCheck = [
|
|
2773
|
+
responseBody.externalMessage,
|
|
2774
|
+
responseBody.message,
|
|
2775
|
+
extractDetailFromBody(responseBody)
|
|
2776
|
+
];
|
|
2777
|
+
return textsToCheck.some((t)=>typeof t === 'string' && SLIPPAGE_BODY_PATTERN.test(t));
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Handles HTTP 5xx server errors and maps them to appropriate KitError instances.
|
|
2781
|
+
*
|
|
2782
|
+
* Recognizes proxy-specific structured codes in `responseBody.code` and routes
|
|
2783
|
+
* known conditions (e.g. {@link ProxyErrorCode.LOW_LIQUIDITY} on 503) to their
|
|
2784
|
+
* dedicated KitError. Falls back to a generic retryable `SERVICE_INTERNAL_ERROR`
|
|
2785
|
+
* when no specific code is present.
|
|
2786
|
+
*
|
|
2787
|
+
* @param statusCode - The HTTP status code
|
|
2788
|
+
* @param serviceName - The name of the service
|
|
2789
|
+
* @param operation - The operation name
|
|
2790
|
+
* @param error - The raw error object
|
|
2791
|
+
* @param responseBody - The parsed JSON response body from the server, if available
|
|
2792
|
+
* @returns A KitError instance
|
|
2793
|
+
*/ function handleServerError(statusCode, serviceName, operation, error, responseBody) {
|
|
2794
|
+
// 503 + 331018 = upstream liquidity insufficient (proxy)
|
|
2795
|
+
if (statusCode === 503 && responseBody?.code === ProxyErrorCode.LOW_LIQUIDITY) {
|
|
2796
|
+
const amountErr = extractAmountError(responseBody);
|
|
2797
|
+
const detail = extractDetailFromBody(responseBody) ?? getErrorMessage(error);
|
|
2798
|
+
return new KitError({
|
|
2799
|
+
...LiquidityError.INSUFFICIENT_LIQUIDITY,
|
|
2800
|
+
recoverability: 'RETRYABLE',
|
|
2801
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2802
|
+
cause: {
|
|
2803
|
+
trace: {
|
|
2804
|
+
rawError: error,
|
|
2805
|
+
minAmount: amountErr?.minAmount,
|
|
2806
|
+
maxAmount: amountErr?.maxAmount,
|
|
2807
|
+
token: amountErr?.token
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
return new KitError({
|
|
2813
|
+
...ServiceError.INTERNAL_ERROR,
|
|
2814
|
+
recoverability: 'RETRYABLE',
|
|
2815
|
+
message: `${serviceName} ${operation} failed: Server error (${statusCode.toString()})`,
|
|
2816
|
+
cause: {
|
|
2817
|
+
trace: error
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Handles network connection errors and maps them to appropriate KitError instances.
|
|
2823
|
+
*
|
|
2824
|
+
* @param serviceName - The name of the service
|
|
2825
|
+
* @param operation - The operation name
|
|
2826
|
+
* @param error - The raw error object
|
|
2827
|
+
* @returns A KitError instance
|
|
2828
|
+
*/ function handleConnectionError(serviceName, operation, error) {
|
|
2829
|
+
return new KitError({
|
|
2830
|
+
...NetworkError.CONNECTION_FAILED,
|
|
2831
|
+
recoverability: 'RETRYABLE',
|
|
2832
|
+
message: `${serviceName} ${operation} failed: Network connection error`,
|
|
2833
|
+
cause: {
|
|
2834
|
+
trace: error
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Handles rate limit errors and maps them to appropriate KitError instances.
|
|
2840
|
+
*
|
|
2841
|
+
* @param serviceName - The name of the service
|
|
2842
|
+
* @param operation - The operation name
|
|
2843
|
+
* @param error - The raw error object
|
|
2844
|
+
* @returns A KitError instance
|
|
2845
|
+
*/ function handleRateLimitError(serviceName, operation, error) {
|
|
2846
|
+
return new KitError({
|
|
2847
|
+
...RateLimitError.RATE_LIMIT_EXCEEDED,
|
|
2848
|
+
recoverability: 'RETRYABLE',
|
|
2849
|
+
message: `${serviceName} ${operation} failed: Too many requests, please retry later`,
|
|
2850
|
+
cause: {
|
|
2851
|
+
trace: error
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Handles timeout errors and maps them to appropriate KitError instances.
|
|
2857
|
+
*
|
|
2858
|
+
* @param serviceName - The name of the service
|
|
2859
|
+
* @param operation - The operation name
|
|
2860
|
+
* @param error - The raw error object
|
|
2861
|
+
* @returns A KitError instance
|
|
2862
|
+
*/ function handleTimeoutError(serviceName, operation, error) {
|
|
2863
|
+
return new KitError({
|
|
2864
|
+
...NetworkError.TIMEOUT,
|
|
2865
|
+
recoverability: 'RETRYABLE',
|
|
2866
|
+
message: `${serviceName} ${operation} failed: Request timeout`,
|
|
2867
|
+
cause: {
|
|
2868
|
+
trace: error
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Type guard that narrows an unknown error to one carrying a non-null
|
|
2874
|
+
* object `responseBody` property (attached by `makeApiRequest`).
|
|
2875
|
+
*
|
|
2876
|
+
* @param error - The raw error from the HTTP layer
|
|
2877
|
+
* @returns `true` when `error.responseBody` is a non-null object
|
|
2878
|
+
* @internal
|
|
2879
|
+
*/ function hasResponseBody(error) {
|
|
2880
|
+
return typeof error === 'object' && error !== null && 'responseBody' in error && typeof error['responseBody'] === 'object' && error['responseBody'] !== null;
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Extract the `responseBody` property that `makeApiRequest` attaches to
|
|
2884
|
+
* HTTP error instances when the server returns a JSON body.
|
|
2885
|
+
*
|
|
2886
|
+
* @param error - The raw error from the HTTP layer
|
|
2887
|
+
* @returns The parsed body cast to {@link ApiErrorResponseBody}, or undefined
|
|
2888
|
+
* @throws Never. Returns `undefined` when the error does not carry a valid
|
|
2889
|
+
* `responseBody`.
|
|
2890
|
+
* @internal
|
|
2891
|
+
*/ function extractResponseBody(error) {
|
|
2892
|
+
if (!hasResponseBody(error)) {
|
|
2893
|
+
return undefined;
|
|
2894
|
+
}
|
|
2895
|
+
return error.responseBody;
|
|
2896
|
+
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Extract a field name from an {@link ApiFieldError}.
|
|
2899
|
+
*
|
|
2900
|
+
* The proxy service may provide the field name as a plain `field` string
|
|
2901
|
+
* or as a `path` array (e.g. `["tokenInChain"]`). This helper resolves
|
|
2902
|
+
* whichever is available, preferring `field` when both exist.
|
|
2903
|
+
*
|
|
2904
|
+
* @param entry - A single error entry from the `errors` array
|
|
2905
|
+
* @returns The field name, or `undefined` when neither is available
|
|
2906
|
+
* @internal
|
|
2907
|
+
*/ function extractFieldName(entry) {
|
|
2908
|
+
if (typeof entry.field === 'string' && entry.field.length > 0) {
|
|
2909
|
+
return entry.field;
|
|
2910
|
+
}
|
|
2911
|
+
if (Array.isArray(entry.path) && entry.path.length > 0) {
|
|
2912
|
+
const first = entry.path[0];
|
|
2913
|
+
if (typeof first === 'string' && first.length > 0) {
|
|
2914
|
+
return first;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
return undefined;
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Join field-level error entries into a single human-readable string.
|
|
2921
|
+
*
|
|
2922
|
+
* Each entry is formatted as `"field: message"` when a field name is
|
|
2923
|
+
* available (via `field` or `path`), or just the message otherwise.
|
|
2924
|
+
* Entries without a usable message (including amount-bound entries that
|
|
2925
|
+
* only carry `minAmount`/`maxAmount`/`token`) are skipped.
|
|
2926
|
+
*
|
|
2927
|
+
* @param errors - The `errors` array from the response body
|
|
2928
|
+
* @returns A joined string, or `undefined` when no usable entries exist
|
|
2929
|
+
* @internal
|
|
2930
|
+
*/ function joinFieldErrors(errors) {
|
|
2931
|
+
const parts = errors.map((entry)=>{
|
|
2932
|
+
if (!isFieldEntry(entry)) {
|
|
2933
|
+
return undefined;
|
|
2934
|
+
}
|
|
2935
|
+
const fieldMsg = typeof entry.message === 'string' && entry.message.length > 0 ? entry.message : undefined;
|
|
2936
|
+
if (fieldMsg === undefined) {
|
|
2937
|
+
return undefined;
|
|
2938
|
+
}
|
|
2939
|
+
const fieldName = extractFieldName(entry);
|
|
2940
|
+
return fieldName === undefined ? fieldMsg : `${fieldName}: ${fieldMsg}`;
|
|
2941
|
+
}).filter((s)=>s !== undefined);
|
|
2942
|
+
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Narrow an {@link ApiErrorItem} to the field-error shape used for
|
|
2946
|
+
* validation messages. Amount-bound entries (which carry no `message`)
|
|
2947
|
+
* are excluded.
|
|
2948
|
+
*
|
|
2949
|
+
* @internal
|
|
2950
|
+
*/ function isFieldEntry(entry) {
|
|
2951
|
+
return 'message' in entry || 'field' in entry || 'path' in entry;
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Narrow an {@link ApiErrorItem} to the amount-bound shape emitted by the
|
|
2955
|
+
* proxy for {@link ProxyErrorCode.INVALID_SWAP_AMOUNT}.
|
|
2956
|
+
*
|
|
2957
|
+
* @internal
|
|
2958
|
+
*/ function isAmountEntry(entry) {
|
|
2959
|
+
return 'minAmount' in entry || 'maxAmount' in entry || 'token' in entry;
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Extract the first amount-bound entry from a response body, if any.
|
|
2963
|
+
*
|
|
2964
|
+
* @internal
|
|
2965
|
+
*/ function extractAmountError(body) {
|
|
2966
|
+
if (body === undefined || !Array.isArray(body.errors)) {
|
|
2967
|
+
return undefined;
|
|
2968
|
+
}
|
|
2969
|
+
return body.errors.find(isAmountEntry);
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Derive a human-readable detail string from an {@link ApiErrorResponseBody}.
|
|
2973
|
+
*
|
|
2974
|
+
* Resolution order:
|
|
2975
|
+
* 1. `body.externalMessage` -- the user-facing string the proxy intends
|
|
2976
|
+
* consumers to display (e.g. "No route found that satisfies the
|
|
2977
|
+
* requested stop limit"). Preferred when available.
|
|
2978
|
+
* 2. `body.message` **and** `body.errors` -- when both are present the
|
|
2979
|
+
* top-level message is combined with the field-level detail so
|
|
2980
|
+
* developers see the full picture
|
|
2981
|
+
* (e.g. `"Validation error: tokenInChain: Invalid input; amount: …"`).
|
|
2982
|
+
* 3. `body.message` alone -- used as-is.
|
|
2983
|
+
* 4. `body.errors` alone -- field-level entries joined with "; ".
|
|
2984
|
+
* 5. `undefined` -- caller should fall back to the raw HTTP status text.
|
|
2985
|
+
*
|
|
2986
|
+
* @param body - The parsed response body, may be undefined
|
|
2987
|
+
* @returns A detail string, or undefined when no useful info is available
|
|
2988
|
+
* @internal
|
|
2989
|
+
*/ function extractDetailFromBody(body) {
|
|
2990
|
+
if (body === undefined) {
|
|
2991
|
+
return undefined;
|
|
2992
|
+
}
|
|
2993
|
+
const externalMessage = typeof body.externalMessage === 'string' && body.externalMessage.length > 0 ? body.externalMessage : undefined;
|
|
2994
|
+
if (externalMessage !== undefined) {
|
|
2995
|
+
return externalMessage;
|
|
2996
|
+
}
|
|
2997
|
+
const topMessage = typeof body.message === 'string' && body.message.length > 0 ? body.message : undefined;
|
|
2998
|
+
const fieldDetail = Array.isArray(body.errors) && body.errors.length > 0 ? joinFieldErrors(body.errors) : undefined;
|
|
2999
|
+
if (topMessage !== undefined && fieldDetail !== undefined) {
|
|
3000
|
+
return `${topMessage}: ${fieldDetail}`;
|
|
3001
|
+
}
|
|
3002
|
+
return topMessage ?? fieldDetail;
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Extracts the HTTP status code from an error message.
|
|
3006
|
+
*
|
|
3007
|
+
* Attempts to parse HTTP status codes from common error message formats,
|
|
3008
|
+
* such as "HTTP 404" or "Status: 500".
|
|
3009
|
+
*
|
|
3010
|
+
* @param msg - The error message to extract from
|
|
3011
|
+
* @returns The extracted HTTP status code, or null if not found
|
|
3012
|
+
*
|
|
3013
|
+
* @example
|
|
3014
|
+
* ```typescript
|
|
3015
|
+
* const code = extractHttpStatusCode('HTTP 404')
|
|
3016
|
+
* // Returns: 404
|
|
3017
|
+
* ```
|
|
3018
|
+
*
|
|
3019
|
+
* @example
|
|
3020
|
+
* ```typescript
|
|
3021
|
+
* const code = extractHttpStatusCode('Status: 500 Internal Server Error')
|
|
3022
|
+
* // Returns: 500
|
|
3023
|
+
* ```
|
|
3024
|
+
*/ function extractHttpStatusCode(msg) {
|
|
3025
|
+
// Pattern: "HTTP 404" or "HTTP 404 - some message" or "Status: 404"
|
|
3026
|
+
const patterns = [
|
|
3027
|
+
/HTTP (\d{3})/i,
|
|
3028
|
+
/Status:\s*(\d{3})/i,
|
|
3029
|
+
/^(\d{3}) -/
|
|
3030
|
+
];
|
|
3031
|
+
for (const pattern of patterns){
|
|
3032
|
+
const match = pattern.exec(msg);
|
|
3033
|
+
const codeStr = match?.at(1);
|
|
3034
|
+
if (codeStr !== undefined) {
|
|
3035
|
+
const code = Number.parseInt(codeStr, 10);
|
|
3036
|
+
// Validate it's a valid HTTP status code
|
|
3037
|
+
if (code >= 100 && code < 600) {
|
|
3038
|
+
return code;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return null;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2466
3045
|
/**
|
|
2467
3046
|
* @packageDocumentation
|
|
2468
3047
|
* @module ChainDefinitions
|
|
@@ -2534,6 +3113,8 @@ class KitError extends Error {
|
|
|
2534
3113
|
Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
|
|
2535
3114
|
Blockchain["Pharos"] = "Pharos";
|
|
2536
3115
|
Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
|
|
3116
|
+
Blockchain["Plasma"] = "Plasma";
|
|
3117
|
+
Blockchain["Plasma_Testnet"] = "Plasma_Testnet";
|
|
2537
3118
|
Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
|
|
2538
3119
|
Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
|
|
2539
3120
|
Blockchain["Plume"] = "Plume";
|
|
@@ -2603,6 +3184,7 @@ var BridgeChain;
|
|
|
2603
3184
|
BridgeChain["Morph"] = "Morph";
|
|
2604
3185
|
BridgeChain["Optimism"] = "Optimism";
|
|
2605
3186
|
BridgeChain["Pharos"] = "Pharos";
|
|
3187
|
+
BridgeChain["Plasma"] = "Plasma";
|
|
2606
3188
|
BridgeChain["Plume"] = "Plume";
|
|
2607
3189
|
BridgeChain["Polygon"] = "Polygon";
|
|
2608
3190
|
BridgeChain["Sei"] = "Sei";
|
|
@@ -2629,6 +3211,7 @@ var BridgeChain;
|
|
|
2629
3211
|
BridgeChain["Morph_Testnet"] = "Morph_Testnet";
|
|
2630
3212
|
BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
|
|
2631
3213
|
BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
|
|
3214
|
+
BridgeChain["Plasma_Testnet"] = "Plasma_Testnet";
|
|
2632
3215
|
BridgeChain["Plume_Testnet"] = "Plume_Testnet";
|
|
2633
3216
|
BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
|
|
2634
3217
|
BridgeChain["Sei_Testnet"] = "Sei_Testnet";
|
|
@@ -3075,6 +3658,8 @@ var EarnChain;
|
|
|
3075
3658
|
* This program handles minting operations for Gateway transactions
|
|
3076
3659
|
* on Solana devnet.
|
|
3077
3660
|
*/ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
|
|
3661
|
+
/** TokenMessengerWithFees address shared by enabled EVM mainnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
|
|
3662
|
+
/** TokenMessengerWithFees address shared by enabled EVM testnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
|
|
3078
3663
|
|
|
3079
3664
|
/**
|
|
3080
3665
|
* Arc Testnet chain definition
|
|
@@ -3112,6 +3697,7 @@ var EarnChain;
|
|
|
3112
3697
|
v2: {
|
|
3113
3698
|
type: 'split',
|
|
3114
3699
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
3700
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3115
3701
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
3116
3702
|
confirmations: 1,
|
|
3117
3703
|
fastConfirmations: 1
|
|
@@ -3179,6 +3765,7 @@ var EarnChain;
|
|
|
3179
3765
|
v2: {
|
|
3180
3766
|
type: 'split',
|
|
3181
3767
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
3768
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3182
3769
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3183
3770
|
confirmations: 65,
|
|
3184
3771
|
fastConfirmations: 1
|
|
@@ -3243,6 +3830,7 @@ var EarnChain;
|
|
|
3243
3830
|
v2: {
|
|
3244
3831
|
type: 'split',
|
|
3245
3832
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
3833
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3246
3834
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
3247
3835
|
confirmations: 65,
|
|
3248
3836
|
fastConfirmations: 1
|
|
@@ -3307,6 +3895,7 @@ var EarnChain;
|
|
|
3307
3895
|
v2: {
|
|
3308
3896
|
type: 'split',
|
|
3309
3897
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
3898
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3310
3899
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3311
3900
|
confirmations: 1,
|
|
3312
3901
|
fastConfirmations: 1
|
|
@@ -3368,6 +3957,7 @@ var EarnChain;
|
|
|
3368
3957
|
v2: {
|
|
3369
3958
|
type: 'split',
|
|
3370
3959
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
3960
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3371
3961
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3372
3962
|
confirmations: 1,
|
|
3373
3963
|
fastConfirmations: 1
|
|
@@ -3435,6 +4025,7 @@ var EarnChain;
|
|
|
3435
4025
|
v2: {
|
|
3436
4026
|
type: 'split',
|
|
3437
4027
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4028
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3438
4029
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3439
4030
|
confirmations: 65,
|
|
3440
4031
|
fastConfirmations: 1
|
|
@@ -3499,6 +4090,7 @@ var EarnChain;
|
|
|
3499
4090
|
v2: {
|
|
3500
4091
|
type: 'split',
|
|
3501
4092
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
4093
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3502
4094
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3503
4095
|
confirmations: 65,
|
|
3504
4096
|
fastConfirmations: 1
|
|
@@ -3609,6 +4201,7 @@ var EarnChain;
|
|
|
3609
4201
|
v2: {
|
|
3610
4202
|
type: 'split',
|
|
3611
4203
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4204
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3612
4205
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3613
4206
|
confirmations: 65,
|
|
3614
4207
|
fastConfirmations: 1
|
|
@@ -3653,6 +4246,7 @@ var EarnChain;
|
|
|
3653
4246
|
v2: {
|
|
3654
4247
|
type: 'split',
|
|
3655
4248
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
4249
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3656
4250
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3657
4251
|
confirmations: 65,
|
|
3658
4252
|
fastConfirmations: 1
|
|
@@ -3884,6 +4478,7 @@ var EarnChain;
|
|
|
3884
4478
|
v2: {
|
|
3885
4479
|
type: 'split',
|
|
3886
4480
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4481
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3887
4482
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3888
4483
|
confirmations: 65,
|
|
3889
4484
|
fastConfirmations: 2
|
|
@@ -3948,6 +4543,7 @@ var EarnChain;
|
|
|
3948
4543
|
v2: {
|
|
3949
4544
|
type: 'split',
|
|
3950
4545
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
4546
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3951
4547
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3952
4548
|
confirmations: 65,
|
|
3953
4549
|
fastConfirmations: 2
|
|
@@ -4058,6 +4654,7 @@ var EarnChain;
|
|
|
4058
4654
|
v2: {
|
|
4059
4655
|
type: 'split',
|
|
4060
4656
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4657
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4061
4658
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4062
4659
|
confirmations: 1,
|
|
4063
4660
|
fastConfirmations: 1
|
|
@@ -4117,6 +4714,7 @@ var EarnChain;
|
|
|
4117
4714
|
v2: {
|
|
4118
4715
|
type: 'split',
|
|
4119
4716
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
4717
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4120
4718
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
4121
4719
|
confirmations: 1,
|
|
4122
4720
|
fastConfirmations: 1
|
|
@@ -4271,6 +4869,7 @@ var EarnChain;
|
|
|
4271
4869
|
v2: {
|
|
4272
4870
|
type: 'split',
|
|
4273
4871
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4872
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4274
4873
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4275
4874
|
confirmations: 65,
|
|
4276
4875
|
fastConfirmations: 1
|
|
@@ -4318,6 +4917,7 @@ var EarnChain;
|
|
|
4318
4917
|
v2: {
|
|
4319
4918
|
type: 'split',
|
|
4320
4919
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
4920
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4321
4921
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
4322
4922
|
confirmations: 65,
|
|
4323
4923
|
fastConfirmations: 1
|
|
@@ -4362,6 +4962,7 @@ var EarnChain;
|
|
|
4362
4962
|
v2: {
|
|
4363
4963
|
type: 'split',
|
|
4364
4964
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4965
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4365
4966
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4366
4967
|
confirmations: 1,
|
|
4367
4968
|
fastConfirmations: 1
|
|
@@ -4407,6 +5008,7 @@ var EarnChain;
|
|
|
4407
5008
|
v2: {
|
|
4408
5009
|
type: 'split',
|
|
4409
5010
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
5011
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4410
5012
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
4411
5013
|
confirmations: 1,
|
|
4412
5014
|
fastConfirmations: 1
|
|
@@ -4453,6 +5055,7 @@ var EarnChain;
|
|
|
4453
5055
|
v2: {
|
|
4454
5056
|
type: 'split',
|
|
4455
5057
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5058
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4456
5059
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4457
5060
|
confirmations: 1,
|
|
4458
5061
|
fastConfirmations: 1
|
|
@@ -4766,6 +5369,7 @@ var EarnChain;
|
|
|
4766
5369
|
v2: {
|
|
4767
5370
|
type: 'split',
|
|
4768
5371
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5372
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4769
5373
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4770
5374
|
confirmations: 65,
|
|
4771
5375
|
fastConfirmations: 1
|
|
@@ -4830,6 +5434,7 @@ var EarnChain;
|
|
|
4830
5434
|
v2: {
|
|
4831
5435
|
type: 'split',
|
|
4832
5436
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
5437
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4833
5438
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
4834
5439
|
confirmations: 65,
|
|
4835
5440
|
fastConfirmations: 1
|
|
@@ -4951,19 +5556,111 @@ var EarnChain;
|
|
|
4951
5556
|
});
|
|
4952
5557
|
|
|
4953
5558
|
/**
|
|
4954
|
-
*
|
|
5559
|
+
* Plasma Mainnet chain definition
|
|
4955
5560
|
* @remarks
|
|
4956
|
-
* This represents the official production network for the
|
|
4957
|
-
*
|
|
4958
|
-
* with
|
|
4959
|
-
*/ const
|
|
5561
|
+
* This represents the official production network for the Plasma blockchain.
|
|
5562
|
+
* Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
|
|
5563
|
+
* stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
|
|
5564
|
+
*/ const Plasma = defineChain({
|
|
4960
5565
|
type: 'evm',
|
|
4961
|
-
chain: Blockchain.
|
|
4962
|
-
name: '
|
|
4963
|
-
title: '
|
|
5566
|
+
chain: Blockchain.Plasma,
|
|
5567
|
+
name: 'Plasma',
|
|
5568
|
+
title: 'Plasma Mainnet',
|
|
4964
5569
|
nativeCurrency: {
|
|
4965
|
-
name: '
|
|
4966
|
-
symbol: '
|
|
5570
|
+
name: 'Plasma',
|
|
5571
|
+
symbol: 'XPL',
|
|
5572
|
+
decimals: 18
|
|
5573
|
+
},
|
|
5574
|
+
chainId: 9745,
|
|
5575
|
+
isTestnet: false,
|
|
5576
|
+
explorerUrl: 'https://plasmascan.to/tx/{hash}',
|
|
5577
|
+
rpcEndpoints: [
|
|
5578
|
+
'https://rpc.plasma.to'
|
|
5579
|
+
],
|
|
5580
|
+
eurcAddress: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
|
|
5581
|
+
usdcAddress: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
|
|
5582
|
+
usdtAddress: null,
|
|
5583
|
+
cctp: {
|
|
5584
|
+
domain: 33,
|
|
5585
|
+
contracts: {
|
|
5586
|
+
v2: {
|
|
5587
|
+
type: 'split',
|
|
5588
|
+
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5589
|
+
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5590
|
+
confirmations: 3,
|
|
5591
|
+
fastConfirmations: 1
|
|
5592
|
+
}
|
|
5593
|
+
},
|
|
5594
|
+
forwarderSupported: {
|
|
5595
|
+
source: false,
|
|
5596
|
+
destination: false
|
|
5597
|
+
}
|
|
5598
|
+
},
|
|
5599
|
+
kitContracts: {
|
|
5600
|
+
bridge: BRIDGE_CONTRACT_EVM_MAINNET
|
|
5601
|
+
}
|
|
5602
|
+
});
|
|
5603
|
+
|
|
5604
|
+
/**
|
|
5605
|
+
* Plasma Testnet chain definition
|
|
5606
|
+
* @remarks
|
|
5607
|
+
* This represents the official test network for the Plasma blockchain.
|
|
5608
|
+
* Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
|
|
5609
|
+
* stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
|
|
5610
|
+
*/ const PlasmaTestnet = defineChain({
|
|
5611
|
+
type: 'evm',
|
|
5612
|
+
chain: Blockchain.Plasma_Testnet,
|
|
5613
|
+
name: 'Plasma Testnet',
|
|
5614
|
+
title: 'Plasma Testnet',
|
|
5615
|
+
nativeCurrency: {
|
|
5616
|
+
name: 'Plasma',
|
|
5617
|
+
symbol: 'XPL',
|
|
5618
|
+
decimals: 18
|
|
5619
|
+
},
|
|
5620
|
+
chainId: 9746,
|
|
5621
|
+
isTestnet: true,
|
|
5622
|
+
explorerUrl: 'https://testnet.plasmascan.to/tx/{hash}',
|
|
5623
|
+
rpcEndpoints: [
|
|
5624
|
+
'https://testnet-rpc.plasma.to'
|
|
5625
|
+
],
|
|
5626
|
+
eurcAddress: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330',
|
|
5627
|
+
usdcAddress: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
|
|
5628
|
+
usdtAddress: null,
|
|
5629
|
+
cctp: {
|
|
5630
|
+
domain: 33,
|
|
5631
|
+
contracts: {
|
|
5632
|
+
v2: {
|
|
5633
|
+
type: 'split',
|
|
5634
|
+
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
5635
|
+
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5636
|
+
confirmations: 3,
|
|
5637
|
+
fastConfirmations: 1
|
|
5638
|
+
}
|
|
5639
|
+
},
|
|
5640
|
+
forwarderSupported: {
|
|
5641
|
+
source: false,
|
|
5642
|
+
destination: false
|
|
5643
|
+
}
|
|
5644
|
+
},
|
|
5645
|
+
kitContracts: {
|
|
5646
|
+
bridge: BRIDGE_CONTRACT_EVM_TESTNET
|
|
5647
|
+
}
|
|
5648
|
+
});
|
|
5649
|
+
|
|
5650
|
+
/**
|
|
5651
|
+
* Plume Mainnet chain definition
|
|
5652
|
+
* @remarks
|
|
5653
|
+
* This represents the official production network for the Plume blockchain.
|
|
5654
|
+
* Plume is a Layer 1 blockchain specialized for DeFi and trading applications
|
|
5655
|
+
* with native orderbook and matching engine.
|
|
5656
|
+
*/ const Plume = defineChain({
|
|
5657
|
+
type: 'evm',
|
|
5658
|
+
chain: Blockchain.Plume,
|
|
5659
|
+
name: 'Plume',
|
|
5660
|
+
title: 'Plume Mainnet',
|
|
5661
|
+
nativeCurrency: {
|
|
5662
|
+
name: 'Plume',
|
|
5663
|
+
symbol: 'PLUME',
|
|
4967
5664
|
decimals: 18
|
|
4968
5665
|
},
|
|
4969
5666
|
chainId: 98866,
|
|
@@ -4981,6 +5678,7 @@ var EarnChain;
|
|
|
4981
5678
|
v2: {
|
|
4982
5679
|
type: 'split',
|
|
4983
5680
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5681
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4984
5682
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4985
5683
|
confirmations: 65,
|
|
4986
5684
|
fastConfirmations: 1
|
|
@@ -5027,6 +5725,7 @@ var EarnChain;
|
|
|
5027
5725
|
v2: {
|
|
5028
5726
|
type: 'split',
|
|
5029
5727
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
5728
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5030
5729
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5031
5730
|
confirmations: 65,
|
|
5032
5731
|
fastConfirmations: 1
|
|
@@ -5128,6 +5827,7 @@ var EarnChain;
|
|
|
5128
5827
|
v2: {
|
|
5129
5828
|
type: 'split',
|
|
5130
5829
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5830
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5131
5831
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5132
5832
|
confirmations: 33,
|
|
5133
5833
|
fastConfirmations: 13
|
|
@@ -5193,6 +5893,7 @@ var EarnChain;
|
|
|
5193
5893
|
v2: {
|
|
5194
5894
|
type: 'split',
|
|
5195
5895
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
5896
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5196
5897
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5197
5898
|
confirmations: 33,
|
|
5198
5899
|
fastConfirmations: 13
|
|
@@ -5252,6 +5953,7 @@ var EarnChain;
|
|
|
5252
5953
|
v2: {
|
|
5253
5954
|
type: 'split',
|
|
5254
5955
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5956
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5255
5957
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5256
5958
|
confirmations: 1,
|
|
5257
5959
|
fastConfirmations: 1
|
|
@@ -5311,6 +6013,7 @@ var EarnChain;
|
|
|
5311
6013
|
v2: {
|
|
5312
6014
|
type: 'split',
|
|
5313
6015
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6016
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5314
6017
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5315
6018
|
confirmations: 1,
|
|
5316
6019
|
fastConfirmations: 1
|
|
@@ -5368,6 +6071,7 @@ var EarnChain;
|
|
|
5368
6071
|
v2: {
|
|
5369
6072
|
type: 'split',
|
|
5370
6073
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6074
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5371
6075
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5372
6076
|
confirmations: 1,
|
|
5373
6077
|
fastConfirmations: 1
|
|
@@ -5426,6 +6130,7 @@ var EarnChain;
|
|
|
5426
6130
|
v2: {
|
|
5427
6131
|
type: 'split',
|
|
5428
6132
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6133
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5429
6134
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5430
6135
|
confirmations: 1,
|
|
5431
6136
|
fastConfirmations: 1
|
|
@@ -5741,6 +6446,7 @@ var EarnChain;
|
|
|
5741
6446
|
v2: {
|
|
5742
6447
|
type: 'split',
|
|
5743
6448
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6449
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5744
6450
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5745
6451
|
confirmations: 65,
|
|
5746
6452
|
fastConfirmations: 1
|
|
@@ -5805,6 +6511,7 @@ var EarnChain;
|
|
|
5805
6511
|
v2: {
|
|
5806
6512
|
type: 'split',
|
|
5807
6513
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6514
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5808
6515
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5809
6516
|
confirmations: 65,
|
|
5810
6517
|
fastConfirmations: 1
|
|
@@ -5862,6 +6569,7 @@ var EarnChain;
|
|
|
5862
6569
|
v2: {
|
|
5863
6570
|
type: 'split',
|
|
5864
6571
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cF5d',
|
|
6572
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5865
6573
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5866
6574
|
confirmations: 65,
|
|
5867
6575
|
fastConfirmations: 1
|
|
@@ -5921,6 +6629,7 @@ var EarnChain;
|
|
|
5921
6629
|
v2: {
|
|
5922
6630
|
type: 'split',
|
|
5923
6631
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
6632
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5924
6633
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
5925
6634
|
confirmations: 65,
|
|
5926
6635
|
fastConfirmations: 1
|
|
@@ -5981,6 +6690,7 @@ var EarnChain;
|
|
|
5981
6690
|
v2: {
|
|
5982
6691
|
type: 'split',
|
|
5983
6692
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6693
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5984
6694
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5985
6695
|
confirmations: 3,
|
|
5986
6696
|
fastConfirmations: 3
|
|
@@ -6026,6 +6736,7 @@ var EarnChain;
|
|
|
6026
6736
|
v2: {
|
|
6027
6737
|
type: 'split',
|
|
6028
6738
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6739
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
6029
6740
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
6030
6741
|
confirmations: 3,
|
|
6031
6742
|
fastConfirmations: 1
|
|
@@ -6236,6 +6947,8 @@ var Chains = /*#__PURE__*/Object.freeze({
|
|
|
6236
6947
|
OptimismSepolia: OptimismSepolia,
|
|
6237
6948
|
Pharos: Pharos,
|
|
6238
6949
|
PharosTestnet: PharosTestnet,
|
|
6950
|
+
Plasma: Plasma,
|
|
6951
|
+
PlasmaTestnet: PlasmaTestnet,
|
|
6239
6952
|
Plume: Plume,
|
|
6240
6953
|
PlumeTestnet: PlumeTestnet,
|
|
6241
6954
|
PolkadotAssetHub: PolkadotAssetHub,
|
|
@@ -6285,6 +6998,34 @@ var Chains = /*#__PURE__*/Object.freeze({
|
|
|
6285
6998
|
return chain.cctp?.contracts.v2 !== undefined;
|
|
6286
6999
|
}
|
|
6287
7000
|
|
|
7001
|
+
/**
|
|
7002
|
+
* Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
|
|
7003
|
+
*
|
|
7004
|
+
* A chain supports source-paid fees when its CCTP v2 configuration carries a
|
|
7005
|
+
* deployed `TokenMessengerWithFees` wrapper address. Bridge Kit routes
|
|
7006
|
+
* `feePayment: 'source'` transfers through this wrapper via
|
|
7007
|
+
* `depositForBurnWithHookAndFees`, so a chain without the wrapper cannot be a
|
|
7008
|
+
* source for receive-exact bridging.
|
|
7009
|
+
*
|
|
7010
|
+
* @param chain - The chain definition to check.
|
|
7011
|
+
* @returns `true` when the chain has a `tokenMessengerWithFees` wrapper
|
|
7012
|
+
* configured, `false` otherwise.
|
|
7013
|
+
*
|
|
7014
|
+
* @example
|
|
7015
|
+
* ```typescript
|
|
7016
|
+
* import { Chains, hasSourceFeeSupport } from '@core/chains'
|
|
7017
|
+
*
|
|
7018
|
+
* hasSourceFeeSupport(Chains.Optimism) // true
|
|
7019
|
+
* hasSourceFeeSupport(Chains.Solana) // false
|
|
7020
|
+
* ```
|
|
7021
|
+
*/ function hasSourceFeeSupport(chain) {
|
|
7022
|
+
if (!isCCTPV2Supported(chain)) {
|
|
7023
|
+
return false;
|
|
7024
|
+
}
|
|
7025
|
+
const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
|
|
7026
|
+
return typeof wrapper === 'string' && wrapper.length > 0;
|
|
7027
|
+
}
|
|
7028
|
+
|
|
6288
7029
|
/**
|
|
6289
7030
|
* Check if a chain supports a specific type of custom smart contract logic.
|
|
6290
7031
|
*
|
|
@@ -8303,6 +9044,7 @@ const swapTokenEnumSchema = z.enum([
|
|
|
8303
9044
|
[Blockchain.Noble]: 'uusdc',
|
|
8304
9045
|
[Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
|
|
8305
9046
|
[Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
|
|
9047
|
+
[Blockchain.Plasma]: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
|
|
8306
9048
|
[Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
|
|
8307
9049
|
[Blockchain.Polkadot_Asset_Hub]: '1337',
|
|
8308
9050
|
[Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
|
|
@@ -8339,6 +9081,7 @@ const swapTokenEnumSchema = z.enum([
|
|
|
8339
9081
|
[Blockchain.Noble_Testnet]: 'uusdc',
|
|
8340
9082
|
[Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
|
|
8341
9083
|
[Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
|
|
9084
|
+
[Blockchain.Plasma_Testnet]: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
|
|
8342
9085
|
[Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
|
|
8343
9086
|
[Blockchain.Polkadot_Westmint]: '31337',
|
|
8344
9087
|
[Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
|
|
@@ -8401,6 +9144,7 @@ const swapTokenEnumSchema = z.enum([
|
|
|
8401
9144
|
[Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
|
|
8402
9145
|
[Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
|
|
8403
9146
|
[Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
|
|
9147
|
+
[Blockchain.Plasma]: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
|
|
8404
9148
|
[Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
|
|
8405
9149
|
[Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
|
|
8406
9150
|
// =========================================================================
|
|
@@ -8409,7 +9153,8 @@ const swapTokenEnumSchema = z.enum([
|
|
|
8409
9153
|
[Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
|
|
8410
9154
|
[Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
|
|
8411
9155
|
[Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
|
|
8412
|
-
[Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
|
|
9156
|
+
[Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
|
|
9157
|
+
[Blockchain.Plasma_Testnet]: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330'
|
|
8413
9158
|
}
|
|
8414
9159
|
};
|
|
8415
9160
|
|
|
@@ -9233,6 +9978,16 @@ const swapTokenEnumSchema = z.enum([
|
|
|
9233
9978
|
*
|
|
9234
9979
|
* Set to 0 when no additional Circle-reserved data is needed.
|
|
9235
9980
|
*/ const CCTP_FORWARD_PAYLOAD_LENGTH = 0;
|
|
9981
|
+
/**
|
|
9982
|
+
* Length in bytes of a Solana owner (ed25519 / PDA) public key.
|
|
9983
|
+
*/ const SOLANA_PUBKEY_LENGTH = 32;
|
|
9984
|
+
/**
|
|
9985
|
+
* Byte length of the Solana ATA-creation forwarding payload appended after the
|
|
9986
|
+
* `cctp-forward` frame: `createAta` (1 byte) + `ataOwner` (32 bytes).
|
|
9987
|
+
*
|
|
9988
|
+
* Circle's Orbit relayer decodes exactly this many bytes; see
|
|
9989
|
+
* {@link buildSolanaAtaForwardingHookData}.
|
|
9990
|
+
*/ const SOLANA_ATA_FORWARD_PAYLOAD_LENGTH = 1 + SOLANA_PUBKEY_LENGTH;
|
|
9236
9991
|
/**
|
|
9237
9992
|
* Build the hookData bytes for CCTP forwarding.
|
|
9238
9993
|
*
|
|
@@ -9289,6 +10044,108 @@ function buildForwardingHookData() {
|
|
|
9289
10044
|
cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
|
|
9290
10045
|
return cachedHookDataHex;
|
|
9291
10046
|
}
|
|
10047
|
+
/**
|
|
10048
|
+
* Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
|
|
10049
|
+
* create the recipient's Associated Token Account (ATA) before minting on Solana.
|
|
10050
|
+
*
|
|
10051
|
+
* When an EVM→Solana bridge is forwarded, the destination mint targets the
|
|
10052
|
+
* recipient's USDC ATA — which does not exist for a fresh wallet. This frame
|
|
10053
|
+
* tells the relayer to prepend an idempotent `createAssociatedTokenAccount`
|
|
10054
|
+
* instruction (the relayer pays the rent) so the mint always succeeds.
|
|
10055
|
+
*
|
|
10056
|
+
* Unlike {@link buildForwardingHookData} (an empty version-0 frame), this emits
|
|
10057
|
+
* a version-0 frame whose 32-bit `dataLength` is set to
|
|
10058
|
+
* {@link SOLANA_ATA_FORWARD_PAYLOAD_LENGTH} (33), followed by the payload the
|
|
10059
|
+
* relayer decodes:
|
|
10060
|
+
* - Byte 0: `createAta` flag, always `1`
|
|
10061
|
+
* - Bytes 1-32: the recipient's 32-byte Solana owner public key (`ataOwner`)
|
|
10062
|
+
*
|
|
10063
|
+
* @remarks
|
|
10064
|
+
* `ataOwner` is the recipient's *wallet* public key, not the derived ATA. The
|
|
10065
|
+
* relayer re-derives the ATA from `ataOwner` and the USDC mint and requires it
|
|
10066
|
+
* to equal the burn's `mintRecipient`, so callers must pass the same owner used
|
|
10067
|
+
* to derive `mintRecipient`. The all-zero key is reserved as "absent owner" and
|
|
10068
|
+
* is rejected.
|
|
10069
|
+
*
|
|
10070
|
+
* @param ataOwner - The recipient's 32-byte Solana owner public key.
|
|
10071
|
+
* @returns A 0x-prefixed hex string: the 32-byte frame followed by the 33-byte
|
|
10072
|
+
* Solana ATA payload.
|
|
10073
|
+
* @throws {KitError} If `ataOwner` is not exactly 32 bytes, or is the all-zero
|
|
10074
|
+
* key (INPUT_VALIDATION_FAILED).
|
|
10075
|
+
*
|
|
10076
|
+
* @example
|
|
10077
|
+
* ```typescript
|
|
10078
|
+
* import { PublicKey } from '@solana/web3.js'
|
|
10079
|
+
* import { buildSolanaAtaForwardingHookData } from '@core/utils'
|
|
10080
|
+
*
|
|
10081
|
+
* const owner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM')
|
|
10082
|
+
* const hookData = buildSolanaAtaForwardingHookData(owner.toBytes())
|
|
10083
|
+
*
|
|
10084
|
+
* // Use with the forwarded depositForBurnWithHook action so the relayer
|
|
10085
|
+
* // creates the recipient ATA before minting.
|
|
10086
|
+
* await adapter.prepareAction('cctp.v2.depositForBurnWithHook', {
|
|
10087
|
+
* amount: BigInt('1000000'),
|
|
10088
|
+
* mintRecipient: '0x...',
|
|
10089
|
+
* maxFee: BigInt('50000'),
|
|
10090
|
+
* minFinalityThreshold: 1000,
|
|
10091
|
+
* fromChain: ethereum,
|
|
10092
|
+
* toChain: solana,
|
|
10093
|
+
* hookData,
|
|
10094
|
+
* })
|
|
10095
|
+
* ```
|
|
10096
|
+
*/ function buildSolanaAtaForwardingHookData(ataOwner) {
|
|
10097
|
+
if (!(ataOwner instanceof Uint8Array) || ataOwner.length !== SOLANA_PUBKEY_LENGTH) {
|
|
10098
|
+
throw createValidationFailedError$1('ataOwner', ataOwner, `Expected a ${String(SOLANA_PUBKEY_LENGTH)}-byte Solana owner public key`);
|
|
10099
|
+
}
|
|
10100
|
+
if (ataOwner.every((byte)=>byte === 0)) {
|
|
10101
|
+
throw createValidationFailedError$1('ataOwner', ataOwner, 'Expected a non-zero Solana owner public key; the all-zero key is reserved as "absent owner"');
|
|
10102
|
+
}
|
|
10103
|
+
// Inner payload: createAta(1) + ataOwner(32).
|
|
10104
|
+
const payload = new Uint8Array(SOLANA_ATA_FORWARD_PAYLOAD_LENGTH);
|
|
10105
|
+
payload[0] = 1 // createAta = true
|
|
10106
|
+
;
|
|
10107
|
+
payload.set(ataOwner, 1);
|
|
10108
|
+
// 32-byte header: 24-byte magic + uint32 version(0) + uint32 dataLength(33).
|
|
10109
|
+
// The relayer reads dataLength from the v0 frame to slice the inner payload,
|
|
10110
|
+
// so it MUST reflect the appended byte count (unlike the GenericExecutor path).
|
|
10111
|
+
const frame = new Uint8Array(32);
|
|
10112
|
+
frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
|
|
10113
|
+
const view = new DataView(frame.buffer);
|
|
10114
|
+
view.setUint32(24, CCTP_FORWARD_VERSION, false) // big-endian, 0
|
|
10115
|
+
;
|
|
10116
|
+
view.setUint32(28, SOLANA_ATA_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 33
|
|
10117
|
+
;
|
|
10118
|
+
return hexlify(concat([
|
|
10119
|
+
frame,
|
|
10120
|
+
payload
|
|
10121
|
+
]));
|
|
10122
|
+
}
|
|
10123
|
+
|
|
10124
|
+
/**
|
|
10125
|
+
* Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
|
|
10126
|
+
*
|
|
10127
|
+
* Mirrors viem's `pad(address, size 32)` and CCTP's `mintRecipient`
|
|
10128
|
+
* convention. Solana addresses are already 32 bytes and need no padding.
|
|
10129
|
+
*
|
|
10130
|
+
* @param address - A 0x-prefixed 20-byte EVM address.
|
|
10131
|
+
* @returns The address left-zero-padded to a 0x-prefixed 32-byte hex string.
|
|
10132
|
+
* @throws {KitError} If `address` is not a valid EVM address (INPUT_VALIDATION_FAILED).
|
|
10133
|
+
*
|
|
10134
|
+
* @example
|
|
10135
|
+
* ```typescript
|
|
10136
|
+
* import { padAddressToBytes32 } from '@core/utils'
|
|
10137
|
+
*
|
|
10138
|
+
* padAddressToBytes32('0x75275Aff2D01699D922f045b69ed291311209738')
|
|
10139
|
+
* // '0x00000000000000000000000075275aff2d01699d922f045b69ed291311209738'
|
|
10140
|
+
* ```
|
|
10141
|
+
*/ function padAddressToBytes32(address) {
|
|
10142
|
+
if (!isAddress(address)) {
|
|
10143
|
+
throw createValidationFailedError$1('address', address, 'Expected a valid 20-byte EVM address');
|
|
10144
|
+
}
|
|
10145
|
+
// bytes32 is raw bytes, not a checksummed address — emit lowercase so it
|
|
10146
|
+
// matches ABI-decoded output.
|
|
10147
|
+
return hexZeroPad(getAddress(address), 32).toLowerCase();
|
|
10148
|
+
}
|
|
9292
10149
|
|
|
9293
10150
|
/**
|
|
9294
10151
|
* Configuration for {@link retryAsync}.
|
|
@@ -9762,7 +10619,7 @@ function resolveOptions(options) {
|
|
|
9762
10619
|
}
|
|
9763
10620
|
|
|
9764
10621
|
var name$2 = "@circle-fin/bridge-kit";
|
|
9765
|
-
var version$3 = "1.
|
|
10622
|
+
var version$3 = "1.14.0";
|
|
9766
10623
|
var pkg$3 = {
|
|
9767
10624
|
name: name$2,
|
|
9768
10625
|
version: version$3};
|
|
@@ -10143,7 +11000,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
|
|
|
10143
11000
|
* const result = evmAddressSchema.safeParse(validAddress)
|
|
10144
11001
|
* console.log(result.success) // true
|
|
10145
11002
|
* ```
|
|
10146
|
-
*/ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
|
|
11003
|
+
*/ const evmAddressSchema$1 = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
|
|
10147
11004
|
/**
|
|
10148
11005
|
* Schema for validating transaction hashes.
|
|
10149
11006
|
*
|
|
@@ -10832,6 +11689,10 @@ var TransferSpeed;
|
|
|
10832
11689
|
token: z.literal('USDC').optional(),
|
|
10833
11690
|
config: z.object({
|
|
10834
11691
|
transferSpeed: z.nativeEnum(TransferSpeed).optional(),
|
|
11692
|
+
feePayment: z.enum([
|
|
11693
|
+
'source',
|
|
11694
|
+
'destination'
|
|
11695
|
+
]).optional(),
|
|
10835
11696
|
maxFee: z.string().min(1, 'Required').pipe(createDecimalStringValidator({
|
|
10836
11697
|
allowZero: true,
|
|
10837
11698
|
regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
|
|
@@ -10839,7 +11700,8 @@ var TransferSpeed;
|
|
|
10839
11700
|
maxDecimals: 6
|
|
10840
11701
|
})(z.string())).optional(),
|
|
10841
11702
|
customFee: customFeeSchema.optional()
|
|
10842
|
-
}).optional()
|
|
11703
|
+
}).optional(),
|
|
11704
|
+
quote: z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex').optional()
|
|
10843
11705
|
});
|
|
10844
11706
|
|
|
10845
11707
|
/**
|
|
@@ -12014,16 +12876,73 @@ var TransferSpeed;
|
|
|
12014
12876
|
};
|
|
12015
12877
|
}
|
|
12016
12878
|
|
|
12879
|
+
/**
|
|
12880
|
+
* Dispatch a bridge step event through the provider's action dispatcher.
|
|
12881
|
+
*
|
|
12882
|
+
* Constructs the appropriate action payload and dispatches it to any registered
|
|
12883
|
+
* event listeners. Handles type-safe dispatching for different step types.
|
|
12884
|
+
* When provided, traceId from the invocation context is included for end-to-end correlation.
|
|
12885
|
+
*
|
|
12886
|
+
* @param name - The step name (approve, burn, fetchAttestation, or mint).
|
|
12887
|
+
* @param step - The completed bridge step containing transaction details and explorerUrl.
|
|
12888
|
+
* @param provider - The CCTP v2 provider with action dispatcher.
|
|
12889
|
+
* @param invocation - Optional invocation context containing traceId for correlation.
|
|
12890
|
+
*
|
|
12891
|
+
* @example
|
|
12892
|
+
* ```typescript
|
|
12893
|
+
* const step: BridgeStep = {
|
|
12894
|
+
* name: 'burn',
|
|
12895
|
+
* state: 'success',
|
|
12896
|
+
* txHash: '0xabc...',
|
|
12897
|
+
* explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
|
|
12898
|
+
* data: { ... }
|
|
12899
|
+
* }
|
|
12900
|
+
* dispatchStepEvent('burn', step, provider, invocationContext)
|
|
12901
|
+
* ```
|
|
12902
|
+
*/ function dispatchStepEvent(name, step, provider, invocation) {
|
|
12903
|
+
if (!provider.actionDispatcher) {
|
|
12904
|
+
return;
|
|
12905
|
+
}
|
|
12906
|
+
// Extract traceId from invocation context if provided
|
|
12907
|
+
const traceId = invocation?.traceId;
|
|
12908
|
+
const actionValues = {
|
|
12909
|
+
protocol: 'cctp',
|
|
12910
|
+
version: 'v2',
|
|
12911
|
+
...traceId !== undefined && {
|
|
12912
|
+
traceId
|
|
12913
|
+
},
|
|
12914
|
+
values: step
|
|
12915
|
+
};
|
|
12916
|
+
switch(name){
|
|
12917
|
+
case 'approve':
|
|
12918
|
+
case 'burn':
|
|
12919
|
+
case 'mint':
|
|
12920
|
+
provider.actionDispatcher.dispatch(name, {
|
|
12921
|
+
...actionValues,
|
|
12922
|
+
method: name
|
|
12923
|
+
});
|
|
12924
|
+
break;
|
|
12925
|
+
case 'fetchAttestation':
|
|
12926
|
+
case 'reAttest':
|
|
12927
|
+
provider.actionDispatcher.dispatch(name, {
|
|
12928
|
+
...actionValues,
|
|
12929
|
+
method: name,
|
|
12930
|
+
values: step
|
|
12931
|
+
});
|
|
12932
|
+
break;
|
|
12933
|
+
}
|
|
12934
|
+
}
|
|
12935
|
+
|
|
12017
12936
|
/**
|
|
12018
12937
|
* Base URL for Circle's IRIS API (mainnet/production).
|
|
12019
12938
|
*
|
|
12020
12939
|
* The IRIS API provides attestation services for CCTP cross-chain transfers.
|
|
12021
|
-
*/ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
12940
|
+
*/ const IRIS_API_BASE_URL$1 = 'https://iris-api.circle.com';
|
|
12022
12941
|
/**
|
|
12023
12942
|
* Base URL for Circle's IRIS API (testnet/sandbox).
|
|
12024
12943
|
*
|
|
12025
12944
|
* Used for development and testing on testnet chains.
|
|
12026
|
-
*/ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
12945
|
+
*/ const IRIS_API_SANDBOX_BASE_URL$1 = 'https://iris-api-sandbox.circle.com';
|
|
12027
12946
|
|
|
12028
12947
|
/**
|
|
12029
12948
|
* Type guard to validate the API response structure.
|
|
@@ -12066,7 +12985,7 @@ const isFastBurnFeeResponse = (data)=>{
|
|
|
12066
12985
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
12067
12986
|
* @returns The complete API URL
|
|
12068
12987
|
*/ function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
12069
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
12988
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
12070
12989
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
|
|
12071
12990
|
}
|
|
12072
12991
|
const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
@@ -12277,6 +13196,77 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12277
13196
|
}
|
|
12278
13197
|
};
|
|
12279
13198
|
|
|
13199
|
+
/**
|
|
13200
|
+
* Build the forwarding `hookData` for a forwarded (Orbit-relayed) CCTP v2 burn,
|
|
13201
|
+
* tailored to the destination chain.
|
|
13202
|
+
*
|
|
13203
|
+
* For EVM destinations the recipient already holds ERC-20 USDC directly, so the
|
|
13204
|
+
* empty version-0 `cctp-forward` frame is sufficient. For Solana destinations
|
|
13205
|
+
* USDC is held in an Associated Token Account (ATA) that may not exist for a
|
|
13206
|
+
* fresh wallet, so this emits a frame carrying `createAta` + `ataOwner` that
|
|
13207
|
+
* instructs the relayer to create the recipient ATA (idempotently, at the
|
|
13208
|
+
* relayer's expense) before minting.
|
|
13209
|
+
*
|
|
13210
|
+
* `@solana/web3.js` is imported lazily so EVM-only consumers never load Solana
|
|
13211
|
+
* code, mirroring {@link getMintRecipientAccount}.
|
|
13212
|
+
*
|
|
13213
|
+
* @param chainType - The destination blockchain type ('evm' or 'solana').
|
|
13214
|
+
* @param ownerAddress - The recipient's wallet address on the destination chain
|
|
13215
|
+
* (base58 for Solana). Must be the same owner used to derive `mintRecipient`.
|
|
13216
|
+
* @returns A 0x-prefixed hookData hex string for the forwarded burn.
|
|
13217
|
+
* @throws {KitError} If `chainType` is neither 'evm' nor 'solana', if
|
|
13218
|
+
* `@solana/web3.js` cannot be loaded, or if `ownerAddress` is not a valid
|
|
13219
|
+
* Solana public key (all FATAL).
|
|
13220
|
+
*
|
|
13221
|
+
* @example
|
|
13222
|
+
* ```typescript
|
|
13223
|
+
* import { getForwarderHookData } from './getForwarderHookData'
|
|
13224
|
+
*
|
|
13225
|
+
* // EVM: empty forwarding frame
|
|
13226
|
+
* const evmHook = await getForwarderHookData('evm', '0x742d35Cc...')
|
|
13227
|
+
*
|
|
13228
|
+
* // Solana: frame instructing the relayer to create the recipient ATA
|
|
13229
|
+
* const solanaHook = await getForwarderHookData(
|
|
13230
|
+
* 'solana',
|
|
13231
|
+
* '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
|
|
13232
|
+
* )
|
|
13233
|
+
* ```
|
|
13234
|
+
*/ const getForwarderHookData = async (/** The destination blockchain type - determines the hookData shape */ chainType, /** The recipient's wallet address (hex for EVM, base58 for Solana) */ ownerAddress)=>{
|
|
13235
|
+
if (chainType === 'evm') {
|
|
13236
|
+
// EVM: the recipient holds USDC directly; no ATA setup is needed.
|
|
13237
|
+
return buildForwardingHookData();
|
|
13238
|
+
}
|
|
13239
|
+
// Fail closed: only EVM and Solana forwarding destinations are supported.
|
|
13240
|
+
// Without this guard any future non-EVM chain type would silently fall
|
|
13241
|
+
// through to the Solana path and mis-encode hookData on a money-movement path.
|
|
13242
|
+
if (chainType !== 'solana') {
|
|
13243
|
+
throw new KitError({
|
|
13244
|
+
...InputError.VALIDATION_FAILED,
|
|
13245
|
+
recoverability: 'FATAL',
|
|
13246
|
+
message: `Forwarded burns are not supported for destination chain type "${chainType}"`
|
|
13247
|
+
});
|
|
13248
|
+
}
|
|
13249
|
+
// Solana: encode the owner so the relayer creates the recipient ATA.
|
|
13250
|
+
// Resolve @solana/web3.js lazily so EVM-only consumers never load Solana code.
|
|
13251
|
+
const { PublicKey } = await import('@solana/web3.js').catch(()=>{
|
|
13252
|
+
throw new KitError({
|
|
13253
|
+
...InputError.VALIDATION_FAILED,
|
|
13254
|
+
recoverability: 'FATAL',
|
|
13255
|
+
message: 'Failed to load @solana/web3.js. Please ensure it is installed: npm install @solana/web3.js'
|
|
13256
|
+
});
|
|
13257
|
+
});
|
|
13258
|
+
try {
|
|
13259
|
+
const owner = new PublicKey(ownerAddress);
|
|
13260
|
+
return buildSolanaAtaForwardingHookData(owner.toBytes());
|
|
13261
|
+
} catch (error) {
|
|
13262
|
+
throw new KitError({
|
|
13263
|
+
...InputError.INVALID_ADDRESS,
|
|
13264
|
+
recoverability: 'FATAL',
|
|
13265
|
+
message: `Failed to build Solana forwarder hookData for recipient "${ownerAddress}": ${error instanceof Error ? error.message : String(error)}`
|
|
13266
|
+
});
|
|
13267
|
+
}
|
|
13268
|
+
};
|
|
13269
|
+
|
|
12280
13270
|
/**
|
|
12281
13271
|
* Validates and converts a fee value to bigint.
|
|
12282
13272
|
*
|
|
@@ -12379,7 +13369,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12379
13369
|
|
|
12380
13370
|
/**
|
|
12381
13371
|
* The zero address, denoting a native-currency fee in a signed quote.
|
|
12382
|
-
*/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
13372
|
+
*/ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
|
|
12383
13373
|
/**
|
|
12384
13374
|
* Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
|
|
12385
13375
|
*
|
|
@@ -12420,7 +13410,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12420
13410
|
if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
|
|
12421
13411
|
throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
|
|
12422
13412
|
}
|
|
12423
|
-
const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS;
|
|
13413
|
+
const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
|
|
12424
13414
|
const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
|
|
12425
13415
|
if (isNativeFee) {
|
|
12426
13416
|
return {
|
|
@@ -12558,7 +13548,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12558
13548
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
12559
13549
|
* @returns The complete API URL with forward=true query parameter
|
|
12560
13550
|
*/ function buildForwardingFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
12561
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
13551
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
12562
13552
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}?forward=true`;
|
|
12563
13553
|
}
|
|
12564
13554
|
/**
|
|
@@ -13619,7 +14609,7 @@ function hasPendingState(analysis, result) {
|
|
|
13619
14609
|
* // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
|
|
13620
14610
|
* ```
|
|
13621
14611
|
*/ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
|
|
13622
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
14612
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
13623
14613
|
const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
|
|
13624
14614
|
url.searchParams.set('transactionHash', transactionHash);
|
|
13625
14615
|
return url.toString();
|
|
@@ -13787,7 +14777,7 @@ function hasPendingState(analysis, result) {
|
|
|
13787
14777
|
* // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
|
|
13788
14778
|
* ```
|
|
13789
14779
|
*/ const buildReAttestUrl = (nonce, isTestnet)=>{
|
|
13790
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
14780
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
13791
14781
|
const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
|
|
13792
14782
|
return url.toString();
|
|
13793
14783
|
};
|
|
@@ -14242,7 +15232,8 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14242
15232
|
* - `destinationChain` — present and supports CCTP v2
|
|
14243
15233
|
* - source and destination chains must both be testnet or both mainnet
|
|
14244
15234
|
* - source and destination chains must differ
|
|
14245
|
-
* - `executor
|
|
15235
|
+
* - destination — either `executor`, or both `mintRecipient` and
|
|
15236
|
+
* `destinationCaller`; not both
|
|
14246
15237
|
* - `amount` — bigint or non-empty string coercible to bigint
|
|
14247
15238
|
* - `feeTotalAmount` — bigint or non-empty string coercible to bigint
|
|
14248
15239
|
* - `feeToken` — valid EVM address (`0x` + 40 hex chars)
|
|
@@ -14284,10 +15275,17 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14284
15275
|
if (source.chain.name === dest.name) {
|
|
14285
15276
|
throw createUnsupportedRouteError(source.chain.name, dest.name);
|
|
14286
15277
|
}
|
|
14287
|
-
//
|
|
15278
|
+
// Destination: GenericExecutor shorthand or explicit recipient + caller.
|
|
14288
15279
|
const executor = p['executor'];
|
|
14289
|
-
|
|
14290
|
-
|
|
15280
|
+
const mintRecipient = p['mintRecipient'];
|
|
15281
|
+
const destinationCaller = p['destinationCaller'];
|
|
15282
|
+
const hasExecutor = typeof executor === 'string' && executor !== '';
|
|
15283
|
+
const hasDirectDestination = typeof mintRecipient === 'string' && mintRecipient !== '' && typeof destinationCaller === 'string' && destinationCaller !== '';
|
|
15284
|
+
if (!hasExecutor && !hasDirectDestination) {
|
|
15285
|
+
throw createValidationFailedError$1('destination', undefined, 'Provide executor, or both mintRecipient and destinationCaller');
|
|
15286
|
+
}
|
|
15287
|
+
if (hasExecutor && hasDirectDestination) {
|
|
15288
|
+
throw createValidationFailedError$1('destination', undefined, 'Provide executor or direct destination fields, not both');
|
|
14291
15289
|
}
|
|
14292
15290
|
// amount
|
|
14293
15291
|
const rawAmount = p['amount'];
|
|
@@ -14310,7 +15308,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14310
15308
|
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
|
|
14311
15309
|
}
|
|
14312
15310
|
// feeToken
|
|
14313
|
-
if (!evmAddressSchema.safeParse(p['feeToken']).success) {
|
|
15311
|
+
if (!evmAddressSchema$1.safeParse(p['feeToken']).success) {
|
|
14314
15312
|
throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
|
|
14315
15313
|
}
|
|
14316
15314
|
// claim
|
|
@@ -14322,7 +15320,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14322
15320
|
if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
|
|
14323
15321
|
throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
|
|
14324
15322
|
}
|
|
14325
|
-
if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
|
|
15323
|
+
if (!evmAddressSchema$1.safeParse(claim['refundAddress']).success) {
|
|
14326
15324
|
throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
|
|
14327
15325
|
}
|
|
14328
15326
|
// hookData (optional)
|
|
@@ -14705,79 +15703,22 @@ const mockAttestationMessage = {
|
|
|
14705
15703
|
}
|
|
14706
15704
|
|
|
14707
15705
|
/**
|
|
14708
|
-
*
|
|
14709
|
-
*
|
|
14710
|
-
* Constructs the appropriate action payload and dispatches it to any registered
|
|
14711
|
-
* event listeners. Handles type-safe dispatching for different step types.
|
|
14712
|
-
* When provided, traceId from the invocation context is included for end-to-end correlation.
|
|
15706
|
+
* Check whether the source adapter supports EIP-5792 atomic batching and
|
|
15707
|
+
* the consumer has not explicitly opted out via `config.batchTransactions`.
|
|
14713
15708
|
*
|
|
14714
|
-
* @param
|
|
14715
|
-
* @
|
|
14716
|
-
* @param provider - The CCTP v2 provider with action dispatcher.
|
|
14717
|
-
* @param invocation - Optional invocation context containing traceId for correlation.
|
|
15709
|
+
* @param params - Bridge parameters (used for adapter and config access).
|
|
15710
|
+
* @returns `true` when batched execution should be attempted.
|
|
14718
15711
|
*
|
|
14719
15712
|
* @example
|
|
14720
15713
|
* ```typescript
|
|
14721
|
-
* const
|
|
14722
|
-
*
|
|
14723
|
-
*
|
|
14724
|
-
* txHash: '0xabc...',
|
|
14725
|
-
* explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
|
|
14726
|
-
* data: { ... }
|
|
15714
|
+
* const useBatched = await shouldUseBatchedExecution(params)
|
|
15715
|
+
* if (useBatched) {
|
|
15716
|
+
* // take the batched approve + burn path
|
|
14727
15717
|
* }
|
|
14728
|
-
* dispatchStepEvent('burn', step, provider, invocationContext)
|
|
14729
15718
|
* ```
|
|
14730
|
-
*/ function
|
|
14731
|
-
if (
|
|
14732
|
-
return;
|
|
14733
|
-
}
|
|
14734
|
-
// Extract traceId from invocation context if provided
|
|
14735
|
-
const traceId = invocation?.traceId;
|
|
14736
|
-
const actionValues = {
|
|
14737
|
-
protocol: 'cctp',
|
|
14738
|
-
version: 'v2',
|
|
14739
|
-
...traceId !== undefined && {
|
|
14740
|
-
traceId
|
|
14741
|
-
},
|
|
14742
|
-
values: step
|
|
14743
|
-
};
|
|
14744
|
-
switch(name){
|
|
14745
|
-
case 'approve':
|
|
14746
|
-
case 'burn':
|
|
14747
|
-
case 'mint':
|
|
14748
|
-
provider.actionDispatcher.dispatch(name, {
|
|
14749
|
-
...actionValues,
|
|
14750
|
-
method: name
|
|
14751
|
-
});
|
|
14752
|
-
break;
|
|
14753
|
-
case 'fetchAttestation':
|
|
14754
|
-
case 'reAttest':
|
|
14755
|
-
provider.actionDispatcher.dispatch(name, {
|
|
14756
|
-
...actionValues,
|
|
14757
|
-
method: name,
|
|
14758
|
-
values: step
|
|
14759
|
-
});
|
|
14760
|
-
break;
|
|
14761
|
-
}
|
|
14762
|
-
}
|
|
14763
|
-
|
|
14764
|
-
/**
|
|
14765
|
-
* Check whether the source adapter supports EIP-5792 atomic batching and
|
|
14766
|
-
* the consumer has not explicitly opted out via `config.batchTransactions`.
|
|
14767
|
-
*
|
|
14768
|
-
* @param params - Bridge parameters (used for adapter and config access).
|
|
14769
|
-
* @returns `true` when batched execution should be attempted.
|
|
14770
|
-
*
|
|
14771
|
-
* @example
|
|
14772
|
-
* ```typescript
|
|
14773
|
-
* const useBatched = await shouldUseBatchedExecution(params)
|
|
14774
|
-
* if (useBatched) {
|
|
14775
|
-
* // take the batched approve + burn path
|
|
14776
|
-
* }
|
|
14777
|
-
* ```
|
|
14778
|
-
*/ async function shouldUseBatchedExecution(params) {
|
|
14779
|
-
if (params.config?.batchTransactions === false) {
|
|
14780
|
-
return false;
|
|
15719
|
+
*/ async function shouldUseBatchedExecution(params) {
|
|
15720
|
+
if (params.config?.batchTransactions === false) {
|
|
15721
|
+
return false;
|
|
14781
15722
|
}
|
|
14782
15723
|
const { chain } = params.source;
|
|
14783
15724
|
if (chain.type !== 'evm') {
|
|
@@ -15007,7 +15948,7 @@ const mockAttestationMessage = {
|
|
|
15007
15948
|
return step;
|
|
15008
15949
|
}
|
|
15009
15950
|
|
|
15010
|
-
var version$2 = "1.
|
|
15951
|
+
var version$2 = "1.12.0";
|
|
15011
15952
|
var pkg$2 = {
|
|
15012
15953
|
version: version$2};
|
|
15013
15954
|
|
|
@@ -15818,6 +16759,34 @@ function assertCCTPV2Config(config) {
|
|
|
15818
16759
|
this.config = config;
|
|
15819
16760
|
}
|
|
15820
16761
|
/**
|
|
16762
|
+
* Emit a bridge step event through the provider's registered action
|
|
16763
|
+
* dispatcher.
|
|
16764
|
+
*
|
|
16765
|
+
* Kit-level orchestration that drives the burn primitives directly instead
|
|
16766
|
+
* of {@link CCTPV2BridgingProvider.bridge} (for example the receive-exact
|
|
16767
|
+
* source-fee flow) uses this to surface the same `approve`/`burn`/`mint`
|
|
16768
|
+
* events as the standard bridge path. It is a no-op when no dispatcher is
|
|
16769
|
+
* registered.
|
|
16770
|
+
*
|
|
16771
|
+
* @param name - The step name (`approve`, `burn`, `mint`, ...).
|
|
16772
|
+
* @param step - The completed bridge step to broadcast.
|
|
16773
|
+
* @param invocation - Optional invocation context carrying a `traceId` for
|
|
16774
|
+
* end-to-end correlation.
|
|
16775
|
+
* @returns Nothing.
|
|
16776
|
+
*
|
|
16777
|
+
* @example
|
|
16778
|
+
* ```typescript
|
|
16779
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
16780
|
+
* provider.emitBridgeStep('burn', {
|
|
16781
|
+
* name: 'burn',
|
|
16782
|
+
* state: 'success',
|
|
16783
|
+
* txHash: '0xabc...',
|
|
16784
|
+
* })
|
|
16785
|
+
* ```
|
|
16786
|
+
*/ emitBridgeStep(name, step, invocation) {
|
|
16787
|
+
dispatchStepEvent(name, step, this, invocation);
|
|
16788
|
+
}
|
|
16789
|
+
/**
|
|
15821
16790
|
* Resolves the effective polling configuration for an attestation request.
|
|
15822
16791
|
*
|
|
15823
16792
|
* Precedence (lowest to highest): provider `config.attestation`, then the
|
|
@@ -16643,8 +17612,11 @@ function assertCCTPV2Config(config) {
|
|
|
16643
17612
|
// 2. Forwarder: Does the user want Circle's relayer to handle attestation/mint?
|
|
16644
17613
|
const useCustomBurn = hasCustomContractSupport(source.chain, 'bridge');
|
|
16645
17614
|
const useForwarder = destination.useForwarder === true;
|
|
16646
|
-
// Build hookData once if forwarder is enabled
|
|
16647
|
-
|
|
17615
|
+
// Build hookData once if forwarder is enabled. EVM destinations get the
|
|
17616
|
+
// empty forwarding frame; Solana destinations get a frame instructing the
|
|
17617
|
+
// relayer to create the recipient's ATA (using the same owner that derived
|
|
17618
|
+
// `mintRecipient`) so the mint succeeds even for a fresh wallet.
|
|
17619
|
+
const hookData = useForwarder ? await getForwarderHookData(destination.chain.type, destinationAddressForMint) : undefined;
|
|
16648
17620
|
if (useCustomBurn) {
|
|
16649
17621
|
// Custom burn path: use bridge contract (with or without hook)
|
|
16650
17622
|
const customBurnParams = {
|
|
@@ -16672,13 +17644,14 @@ function assertCCTPV2Config(config) {
|
|
|
16672
17644
|
/**
|
|
16673
17645
|
* Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
|
|
16674
17646
|
*
|
|
16675
|
-
*
|
|
16676
|
-
*
|
|
16677
|
-
*
|
|
16678
|
-
*
|
|
17647
|
+
* Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
|
|
17648
|
+
* collected up front on the source chain against a signed quote. The
|
|
17649
|
+
* destination may use the GenericExecutor shorthand, or an explicit mint
|
|
17650
|
+
* recipient and destination caller for direct forwarding.
|
|
16679
17651
|
*
|
|
16680
|
-
* This is the low-level on-chain primitive behind the
|
|
16681
|
-
*
|
|
17652
|
+
* This is the low-level on-chain primitive behind the Unified Balance Kit
|
|
17653
|
+
* `fastCrossChainDeposit` and the Bridge Kit source-fee
|
|
17654
|
+
* (`feePayment: 'source'`) flow. The `hookData` and signed-quote
|
|
16682
17655
|
* `claim` are produced elsewhere and passed in here:
|
|
16683
17656
|
* - `hookData`: `buildForwardingHookDataWithPayload(version,
|
|
16684
17657
|
* buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
|
|
@@ -16695,32 +17668,48 @@ function assertCCTPV2Config(config) {
|
|
|
16695
17668
|
* approval covers both; the redundant second approval is skipped.
|
|
16696
17669
|
*
|
|
16697
17670
|
* @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
|
|
16698
|
-
* @param params - The burn amount,
|
|
17671
|
+
* @param params - The burn amount, destination, hook data, signed quote, and fee.
|
|
16699
17672
|
* @returns The prepared approvals, the prepared burn, and the resolved fee plan.
|
|
16700
17673
|
* @throws {KitError} If the wallet context is invalid, `destinationChain` does not
|
|
16701
|
-
* support CCTP v2, the
|
|
16702
|
-
* a bigint or a numeric string coercible to bigint,
|
|
16703
|
-
* `cctp-forward` frame (guaranteed
|
|
16704
|
-
* context cannot be resolved.
|
|
17674
|
+
* support CCTP v2, the destination fields are missing, `amount` or
|
|
17675
|
+
* `feeTotalAmount` is not a bigint or a numeric string coercible to bigint,
|
|
17676
|
+
* the hook data lacks a `cctp-forward` frame (guaranteed
|
|
17677
|
+
* `ForwardFeeWithoutHook`), or the operation context cannot be resolved.
|
|
16705
17678
|
*
|
|
16706
17679
|
* @example
|
|
16707
17680
|
* ```typescript
|
|
17681
|
+
* import {
|
|
17682
|
+
* CCTPV2BridgingProvider,
|
|
17683
|
+
* type BurnWithFeesParams,
|
|
17684
|
+
* } from '@circle-fin/provider-cctp-v2'
|
|
17685
|
+
*
|
|
17686
|
+
* declare const source: BurnWithFeesParams['source']
|
|
17687
|
+
* declare const destinationChain: BurnWithFeesParams['destinationChain']
|
|
17688
|
+
* declare const recipient: string
|
|
17689
|
+
* declare const hookData: string
|
|
17690
|
+
* declare const claim: BurnWithFeesParams['claim']
|
|
17691
|
+
*
|
|
17692
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
16708
17693
|
* const { approvals, burn } = await provider.burnWithFees({
|
|
16709
17694
|
* source,
|
|
16710
|
-
* destinationChain
|
|
17695
|
+
* destinationChain,
|
|
16711
17696
|
* amount: 1_000_000n,
|
|
16712
|
-
*
|
|
16713
|
-
*
|
|
16714
|
-
*
|
|
16715
|
-
*
|
|
16716
|
-
*
|
|
17697
|
+
* mintRecipient: recipient,
|
|
17698
|
+
* destinationCaller: '0x0000000000000000000000000000000000000000',
|
|
17699
|
+
* hookData,
|
|
17700
|
+
* claim,
|
|
17701
|
+
* feeToken: source.chain.usdcAddress,
|
|
17702
|
+
* feeTotalAmount: 10_000n,
|
|
16717
17703
|
* })
|
|
16718
17704
|
* for (const approval of approvals) await approval.execute()
|
|
16719
17705
|
* const txHash = await burn.execute()
|
|
16720
17706
|
* ```
|
|
16721
17707
|
*/ async burnWithFees(params) {
|
|
16722
17708
|
assertBurnWithFeesParams(params);
|
|
16723
|
-
const { source, destinationChain,
|
|
17709
|
+
const { source, destinationChain, hookData, claim, feeToken } = params;
|
|
17710
|
+
const hasExecutor = 'executor' in params && params.executor !== undefined;
|
|
17711
|
+
const mintRecipient = hasExecutor ? params.executor : params.mintRecipient;
|
|
17712
|
+
const destinationCaller = hasExecutor ? params.executor : params.destinationCaller;
|
|
16724
17713
|
const amount = BigInt(params.amount);
|
|
16725
17714
|
const feeTotalAmount = BigInt(params.feeTotalAmount);
|
|
16726
17715
|
// Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
|
|
@@ -16751,13 +17740,14 @@ function assertCCTPV2Config(config) {
|
|
|
16751
17740
|
delegate: wrapperAddress,
|
|
16752
17741
|
amount: approval.amount
|
|
16753
17742
|
}, context)));
|
|
16754
|
-
// Build the burn
|
|
17743
|
+
// Build the burn with either the GenericExecutor shorthand or the explicit
|
|
17744
|
+
// direct-forwarding recipient and caller.
|
|
16755
17745
|
const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
|
|
16756
17746
|
fromChain: source.chain,
|
|
16757
17747
|
toChain: destinationChain,
|
|
16758
17748
|
amount,
|
|
16759
|
-
mintRecipient
|
|
16760
|
-
destinationCaller
|
|
17749
|
+
mintRecipient,
|
|
17750
|
+
destinationCaller,
|
|
16761
17751
|
hookData,
|
|
16762
17752
|
claim,
|
|
16763
17753
|
feeToken,
|
|
@@ -16914,6 +17904,948 @@ function assertCCTPV2Config(config) {
|
|
|
16914
17904
|
]
|
|
16915
17905
|
];
|
|
16916
17906
|
|
|
17907
|
+
/**
|
|
17908
|
+
* Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
|
|
17909
|
+
*/ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
17910
|
+
/**
|
|
17911
|
+
* Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
|
|
17912
|
+
*/ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
17913
|
+
/**
|
|
17914
|
+
* Native fee-token sentinel (the zero address).
|
|
17915
|
+
*
|
|
17916
|
+
* When `feeToken` is the zero address the quote prices fees in the source
|
|
17917
|
+
* chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
|
|
17918
|
+
* address instead to denominate fees in USDC.
|
|
17919
|
+
*/ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
|
|
17920
|
+
/**
|
|
17921
|
+
* API path prefix for the CCTP v2 USDC burn quote endpoint.
|
|
17922
|
+
*
|
|
17923
|
+
* The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
|
|
17924
|
+
* `usdc` is a fixed literal, not a token parameter.
|
|
17925
|
+
*/ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
|
|
17926
|
+
/**
|
|
17927
|
+
* API path prefix for the CCTP v2 USDC quote validate endpoint.
|
|
17928
|
+
*
|
|
17929
|
+
* The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
|
|
17930
|
+
* `POST { abiSignature, args }` body and returns whether the signed quote is
|
|
17931
|
+
* currently claimable together with its authoritative expiry status.
|
|
17932
|
+
*/ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
|
|
17933
|
+
/**
|
|
17934
|
+
* Default polling configuration for Quote API calls.
|
|
17935
|
+
*
|
|
17936
|
+
* A signed quote is short-lived (typically ~2 minutes, varying per chain) and
|
|
17937
|
+
* a feature-flag-disabled source chain returns a
|
|
17938
|
+
* permanent `503 SERVICE_NOT_ENABLED`, so retrying buys little and risks
|
|
17939
|
+
* outliving the quote. The client therefore makes a single attempt
|
|
17940
|
+
* (`maxRetries: 1`) with a 15s timeout, mirroring the reference
|
|
17941
|
+
* implementation; callers refresh by requesting a new quote rather than
|
|
17942
|
+
* relying on transport retries.
|
|
17943
|
+
*
|
|
17944
|
+
* No `headers` are set here: `pollApiWithValidation` always injects
|
|
17945
|
+
* `Content-Type: application/json` and adds `User-Agent` in Node. Browser
|
|
17946
|
+
* requests omit a user-agent header to avoid a CORS preflight, so duplicating
|
|
17947
|
+
* either header here would be dead configuration.
|
|
17948
|
+
*/ const FEE_QUOTE_DEFAULT_CONFIG = {
|
|
17949
|
+
timeout: 15_000,
|
|
17950
|
+
maxRetries: 1,
|
|
17951
|
+
retryDelay: 200
|
|
17952
|
+
};
|
|
17953
|
+
|
|
17954
|
+
/** Decimal string in token minor units, constrained to be strictly positive. */ const positiveAmountSchema = z.string().regex(/^\d+$/, 'must be a non-negative integer string')// Re-check the digit shape here: zod still runs this refinement when the
|
|
17955
|
+
// regex check above fails ("dirty"), so guard BigInt() against throwing on a
|
|
17956
|
+
// non-numeric value before comparing.
|
|
17957
|
+
.refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
|
|
17958
|
+
/**
|
|
17959
|
+
* A 20-byte EVM address in `0x` hex.
|
|
17960
|
+
*
|
|
17961
|
+
* The MVP prepaid-`FORWARD` `burn/usdc` path targets EVM contracts
|
|
17962
|
+
* (`TokenMessengerWithFees` / `GenericExecutor`), so `feeToken` and
|
|
17963
|
+
* `destinationCaller` are constrained to EVM addresses by design. This is an
|
|
17964
|
+
* intentional scope limit, not a permanent one: it can be widened to other
|
|
17965
|
+
* address formats as the fee service expands to more chains.
|
|
17966
|
+
*/ const evmAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'must be a 20-byte 0x address');
|
|
17967
|
+
/** Even-length `0x` hex (the empty `0x` is allowed). */ const hexSchema = z.string().regex(/^0x([a-fA-F0-9]{2})*$/, 'must be even-length 0x hex');
|
|
17968
|
+
/** Non-empty, even-length `0x` hex. */ const nonEmptyHexSchema = z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex');
|
|
17969
|
+
/** A 32-byte `0x` hex hash. */ const bytes32Schema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'must be a 32-byte 0x hash');
|
|
17970
|
+
/** Decimal string in minor units, allowing zero. */ const numericStringSchema = z.string().regex(/^\d+$/, 'must be a non-negative integer string');
|
|
17971
|
+
/** An `https:` URL, used for the optional base-URL override. */ const httpsUrlSchema = z.string().refine((value)=>{
|
|
17972
|
+
try {
|
|
17973
|
+
return new URL(value).protocol === 'https:';
|
|
17974
|
+
} catch {
|
|
17975
|
+
return false;
|
|
17976
|
+
}
|
|
17977
|
+
}, 'must be an https URL');
|
|
17978
|
+
const forwardParamsSchema = z.object({
|
|
17979
|
+
hookData: hexSchema.optional(),
|
|
17980
|
+
destinationCaller: evmAddressSchema.optional()
|
|
17981
|
+
}).strict();
|
|
17982
|
+
const forwardRequestSchema = z.object({
|
|
17983
|
+
type: z.literal('FORWARD'),
|
|
17984
|
+
params: forwardParamsSchema.optional()
|
|
17985
|
+
}).strict();
|
|
17986
|
+
const preFinalityRequestSchema = z.object({
|
|
17987
|
+
type: z.literal('PRE_FINALITY')
|
|
17988
|
+
}).strict();
|
|
17989
|
+
/** A single quote request item (`FORWARD` or `PRE_FINALITY`). */ const feeQuoteRequestSchema = z.discriminatedUnion('type', [
|
|
17990
|
+
forwardRequestSchema,
|
|
17991
|
+
preFinalityRequestSchema
|
|
17992
|
+
]);
|
|
17993
|
+
/** A non-empty list of quote request items with unique types. */ const feeQuoteRequestsSchema = z.array(feeQuoteRequestSchema).min(1, 'at least one request item is required').refine((items)=>new Set(items.map((item)=>item.type)).size === items.length, 'request item types must be unique');
|
|
17994
|
+
/**
|
|
17995
|
+
* A structured `Partial<ApiPollingConfig>` polling override.
|
|
17996
|
+
*
|
|
17997
|
+
* Validates the field types callers actually set, so a plain-JS caller passing
|
|
17998
|
+
* `{ timeout: 'soon' }` is rejected at the boundary rather than failing opaquely
|
|
17999
|
+
* inside the transport. Unknown keys pass through so a future `ApiPollingConfig`
|
|
18000
|
+
* field is forwarded rather than silently dropped.
|
|
18001
|
+
*/ const apiPollingConfigSchema = z.object({
|
|
18002
|
+
timeout: z.number().int().positive().optional(),
|
|
18003
|
+
maxRetries: z.number().int().nonnegative().optional(),
|
|
18004
|
+
retryDelay: z.number().int().nonnegative().optional(),
|
|
18005
|
+
backoff: z.enum([
|
|
18006
|
+
'fixed',
|
|
18007
|
+
'exponential'
|
|
18008
|
+
]).optional(),
|
|
18009
|
+
maxRetryDelayMs: z.number().int().positive().optional(),
|
|
18010
|
+
headers: z.record(z.string()).optional()
|
|
18011
|
+
}).passthrough();
|
|
18012
|
+
/**
|
|
18013
|
+
* The validatable input for {@link fetchFeeQuote}.
|
|
18014
|
+
*
|
|
18015
|
+
* This is the single source of truth for input validation, including the CCTP
|
|
18016
|
+
* domains and the `isTestnet` environment flag. Validating `isTestnet` at
|
|
18017
|
+
* runtime matters because a plain-JS caller who omits it would otherwise leave
|
|
18018
|
+
* it `undefined`, which is falsy and silently selects the production base URL.
|
|
18019
|
+
* (`buildFeeQuoteUrl` independently re-validates the domains for standalone
|
|
18020
|
+
* callers.)
|
|
18021
|
+
*/ const fetchFeeQuoteInputSchema = z.object({
|
|
18022
|
+
sourceDomain: z.number().int().nonnegative(),
|
|
18023
|
+
destinationDomain: z.number().int().nonnegative(),
|
|
18024
|
+
amount: positiveAmountSchema,
|
|
18025
|
+
feeToken: evmAddressSchema.optional(),
|
|
18026
|
+
requests: feeQuoteRequestsSchema,
|
|
18027
|
+
isTestnet: z.boolean(),
|
|
18028
|
+
baseUrl: httpsUrlSchema.optional(),
|
|
18029
|
+
config: apiPollingConfigSchema.optional()
|
|
18030
|
+
}).strict();
|
|
18031
|
+
const feeQuoteItemSchema = z.object({
|
|
18032
|
+
type: z.string().min(1),
|
|
18033
|
+
amount: numericStringSchema,
|
|
18034
|
+
args: z.array(z.string()),
|
|
18035
|
+
argsHash: bytes32Schema
|
|
18036
|
+
}).passthrough();
|
|
18037
|
+
const exchangeRatesSchema = z.object({
|
|
18038
|
+
feeTokenUsd: z.string(),
|
|
18039
|
+
destinationTokenUsd: z.string()
|
|
18040
|
+
}).passthrough();
|
|
18041
|
+
const metadataSchema = z.object({
|
|
18042
|
+
destinationGasPrice: z.string().optional(),
|
|
18043
|
+
exchangeRates: exchangeRatesSchema.optional()
|
|
18044
|
+
}).passthrough();
|
|
18045
|
+
/** The `expiry` object the Quote API nests the quote TTL under. */ const expirySchema = z.discriminatedUnion('mode', [
|
|
18046
|
+
z.object({
|
|
18047
|
+
mode: z.literal('TIMESTAMP'),
|
|
18048
|
+
expiresAt: z.number().int().nonnegative()
|
|
18049
|
+
}).passthrough(),
|
|
18050
|
+
z.object({
|
|
18051
|
+
mode: z.literal('BLOCK_NUMBER'),
|
|
18052
|
+
expiresAtBlock: z.number().int().nonnegative(),
|
|
18053
|
+
blockEstimatedAt: z.number().int().nonnegative().optional()
|
|
18054
|
+
}).passthrough()
|
|
18055
|
+
]);
|
|
18056
|
+
/** Schema for a signed fee quote returned by the Quote API. */ const signedFeeQuoteSchema = z.object({
|
|
18057
|
+
// The runtime YAML spec maps signedQuote to a looser `hex` (which allows
|
|
18058
|
+
// an empty `0x`); we keep the stricter non-empty form. Do not relax
|
|
18059
|
+
// without a reason.
|
|
18060
|
+
signedQuote: nonEmptyHexSchema,
|
|
18061
|
+
issuedAt: z.number().int().nonnegative(),
|
|
18062
|
+
// The API returns a mode-specific timestamp or source-block deadline.
|
|
18063
|
+
expiry: expirySchema,
|
|
18064
|
+
feeTotalAmount: numericStringSchema,
|
|
18065
|
+
feeToken: evmAddressSchema,
|
|
18066
|
+
nonce: numericStringSchema,
|
|
18067
|
+
items: z.array(feeQuoteItemSchema),
|
|
18068
|
+
metadata: metadataSchema.optional()
|
|
18069
|
+
}).passthrough();
|
|
18070
|
+
/**
|
|
18071
|
+
* Validate that an unknown value is a signed fee quote.
|
|
18072
|
+
*
|
|
18073
|
+
* @param value - The unknown value to validate.
|
|
18074
|
+
* @returns `true` when the value matches the signed-quote response shape.
|
|
18075
|
+
*
|
|
18076
|
+
* @example
|
|
18077
|
+
* ```typescript
|
|
18078
|
+
* import { isSignedFeeQuote } from '@circle-fin/provider-fee-v1'
|
|
18079
|
+
*
|
|
18080
|
+
* declare const payload: unknown
|
|
18081
|
+
* if (isSignedFeeQuote(payload)) {
|
|
18082
|
+
* console.log(payload.feeTotalAmount)
|
|
18083
|
+
* }
|
|
18084
|
+
* ```
|
|
18085
|
+
*/ function isSignedFeeQuote(value) {
|
|
18086
|
+
return signedFeeQuoteSchema.safeParse(value).success;
|
|
18087
|
+
}
|
|
18088
|
+
/** Validate input to {@link validateQuote}. */ const validateQuoteInputSchema = z.object({
|
|
18089
|
+
sourceDomain: z.number().int().nonnegative(),
|
|
18090
|
+
abiSignature: z.string().min(1),
|
|
18091
|
+
args: z.array(z.union([
|
|
18092
|
+
z.string(),
|
|
18093
|
+
z.array(z.string())
|
|
18094
|
+
])),
|
|
18095
|
+
isTestnet: z.boolean(),
|
|
18096
|
+
baseUrl: httpsUrlSchema.optional(),
|
|
18097
|
+
config: apiPollingConfigSchema.optional()
|
|
18098
|
+
}).strict();
|
|
18099
|
+
const quoteExpiryStatusSchema = z.discriminatedUnion('mode', [
|
|
18100
|
+
z.object({
|
|
18101
|
+
mode: z.literal('TIMESTAMP'),
|
|
18102
|
+
expired: z.boolean(),
|
|
18103
|
+
secondsRemaining: z.number().int().nonnegative(),
|
|
18104
|
+
expiresAt: z.number().int().nonnegative()
|
|
18105
|
+
}).passthrough(),
|
|
18106
|
+
z.object({
|
|
18107
|
+
mode: z.literal('BLOCK_NUMBER'),
|
|
18108
|
+
expired: z.boolean(),
|
|
18109
|
+
secondsRemaining: z.number().int().nonnegative(),
|
|
18110
|
+
expiresAtBlock: z.number().int().nonnegative(),
|
|
18111
|
+
blockEstimatedAt: z.number().int().nonnegative().optional()
|
|
18112
|
+
}).passthrough()
|
|
18113
|
+
]);
|
|
18114
|
+
const validateQuoteItemSchema = z.object({
|
|
18115
|
+
type: z.string().min(1),
|
|
18116
|
+
argsMatch: z.boolean(),
|
|
18117
|
+
amount: numericStringSchema.optional(),
|
|
18118
|
+
args: z.array(z.string()).optional(),
|
|
18119
|
+
argsHash: bytes32Schema.optional(),
|
|
18120
|
+
computedArgsHash: bytes32Schema.optional()
|
|
18121
|
+
}).passthrough();
|
|
18122
|
+
/**
|
|
18123
|
+
* Schema for a response from the Quote API validation endpoint.
|
|
18124
|
+
*
|
|
18125
|
+
* The endpoint takes the source domain as a URL path parameter and does not
|
|
18126
|
+
* return it in the response body, so `sourceDomain` is intentionally not part
|
|
18127
|
+
* of this schema.
|
|
18128
|
+
*/ const validateQuoteResultSchema = z.object({
|
|
18129
|
+
signedQuote: nonEmptyHexSchema,
|
|
18130
|
+
expiry: quoteExpiryStatusSchema,
|
|
18131
|
+
feeTotalAmount: numericStringSchema,
|
|
18132
|
+
feeToken: evmAddressSchema,
|
|
18133
|
+
nonce: numericStringSchema,
|
|
18134
|
+
claimable: z.boolean(),
|
|
18135
|
+
// Preserve newly introduced server-side reasons as opaque strings rather
|
|
18136
|
+
// than rejecting the entire safety response before the SDK is updated.
|
|
18137
|
+
failedChecks: z.array(z.string().min(1)),
|
|
18138
|
+
items: z.array(validateQuoteItemSchema)
|
|
18139
|
+
}).passthrough();
|
|
18140
|
+
/**
|
|
18141
|
+
* Validate a Quote API validation response.
|
|
18142
|
+
*
|
|
18143
|
+
* @param value - The unknown response value.
|
|
18144
|
+
* @returns `true` when the value has the expected validation response shape.
|
|
18145
|
+
*
|
|
18146
|
+
* @example
|
|
18147
|
+
* ```typescript
|
|
18148
|
+
* import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
|
|
18149
|
+
*
|
|
18150
|
+
* declare const response: unknown
|
|
18151
|
+
* if (isValidateQuoteResult(response)) {
|
|
18152
|
+
* console.log(response.claimable)
|
|
18153
|
+
* }
|
|
18154
|
+
* ```
|
|
18155
|
+
*/ function isValidateQuoteResult(value) {
|
|
18156
|
+
return validateQuoteResultSchema.safeParse(value).success;
|
|
18157
|
+
}
|
|
18158
|
+
|
|
18159
|
+
/**
|
|
18160
|
+
* Validate that a CCTP domain id is a non-negative integer.
|
|
18161
|
+
*
|
|
18162
|
+
* @param value - The domain id to validate.
|
|
18163
|
+
* @param label - The parameter name, used in the error message.
|
|
18164
|
+
* @returns Nothing.
|
|
18165
|
+
* @throws {@link KitError} When the value is not a non-negative integer.
|
|
18166
|
+
* @internal
|
|
18167
|
+
*/ function assertDomain(value, label) {
|
|
18168
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
18169
|
+
throw new KitError({
|
|
18170
|
+
...InputError.VALIDATION_FAILED,
|
|
18171
|
+
recoverability: 'FATAL',
|
|
18172
|
+
message: `Quote API getFeeQuote failed: ${label} must be a ` + `non-negative integer, received ${String(value)}`,
|
|
18173
|
+
cause: {
|
|
18174
|
+
trace: {
|
|
18175
|
+
[label]: value
|
|
18176
|
+
}
|
|
18177
|
+
}
|
|
18178
|
+
});
|
|
18179
|
+
}
|
|
18180
|
+
}
|
|
18181
|
+
/**
|
|
18182
|
+
* Build the Quote API URL for a CCTP v2 USDC burn quote.
|
|
18183
|
+
*
|
|
18184
|
+
* Resolves the environment base URL (or an explicit `baseUrl` override) and
|
|
18185
|
+
* appends the burn/usdc path with the source and destination CCTP domains.
|
|
18186
|
+
* `usdc` is a fixed path literal, not a token parameter.
|
|
18187
|
+
*
|
|
18188
|
+
* @param params - The domains and environment selector.
|
|
18189
|
+
* @returns The fully-qualified Quote API URL.
|
|
18190
|
+
* @throws {@link KitError} When either domain is not a non-negative integer.
|
|
18191
|
+
*
|
|
18192
|
+
* @example
|
|
18193
|
+
* ```typescript
|
|
18194
|
+
* import { buildFeeQuoteUrl } from '@circle-fin/provider-fee-v1'
|
|
18195
|
+
*
|
|
18196
|
+
* const url = buildFeeQuoteUrl({
|
|
18197
|
+
* sourceDomain: 3,
|
|
18198
|
+
* destinationDomain: 26,
|
|
18199
|
+
* isTestnet: false,
|
|
18200
|
+
* })
|
|
18201
|
+
* // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
|
|
18202
|
+
* ```
|
|
18203
|
+
*/ function buildFeeQuoteUrl(params) {
|
|
18204
|
+
const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
|
|
18205
|
+
assertDomain(sourceDomain, 'sourceDomain');
|
|
18206
|
+
assertDomain(destinationDomain, 'destinationDomain');
|
|
18207
|
+
const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
|
|
18208
|
+
return new URL(`${QUOTE_BURN_USDC_PATH}/${String(sourceDomain)}/${String(destinationDomain)}`, resolvedBaseUrl).toString();
|
|
18209
|
+
}
|
|
18210
|
+
|
|
18211
|
+
/**
|
|
18212
|
+
* Assert that a quote's per-item fee amounts sum to its `feeTotalAmount`.
|
|
18213
|
+
*
|
|
18214
|
+
* A defensive integrity check on the Quote API response, enforced internally
|
|
18215
|
+
* by `fetchFeeQuote`. It does not compare against the destination-side
|
|
18216
|
+
* `feeExecuted`, which is expected to be zero for prepaid forward-only burns.
|
|
18217
|
+
*
|
|
18218
|
+
* @param quote - The signed fee quote to check.
|
|
18219
|
+
* @returns Nothing.
|
|
18220
|
+
* @throws {@link KitError} When the item amounts do not sum to `feeTotalAmount`.
|
|
18221
|
+
* @internal
|
|
18222
|
+
*/ function assertFeeItemsSumToTotal(quote) {
|
|
18223
|
+
const itemsTotal = quote.items.reduce((sum, item)=>sum + BigInt(item.amount), 0n);
|
|
18224
|
+
const declaredTotal = BigInt(quote.feeTotalAmount);
|
|
18225
|
+
if (itemsTotal !== declaredTotal) {
|
|
18226
|
+
throw new KitError({
|
|
18227
|
+
...InputError.VALIDATION_FAILED,
|
|
18228
|
+
recoverability: 'FATAL',
|
|
18229
|
+
message: `Quote API getFeeQuote failed: fee items sum ` + `(${itemsTotal.toString()}) does not equal feeTotalAmount ` + `(${declaredTotal.toString()})`,
|
|
18230
|
+
cause: {
|
|
18231
|
+
trace: {
|
|
18232
|
+
itemsTotal: itemsTotal.toString(),
|
|
18233
|
+
feeTotalAmount: quote.feeTotalAmount
|
|
18234
|
+
}
|
|
18235
|
+
}
|
|
18236
|
+
});
|
|
18237
|
+
}
|
|
18238
|
+
}
|
|
18239
|
+
|
|
18240
|
+
/**
|
|
18241
|
+
* Determine whether a Quote API error represents a disabled source chain.
|
|
18242
|
+
*
|
|
18243
|
+
* @param error - The error thrown by the HTTP layer.
|
|
18244
|
+
* @returns `true` when the error contains the `SERVICE_NOT_ENABLED` marker.
|
|
18245
|
+
* @internal
|
|
18246
|
+
*/ function isServiceNotEnabled(error) {
|
|
18247
|
+
const body = typeof error === 'object' && error !== null && 'responseBody' in error ? error.responseBody : undefined;
|
|
18248
|
+
if (typeof body === 'object' && body !== null) {
|
|
18249
|
+
const fields = body;
|
|
18250
|
+
const candidates = [
|
|
18251
|
+
fields['errorCode'],
|
|
18252
|
+
fields['code'],
|
|
18253
|
+
fields['message'],
|
|
18254
|
+
fields['externalMessage'],
|
|
18255
|
+
fields['error']
|
|
18256
|
+
];
|
|
18257
|
+
if (candidates.some((value)=>typeof value === 'string' && value.toUpperCase().includes('SERVICE_NOT_ENABLED'))) {
|
|
18258
|
+
return true;
|
|
18259
|
+
}
|
|
18260
|
+
}
|
|
18261
|
+
return getErrorMessage(error).toUpperCase().includes('SERVICE_NOT_ENABLED');
|
|
18262
|
+
}
|
|
18263
|
+
|
|
18264
|
+
const SERVICE$1 = 'Quote API';
|
|
18265
|
+
const OPERATION$1 = 'getFeeQuote';
|
|
18266
|
+
/**
|
|
18267
|
+
* Serialize request items for the wire body.
|
|
18268
|
+
*
|
|
18269
|
+
* `PRE_FINALITY` is emitted with no `params` key, and a `FORWARD` item only
|
|
18270
|
+
* carries the binding fields that are present.
|
|
18271
|
+
*
|
|
18272
|
+
* @param requests - The request items to serialize.
|
|
18273
|
+
* @returns The serialized request items.
|
|
18274
|
+
* @internal
|
|
18275
|
+
*/ function serializeRequests(requests) {
|
|
18276
|
+
return requests.map((request)=>{
|
|
18277
|
+
if (request.type === 'PRE_FINALITY') {
|
|
18278
|
+
return {
|
|
18279
|
+
type: 'PRE_FINALITY'
|
|
18280
|
+
};
|
|
18281
|
+
}
|
|
18282
|
+
const params = request.params;
|
|
18283
|
+
if (params === undefined) {
|
|
18284
|
+
return {
|
|
18285
|
+
type: 'FORWARD'
|
|
18286
|
+
};
|
|
18287
|
+
}
|
|
18288
|
+
const forwardParams = {};
|
|
18289
|
+
if (params.hookData !== undefined) {
|
|
18290
|
+
forwardParams.hookData = params.hookData;
|
|
18291
|
+
}
|
|
18292
|
+
if (params.destinationCaller !== undefined) {
|
|
18293
|
+
forwardParams.destinationCaller = params.destinationCaller;
|
|
18294
|
+
}
|
|
18295
|
+
return {
|
|
18296
|
+
type: 'FORWARD',
|
|
18297
|
+
params: forwardParams
|
|
18298
|
+
};
|
|
18299
|
+
});
|
|
18300
|
+
}
|
|
18301
|
+
/**
|
|
18302
|
+
* Fetch a signed fee quote from Circle's Quote API for a CCTP v2 USDC burn.
|
|
18303
|
+
*
|
|
18304
|
+
* Validates inputs, POSTs to
|
|
18305
|
+
* `/v2/quote/burn/usdc/{sourceDomain}/{destinationDomain}` with a single
|
|
18306
|
+
* attempt (the signed quote is short-lived), and returns the typed quote. A
|
|
18307
|
+
* disabled source chain (`503 SERVICE_NOT_ENABLED`) surfaces as a fatal,
|
|
18308
|
+
* non-retryable error; other failures are mapped to a {@link KitError} via the
|
|
18309
|
+
* shared API error parser.
|
|
18310
|
+
*
|
|
18311
|
+
* @param params - The domains, amount, request items, and environment.
|
|
18312
|
+
* @returns The signed fee quote.
|
|
18313
|
+
* @throws {@link KitError} On invalid input, a disabled source chain, an HTTP
|
|
18314
|
+
* error, or an invalid response shape.
|
|
18315
|
+
*
|
|
18316
|
+
* @example
|
|
18317
|
+
* ```typescript
|
|
18318
|
+
* import { fetchFeeQuote } from '@circle-fin/provider-fee-v1'
|
|
18319
|
+
*
|
|
18320
|
+
* const quote = await fetchFeeQuote({
|
|
18321
|
+
* sourceDomain: 3,
|
|
18322
|
+
* destinationDomain: 26,
|
|
18323
|
+
* amount: '1000000',
|
|
18324
|
+
* requests: [{ type: 'FORWARD' }, { type: 'PRE_FINALITY' }],
|
|
18325
|
+
* isTestnet: false,
|
|
18326
|
+
* })
|
|
18327
|
+
* console.log(quote.feeTotalAmount, quote.expiry)
|
|
18328
|
+
* ```
|
|
18329
|
+
*/ async function fetchFeeQuote(params) {
|
|
18330
|
+
const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
|
|
18331
|
+
const parsed = fetchFeeQuoteInputSchema.safeParse({
|
|
18332
|
+
sourceDomain,
|
|
18333
|
+
destinationDomain,
|
|
18334
|
+
amount,
|
|
18335
|
+
feeToken,
|
|
18336
|
+
requests,
|
|
18337
|
+
isTestnet,
|
|
18338
|
+
baseUrl,
|
|
18339
|
+
config
|
|
18340
|
+
});
|
|
18341
|
+
if (!parsed.success) {
|
|
18342
|
+
const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
18343
|
+
throw new KitError({
|
|
18344
|
+
...InputError.VALIDATION_FAILED,
|
|
18345
|
+
recoverability: 'FATAL',
|
|
18346
|
+
message: `${SERVICE$1} ${OPERATION$1} failed: ${detail}`,
|
|
18347
|
+
cause: {
|
|
18348
|
+
trace: parsed.error.issues
|
|
18349
|
+
}
|
|
18350
|
+
});
|
|
18351
|
+
}
|
|
18352
|
+
const url = baseUrl === undefined ? buildFeeQuoteUrl({
|
|
18353
|
+
sourceDomain,
|
|
18354
|
+
destinationDomain,
|
|
18355
|
+
isTestnet
|
|
18356
|
+
}) : buildFeeQuoteUrl({
|
|
18357
|
+
sourceDomain,
|
|
18358
|
+
destinationDomain,
|
|
18359
|
+
isTestnet,
|
|
18360
|
+
baseUrl
|
|
18361
|
+
});
|
|
18362
|
+
const body = {
|
|
18363
|
+
amount,
|
|
18364
|
+
feeToken: feeToken ?? NATIVE_FEE_TOKEN,
|
|
18365
|
+
requests: serializeRequests(requests)
|
|
18366
|
+
};
|
|
18367
|
+
const pollingConfig = {
|
|
18368
|
+
...FEE_QUOTE_DEFAULT_CONFIG,
|
|
18369
|
+
...config
|
|
18370
|
+
};
|
|
18371
|
+
let quote;
|
|
18372
|
+
try {
|
|
18373
|
+
quote = await pollApiPost(url, body, isSignedFeeQuote, pollingConfig);
|
|
18374
|
+
} catch (error) {
|
|
18375
|
+
// Only one service-specific code (SERVICE_NOT_ENABLED) needs bespoke
|
|
18376
|
+
// mapping, so it is detected inline rather than via a dedicated
|
|
18377
|
+
// `parseFeeQuoteApiError` parser; everything else flows through the shared
|
|
18378
|
+
// `parseApiError`. Promote to a parser if more coded errors appear.
|
|
18379
|
+
if (isServiceNotEnabled(error)) {
|
|
18380
|
+
throw new KitError({
|
|
18381
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
18382
|
+
recoverability: 'FATAL',
|
|
18383
|
+
message: `${SERVICE$1} ${OPERATION$1} failed: source chain not enabled for fee ` + `quotes (SERVICE_NOT_ENABLED)`,
|
|
18384
|
+
cause: {
|
|
18385
|
+
trace: error
|
|
18386
|
+
}
|
|
18387
|
+
});
|
|
18388
|
+
}
|
|
18389
|
+
throw parseApiError(error, {
|
|
18390
|
+
service: SERVICE$1,
|
|
18391
|
+
operation: OPERATION$1
|
|
18392
|
+
});
|
|
18393
|
+
}
|
|
18394
|
+
// Defense-in-depth: a self-consistent quote's per-item fees sum to the
|
|
18395
|
+
// declared total. Enforced here so callers cannot forget the check.
|
|
18396
|
+
assertFeeItemsSumToTotal(quote);
|
|
18397
|
+
return quote;
|
|
18398
|
+
}
|
|
18399
|
+
|
|
18400
|
+
const SERVICE = 'Quote API';
|
|
18401
|
+
const OPERATION = 'validateQuote';
|
|
18402
|
+
/**
|
|
18403
|
+
* Validate a signed quote against a complete source-chain contract call.
|
|
18404
|
+
*
|
|
18405
|
+
* @param params - The source domain, ABI signature, call arguments, and environment.
|
|
18406
|
+
* @returns The claimability, binding checks, and authoritative expiry status.
|
|
18407
|
+
* @throws {@link KitError} When input, transport, or response validation fails.
|
|
18408
|
+
*
|
|
18409
|
+
* @example
|
|
18410
|
+
* ```typescript
|
|
18411
|
+
* import { validateQuote } from '@circle-fin/provider-fee-v1'
|
|
18412
|
+
*
|
|
18413
|
+
* const result = await validateQuote({
|
|
18414
|
+
* sourceDomain: 3,
|
|
18415
|
+
* // The exact function + arguments, in ABI order, that will be burned on-chain.
|
|
18416
|
+
* abiSignature:
|
|
18417
|
+
* 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))',
|
|
18418
|
+
* args: [
|
|
18419
|
+
* '1000000',
|
|
18420
|
+
* '26',
|
|
18421
|
+
* '0x0000000000000000000000001111111111111111111111111111111111111111',
|
|
18422
|
+
* '0x2222222222222222222222222222222222222222',
|
|
18423
|
+
* '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
18424
|
+
* '0x636374702d666f72776172640000000000000000000000000000000000000000',
|
|
18425
|
+
* ['0x01abcd', '0x3333333333333333333333333333333333333333'],
|
|
18426
|
+
* ],
|
|
18427
|
+
* isTestnet: false,
|
|
18428
|
+
* })
|
|
18429
|
+
* console.log(result.claimable, result.expiry.secondsRemaining)
|
|
18430
|
+
* ```
|
|
18431
|
+
*/ async function validateQuote(params) {
|
|
18432
|
+
const parsed = validateQuoteInputSchema.safeParse(params);
|
|
18433
|
+
if (!parsed.success) {
|
|
18434
|
+
const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
18435
|
+
throw new KitError({
|
|
18436
|
+
...InputError.VALIDATION_FAILED,
|
|
18437
|
+
recoverability: 'FATAL',
|
|
18438
|
+
message: `${SERVICE} ${OPERATION} failed: ${detail}`,
|
|
18439
|
+
cause: {
|
|
18440
|
+
trace: parsed.error.issues
|
|
18441
|
+
}
|
|
18442
|
+
});
|
|
18443
|
+
}
|
|
18444
|
+
const { sourceDomain, abiSignature, args, isTestnet, baseUrl } = parsed.data;
|
|
18445
|
+
// `config` was validated by the schema above; spread the caller's original,
|
|
18446
|
+
// precisely typed `Partial<ApiPollingConfig>` so the merged polling config
|
|
18447
|
+
// stays assignable under `exactOptionalPropertyTypes`.
|
|
18448
|
+
const pollingConfig = {
|
|
18449
|
+
...FEE_QUOTE_DEFAULT_CONFIG,
|
|
18450
|
+
...params.config
|
|
18451
|
+
};
|
|
18452
|
+
const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
|
|
18453
|
+
const url = new URL(`${QUOTE_VALIDATE_USDC_PATH}/${String(sourceDomain)}`, resolvedBaseUrl).toString();
|
|
18454
|
+
try {
|
|
18455
|
+
return await pollApiPost(url, {
|
|
18456
|
+
abiSignature,
|
|
18457
|
+
args
|
|
18458
|
+
}, isValidateQuoteResult, pollingConfig);
|
|
18459
|
+
} catch (error) {
|
|
18460
|
+
if (isServiceNotEnabled(error)) {
|
|
18461
|
+
throw new KitError({
|
|
18462
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
18463
|
+
recoverability: 'FATAL',
|
|
18464
|
+
message: `${SERVICE} ${OPERATION} failed: source chain not enabled for ` + `quote validation (SERVICE_NOT_ENABLED)`,
|
|
18465
|
+
cause: {
|
|
18466
|
+
trace: error
|
|
18467
|
+
}
|
|
18468
|
+
});
|
|
18469
|
+
}
|
|
18470
|
+
throw parseApiError(error, {
|
|
18471
|
+
service: SERVICE,
|
|
18472
|
+
operation: OPERATION
|
|
18473
|
+
});
|
|
18474
|
+
}
|
|
18475
|
+
}
|
|
18476
|
+
|
|
18477
|
+
/** Refresh timestamp quotes this many seconds before submission. */ const QUOTE_EXPIRY_SAFETY_SECONDS = 30;
|
|
18478
|
+
/** ABI signature validated by the Quote API before source-chain submission. */ const BURN_WITH_FEES_ABI_SIGNATURE = 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))';
|
|
18479
|
+
/** Unrestricted CCTP destination caller used by the forwarding relayer. */ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
18480
|
+
function isQuoteNearEstimatedExpiry(quote) {
|
|
18481
|
+
const currentSeconds = Math.floor(Date.now() / 1_000);
|
|
18482
|
+
const expiresAt = quote.expiry.mode === 'TIMESTAMP' ? quote.expiry.expiresAt : quote.expiry.blockEstimatedAt;
|
|
18483
|
+
// A BLOCK_NUMBER quote may omit the advisory `blockEstimatedAt` estimate; when
|
|
18484
|
+
// it is absent, skip this wall-clock pre-check and defer to the authoritative
|
|
18485
|
+
// source-chain-tip validation performed downstream.
|
|
18486
|
+
if (expiresAt === undefined) {
|
|
18487
|
+
return false;
|
|
18488
|
+
}
|
|
18489
|
+
return expiresAt <= currentSeconds + QUOTE_EXPIRY_SAFETY_SECONDS;
|
|
18490
|
+
}
|
|
18491
|
+
function assertSourceFeeRoute(params) {
|
|
18492
|
+
const { source, destination, config } = params;
|
|
18493
|
+
if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
|
|
18494
|
+
throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
|
|
18495
|
+
}
|
|
18496
|
+
const useForwarder = destination.useForwarder;
|
|
18497
|
+
if (useForwarder !== true) {
|
|
18498
|
+
throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
|
|
18499
|
+
}
|
|
18500
|
+
if (config.customFee !== undefined) {
|
|
18501
|
+
throw createValidationFailedError$1('config.customFee', config.customFee, "Custom fees are not supported with feePayment: 'source'. Remove the " + 'per-call customFee or the kit-level custom fee policy for this route.');
|
|
18502
|
+
}
|
|
18503
|
+
}
|
|
18504
|
+
function buildQuoteBinding(params) {
|
|
18505
|
+
assertSourceFeeRoute(params);
|
|
18506
|
+
const mintRecipient = params.destination.recipientAddress ?? params.destination.address;
|
|
18507
|
+
const hookData = buildForwardingHookData();
|
|
18508
|
+
const requests = [
|
|
18509
|
+
{
|
|
18510
|
+
type: 'FORWARD',
|
|
18511
|
+
params: {
|
|
18512
|
+
hookData,
|
|
18513
|
+
destinationCaller: ZERO_ADDRESS
|
|
18514
|
+
}
|
|
18515
|
+
}
|
|
18516
|
+
];
|
|
18517
|
+
if ((params.config.transferSpeed ?? TransferSpeed.FAST) === TransferSpeed.FAST) {
|
|
18518
|
+
requests.push({
|
|
18519
|
+
type: 'PRE_FINALITY'
|
|
18520
|
+
});
|
|
18521
|
+
}
|
|
18522
|
+
return {
|
|
18523
|
+
sourceDomain: params.source.chain.cctp.domain,
|
|
18524
|
+
destinationDomain: params.destination.chain.cctp.domain,
|
|
18525
|
+
isTestnet: params.source.chain.isTestnet,
|
|
18526
|
+
amount: params.amount,
|
|
18527
|
+
mintRecipient,
|
|
18528
|
+
hookData,
|
|
18529
|
+
destinationCaller: ZERO_ADDRESS,
|
|
18530
|
+
feeToken: params.source.chain.usdcAddress,
|
|
18531
|
+
requests
|
|
18532
|
+
};
|
|
18533
|
+
}
|
|
18534
|
+
async function fetchBoundQuote(binding) {
|
|
18535
|
+
try {
|
|
18536
|
+
const quote = await fetchFeeQuote({
|
|
18537
|
+
sourceDomain: binding.sourceDomain,
|
|
18538
|
+
destinationDomain: binding.destinationDomain,
|
|
18539
|
+
amount: binding.amount,
|
|
18540
|
+
feeToken: binding.feeToken,
|
|
18541
|
+
requests: binding.requests,
|
|
18542
|
+
isTestnet: binding.isTestnet
|
|
18543
|
+
});
|
|
18544
|
+
if (quote.feeToken.toLowerCase() !== binding.feeToken.toLowerCase()) {
|
|
18545
|
+
throw createValidationFailedError$1('feeToken', quote.feeToken, 'Fee Service must return source-chain USDC for source-fee bridging');
|
|
18546
|
+
}
|
|
18547
|
+
return quote;
|
|
18548
|
+
} catch (error) {
|
|
18549
|
+
if (isRateLimitError(error)) {
|
|
18550
|
+
throw new KitError({
|
|
18551
|
+
...RateLimitError.RATE_LIMIT_EXCEEDED,
|
|
18552
|
+
recoverability: 'RETRYABLE',
|
|
18553
|
+
message: 'Fee Service rate limit exceeded. Retry with caller-managed exponential backoff; Bridge Kit does not retry signed quote requests automatically.',
|
|
18554
|
+
cause: {
|
|
18555
|
+
trace: error
|
|
18556
|
+
}
|
|
18557
|
+
});
|
|
18558
|
+
}
|
|
18559
|
+
throw error;
|
|
18560
|
+
}
|
|
18561
|
+
}
|
|
18562
|
+
async function fetchSubmissionQuote(binding) {
|
|
18563
|
+
let quote = await fetchBoundQuote(binding);
|
|
18564
|
+
if (isQuoteNearEstimatedExpiry(quote)) {
|
|
18565
|
+
quote = await fetchBoundQuote(binding);
|
|
18566
|
+
}
|
|
18567
|
+
if (isQuoteNearEstimatedExpiry(quote)) {
|
|
18568
|
+
throw createValidationFailedError$1('quote', undefined, 'Fee Service returned a quote too close to expiry for safe submission');
|
|
18569
|
+
}
|
|
18570
|
+
return quote;
|
|
18571
|
+
}
|
|
18572
|
+
async function validateBoundQuote(binding, signedQuote, refundAddress) {
|
|
18573
|
+
return validateQuote({
|
|
18574
|
+
sourceDomain: binding.sourceDomain,
|
|
18575
|
+
abiSignature: BURN_WITH_FEES_ABI_SIGNATURE,
|
|
18576
|
+
args: [
|
|
18577
|
+
binding.amount,
|
|
18578
|
+
String(binding.destinationDomain),
|
|
18579
|
+
padAddressToBytes32(binding.mintRecipient),
|
|
18580
|
+
binding.feeToken,
|
|
18581
|
+
padAddressToBytes32(binding.destinationCaller),
|
|
18582
|
+
binding.hookData,
|
|
18583
|
+
[
|
|
18584
|
+
signedQuote,
|
|
18585
|
+
refundAddress
|
|
18586
|
+
]
|
|
18587
|
+
],
|
|
18588
|
+
isTestnet: binding.isTestnet
|
|
18589
|
+
});
|
|
18590
|
+
}
|
|
18591
|
+
function isValidationSafe(binding, signedQuote, validation, expectedQuote) {
|
|
18592
|
+
return validation.claimable && !validation.expiry.expired && validation.expiry.secondsRemaining > QUOTE_EXPIRY_SAFETY_SECONDS && validation.signedQuote.toLowerCase() === signedQuote.toLowerCase() && validation.feeToken.toLowerCase() === binding.feeToken.toLowerCase() && (expectedQuote === undefined || validation.feeTotalAmount === expectedQuote.feeTotalAmount && validation.nonce === expectedQuote.nonce);
|
|
18593
|
+
}
|
|
18594
|
+
function unsafeQuoteError(validation) {
|
|
18595
|
+
const detail = validation.failedChecks.length > 0 ? ` (${validation.failedChecks.join(', ')})` : '';
|
|
18596
|
+
return createValidationFailedError$1('quote', undefined, `The fee quote is not safe for submission${detail}; ` + 'call estimate again to obtain a valid quote');
|
|
18597
|
+
}
|
|
18598
|
+
function toExecutionFeeQuote(quote) {
|
|
18599
|
+
return {
|
|
18600
|
+
signedQuote: quote.signedQuote,
|
|
18601
|
+
feeToken: quote.feeToken,
|
|
18602
|
+
feeTotalAmount: quote.feeTotalAmount
|
|
18603
|
+
};
|
|
18604
|
+
}
|
|
18605
|
+
function toFeeItems(quote) {
|
|
18606
|
+
return quote.items.map((item)=>({
|
|
18607
|
+
type: item.type,
|
|
18608
|
+
amount: formatUnits(item.amount, 6),
|
|
18609
|
+
args: item.args,
|
|
18610
|
+
argsHash: item.argsHash
|
|
18611
|
+
}));
|
|
18612
|
+
}
|
|
18613
|
+
/**
|
|
18614
|
+
* Estimate a source-fee bridge using a source-denominated signed fee quote.
|
|
18615
|
+
*
|
|
18616
|
+
* @internal
|
|
18617
|
+
*/ async function estimateSourceFeeBridge(params) {
|
|
18618
|
+
const binding = buildQuoteBinding(params);
|
|
18619
|
+
const quote = await fetchSubmissionQuote(binding);
|
|
18620
|
+
const feeTotal = formatUnits(quote.feeTotalAmount, 6);
|
|
18621
|
+
return {
|
|
18622
|
+
token: 'USDC',
|
|
18623
|
+
amount: formatUnits(params.amount, 6),
|
|
18624
|
+
source: {
|
|
18625
|
+
address: params.source.address,
|
|
18626
|
+
chain: params.source.chain.chain
|
|
18627
|
+
},
|
|
18628
|
+
destination: {
|
|
18629
|
+
address: params.destination.address,
|
|
18630
|
+
chain: params.destination.chain.chain,
|
|
18631
|
+
...params.destination.recipientAddress !== undefined && {
|
|
18632
|
+
recipientAddress: params.destination.recipientAddress
|
|
18633
|
+
}
|
|
18634
|
+
},
|
|
18635
|
+
gasFees: [],
|
|
18636
|
+
fees: quote.items.map((item)=>({
|
|
18637
|
+
type: item.type === 'FORWARD' ? 'forwarder' : 'provider',
|
|
18638
|
+
token: 'USDC',
|
|
18639
|
+
amount: formatUnits(item.amount, 6)
|
|
18640
|
+
})),
|
|
18641
|
+
amountReceived: formatUnits(params.amount, 6),
|
|
18642
|
+
feeTotal,
|
|
18643
|
+
feeItems: toFeeItems(quote),
|
|
18644
|
+
totalDebit: formatUnits((BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString(), 6),
|
|
18645
|
+
quoteExpiry: quote.expiry,
|
|
18646
|
+
quote: quote.signedQuote
|
|
18647
|
+
};
|
|
18648
|
+
}
|
|
18649
|
+
async function readAllowance(params, delegate) {
|
|
18650
|
+
const operationContext = {
|
|
18651
|
+
chain: params.source.chain,
|
|
18652
|
+
address: params.source.address
|
|
18653
|
+
};
|
|
18654
|
+
const prepared = await params.source.adapter.prepareAction('usdc.allowance', {
|
|
18655
|
+
walletAddress: params.source.address,
|
|
18656
|
+
delegate
|
|
18657
|
+
}, operationContext);
|
|
18658
|
+
return BigInt(String(await prepared.execute()));
|
|
18659
|
+
}
|
|
18660
|
+
async function executeAndConfirm(request, params, provider) {
|
|
18661
|
+
const txHash = await request.execute();
|
|
18662
|
+
const data = await provider.waitForTransaction(params.source.adapter, txHash, params.source.chain);
|
|
18663
|
+
return {
|
|
18664
|
+
txHash,
|
|
18665
|
+
data
|
|
18666
|
+
};
|
|
18667
|
+
}
|
|
18668
|
+
async function prepareAndPreflight(params, binding, quote, provider) {
|
|
18669
|
+
assertSourceFeeRoute(params);
|
|
18670
|
+
const totalDebit = (BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString();
|
|
18671
|
+
const operationContext = {
|
|
18672
|
+
chain: params.source.chain,
|
|
18673
|
+
address: params.source.address
|
|
18674
|
+
};
|
|
18675
|
+
await validateBalanceForTransaction({
|
|
18676
|
+
adapter: params.source.adapter,
|
|
18677
|
+
amount: totalDebit,
|
|
18678
|
+
token: 'USDC',
|
|
18679
|
+
tokenAddress: params.source.chain.usdcAddress,
|
|
18680
|
+
operationContext
|
|
18681
|
+
});
|
|
18682
|
+
const prepared = await provider.burnWithFees({
|
|
18683
|
+
source: params.source,
|
|
18684
|
+
destinationChain: params.destination.chain,
|
|
18685
|
+
amount: params.amount,
|
|
18686
|
+
mintRecipient: binding.mintRecipient,
|
|
18687
|
+
destinationCaller: binding.destinationCaller,
|
|
18688
|
+
hookData: binding.hookData,
|
|
18689
|
+
claim: {
|
|
18690
|
+
signedQuote: quote.signedQuote,
|
|
18691
|
+
refundAddress: params.source.address
|
|
18692
|
+
},
|
|
18693
|
+
feeToken: quote.feeToken,
|
|
18694
|
+
feeTotalAmount: quote.feeTotalAmount
|
|
18695
|
+
});
|
|
18696
|
+
const wrapper = resolveCCTPV2ContractAddress(params.source.chain, 'tokenMessengerWithFees');
|
|
18697
|
+
const allowance = await readAllowance(params, wrapper);
|
|
18698
|
+
if (allowance >= BigInt(totalDebit)) {
|
|
18699
|
+
return {
|
|
18700
|
+
prepared,
|
|
18701
|
+
approveStep: {
|
|
18702
|
+
name: 'approve',
|
|
18703
|
+
state: 'noop'
|
|
18704
|
+
}
|
|
18705
|
+
};
|
|
18706
|
+
}
|
|
18707
|
+
let lastApproval;
|
|
18708
|
+
for (const approval of prepared.approvals){
|
|
18709
|
+
lastApproval = await executeAndConfirm(approval, params, provider);
|
|
18710
|
+
}
|
|
18711
|
+
return {
|
|
18712
|
+
prepared,
|
|
18713
|
+
approveStep: {
|
|
18714
|
+
name: 'approve',
|
|
18715
|
+
state: 'success',
|
|
18716
|
+
data: lastApproval?.data,
|
|
18717
|
+
...lastApproval?.txHash !== undefined && {
|
|
18718
|
+
txHash: lastApproval.txHash,
|
|
18719
|
+
explorerUrl: buildExplorerUrl(params.source.chain, lastApproval.txHash)
|
|
18720
|
+
}
|
|
18721
|
+
}
|
|
18722
|
+
};
|
|
18723
|
+
}
|
|
18724
|
+
async function resolveExecutionQuote(binding, suppliedQuote, refundAddress) {
|
|
18725
|
+
if (suppliedQuote === undefined) {
|
|
18726
|
+
const fetchedQuote = await fetchSubmissionQuote(binding);
|
|
18727
|
+
return {
|
|
18728
|
+
quote: toExecutionFeeQuote(fetchedQuote),
|
|
18729
|
+
fetchedQuote
|
|
18730
|
+
};
|
|
18731
|
+
}
|
|
18732
|
+
const validation = await validateBoundQuote(binding, suppliedQuote, refundAddress);
|
|
18733
|
+
if (!isValidationSafe(binding, suppliedQuote, validation)) {
|
|
18734
|
+
throw unsafeQuoteError(validation);
|
|
18735
|
+
}
|
|
18736
|
+
return {
|
|
18737
|
+
quote: toExecutionFeeQuote(validation),
|
|
18738
|
+
fetchedQuote: undefined
|
|
18739
|
+
};
|
|
18740
|
+
}
|
|
18741
|
+
/**
|
|
18742
|
+
* Execute a source-fee bridge while preserving receive-exact semantics.
|
|
18743
|
+
*
|
|
18744
|
+
* @internal
|
|
18745
|
+
*/ async function executeSourceFeeBridge(rawParams, params, provider) {
|
|
18746
|
+
// Narrows `params` to the CCTP v2 route type for the rest of this function.
|
|
18747
|
+
// buildQuoteBinding asserts too, but that narrows its own scope, not this one.
|
|
18748
|
+
assertSourceFeeRoute(params);
|
|
18749
|
+
const binding = buildQuoteBinding(params);
|
|
18750
|
+
const suppliedQuote = rawParams.quote;
|
|
18751
|
+
let { quote, fetchedQuote } = await resolveExecutionQuote(binding, suppliedQuote, params.source.address);
|
|
18752
|
+
let { prepared, approveStep } = await prepareAndPreflight(params, binding, quote, provider);
|
|
18753
|
+
// Validate against the current source-chain tip after approval confirmation.
|
|
18754
|
+
// BLOCK_NUMBER expiries cannot be checked safely with wall-clock time alone.
|
|
18755
|
+
let validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
|
|
18756
|
+
if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
|
|
18757
|
+
if (suppliedQuote === undefined) {
|
|
18758
|
+
fetchedQuote = await fetchSubmissionQuote(binding);
|
|
18759
|
+
quote = toExecutionFeeQuote(fetchedQuote);
|
|
18760
|
+
const refreshedPreparation = await prepareAndPreflight(params, binding, quote, provider);
|
|
18761
|
+
prepared = refreshedPreparation.prepared;
|
|
18762
|
+
if (refreshedPreparation.approveStep.state !== 'noop') {
|
|
18763
|
+
approveStep = refreshedPreparation.approveStep;
|
|
18764
|
+
}
|
|
18765
|
+
validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
|
|
18766
|
+
if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
|
|
18767
|
+
throw unsafeQuoteError(validation);
|
|
18768
|
+
}
|
|
18769
|
+
} else {
|
|
18770
|
+
throw unsafeQuoteError(validation);
|
|
18771
|
+
}
|
|
18772
|
+
}
|
|
18773
|
+
// Surface the same step events as the standard bridge() path so
|
|
18774
|
+
// kit.on('approve'|'burn'|'mint', ...) handlers fire for source-fee bridges.
|
|
18775
|
+
if (approveStep.state !== 'noop') {
|
|
18776
|
+
provider.emitBridgeStep('approve', approveStep);
|
|
18777
|
+
}
|
|
18778
|
+
const burn = await executeAndConfirm(prepared.burn, params, provider);
|
|
18779
|
+
const burnStep = {
|
|
18780
|
+
name: 'burn',
|
|
18781
|
+
state: 'success',
|
|
18782
|
+
txHash: burn.txHash,
|
|
18783
|
+
data: burn.data,
|
|
18784
|
+
explorerUrl: buildExplorerUrl(params.source.chain, burn.txHash)
|
|
18785
|
+
};
|
|
18786
|
+
provider.emitBridgeStep('burn', burnStep);
|
|
18787
|
+
const resultBase = {
|
|
18788
|
+
amount: params.amount,
|
|
18789
|
+
token: 'USDC',
|
|
18790
|
+
config: params.config,
|
|
18791
|
+
provider: provider.name,
|
|
18792
|
+
source: {
|
|
18793
|
+
address: params.source.address,
|
|
18794
|
+
chain: params.source.chain
|
|
18795
|
+
},
|
|
18796
|
+
destination: {
|
|
18797
|
+
address: params.destination.address,
|
|
18798
|
+
chain: params.destination.chain,
|
|
18799
|
+
...params.destination.recipientAddress !== undefined && {
|
|
18800
|
+
recipientAddress: params.destination.recipientAddress
|
|
18801
|
+
},
|
|
18802
|
+
useForwarder: true
|
|
18803
|
+
}
|
|
18804
|
+
};
|
|
18805
|
+
const attestation = await provider.fetchRelayerMint(params.source, burn.txHash);
|
|
18806
|
+
const forwardTxHash = attestation.forwardTxHash;
|
|
18807
|
+
// The burn already moved funds. If the relayer confirms without a
|
|
18808
|
+
// destination hash, surface an error-state result that preserves the burn
|
|
18809
|
+
// step (so `retry()` can resume the mint) instead of throwing and discarding
|
|
18810
|
+
// the completed burn.
|
|
18811
|
+
if (typeof forwardTxHash !== 'string' || forwardTxHash.trim() === '') {
|
|
18812
|
+
const mintStep = {
|
|
18813
|
+
name: 'mint',
|
|
18814
|
+
state: 'error',
|
|
18815
|
+
forwarded: true,
|
|
18816
|
+
errorCategory: 'failed_offchain',
|
|
18817
|
+
errorMessage: 'Relayer confirmation did not include a destination transaction hash'
|
|
18818
|
+
};
|
|
18819
|
+
provider.emitBridgeStep('mint', mintStep);
|
|
18820
|
+
return {
|
|
18821
|
+
...resultBase,
|
|
18822
|
+
state: 'error',
|
|
18823
|
+
steps: [
|
|
18824
|
+
approveStep,
|
|
18825
|
+
burnStep,
|
|
18826
|
+
mintStep
|
|
18827
|
+
]
|
|
18828
|
+
};
|
|
18829
|
+
}
|
|
18830
|
+
const mintStep = {
|
|
18831
|
+
name: 'mint',
|
|
18832
|
+
state: 'success',
|
|
18833
|
+
forwarded: true,
|
|
18834
|
+
txHash: forwardTxHash,
|
|
18835
|
+
explorerUrl: buildExplorerUrl(params.destination.chain, forwardTxHash)
|
|
18836
|
+
};
|
|
18837
|
+
provider.emitBridgeStep('mint', mintStep);
|
|
18838
|
+
return {
|
|
18839
|
+
...resultBase,
|
|
18840
|
+
state: 'success',
|
|
18841
|
+
steps: [
|
|
18842
|
+
approveStep,
|
|
18843
|
+
burnStep,
|
|
18844
|
+
mintStep
|
|
18845
|
+
]
|
|
18846
|
+
};
|
|
18847
|
+
}
|
|
18848
|
+
|
|
16917
18849
|
/** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg$3.name);
|
|
16918
18850
|
/**
|
|
16919
18851
|
* Pick the most-relevant `txHash` to attach to an error telemetry payload.
|
|
@@ -17141,11 +19073,18 @@ function assertCCTPV2Config(config) {
|
|
|
17141
19073
|
this.validateNetworkCompatibility(resolvedParams);
|
|
17142
19074
|
// Merge the custom fee config into the resolved params
|
|
17143
19075
|
const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
//
|
|
17147
|
-
|
|
17148
|
-
|
|
19076
|
+
let result;
|
|
19077
|
+
// Execute the explicit source-fee path without changing legacy
|
|
19078
|
+
// useForwarder behavior for callers that did not opt in.
|
|
19079
|
+
if (params.config?.feePayment === 'source') {
|
|
19080
|
+
const sourceFeeProvider = this.findSourceFeeProvider(finalResolvedParams);
|
|
19081
|
+
result = formatBridgeResult(await executeSourceFeeBridge(params, finalResolvedParams, sourceFeeProvider), 'to-human-readable');
|
|
19082
|
+
} else {
|
|
19083
|
+
// Find a provider that supports this route
|
|
19084
|
+
const provider = this.findProviderForRoute(finalResolvedParams);
|
|
19085
|
+
// Execute the transfer using the provider and format the result.
|
|
19086
|
+
result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
|
|
19087
|
+
}
|
|
17149
19088
|
// Emit error telemetry when the provider returns an error state
|
|
17150
19089
|
// (provider records step failures in the result instead of throwing).
|
|
17151
19090
|
if (result.state === 'error') {
|
|
@@ -17268,42 +19207,7 @@ function assertCCTPV2Config(config) {
|
|
|
17268
19207
|
tokenIn: result.token
|
|
17269
19208
|
});
|
|
17270
19209
|
}
|
|
17271
|
-
|
|
17272
|
-
* Estimate the cost and fees for a cross-chain USDC bridge operation.
|
|
17273
|
-
*
|
|
17274
|
-
* This method calculates the expected gas fees and protocol costs for bridging
|
|
17275
|
-
* without actually executing the transaction. It performs the same validation
|
|
17276
|
-
* as the bridge method but stops before execution.
|
|
17277
|
-
*
|
|
17278
|
-
* @param params - The bridge parameters for cost estimation, including optional invocation metadata
|
|
17279
|
-
* @returns Promise resolving to detailed cost breakdown including gas estimates
|
|
17280
|
-
* @throws {KitError} When the parameters are invalid.
|
|
17281
|
-
* @throws {UnsupportedRouteError} When the route is not supported.
|
|
17282
|
-
*
|
|
17283
|
-
* @example
|
|
17284
|
-
* ```typescript
|
|
17285
|
-
* // Basic usage
|
|
17286
|
-
* const estimate = await kit.estimate({
|
|
17287
|
-
* from: { adapter: adapter, chain: 'Ethereum' },
|
|
17288
|
-
* to: { adapter: adapter, chain: 'Base' },
|
|
17289
|
-
* amount: '10.50',
|
|
17290
|
-
* token: 'USDC'
|
|
17291
|
-
* })
|
|
17292
|
-
* console.log('Estimated cost:', estimate.totalCost)
|
|
17293
|
-
*
|
|
17294
|
-
* // With custom invocation metadata
|
|
17295
|
-
* const estimate = await kit.estimate({
|
|
17296
|
-
* from: { adapter: adapter, chain: 'Ethereum' },
|
|
17297
|
-
* to: { adapter: adapter, chain: 'Base' },
|
|
17298
|
-
* amount: '10.50',
|
|
17299
|
-
* token: 'USDC',
|
|
17300
|
-
* invocationMeta: {
|
|
17301
|
-
* traceId: 'custom-trace-id',
|
|
17302
|
-
* callers: [{ type: 'app', name: 'MyDApp', version: '1.0.0' }],
|
|
17303
|
-
* },
|
|
17304
|
-
* })
|
|
17305
|
-
* ```
|
|
17306
|
-
*/ async estimate(params) {
|
|
19210
|
+
async estimate(params) {
|
|
17307
19211
|
return withErrorTelemetry(async ()=>{
|
|
17308
19212
|
// First validate the parameters
|
|
17309
19213
|
assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
|
|
@@ -17313,6 +19217,10 @@ function assertCCTPV2Config(config) {
|
|
|
17313
19217
|
this.validateNetworkCompatibility(resolvedParams);
|
|
17314
19218
|
// Merge the custom fee config into the resolved params
|
|
17315
19219
|
const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
|
|
19220
|
+
if (params.config?.feePayment === 'source') {
|
|
19221
|
+
this.findSourceFeeProvider(finalResolvedParams);
|
|
19222
|
+
return estimateSourceFeeBridge(finalResolvedParams);
|
|
19223
|
+
}
|
|
17316
19224
|
// Find a provider that supports this route
|
|
17317
19225
|
const provider = this.findProviderForRoute(finalResolvedParams);
|
|
17318
19226
|
// Estimate the transfer using the provider and format amounts to human-readable strings
|
|
@@ -17361,6 +19269,9 @@ function assertCCTPV2Config(config) {
|
|
|
17361
19269
|
* // Get only chains that support forwarding
|
|
17362
19270
|
* const forwarderChains = kit.getSupportedChains({ forwarderSupported: true })
|
|
17363
19271
|
*
|
|
19272
|
+
* // Get only chains that can pay fees on the source chain (receive-exact)
|
|
19273
|
+
* const sourceFeeChains = kit.getSupportedChains({ sourceFeeSupported: true })
|
|
19274
|
+
*
|
|
17364
19275
|
* console.log('Supported chains:')
|
|
17365
19276
|
* allChains.forEach(chain => {
|
|
17366
19277
|
* console.log(`- ${chain.name} (${chain.type})`)
|
|
@@ -17409,6 +19320,10 @@ function assertCCTPV2Config(config) {
|
|
|
17409
19320
|
return options.forwarderSupported ? fs.source || fs.destination : !fs.source && !fs.destination;
|
|
17410
19321
|
});
|
|
17411
19322
|
}
|
|
19323
|
+
// Apply source-paid ("receive-exact") fee support filter if provided
|
|
19324
|
+
if (options?.sourceFeeSupported !== undefined) {
|
|
19325
|
+
chains = chains.filter((chain)=>hasSourceFeeSupport(chain) === options.sourceFeeSupported);
|
|
19326
|
+
}
|
|
17412
19327
|
return chains;
|
|
17413
19328
|
}
|
|
17414
19329
|
/**
|
|
@@ -17443,6 +19358,20 @@ function assertCCTPV2Config(config) {
|
|
|
17443
19358
|
return provider;
|
|
17444
19359
|
}
|
|
17445
19360
|
/**
|
|
19361
|
+
* Find the default CCTP v2 provider for a source-fee forwarding route.
|
|
19362
|
+
*
|
|
19363
|
+
* @param params - The resolved provider parameters.
|
|
19364
|
+
* @returns The CCTP v2 provider that supports the forwarded route.
|
|
19365
|
+
* @throws {UnsupportedRouteError} When no source-fee provider supports the route.
|
|
19366
|
+
* @internal
|
|
19367
|
+
*/ findSourceFeeProvider(params) {
|
|
19368
|
+
const provider = this.providers.find((candidate)=>candidate instanceof CCTPV2BridgingProvider && candidate.supportsRoute(params.source.chain, params.destination.chain, params.token, true));
|
|
19369
|
+
if (!(provider instanceof CCTPV2BridgingProvider)) {
|
|
19370
|
+
throw createUnsupportedRouteError(params.source.chain.name, params.destination.chain.name);
|
|
19371
|
+
}
|
|
19372
|
+
return provider;
|
|
19373
|
+
}
|
|
19374
|
+
/**
|
|
17446
19375
|
* Merge custom fee configuration into provider parameters.
|
|
17447
19376
|
*
|
|
17448
19377
|
* Prioritizes any custom fee configuration already present on the
|
|
@@ -17651,7 +19580,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
|
|
|
17651
19580
|
};
|
|
17652
19581
|
|
|
17653
19582
|
var name$1 = "@circle-fin/swap-kit";
|
|
17654
|
-
var version$1 = "1.
|
|
19583
|
+
var version$1 = "1.6.0";
|
|
17655
19584
|
var pkg$1 = {
|
|
17656
19585
|
name: name$1,
|
|
17657
19586
|
version: version$1};
|
|
@@ -17665,7 +19594,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17665
19594
|
* Catches obviously malformed addresses at parse time; chain-specific validation
|
|
17666
19595
|
* is performed in buildServiceParams.
|
|
17667
19596
|
*/ const destinationAddressSchema = z.union([
|
|
17668
|
-
evmAddressSchema,
|
|
19597
|
+
evmAddressSchema$1,
|
|
17669
19598
|
solanaAddressSchema
|
|
17670
19599
|
]);
|
|
17671
19600
|
/**
|
|
@@ -17711,9 +19640,16 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17711
19640
|
message: 'stopLimit must be greater than 0'
|
|
17712
19641
|
}).optional(),
|
|
17713
19642
|
customFee: serviceSwapCustomFeeSchema.optional(),
|
|
19643
|
+
apiKey: z.string({
|
|
19644
|
+
invalid_type_error: 'apiKey must be a string'
|
|
19645
|
+
})// Tolerate '' so the `process.env.CIRCLE_API_KEY ?? ''` idiom falls back to
|
|
19646
|
+
// kitKey via resolveApiKey instead of being rejected here.
|
|
19647
|
+
.optional(),
|
|
17714
19648
|
kitKey: z.string({
|
|
17715
19649
|
invalid_type_error: 'kitKey must be a string'
|
|
17716
|
-
})
|
|
19650
|
+
})// Tolerate '' so an unset kit-key env var yields the permissionless path,
|
|
19651
|
+
// matching buildServiceParams (which already omits an empty credential).
|
|
19652
|
+
.optional(),
|
|
17717
19653
|
provider: z.string({
|
|
17718
19654
|
invalid_type_error: 'provider must be a string'
|
|
17719
19655
|
}).min(1, 'provider must be a non-empty string').optional(),
|
|
@@ -17886,6 +19822,41 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17886
19822
|
*
|
|
17887
19823
|
* @internal
|
|
17888
19824
|
*/ const MAX_RATE_ADDRESSES_PER_REQUEST = 100;
|
|
19825
|
+
/**
|
|
19826
|
+
* Environment prefixes carried by Circle platform API keys.
|
|
19827
|
+
*
|
|
19828
|
+
* A Circle API key is `<ENV>_API_KEY:<keyId>:<keySecret>`, where `<ENV>` is one
|
|
19829
|
+
* of these prefixes.
|
|
19830
|
+
*
|
|
19831
|
+
* @internal
|
|
19832
|
+
*/ const API_KEY_ENV_PREFIXES = [
|
|
19833
|
+
'TEST',
|
|
19834
|
+
'LIVE',
|
|
19835
|
+
'SAND',
|
|
19836
|
+
'SANDBOX',
|
|
19837
|
+
'SMOK',
|
|
19838
|
+
'PROD',
|
|
19839
|
+
'STAG',
|
|
19840
|
+
'DEV'
|
|
19841
|
+
];
|
|
19842
|
+
/**
|
|
19843
|
+
* Accepted credential formats for Stablecoin Service authentication.
|
|
19844
|
+
*
|
|
19845
|
+
* Matches a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
|
|
19846
|
+
* the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`). API keys are the
|
|
19847
|
+
* recommended credential; kit keys remain accepted as the legacy path.
|
|
19848
|
+
*
|
|
19849
|
+
* @remarks
|
|
19850
|
+
* This is a local pre-flight check, not the authority — the Stablecoin Service
|
|
19851
|
+
* validates the credential and answers 401 when it rejects one. The prefix list
|
|
19852
|
+
* is therefore deliberately permissive: a valid key carrying a prefix this SDK
|
|
19853
|
+
* has not been taught about should reach the service and be judged there rather
|
|
19854
|
+
* than be refused locally, since refusing locally is indistinguishable from an
|
|
19855
|
+
* outage to the caller. Kept as the single source of truth so the pattern is
|
|
19856
|
+
* not restated per call site.
|
|
19857
|
+
*
|
|
19858
|
+
* @internal
|
|
19859
|
+
*/ const API_KEY_PATTERN = new RegExp(`^(?:KIT_KEY|(?:${API_KEY_ENV_PREFIXES.join('|')})_API_KEY)` + ':[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$');
|
|
17889
19860
|
|
|
17890
19861
|
/**
|
|
17891
19862
|
* Zod schema for validating stop limits.
|
|
@@ -17954,13 +19925,14 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17954
19925
|
/**
|
|
17955
19926
|
* Zod schema for validating API keys.
|
|
17956
19927
|
*
|
|
17957
|
-
*
|
|
19928
|
+
* Accepts a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
|
|
19929
|
+
* the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`).
|
|
17958
19930
|
*
|
|
17959
19931
|
* @example
|
|
17960
19932
|
* ```typescript
|
|
17961
19933
|
* import { apiKeySchema } from '@core/service-client'
|
|
17962
19934
|
*
|
|
17963
|
-
* const result = apiKeySchema.safeParse('
|
|
19935
|
+
* const result = apiKeySchema.safeParse('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
|
|
17964
19936
|
* if (!result.success) {
|
|
17965
19937
|
* console.error('Invalid API key format:', result.error.issues)
|
|
17966
19938
|
* }
|
|
@@ -17968,7 +19940,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17968
19940
|
*/ const apiKeySchema = z.string({
|
|
17969
19941
|
required_error: 'API key is required',
|
|
17970
19942
|
invalid_type_error: 'Invalid API key format'
|
|
17971
|
-
}).regex(
|
|
19943
|
+
}).regex(API_KEY_PATTERN, 'Invalid API key format');
|
|
17972
19944
|
/**
|
|
17973
19945
|
* Zod schema for platform fees configuration.
|
|
17974
19946
|
*
|
|
@@ -18407,6 +20379,40 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
18407
20379
|
transaction: createSwapTransactionSchema
|
|
18408
20380
|
});
|
|
18409
20381
|
|
|
20382
|
+
/**
|
|
20383
|
+
* Resolve the credential to authenticate a Stablecoin Service request with.
|
|
20384
|
+
*
|
|
20385
|
+
* `apiKey` is the supported field; `kitKey` is the deprecated alias kept for
|
|
20386
|
+
* existing integrations. When both are supplied `apiKey` wins, so a caller
|
|
20387
|
+
* migrating field-by-field cannot be silently pinned to a stale credential.
|
|
20388
|
+
*
|
|
20389
|
+
* An empty-string value is treated as absent on either field. Without this, the
|
|
20390
|
+
* common `process.env.CIRCLE_API_KEY ?? ''` idiom (which yields `''` when the
|
|
20391
|
+
* variable is unset) would either shadow a working `kitKey` or, on a bare
|
|
20392
|
+
* `kitKey: ''`, reach downstream validation as an invalid credential instead of
|
|
20393
|
+
* falling through to the permissionless path.
|
|
20394
|
+
*
|
|
20395
|
+
* @param source - Object carrying either credential field, or neither.
|
|
20396
|
+
* @returns The credential to use, or `undefined` for the permissionless
|
|
20397
|
+
* (keyless) path.
|
|
20398
|
+
*
|
|
20399
|
+
* @example
|
|
20400
|
+
* ```typescript
|
|
20401
|
+
* import { resolveApiKey } from '@core/service-client'
|
|
20402
|
+
*
|
|
20403
|
+
* resolveApiKey({ apiKey: 'TEST_API_KEY:id:secret' }) // 'TEST_API_KEY:id:secret'
|
|
20404
|
+
* resolveApiKey({ kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
|
|
20405
|
+
* resolveApiKey({ apiKey: '', kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
|
|
20406
|
+
* resolveApiKey({ kitKey: '' }) // undefined
|
|
20407
|
+
* resolveApiKey({}) // undefined
|
|
20408
|
+
* ```
|
|
20409
|
+
*/ const resolveApiKey = (source)=>{
|
|
20410
|
+
// Treat an empty-string value as absent on either field so the
|
|
20411
|
+
// `env ?? ''` idiom falls through to the next credential (or permissionless).
|
|
20412
|
+
const normalize = (value)=>value !== undefined && value !== '' ? value : undefined;
|
|
20413
|
+
return normalize(source.apiKey) ?? normalize(source.kitKey);
|
|
20414
|
+
};
|
|
20415
|
+
|
|
18410
20416
|
/**
|
|
18411
20417
|
* Zod schema for validating EVM adapter capabilities.
|
|
18412
20418
|
*
|
|
@@ -18836,7 +20842,7 @@ const abiParameterSchema = z.object({
|
|
|
18836
20842
|
*/ z.object({
|
|
18837
20843
|
type: z.literal('evm'),
|
|
18838
20844
|
abi: abiSchema,
|
|
18839
|
-
address: evmAddressSchema,
|
|
20845
|
+
address: evmAddressSchema$1,
|
|
18840
20846
|
functionName: z.string({
|
|
18841
20847
|
required_error: 'Function name is required',
|
|
18842
20848
|
invalid_type_error: 'Function name must be a string'
|
|
@@ -18873,7 +20879,7 @@ const abiParameterSchema = z.object({
|
|
|
18873
20879
|
* }
|
|
18874
20880
|
* ```
|
|
18875
20881
|
*/ z.object({
|
|
18876
|
-
address: evmAddressSchema,
|
|
20882
|
+
address: evmAddressSchema$1,
|
|
18877
20883
|
value: z.bigint({
|
|
18878
20884
|
required_error: 'Value is required for native transfers',
|
|
18879
20885
|
invalid_type_error: 'Value must be a bigint'
|
|
@@ -18959,7 +20965,7 @@ z.object({
|
|
|
18959
20965
|
signature: evmSignatureSchema,
|
|
18960
20966
|
tokenInputs: z.array(z.object({
|
|
18961
20967
|
permitType: z.nativeEnum(PermitType),
|
|
18962
|
-
token: evmAddressSchema,
|
|
20968
|
+
token: evmAddressSchema$1,
|
|
18963
20969
|
amount: z.bigint().refine((value)=>value >= 0n, {
|
|
18964
20970
|
message: 'amount must be a non-negative bigint'
|
|
18965
20971
|
}),
|
|
@@ -19379,7 +21385,7 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
|
|
|
19379
21385
|
/**
|
|
19380
21386
|
* Fee recipient address (required).
|
|
19381
21387
|
* Must be a valid EVM address or Solana address.
|
|
19382
|
-
*/ recipientAddress: z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
21388
|
+
*/ recipientAddress: z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
19383
21389
|
message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
|
|
19384
21390
|
})
|
|
19385
21391
|
}).strict();
|
|
@@ -19391,7 +21397,8 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
|
|
|
19391
21397
|
* - slippageBps: Optional positive number for slippage tolerance
|
|
19392
21398
|
* - stopLimit: Optional decimal string for minimum output
|
|
19393
21399
|
* - customFee: Optional fee configuration
|
|
19394
|
-
* -
|
|
21400
|
+
* - apiKey: Optional credential string
|
|
21401
|
+
* - kitKey: Optional credential string (deprecated alias for apiKey)
|
|
19395
21402
|
*/ const swapConfigSchema = z.object({
|
|
19396
21403
|
allowanceStrategy: allowanceStrategySchema.optional(),
|
|
19397
21404
|
slippageBps: z.number().int().min(0).optional(),
|
|
@@ -19401,11 +21408,12 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
|
|
|
19401
21408
|
attributeName: 'stopLimit'
|
|
19402
21409
|
})(z.string())).optional(),
|
|
19403
21410
|
customFee: swapCustomFeeSchema.optional(),
|
|
21411
|
+
apiKey: z.string().optional(),
|
|
19404
21412
|
kitKey: z.string().optional()
|
|
19405
21413
|
});
|
|
19406
21414
|
const swapDestinationSchema = z.object({
|
|
19407
21415
|
chain: optionalSwapChainIdentifierField,
|
|
19408
|
-
recipientAddress: z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
21416
|
+
recipientAddress: z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
19409
21417
|
message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
|
|
19410
21418
|
}).optional()
|
|
19411
21419
|
}).strict();
|
|
@@ -19594,7 +21602,7 @@ new Set(Object.values(Blockchain));
|
|
|
19594
21602
|
registerKit(`${pkg$1.name}/${pkg$1.version}`);
|
|
19595
21603
|
|
|
19596
21604
|
var name = "@circle-fin/earn-kit";
|
|
19597
|
-
var version = "1.
|
|
21605
|
+
var version = "1.6.0";
|
|
19598
21606
|
var pkg = {
|
|
19599
21607
|
name: name,
|
|
19600
21608
|
version: version};
|
|
@@ -19666,7 +21674,7 @@ function isNonNegativeBigIntLike(value) {
|
|
|
19666
21674
|
}
|
|
19667
21675
|
}
|
|
19668
21676
|
const hexSignatureSchema = evmSignatureSchema;
|
|
19669
|
-
const hexAddressSchema = evmAddressSchema;
|
|
21677
|
+
const hexAddressSchema = evmAddressSchema$1;
|
|
19670
21678
|
// '0x' prefix + 32 bytes * 2 hex chars.
|
|
19671
21679
|
const BYTES32_HEX_LENGTH = 66;
|
|
19672
21680
|
const bridgeFeeTokenSchema = hexAddressSchema;
|
|
@@ -20571,7 +22579,7 @@ createTokenRegistry();
|
|
|
20571
22579
|
* fast instead of round-tripping to the service.
|
|
20572
22580
|
*
|
|
20573
22581
|
* @internal
|
|
20574
|
-
*/ const recipientEvmAddressSchema = evmAddressSchema.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
|
|
22582
|
+
*/ const recipientEvmAddressSchema = evmAddressSchema$1.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
|
|
20575
22583
|
/**
|
|
20576
22584
|
* Schema for the adapter context within earn operations.
|
|
20577
22585
|
*
|
|
@@ -20603,19 +22611,39 @@ const sourceAdapterContextSchema = z.object({
|
|
|
20603
22611
|
/**
|
|
20604
22612
|
* Schema for the EarnConfig options.
|
|
20605
22613
|
*
|
|
20606
|
-
* Validate the
|
|
20607
|
-
*
|
|
20608
|
-
*
|
|
20609
|
-
*
|
|
20610
|
-
*
|
|
20611
|
-
*
|
|
22614
|
+
* Validate the *resolved* credential using the standard `apiKeySchema` format
|
|
22615
|
+
* (`<ENV>_API_KEY:<keyId>:<keySecret>`, or a legacy
|
|
22616
|
+
* `KIT_KEY:<keyId>:<keySecret>`). `apiKey` takes precedence over the deprecated
|
|
22617
|
+
* `kitKey`, so a malformed `kitKey` that is being ignored must not fail a config
|
|
22618
|
+
* that supplies a valid `apiKey` (and vice versa) — only the credential that
|
|
22619
|
+
* would actually be sent is format-checked. When neither is supplied the SDK
|
|
22620
|
+
* operates in permissionless mode. `baseUrl` overrides the Earn Service endpoint
|
|
22621
|
+
* (e.g. staging); `batchTransactions: false` opts out of atomic batched
|
|
22622
|
+
* execution. All are forwarded to the provider, so this `.strict()` schema must
|
|
22623
|
+
* accept them or a valid config object is rejected.
|
|
20612
22624
|
*
|
|
20613
22625
|
* @internal
|
|
20614
22626
|
*/ const earnConfigSchema = z.object({
|
|
20615
|
-
|
|
22627
|
+
apiKey: z.string().optional(),
|
|
22628
|
+
kitKey: z.string().optional(),
|
|
20616
22629
|
baseUrl: z.string().optional(),
|
|
20617
22630
|
batchTransactions: z.boolean().optional()
|
|
20618
|
-
}).strict()
|
|
22631
|
+
}).strict().superRefine((config, ctx)=>{
|
|
22632
|
+
const credential = resolveApiKey(config);
|
|
22633
|
+
if (credential === undefined) {
|
|
22634
|
+
return;
|
|
22635
|
+
}
|
|
22636
|
+
const result = apiKeySchema.safeParse(credential);
|
|
22637
|
+
if (!result.success) {
|
|
22638
|
+
ctx.addIssue({
|
|
22639
|
+
code: z.ZodIssueCode.custom,
|
|
22640
|
+
path: [
|
|
22641
|
+
credential === config.apiKey ? 'apiKey' : 'kitKey'
|
|
22642
|
+
],
|
|
22643
|
+
message: result.error.issues[0]?.message ?? 'Invalid API key format'
|
|
22644
|
+
});
|
|
22645
|
+
}
|
|
22646
|
+
});
|
|
20619
22647
|
/**
|
|
20620
22648
|
* Canonical decimal form: a leading digit with no leading zeros (a single
|
|
20621
22649
|
* '0' is only allowed immediately before the decimal point). Rejects the
|
|
@@ -20687,7 +22715,7 @@ const sourceAdapterContextSchema = z.object({
|
|
|
20687
22715
|
* currently supports EVM vault addresses on Arc Testnet.
|
|
20688
22716
|
*
|
|
20689
22717
|
* @internal
|
|
20690
|
-
*/ const vaultAddressSchema = evmAddressSchema.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
|
|
22718
|
+
*/ const vaultAddressSchema = evmAddressSchema$1.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
|
|
20691
22719
|
/**
|
|
20692
22720
|
* Validation schema for VaultQuery.
|
|
20693
22721
|
*
|
|
@@ -20962,7 +22990,7 @@ const sameChainGetDepositQuoteParamsSchema = z.object({
|
|
|
20962
22990
|
const crossChainGetDepositQuoteParamsSchema = z.object({
|
|
20963
22991
|
from: sourceAdapterContextSchema,
|
|
20964
22992
|
chain: earnBridgeDestinationChainIdentifierSchema,
|
|
20965
|
-
address: evmAddressSchema,
|
|
22993
|
+
address: evmAddressSchema$1,
|
|
20966
22994
|
vaultAddress: vaultAddressSchema,
|
|
20967
22995
|
amount: amountSchema,
|
|
20968
22996
|
transferSpeed: z.enum([
|