@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.cjs
CHANGED
|
@@ -792,6 +792,32 @@ class KitError extends Error {
|
|
|
792
792
|
type: 'ONCHAIN'
|
|
793
793
|
}
|
|
794
794
|
};
|
|
795
|
+
/**
|
|
796
|
+
* Standardized error definitions for LIQUIDITY type errors.
|
|
797
|
+
*
|
|
798
|
+
* LIQUIDITY errors indicate that an upstream provider or AMM cannot fulfill
|
|
799
|
+
* the requested swap size due to insufficient liquidity at the moment.
|
|
800
|
+
* These are typically transient — retrying later or reducing the amount
|
|
801
|
+
* may succeed once liquidity replenishes.
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```typescript
|
|
805
|
+
* import { LiquidityError } from '@core/errors'
|
|
806
|
+
*
|
|
807
|
+
* const error = new KitError({
|
|
808
|
+
* ...LiquidityError.INSUFFICIENT_LIQUIDITY,
|
|
809
|
+
* recoverability: 'RETRYABLE',
|
|
810
|
+
* message: 'Insufficient liquidity for the requested swap',
|
|
811
|
+
* cause: { trace: { token: '0xA0b86991...' } }
|
|
812
|
+
* })
|
|
813
|
+
* ```
|
|
814
|
+
*/ const LiquidityError = {
|
|
815
|
+
/** Upstream provider has a route but cannot fulfill the requested size right now */ INSUFFICIENT_LIQUIDITY: {
|
|
816
|
+
code: 6001,
|
|
817
|
+
name: 'LIQUIDITY_INSUFFICIENT',
|
|
818
|
+
type: 'LIQUIDITY'
|
|
819
|
+
}
|
|
820
|
+
};
|
|
795
821
|
/**
|
|
796
822
|
* Standardized error definitions for RPC type errors.
|
|
797
823
|
*
|
|
@@ -839,7 +865,10 @@ class KitError extends Error {
|
|
|
839
865
|
type: 'NETWORK'
|
|
840
866
|
},
|
|
841
867
|
/** Network request timeout */ TIMEOUT: {
|
|
842
|
-
code: 3002
|
|
868
|
+
code: 3002,
|
|
869
|
+
name: 'NETWORK_TIMEOUT',
|
|
870
|
+
type: 'NETWORK'
|
|
871
|
+
},
|
|
843
872
|
/** Circle relayer failed to process the forwarding/mint transaction */ RELAYER_FORWARD_FAILED: {
|
|
844
873
|
code: 3003,
|
|
845
874
|
name: 'NETWORK_RELAYER_FORWARD_FAILED',
|
|
@@ -850,6 +879,58 @@ class KitError extends Error {
|
|
|
850
879
|
name: 'NETWORK_RELAYER_PENDING',
|
|
851
880
|
type: 'NETWORK'
|
|
852
881
|
}};
|
|
882
|
+
/**
|
|
883
|
+
* Standardized error definitions for RATE_LIMIT type errors.
|
|
884
|
+
*
|
|
885
|
+
* RATE_LIMIT errors indicate API throttling, request frequency limits errors.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```typescript
|
|
889
|
+
* import { RateLimitError } from '@core/errors'
|
|
890
|
+
*
|
|
891
|
+
* const error = new KitError({
|
|
892
|
+
* ...RateLimitError.RATE_LIMIT_EXCEEDED,
|
|
893
|
+
* recoverability: 'RETRYABLE',
|
|
894
|
+
* message: 'Rate limit exceeded, please retry later',
|
|
895
|
+
* cause: { trace: { error: '429 Too Many Requests' } }
|
|
896
|
+
* })
|
|
897
|
+
* ```
|
|
898
|
+
*/ const RateLimitError = {
|
|
899
|
+
/** Rate limit exceeded */ RATE_LIMIT_EXCEEDED: {
|
|
900
|
+
code: 7001,
|
|
901
|
+
name: 'RATE_LIMIT_EXCEEDED',
|
|
902
|
+
type: 'RATE_LIMIT'
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
/**
|
|
906
|
+
* Standardized error definitions for SERVICE type errors.
|
|
907
|
+
*
|
|
908
|
+
* SERVICE errors indicate internal service failures, HTTP 5xx errors,
|
|
909
|
+
* or backend processing issues that are retryable.
|
|
910
|
+
*
|
|
911
|
+
* @example
|
|
912
|
+
* ```typescript
|
|
913
|
+
* import { ServiceError } from '@core/errors'
|
|
914
|
+
*
|
|
915
|
+
* const error = new KitError({
|
|
916
|
+
* ...ServiceError.INTERNAL_ERROR,
|
|
917
|
+
* recoverability: 'RETRYABLE',
|
|
918
|
+
* message: 'Service encountered an internal error (500)',
|
|
919
|
+
* cause: { trace: { statusCode: 500 } }
|
|
920
|
+
* })
|
|
921
|
+
* ```
|
|
922
|
+
*/ const ServiceError = {
|
|
923
|
+
/** Internal server error (HTTP 5xx) */ INTERNAL_ERROR: {
|
|
924
|
+
code: 8001,
|
|
925
|
+
name: 'SERVICE_INTERNAL_ERROR',
|
|
926
|
+
type: 'SERVICE'
|
|
927
|
+
},
|
|
928
|
+
/** Unknown or unclassified error that cannot be categorized */ UNKNOWN_ERROR: {
|
|
929
|
+
code: 8002,
|
|
930
|
+
name: 'SERVICE_UNKNOWN_ERROR',
|
|
931
|
+
type: 'SERVICE'
|
|
932
|
+
}
|
|
933
|
+
};
|
|
853
934
|
|
|
854
935
|
/**
|
|
855
936
|
* Creates error for network type mismatch between source and destination.
|
|
@@ -2221,6 +2302,32 @@ class KitError extends Error {
|
|
|
2221
2302
|
}
|
|
2222
2303
|
return false;
|
|
2223
2304
|
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Type guard to check if error is KitError with RATE_LIMIT type.
|
|
2307
|
+
*
|
|
2308
|
+
* RATE_LIMIT errors indicate API throttling or request frequency limits.
|
|
2309
|
+
* These errors are typically RETRYABLE after a delay.
|
|
2310
|
+
*
|
|
2311
|
+
* @param error - Unknown error to check
|
|
2312
|
+
* @returns True if error is KitError with RATE_LIMIT type
|
|
2313
|
+
*
|
|
2314
|
+
* @example
|
|
2315
|
+
* ```typescript
|
|
2316
|
+
* import { isRateLimitError } from '@core/errors'
|
|
2317
|
+
*
|
|
2318
|
+
* try {
|
|
2319
|
+
* await kit.bridge(params)
|
|
2320
|
+
* } catch (error) {
|
|
2321
|
+
* if (isRateLimitError(error)) {
|
|
2322
|
+
* console.log('Rate limited, retrying in 60s')
|
|
2323
|
+
* await sleep(60000)
|
|
2324
|
+
* retry()
|
|
2325
|
+
* }
|
|
2326
|
+
* }
|
|
2327
|
+
* ```
|
|
2328
|
+
*/ function isRateLimitError(error) {
|
|
2329
|
+
return isKitError(error) && error.type === ERROR_TYPES.RATE_LIMIT;
|
|
2330
|
+
}
|
|
2224
2331
|
/**
|
|
2225
2332
|
* Safely extracts error message from any error type.
|
|
2226
2333
|
*
|
|
@@ -2470,6 +2577,478 @@ class KitError extends Error {
|
|
|
2470
2577
|
return chain;
|
|
2471
2578
|
}
|
|
2472
2579
|
|
|
2580
|
+
/**
|
|
2581
|
+
* Proxy-specific structured error codes carried in `responseBody.code`.
|
|
2582
|
+
*
|
|
2583
|
+
* These are NOT HTTP status codes — they are application-level identifiers
|
|
2584
|
+
* the stablecoin-kits-proxy emits inside JSON error bodies so the kit can
|
|
2585
|
+
* distinguish conditions that share the same HTTP status (e.g. a 400 caused
|
|
2586
|
+
* by an out-of-range swap amount vs. a generic validation failure).
|
|
2587
|
+
*
|
|
2588
|
+
* @internal
|
|
2589
|
+
*/ const ProxyErrorCode = {
|
|
2590
|
+
/** Swap amount outside the upstream provider's accepted bounds (HTTP 400) */ INVALID_SWAP_AMOUNT: 331017,
|
|
2591
|
+
/** Upstream liquidity insufficient for the requested size (HTTP 503) */ LOW_LIQUIDITY: 331018
|
|
2592
|
+
};
|
|
2593
|
+
/**
|
|
2594
|
+
* Parses raw HTTP API errors into structured KitError instances.
|
|
2595
|
+
*
|
|
2596
|
+
* This function uses pattern matching to identify common HTTP error types
|
|
2597
|
+
* and converts them into standardized KitError format. It handles errors
|
|
2598
|
+
* from fetch, HTTP status codes, timeouts, and network failures.
|
|
2599
|
+
*
|
|
2600
|
+
* The parser recognizes the following error patterns:
|
|
2601
|
+
* - Client errors (4xx) - validation, authentication, not found
|
|
2602
|
+
* - Server errors (5xx) - service unavailability
|
|
2603
|
+
* - Timeout errors
|
|
2604
|
+
* - Network connectivity errors
|
|
2605
|
+
* - Rate limiting
|
|
2606
|
+
*
|
|
2607
|
+
* Unrecognized errors are treated as fatal `SERVICE` errors, as their
|
|
2608
|
+
* cause and recoverability are unknown.
|
|
2609
|
+
*
|
|
2610
|
+
* @param error - The raw error from the API call
|
|
2611
|
+
* @param context - Context information including operation name
|
|
2612
|
+
* @returns A structured KitError instance
|
|
2613
|
+
*
|
|
2614
|
+
* @example
|
|
2615
|
+
* ```typescript
|
|
2616
|
+
* try {
|
|
2617
|
+
* const response = await fetch(url)
|
|
2618
|
+
* } catch (error) {
|
|
2619
|
+
* throw parseApiError(error, { operation: 'getQuote' })
|
|
2620
|
+
* }
|
|
2621
|
+
* ```
|
|
2622
|
+
*/ function parseApiError(error, context) {
|
|
2623
|
+
// If it's already a KitError, return it as-is
|
|
2624
|
+
if (error instanceof KitError) {
|
|
2625
|
+
return error;
|
|
2626
|
+
}
|
|
2627
|
+
const msg = getErrorMessage(error);
|
|
2628
|
+
const statusCode = extractHttpStatusCode(msg);
|
|
2629
|
+
const serviceName = context.service ?? 'Stablecoin Service';
|
|
2630
|
+
const operation = context.operation ?? 'API';
|
|
2631
|
+
const responseBody = extractResponseBody(error);
|
|
2632
|
+
// Rate limit errors (429)
|
|
2633
|
+
if (statusCode === 429 || /too many requests|rate limit exceeded/i.test(msg)) {
|
|
2634
|
+
return handleRateLimitError(serviceName, operation, error);
|
|
2635
|
+
}
|
|
2636
|
+
// HTTP 4xx Client Errors
|
|
2637
|
+
if (statusCode !== null && statusCode >= 400 && statusCode < 500) {
|
|
2638
|
+
return handleClientError(statusCode, serviceName, operation, error, msg, responseBody);
|
|
2639
|
+
}
|
|
2640
|
+
// HTTP 5xx Server Errors
|
|
2641
|
+
if (statusCode !== null && statusCode >= 500 && statusCode < 600) {
|
|
2642
|
+
return handleServerError(statusCode, serviceName, operation, error, responseBody);
|
|
2643
|
+
}
|
|
2644
|
+
// Timeout errors
|
|
2645
|
+
if (/timeout|timed out/i.test(msg)) {
|
|
2646
|
+
return handleTimeoutError(serviceName, operation, error);
|
|
2647
|
+
}
|
|
2648
|
+
// Network connectivity errors
|
|
2649
|
+
if (/connection (refused|failed)|network|ENOTFOUND|ECONNREFUSED|ETIMEDOUT/i.test(msg)) {
|
|
2650
|
+
return handleConnectionError(serviceName, operation, error);
|
|
2651
|
+
}
|
|
2652
|
+
// Fallback: Unknown error - use UNKNOWN_ERROR with fatal recoverability since we don't know what the error is
|
|
2653
|
+
return new KitError({
|
|
2654
|
+
...ServiceError.UNKNOWN_ERROR,
|
|
2655
|
+
recoverability: 'FATAL',
|
|
2656
|
+
message: `${serviceName} ${operation} failed: ${msg.length > 0 ? msg : 'Unknown error'}`,
|
|
2657
|
+
cause: {
|
|
2658
|
+
trace: error
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Handles HTTP 4xx client errors and maps them to appropriate KitError instances.
|
|
2664
|
+
*
|
|
2665
|
+
* @param statusCode - The HTTP status code
|
|
2666
|
+
* @param serviceName - The name of the service
|
|
2667
|
+
* @param operation - The operation name
|
|
2668
|
+
* @param error - The raw error object
|
|
2669
|
+
* @param msg - The message extracted from the error
|
|
2670
|
+
* @param responseBody - The parsed JSON response body from the server, if available
|
|
2671
|
+
* @returns A KitError instance
|
|
2672
|
+
*/ function handleClientError(statusCode, serviceName, operation, error, msg, responseBody) {
|
|
2673
|
+
const detail = extractDetailFromBody(responseBody) ?? msg;
|
|
2674
|
+
switch(statusCode){
|
|
2675
|
+
// 401/403 - Authentication/Authorization
|
|
2676
|
+
case 401:
|
|
2677
|
+
case 403:
|
|
2678
|
+
return new KitError({
|
|
2679
|
+
...InputError.VALIDATION_FAILED,
|
|
2680
|
+
recoverability: 'FATAL',
|
|
2681
|
+
message: `${serviceName} ${operation} failed: Invalid or missing API key or authorization`,
|
|
2682
|
+
cause: {
|
|
2683
|
+
trace: error
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
// 404 - Not found - unsupported route OR stop-limit / slippage constraint not met
|
|
2687
|
+
case 404:
|
|
2688
|
+
if (isSlippageConstraintFailure(responseBody)) {
|
|
2689
|
+
return new KitError({
|
|
2690
|
+
...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
|
|
2691
|
+
recoverability: 'RETRYABLE',
|
|
2692
|
+
message: `${serviceName} ${operation} failed: ${detail}. ` + 'Try increasing slippageBps or adjusting stopLimit.',
|
|
2693
|
+
cause: {
|
|
2694
|
+
trace: error
|
|
2695
|
+
}
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
return new KitError({
|
|
2699
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
2700
|
+
recoverability: 'FATAL',
|
|
2701
|
+
message: `${serviceName} ${operation} failed: Route or resource not found. Details: ${detail}`,
|
|
2702
|
+
cause: {
|
|
2703
|
+
trace: error
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
// 422 Unprocessable Entity
|
|
2707
|
+
// Proxy service is mapping 422 to INSUFFICIENT_SWAP_AMOUNT
|
|
2708
|
+
case 422:
|
|
2709
|
+
return new KitError({
|
|
2710
|
+
...InputError.INSUFFICIENT_SWAP_AMOUNT,
|
|
2711
|
+
recoverability: 'FATAL',
|
|
2712
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2713
|
+
cause: {
|
|
2714
|
+
trace: error
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2717
|
+
// 400 Bad Request - Invalid token, amount-out-of-range, or validation failed
|
|
2718
|
+
// Proxy maps 400 to UNSUPPORTED_TOKEN | AMOUNT_OUT_OF_RANGE | VALIDATION_FAILED
|
|
2719
|
+
case 400:
|
|
2720
|
+
if (responseBody?.code === ProxyErrorCode.INVALID_SWAP_AMOUNT) {
|
|
2721
|
+
const amountErr = extractAmountError(responseBody);
|
|
2722
|
+
return new KitError({
|
|
2723
|
+
...InputError.AMOUNT_OUT_OF_RANGE,
|
|
2724
|
+
recoverability: 'FATAL',
|
|
2725
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2726
|
+
cause: {
|
|
2727
|
+
trace: {
|
|
2728
|
+
rawError: error,
|
|
2729
|
+
minAmount: amountErr?.minAmount,
|
|
2730
|
+
maxAmount: amountErr?.maxAmount,
|
|
2731
|
+
token: amountErr?.token
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
return new KitError({
|
|
2737
|
+
...InputError.VALIDATION_FAILED,
|
|
2738
|
+
recoverability: 'FATAL',
|
|
2739
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2740
|
+
cause: {
|
|
2741
|
+
trace: error
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
default:
|
|
2745
|
+
// Other 4xx errors - treat as validation failures
|
|
2746
|
+
return new KitError({
|
|
2747
|
+
...InputError.VALIDATION_FAILED,
|
|
2748
|
+
recoverability: 'FATAL',
|
|
2749
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2750
|
+
cause: {
|
|
2751
|
+
trace: error
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Pattern that matches proxy response body text indicating the 404 was
|
|
2758
|
+
* caused by a slippage / price constraint rather than a truly unsupported
|
|
2759
|
+
* route. Kept case-insensitive so future proxy wording changes are tolerated.
|
|
2760
|
+
*
|
|
2761
|
+
* @internal
|
|
2762
|
+
*/ const SLIPPAGE_BODY_PATTERN = /slippage|stop.?limit|price.?impact|minimum.?output|SLIPPAGE_CONSTRAINT_NOT_MET/i;
|
|
2763
|
+
/**
|
|
2764
|
+
* Determine whether a 404 was caused by an unmet slippage or price
|
|
2765
|
+
* constraint rather than a genuinely unsupported route.
|
|
2766
|
+
*
|
|
2767
|
+
* Detection relies on the proxy response body containing slippage-related
|
|
2768
|
+
* language or a structured reason code. This avoids false positives that
|
|
2769
|
+
* would occur if we guessed based on request parameters alone (a user
|
|
2770
|
+
* can set `slippageBps` and still hit a truly unsupported route).
|
|
2771
|
+
*
|
|
2772
|
+
* @param responseBody - The parsed JSON body returned by the proxy
|
|
2773
|
+
* @returns `true` when the 404 should be treated as a slippage constraint failure
|
|
2774
|
+
* @internal
|
|
2775
|
+
*/ function isSlippageConstraintFailure(responseBody) {
|
|
2776
|
+
if (responseBody === undefined) {
|
|
2777
|
+
return false;
|
|
2778
|
+
}
|
|
2779
|
+
const textsToCheck = [
|
|
2780
|
+
responseBody.externalMessage,
|
|
2781
|
+
responseBody.message,
|
|
2782
|
+
extractDetailFromBody(responseBody)
|
|
2783
|
+
];
|
|
2784
|
+
return textsToCheck.some((t)=>typeof t === 'string' && SLIPPAGE_BODY_PATTERN.test(t));
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Handles HTTP 5xx server errors and maps them to appropriate KitError instances.
|
|
2788
|
+
*
|
|
2789
|
+
* Recognizes proxy-specific structured codes in `responseBody.code` and routes
|
|
2790
|
+
* known conditions (e.g. {@link ProxyErrorCode.LOW_LIQUIDITY} on 503) to their
|
|
2791
|
+
* dedicated KitError. Falls back to a generic retryable `SERVICE_INTERNAL_ERROR`
|
|
2792
|
+
* when no specific code is present.
|
|
2793
|
+
*
|
|
2794
|
+
* @param statusCode - The HTTP status code
|
|
2795
|
+
* @param serviceName - The name of the service
|
|
2796
|
+
* @param operation - The operation name
|
|
2797
|
+
* @param error - The raw error object
|
|
2798
|
+
* @param responseBody - The parsed JSON response body from the server, if available
|
|
2799
|
+
* @returns A KitError instance
|
|
2800
|
+
*/ function handleServerError(statusCode, serviceName, operation, error, responseBody) {
|
|
2801
|
+
// 503 + 331018 = upstream liquidity insufficient (proxy)
|
|
2802
|
+
if (statusCode === 503 && responseBody?.code === ProxyErrorCode.LOW_LIQUIDITY) {
|
|
2803
|
+
const amountErr = extractAmountError(responseBody);
|
|
2804
|
+
const detail = extractDetailFromBody(responseBody) ?? getErrorMessage(error);
|
|
2805
|
+
return new KitError({
|
|
2806
|
+
...LiquidityError.INSUFFICIENT_LIQUIDITY,
|
|
2807
|
+
recoverability: 'RETRYABLE',
|
|
2808
|
+
message: `${serviceName} ${operation} failed: ${detail}`,
|
|
2809
|
+
cause: {
|
|
2810
|
+
trace: {
|
|
2811
|
+
rawError: error,
|
|
2812
|
+
minAmount: amountErr?.minAmount,
|
|
2813
|
+
maxAmount: amountErr?.maxAmount,
|
|
2814
|
+
token: amountErr?.token
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
return new KitError({
|
|
2820
|
+
...ServiceError.INTERNAL_ERROR,
|
|
2821
|
+
recoverability: 'RETRYABLE',
|
|
2822
|
+
message: `${serviceName} ${operation} failed: Server error (${statusCode.toString()})`,
|
|
2823
|
+
cause: {
|
|
2824
|
+
trace: error
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Handles network connection errors and maps them to appropriate KitError instances.
|
|
2830
|
+
*
|
|
2831
|
+
* @param serviceName - The name of the service
|
|
2832
|
+
* @param operation - The operation name
|
|
2833
|
+
* @param error - The raw error object
|
|
2834
|
+
* @returns A KitError instance
|
|
2835
|
+
*/ function handleConnectionError(serviceName, operation, error) {
|
|
2836
|
+
return new KitError({
|
|
2837
|
+
...NetworkError.CONNECTION_FAILED,
|
|
2838
|
+
recoverability: 'RETRYABLE',
|
|
2839
|
+
message: `${serviceName} ${operation} failed: Network connection error`,
|
|
2840
|
+
cause: {
|
|
2841
|
+
trace: error
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Handles rate limit errors and maps them to appropriate KitError instances.
|
|
2847
|
+
*
|
|
2848
|
+
* @param serviceName - The name of the service
|
|
2849
|
+
* @param operation - The operation name
|
|
2850
|
+
* @param error - The raw error object
|
|
2851
|
+
* @returns A KitError instance
|
|
2852
|
+
*/ function handleRateLimitError(serviceName, operation, error) {
|
|
2853
|
+
return new KitError({
|
|
2854
|
+
...RateLimitError.RATE_LIMIT_EXCEEDED,
|
|
2855
|
+
recoverability: 'RETRYABLE',
|
|
2856
|
+
message: `${serviceName} ${operation} failed: Too many requests, please retry later`,
|
|
2857
|
+
cause: {
|
|
2858
|
+
trace: error
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Handles timeout errors and maps them to appropriate KitError instances.
|
|
2864
|
+
*
|
|
2865
|
+
* @param serviceName - The name of the service
|
|
2866
|
+
* @param operation - The operation name
|
|
2867
|
+
* @param error - The raw error object
|
|
2868
|
+
* @returns A KitError instance
|
|
2869
|
+
*/ function handleTimeoutError(serviceName, operation, error) {
|
|
2870
|
+
return new KitError({
|
|
2871
|
+
...NetworkError.TIMEOUT,
|
|
2872
|
+
recoverability: 'RETRYABLE',
|
|
2873
|
+
message: `${serviceName} ${operation} failed: Request timeout`,
|
|
2874
|
+
cause: {
|
|
2875
|
+
trace: error
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Type guard that narrows an unknown error to one carrying a non-null
|
|
2881
|
+
* object `responseBody` property (attached by `makeApiRequest`).
|
|
2882
|
+
*
|
|
2883
|
+
* @param error - The raw error from the HTTP layer
|
|
2884
|
+
* @returns `true` when `error.responseBody` is a non-null object
|
|
2885
|
+
* @internal
|
|
2886
|
+
*/ function hasResponseBody(error) {
|
|
2887
|
+
return typeof error === 'object' && error !== null && 'responseBody' in error && typeof error['responseBody'] === 'object' && error['responseBody'] !== null;
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Extract the `responseBody` property that `makeApiRequest` attaches to
|
|
2891
|
+
* HTTP error instances when the server returns a JSON body.
|
|
2892
|
+
*
|
|
2893
|
+
* @param error - The raw error from the HTTP layer
|
|
2894
|
+
* @returns The parsed body cast to {@link ApiErrorResponseBody}, or undefined
|
|
2895
|
+
* @throws Never. Returns `undefined` when the error does not carry a valid
|
|
2896
|
+
* `responseBody`.
|
|
2897
|
+
* @internal
|
|
2898
|
+
*/ function extractResponseBody(error) {
|
|
2899
|
+
if (!hasResponseBody(error)) {
|
|
2900
|
+
return undefined;
|
|
2901
|
+
}
|
|
2902
|
+
return error.responseBody;
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Extract a field name from an {@link ApiFieldError}.
|
|
2906
|
+
*
|
|
2907
|
+
* The proxy service may provide the field name as a plain `field` string
|
|
2908
|
+
* or as a `path` array (e.g. `["tokenInChain"]`). This helper resolves
|
|
2909
|
+
* whichever is available, preferring `field` when both exist.
|
|
2910
|
+
*
|
|
2911
|
+
* @param entry - A single error entry from the `errors` array
|
|
2912
|
+
* @returns The field name, or `undefined` when neither is available
|
|
2913
|
+
* @internal
|
|
2914
|
+
*/ function extractFieldName(entry) {
|
|
2915
|
+
if (typeof entry.field === 'string' && entry.field.length > 0) {
|
|
2916
|
+
return entry.field;
|
|
2917
|
+
}
|
|
2918
|
+
if (Array.isArray(entry.path) && entry.path.length > 0) {
|
|
2919
|
+
const first = entry.path[0];
|
|
2920
|
+
if (typeof first === 'string' && first.length > 0) {
|
|
2921
|
+
return first;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
return undefined;
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Join field-level error entries into a single human-readable string.
|
|
2928
|
+
*
|
|
2929
|
+
* Each entry is formatted as `"field: message"` when a field name is
|
|
2930
|
+
* available (via `field` or `path`), or just the message otherwise.
|
|
2931
|
+
* Entries without a usable message (including amount-bound entries that
|
|
2932
|
+
* only carry `minAmount`/`maxAmount`/`token`) are skipped.
|
|
2933
|
+
*
|
|
2934
|
+
* @param errors - The `errors` array from the response body
|
|
2935
|
+
* @returns A joined string, or `undefined` when no usable entries exist
|
|
2936
|
+
* @internal
|
|
2937
|
+
*/ function joinFieldErrors(errors) {
|
|
2938
|
+
const parts = errors.map((entry)=>{
|
|
2939
|
+
if (!isFieldEntry(entry)) {
|
|
2940
|
+
return undefined;
|
|
2941
|
+
}
|
|
2942
|
+
const fieldMsg = typeof entry.message === 'string' && entry.message.length > 0 ? entry.message : undefined;
|
|
2943
|
+
if (fieldMsg === undefined) {
|
|
2944
|
+
return undefined;
|
|
2945
|
+
}
|
|
2946
|
+
const fieldName = extractFieldName(entry);
|
|
2947
|
+
return fieldName === undefined ? fieldMsg : `${fieldName}: ${fieldMsg}`;
|
|
2948
|
+
}).filter((s)=>s !== undefined);
|
|
2949
|
+
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Narrow an {@link ApiErrorItem} to the field-error shape used for
|
|
2953
|
+
* validation messages. Amount-bound entries (which carry no `message`)
|
|
2954
|
+
* are excluded.
|
|
2955
|
+
*
|
|
2956
|
+
* @internal
|
|
2957
|
+
*/ function isFieldEntry(entry) {
|
|
2958
|
+
return 'message' in entry || 'field' in entry || 'path' in entry;
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Narrow an {@link ApiErrorItem} to the amount-bound shape emitted by the
|
|
2962
|
+
* proxy for {@link ProxyErrorCode.INVALID_SWAP_AMOUNT}.
|
|
2963
|
+
*
|
|
2964
|
+
* @internal
|
|
2965
|
+
*/ function isAmountEntry(entry) {
|
|
2966
|
+
return 'minAmount' in entry || 'maxAmount' in entry || 'token' in entry;
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Extract the first amount-bound entry from a response body, if any.
|
|
2970
|
+
*
|
|
2971
|
+
* @internal
|
|
2972
|
+
*/ function extractAmountError(body) {
|
|
2973
|
+
if (body === undefined || !Array.isArray(body.errors)) {
|
|
2974
|
+
return undefined;
|
|
2975
|
+
}
|
|
2976
|
+
return body.errors.find(isAmountEntry);
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Derive a human-readable detail string from an {@link ApiErrorResponseBody}.
|
|
2980
|
+
*
|
|
2981
|
+
* Resolution order:
|
|
2982
|
+
* 1. `body.externalMessage` -- the user-facing string the proxy intends
|
|
2983
|
+
* consumers to display (e.g. "No route found that satisfies the
|
|
2984
|
+
* requested stop limit"). Preferred when available.
|
|
2985
|
+
* 2. `body.message` **and** `body.errors` -- when both are present the
|
|
2986
|
+
* top-level message is combined with the field-level detail so
|
|
2987
|
+
* developers see the full picture
|
|
2988
|
+
* (e.g. `"Validation error: tokenInChain: Invalid input; amount: …"`).
|
|
2989
|
+
* 3. `body.message` alone -- used as-is.
|
|
2990
|
+
* 4. `body.errors` alone -- field-level entries joined with "; ".
|
|
2991
|
+
* 5. `undefined` -- caller should fall back to the raw HTTP status text.
|
|
2992
|
+
*
|
|
2993
|
+
* @param body - The parsed response body, may be undefined
|
|
2994
|
+
* @returns A detail string, or undefined when no useful info is available
|
|
2995
|
+
* @internal
|
|
2996
|
+
*/ function extractDetailFromBody(body) {
|
|
2997
|
+
if (body === undefined) {
|
|
2998
|
+
return undefined;
|
|
2999
|
+
}
|
|
3000
|
+
const externalMessage = typeof body.externalMessage === 'string' && body.externalMessage.length > 0 ? body.externalMessage : undefined;
|
|
3001
|
+
if (externalMessage !== undefined) {
|
|
3002
|
+
return externalMessage;
|
|
3003
|
+
}
|
|
3004
|
+
const topMessage = typeof body.message === 'string' && body.message.length > 0 ? body.message : undefined;
|
|
3005
|
+
const fieldDetail = Array.isArray(body.errors) && body.errors.length > 0 ? joinFieldErrors(body.errors) : undefined;
|
|
3006
|
+
if (topMessage !== undefined && fieldDetail !== undefined) {
|
|
3007
|
+
return `${topMessage}: ${fieldDetail}`;
|
|
3008
|
+
}
|
|
3009
|
+
return topMessage ?? fieldDetail;
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Extracts the HTTP status code from an error message.
|
|
3013
|
+
*
|
|
3014
|
+
* Attempts to parse HTTP status codes from common error message formats,
|
|
3015
|
+
* such as "HTTP 404" or "Status: 500".
|
|
3016
|
+
*
|
|
3017
|
+
* @param msg - The error message to extract from
|
|
3018
|
+
* @returns The extracted HTTP status code, or null if not found
|
|
3019
|
+
*
|
|
3020
|
+
* @example
|
|
3021
|
+
* ```typescript
|
|
3022
|
+
* const code = extractHttpStatusCode('HTTP 404')
|
|
3023
|
+
* // Returns: 404
|
|
3024
|
+
* ```
|
|
3025
|
+
*
|
|
3026
|
+
* @example
|
|
3027
|
+
* ```typescript
|
|
3028
|
+
* const code = extractHttpStatusCode('Status: 500 Internal Server Error')
|
|
3029
|
+
* // Returns: 500
|
|
3030
|
+
* ```
|
|
3031
|
+
*/ function extractHttpStatusCode(msg) {
|
|
3032
|
+
// Pattern: "HTTP 404" or "HTTP 404 - some message" or "Status: 404"
|
|
3033
|
+
const patterns = [
|
|
3034
|
+
/HTTP (\d{3})/i,
|
|
3035
|
+
/Status:\s*(\d{3})/i,
|
|
3036
|
+
/^(\d{3}) -/
|
|
3037
|
+
];
|
|
3038
|
+
for (const pattern of patterns){
|
|
3039
|
+
const match = pattern.exec(msg);
|
|
3040
|
+
const codeStr = match?.at(1);
|
|
3041
|
+
if (codeStr !== undefined) {
|
|
3042
|
+
const code = Number.parseInt(codeStr, 10);
|
|
3043
|
+
// Validate it's a valid HTTP status code
|
|
3044
|
+
if (code >= 100 && code < 600) {
|
|
3045
|
+
return code;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
|
|
2473
3052
|
/**
|
|
2474
3053
|
* @packageDocumentation
|
|
2475
3054
|
* @module ChainDefinitions
|
|
@@ -2541,6 +3120,8 @@ class KitError extends Error {
|
|
|
2541
3120
|
Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
|
|
2542
3121
|
Blockchain["Pharos"] = "Pharos";
|
|
2543
3122
|
Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
|
|
3123
|
+
Blockchain["Plasma"] = "Plasma";
|
|
3124
|
+
Blockchain["Plasma_Testnet"] = "Plasma_Testnet";
|
|
2544
3125
|
Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
|
|
2545
3126
|
Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
|
|
2546
3127
|
Blockchain["Plume"] = "Plume";
|
|
@@ -2610,6 +3191,7 @@ var BridgeChain;
|
|
|
2610
3191
|
BridgeChain["Morph"] = "Morph";
|
|
2611
3192
|
BridgeChain["Optimism"] = "Optimism";
|
|
2612
3193
|
BridgeChain["Pharos"] = "Pharos";
|
|
3194
|
+
BridgeChain["Plasma"] = "Plasma";
|
|
2613
3195
|
BridgeChain["Plume"] = "Plume";
|
|
2614
3196
|
BridgeChain["Polygon"] = "Polygon";
|
|
2615
3197
|
BridgeChain["Sei"] = "Sei";
|
|
@@ -2636,6 +3218,7 @@ var BridgeChain;
|
|
|
2636
3218
|
BridgeChain["Morph_Testnet"] = "Morph_Testnet";
|
|
2637
3219
|
BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
|
|
2638
3220
|
BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
|
|
3221
|
+
BridgeChain["Plasma_Testnet"] = "Plasma_Testnet";
|
|
2639
3222
|
BridgeChain["Plume_Testnet"] = "Plume_Testnet";
|
|
2640
3223
|
BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
|
|
2641
3224
|
BridgeChain["Sei_Testnet"] = "Sei_Testnet";
|
|
@@ -3082,6 +3665,8 @@ var EarnChain;
|
|
|
3082
3665
|
* This program handles minting operations for Gateway transactions
|
|
3083
3666
|
* on Solana devnet.
|
|
3084
3667
|
*/ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
|
|
3668
|
+
/** TokenMessengerWithFees address shared by enabled EVM mainnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
|
|
3669
|
+
/** TokenMessengerWithFees address shared by enabled EVM testnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
|
|
3085
3670
|
|
|
3086
3671
|
/**
|
|
3087
3672
|
* Arc Testnet chain definition
|
|
@@ -3119,6 +3704,7 @@ var EarnChain;
|
|
|
3119
3704
|
v2: {
|
|
3120
3705
|
type: 'split',
|
|
3121
3706
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
3707
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3122
3708
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
3123
3709
|
confirmations: 1,
|
|
3124
3710
|
fastConfirmations: 1
|
|
@@ -3186,6 +3772,7 @@ var EarnChain;
|
|
|
3186
3772
|
v2: {
|
|
3187
3773
|
type: 'split',
|
|
3188
3774
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
3775
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3189
3776
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3190
3777
|
confirmations: 65,
|
|
3191
3778
|
fastConfirmations: 1
|
|
@@ -3250,6 +3837,7 @@ var EarnChain;
|
|
|
3250
3837
|
v2: {
|
|
3251
3838
|
type: 'split',
|
|
3252
3839
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
3840
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3253
3841
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
3254
3842
|
confirmations: 65,
|
|
3255
3843
|
fastConfirmations: 1
|
|
@@ -3314,6 +3902,7 @@ var EarnChain;
|
|
|
3314
3902
|
v2: {
|
|
3315
3903
|
type: 'split',
|
|
3316
3904
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
3905
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3317
3906
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3318
3907
|
confirmations: 1,
|
|
3319
3908
|
fastConfirmations: 1
|
|
@@ -3375,6 +3964,7 @@ var EarnChain;
|
|
|
3375
3964
|
v2: {
|
|
3376
3965
|
type: 'split',
|
|
3377
3966
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
3967
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3378
3968
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3379
3969
|
confirmations: 1,
|
|
3380
3970
|
fastConfirmations: 1
|
|
@@ -3442,6 +4032,7 @@ var EarnChain;
|
|
|
3442
4032
|
v2: {
|
|
3443
4033
|
type: 'split',
|
|
3444
4034
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4035
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3445
4036
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3446
4037
|
confirmations: 65,
|
|
3447
4038
|
fastConfirmations: 1
|
|
@@ -3506,6 +4097,7 @@ var EarnChain;
|
|
|
3506
4097
|
v2: {
|
|
3507
4098
|
type: 'split',
|
|
3508
4099
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
4100
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3509
4101
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3510
4102
|
confirmations: 65,
|
|
3511
4103
|
fastConfirmations: 1
|
|
@@ -3616,6 +4208,7 @@ var EarnChain;
|
|
|
3616
4208
|
v2: {
|
|
3617
4209
|
type: 'split',
|
|
3618
4210
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4211
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3619
4212
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3620
4213
|
confirmations: 65,
|
|
3621
4214
|
fastConfirmations: 1
|
|
@@ -3660,6 +4253,7 @@ var EarnChain;
|
|
|
3660
4253
|
v2: {
|
|
3661
4254
|
type: 'split',
|
|
3662
4255
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
4256
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3663
4257
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3664
4258
|
confirmations: 65,
|
|
3665
4259
|
fastConfirmations: 1
|
|
@@ -3891,6 +4485,7 @@ var EarnChain;
|
|
|
3891
4485
|
v2: {
|
|
3892
4486
|
type: 'split',
|
|
3893
4487
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4488
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
3894
4489
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
3895
4490
|
confirmations: 65,
|
|
3896
4491
|
fastConfirmations: 2
|
|
@@ -3955,6 +4550,7 @@ var EarnChain;
|
|
|
3955
4550
|
v2: {
|
|
3956
4551
|
type: 'split',
|
|
3957
4552
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
4553
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
3958
4554
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
3959
4555
|
confirmations: 65,
|
|
3960
4556
|
fastConfirmations: 2
|
|
@@ -4065,6 +4661,7 @@ var EarnChain;
|
|
|
4065
4661
|
v2: {
|
|
4066
4662
|
type: 'split',
|
|
4067
4663
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4664
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4068
4665
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4069
4666
|
confirmations: 1,
|
|
4070
4667
|
fastConfirmations: 1
|
|
@@ -4124,6 +4721,7 @@ var EarnChain;
|
|
|
4124
4721
|
v2: {
|
|
4125
4722
|
type: 'split',
|
|
4126
4723
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
4724
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4127
4725
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
4128
4726
|
confirmations: 1,
|
|
4129
4727
|
fastConfirmations: 1
|
|
@@ -4278,6 +4876,7 @@ var EarnChain;
|
|
|
4278
4876
|
v2: {
|
|
4279
4877
|
type: 'split',
|
|
4280
4878
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4879
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4281
4880
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4282
4881
|
confirmations: 65,
|
|
4283
4882
|
fastConfirmations: 1
|
|
@@ -4325,6 +4924,7 @@ var EarnChain;
|
|
|
4325
4924
|
v2: {
|
|
4326
4925
|
type: 'split',
|
|
4327
4926
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
4927
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4328
4928
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
4329
4929
|
confirmations: 65,
|
|
4330
4930
|
fastConfirmations: 1
|
|
@@ -4369,6 +4969,7 @@ var EarnChain;
|
|
|
4369
4969
|
v2: {
|
|
4370
4970
|
type: 'split',
|
|
4371
4971
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
4972
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4372
4973
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4373
4974
|
confirmations: 1,
|
|
4374
4975
|
fastConfirmations: 1
|
|
@@ -4414,6 +5015,7 @@ var EarnChain;
|
|
|
4414
5015
|
v2: {
|
|
4415
5016
|
type: 'split',
|
|
4416
5017
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
5018
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4417
5019
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
4418
5020
|
confirmations: 1,
|
|
4419
5021
|
fastConfirmations: 1
|
|
@@ -4460,6 +5062,7 @@ var EarnChain;
|
|
|
4460
5062
|
v2: {
|
|
4461
5063
|
type: 'split',
|
|
4462
5064
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5065
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4463
5066
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4464
5067
|
confirmations: 1,
|
|
4465
5068
|
fastConfirmations: 1
|
|
@@ -4773,6 +5376,7 @@ var EarnChain;
|
|
|
4773
5376
|
v2: {
|
|
4774
5377
|
type: 'split',
|
|
4775
5378
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5379
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4776
5380
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4777
5381
|
confirmations: 65,
|
|
4778
5382
|
fastConfirmations: 1
|
|
@@ -4837,6 +5441,7 @@ var EarnChain;
|
|
|
4837
5441
|
v2: {
|
|
4838
5442
|
type: 'split',
|
|
4839
5443
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
5444
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
4840
5445
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
4841
5446
|
confirmations: 65,
|
|
4842
5447
|
fastConfirmations: 1
|
|
@@ -4958,19 +5563,111 @@ var EarnChain;
|
|
|
4958
5563
|
});
|
|
4959
5564
|
|
|
4960
5565
|
/**
|
|
4961
|
-
*
|
|
5566
|
+
* Plasma Mainnet chain definition
|
|
4962
5567
|
* @remarks
|
|
4963
|
-
* This represents the official production network for the
|
|
4964
|
-
*
|
|
4965
|
-
* with
|
|
4966
|
-
*/ const
|
|
5568
|
+
* This represents the official production network for the Plasma blockchain.
|
|
5569
|
+
* Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
|
|
5570
|
+
* stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
|
|
5571
|
+
*/ const Plasma = defineChain({
|
|
4967
5572
|
type: 'evm',
|
|
4968
|
-
chain: Blockchain.
|
|
4969
|
-
name: '
|
|
4970
|
-
title: '
|
|
5573
|
+
chain: Blockchain.Plasma,
|
|
5574
|
+
name: 'Plasma',
|
|
5575
|
+
title: 'Plasma Mainnet',
|
|
4971
5576
|
nativeCurrency: {
|
|
4972
|
-
name: '
|
|
4973
|
-
symbol: '
|
|
5577
|
+
name: 'Plasma',
|
|
5578
|
+
symbol: 'XPL',
|
|
5579
|
+
decimals: 18
|
|
5580
|
+
},
|
|
5581
|
+
chainId: 9745,
|
|
5582
|
+
isTestnet: false,
|
|
5583
|
+
explorerUrl: 'https://plasmascan.to/tx/{hash}',
|
|
5584
|
+
rpcEndpoints: [
|
|
5585
|
+
'https://rpc.plasma.to'
|
|
5586
|
+
],
|
|
5587
|
+
eurcAddress: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
|
|
5588
|
+
usdcAddress: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
|
|
5589
|
+
usdtAddress: null,
|
|
5590
|
+
cctp: {
|
|
5591
|
+
domain: 33,
|
|
5592
|
+
contracts: {
|
|
5593
|
+
v2: {
|
|
5594
|
+
type: 'split',
|
|
5595
|
+
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5596
|
+
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5597
|
+
confirmations: 3,
|
|
5598
|
+
fastConfirmations: 1
|
|
5599
|
+
}
|
|
5600
|
+
},
|
|
5601
|
+
forwarderSupported: {
|
|
5602
|
+
source: false,
|
|
5603
|
+
destination: false
|
|
5604
|
+
}
|
|
5605
|
+
},
|
|
5606
|
+
kitContracts: {
|
|
5607
|
+
bridge: BRIDGE_CONTRACT_EVM_MAINNET
|
|
5608
|
+
}
|
|
5609
|
+
});
|
|
5610
|
+
|
|
5611
|
+
/**
|
|
5612
|
+
* Plasma Testnet chain definition
|
|
5613
|
+
* @remarks
|
|
5614
|
+
* This represents the official test network for the Plasma blockchain.
|
|
5615
|
+
* Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
|
|
5616
|
+
* stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
|
|
5617
|
+
*/ const PlasmaTestnet = defineChain({
|
|
5618
|
+
type: 'evm',
|
|
5619
|
+
chain: Blockchain.Plasma_Testnet,
|
|
5620
|
+
name: 'Plasma Testnet',
|
|
5621
|
+
title: 'Plasma Testnet',
|
|
5622
|
+
nativeCurrency: {
|
|
5623
|
+
name: 'Plasma',
|
|
5624
|
+
symbol: 'XPL',
|
|
5625
|
+
decimals: 18
|
|
5626
|
+
},
|
|
5627
|
+
chainId: 9746,
|
|
5628
|
+
isTestnet: true,
|
|
5629
|
+
explorerUrl: 'https://testnet.plasmascan.to/tx/{hash}',
|
|
5630
|
+
rpcEndpoints: [
|
|
5631
|
+
'https://testnet-rpc.plasma.to'
|
|
5632
|
+
],
|
|
5633
|
+
eurcAddress: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330',
|
|
5634
|
+
usdcAddress: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
|
|
5635
|
+
usdtAddress: null,
|
|
5636
|
+
cctp: {
|
|
5637
|
+
domain: 33,
|
|
5638
|
+
contracts: {
|
|
5639
|
+
v2: {
|
|
5640
|
+
type: 'split',
|
|
5641
|
+
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
5642
|
+
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5643
|
+
confirmations: 3,
|
|
5644
|
+
fastConfirmations: 1
|
|
5645
|
+
}
|
|
5646
|
+
},
|
|
5647
|
+
forwarderSupported: {
|
|
5648
|
+
source: false,
|
|
5649
|
+
destination: false
|
|
5650
|
+
}
|
|
5651
|
+
},
|
|
5652
|
+
kitContracts: {
|
|
5653
|
+
bridge: BRIDGE_CONTRACT_EVM_TESTNET
|
|
5654
|
+
}
|
|
5655
|
+
});
|
|
5656
|
+
|
|
5657
|
+
/**
|
|
5658
|
+
* Plume Mainnet chain definition
|
|
5659
|
+
* @remarks
|
|
5660
|
+
* This represents the official production network for the Plume blockchain.
|
|
5661
|
+
* Plume is a Layer 1 blockchain specialized for DeFi and trading applications
|
|
5662
|
+
* with native orderbook and matching engine.
|
|
5663
|
+
*/ const Plume = defineChain({
|
|
5664
|
+
type: 'evm',
|
|
5665
|
+
chain: Blockchain.Plume,
|
|
5666
|
+
name: 'Plume',
|
|
5667
|
+
title: 'Plume Mainnet',
|
|
5668
|
+
nativeCurrency: {
|
|
5669
|
+
name: 'Plume',
|
|
5670
|
+
symbol: 'PLUME',
|
|
4974
5671
|
decimals: 18
|
|
4975
5672
|
},
|
|
4976
5673
|
chainId: 98866,
|
|
@@ -4988,6 +5685,7 @@ var EarnChain;
|
|
|
4988
5685
|
v2: {
|
|
4989
5686
|
type: 'split',
|
|
4990
5687
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5688
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
4991
5689
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
4992
5690
|
confirmations: 65,
|
|
4993
5691
|
fastConfirmations: 1
|
|
@@ -5034,6 +5732,7 @@ var EarnChain;
|
|
|
5034
5732
|
v2: {
|
|
5035
5733
|
type: 'split',
|
|
5036
5734
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
5735
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5037
5736
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5038
5737
|
confirmations: 65,
|
|
5039
5738
|
fastConfirmations: 1
|
|
@@ -5135,6 +5834,7 @@ var EarnChain;
|
|
|
5135
5834
|
v2: {
|
|
5136
5835
|
type: 'split',
|
|
5137
5836
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5837
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5138
5838
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5139
5839
|
confirmations: 33,
|
|
5140
5840
|
fastConfirmations: 13
|
|
@@ -5200,6 +5900,7 @@ var EarnChain;
|
|
|
5200
5900
|
v2: {
|
|
5201
5901
|
type: 'split',
|
|
5202
5902
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
5903
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5203
5904
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5204
5905
|
confirmations: 33,
|
|
5205
5906
|
fastConfirmations: 13
|
|
@@ -5259,6 +5960,7 @@ var EarnChain;
|
|
|
5259
5960
|
v2: {
|
|
5260
5961
|
type: 'split',
|
|
5261
5962
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
5963
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5262
5964
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5263
5965
|
confirmations: 1,
|
|
5264
5966
|
fastConfirmations: 1
|
|
@@ -5318,6 +6020,7 @@ var EarnChain;
|
|
|
5318
6020
|
v2: {
|
|
5319
6021
|
type: 'split',
|
|
5320
6022
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6023
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5321
6024
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5322
6025
|
confirmations: 1,
|
|
5323
6026
|
fastConfirmations: 1
|
|
@@ -5375,6 +6078,7 @@ var EarnChain;
|
|
|
5375
6078
|
v2: {
|
|
5376
6079
|
type: 'split',
|
|
5377
6080
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6081
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5378
6082
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5379
6083
|
confirmations: 1,
|
|
5380
6084
|
fastConfirmations: 1
|
|
@@ -5433,6 +6137,7 @@ var EarnChain;
|
|
|
5433
6137
|
v2: {
|
|
5434
6138
|
type: 'split',
|
|
5435
6139
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6140
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5436
6141
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5437
6142
|
confirmations: 1,
|
|
5438
6143
|
fastConfirmations: 1
|
|
@@ -5748,6 +6453,7 @@ var EarnChain;
|
|
|
5748
6453
|
v2: {
|
|
5749
6454
|
type: 'split',
|
|
5750
6455
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6456
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5751
6457
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5752
6458
|
confirmations: 65,
|
|
5753
6459
|
fastConfirmations: 1
|
|
@@ -5812,6 +6518,7 @@ var EarnChain;
|
|
|
5812
6518
|
v2: {
|
|
5813
6519
|
type: 'split',
|
|
5814
6520
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6521
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5815
6522
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
5816
6523
|
confirmations: 65,
|
|
5817
6524
|
fastConfirmations: 1
|
|
@@ -5869,6 +6576,7 @@ var EarnChain;
|
|
|
5869
6576
|
v2: {
|
|
5870
6577
|
type: 'split',
|
|
5871
6578
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cF5d',
|
|
6579
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5872
6580
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5873
6581
|
confirmations: 65,
|
|
5874
6582
|
fastConfirmations: 1
|
|
@@ -5928,6 +6636,7 @@ var EarnChain;
|
|
|
5928
6636
|
v2: {
|
|
5929
6637
|
type: 'split',
|
|
5930
6638
|
tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
6639
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
5931
6640
|
messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
|
|
5932
6641
|
confirmations: 65,
|
|
5933
6642
|
fastConfirmations: 1
|
|
@@ -5988,6 +6697,7 @@ var EarnChain;
|
|
|
5988
6697
|
v2: {
|
|
5989
6698
|
type: 'split',
|
|
5990
6699
|
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6700
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
|
|
5991
6701
|
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
5992
6702
|
confirmations: 3,
|
|
5993
6703
|
fastConfirmations: 3
|
|
@@ -6033,6 +6743,7 @@ var EarnChain;
|
|
|
6033
6743
|
v2: {
|
|
6034
6744
|
type: 'split',
|
|
6035
6745
|
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6746
|
+
tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
|
|
6036
6747
|
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
6037
6748
|
confirmations: 3,
|
|
6038
6749
|
fastConfirmations: 1
|
|
@@ -6243,6 +6954,8 @@ var Chains = {
|
|
|
6243
6954
|
OptimismSepolia: OptimismSepolia,
|
|
6244
6955
|
Pharos: Pharos,
|
|
6245
6956
|
PharosTestnet: PharosTestnet,
|
|
6957
|
+
Plasma: Plasma,
|
|
6958
|
+
PlasmaTestnet: PlasmaTestnet,
|
|
6246
6959
|
Plume: Plume,
|
|
6247
6960
|
PlumeTestnet: PlumeTestnet,
|
|
6248
6961
|
PolkadotAssetHub: PolkadotAssetHub,
|
|
@@ -6292,6 +7005,34 @@ var Chains = {
|
|
|
6292
7005
|
return chain.cctp?.contracts.v2 !== undefined;
|
|
6293
7006
|
}
|
|
6294
7007
|
|
|
7008
|
+
/**
|
|
7009
|
+
* Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
|
|
7010
|
+
*
|
|
7011
|
+
* A chain supports source-paid fees when its CCTP v2 configuration carries a
|
|
7012
|
+
* deployed `TokenMessengerWithFees` wrapper address. Bridge Kit routes
|
|
7013
|
+
* `feePayment: 'source'` transfers through this wrapper via
|
|
7014
|
+
* `depositForBurnWithHookAndFees`, so a chain without the wrapper cannot be a
|
|
7015
|
+
* source for receive-exact bridging.
|
|
7016
|
+
*
|
|
7017
|
+
* @param chain - The chain definition to check.
|
|
7018
|
+
* @returns `true` when the chain has a `tokenMessengerWithFees` wrapper
|
|
7019
|
+
* configured, `false` otherwise.
|
|
7020
|
+
*
|
|
7021
|
+
* @example
|
|
7022
|
+
* ```typescript
|
|
7023
|
+
* import { Chains, hasSourceFeeSupport } from '@core/chains'
|
|
7024
|
+
*
|
|
7025
|
+
* hasSourceFeeSupport(Chains.Optimism) // true
|
|
7026
|
+
* hasSourceFeeSupport(Chains.Solana) // false
|
|
7027
|
+
* ```
|
|
7028
|
+
*/ function hasSourceFeeSupport(chain) {
|
|
7029
|
+
if (!isCCTPV2Supported(chain)) {
|
|
7030
|
+
return false;
|
|
7031
|
+
}
|
|
7032
|
+
const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
|
|
7033
|
+
return typeof wrapper === 'string' && wrapper.length > 0;
|
|
7034
|
+
}
|
|
7035
|
+
|
|
6295
7036
|
/**
|
|
6296
7037
|
* Check if a chain supports a specific type of custom smart contract logic.
|
|
6297
7038
|
*
|
|
@@ -8310,6 +9051,7 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8310
9051
|
[Blockchain.Noble]: 'uusdc',
|
|
8311
9052
|
[Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
|
|
8312
9053
|
[Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
|
|
9054
|
+
[Blockchain.Plasma]: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
|
|
8313
9055
|
[Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
|
|
8314
9056
|
[Blockchain.Polkadot_Asset_Hub]: '1337',
|
|
8315
9057
|
[Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
|
|
@@ -8346,6 +9088,7 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8346
9088
|
[Blockchain.Noble_Testnet]: 'uusdc',
|
|
8347
9089
|
[Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
|
|
8348
9090
|
[Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
|
|
9091
|
+
[Blockchain.Plasma_Testnet]: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
|
|
8349
9092
|
[Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
|
|
8350
9093
|
[Blockchain.Polkadot_Westmint]: '31337',
|
|
8351
9094
|
[Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
|
|
@@ -8408,6 +9151,7 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8408
9151
|
[Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
|
|
8409
9152
|
[Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
|
|
8410
9153
|
[Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
|
|
9154
|
+
[Blockchain.Plasma]: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
|
|
8411
9155
|
[Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
|
|
8412
9156
|
[Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
|
|
8413
9157
|
// =========================================================================
|
|
@@ -8416,7 +9160,8 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8416
9160
|
[Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
|
|
8417
9161
|
[Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
|
|
8418
9162
|
[Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
|
|
8419
|
-
[Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
|
|
9163
|
+
[Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
|
|
9164
|
+
[Blockchain.Plasma_Testnet]: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330'
|
|
8420
9165
|
}
|
|
8421
9166
|
};
|
|
8422
9167
|
|
|
@@ -9240,6 +9985,16 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
9240
9985
|
*
|
|
9241
9986
|
* Set to 0 when no additional Circle-reserved data is needed.
|
|
9242
9987
|
*/ const CCTP_FORWARD_PAYLOAD_LENGTH = 0;
|
|
9988
|
+
/**
|
|
9989
|
+
* Length in bytes of a Solana owner (ed25519 / PDA) public key.
|
|
9990
|
+
*/ const SOLANA_PUBKEY_LENGTH = 32;
|
|
9991
|
+
/**
|
|
9992
|
+
* Byte length of the Solana ATA-creation forwarding payload appended after the
|
|
9993
|
+
* `cctp-forward` frame: `createAta` (1 byte) + `ataOwner` (32 bytes).
|
|
9994
|
+
*
|
|
9995
|
+
* Circle's Orbit relayer decodes exactly this many bytes; see
|
|
9996
|
+
* {@link buildSolanaAtaForwardingHookData}.
|
|
9997
|
+
*/ const SOLANA_ATA_FORWARD_PAYLOAD_LENGTH = 1 + SOLANA_PUBKEY_LENGTH;
|
|
9243
9998
|
/**
|
|
9244
9999
|
* Build the hookData bytes for CCTP forwarding.
|
|
9245
10000
|
*
|
|
@@ -9296,6 +10051,108 @@ function buildForwardingHookData() {
|
|
|
9296
10051
|
cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
|
|
9297
10052
|
return cachedHookDataHex;
|
|
9298
10053
|
}
|
|
10054
|
+
/**
|
|
10055
|
+
* Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
|
|
10056
|
+
* create the recipient's Associated Token Account (ATA) before minting on Solana.
|
|
10057
|
+
*
|
|
10058
|
+
* When an EVM→Solana bridge is forwarded, the destination mint targets the
|
|
10059
|
+
* recipient's USDC ATA — which does not exist for a fresh wallet. This frame
|
|
10060
|
+
* tells the relayer to prepend an idempotent `createAssociatedTokenAccount`
|
|
10061
|
+
* instruction (the relayer pays the rent) so the mint always succeeds.
|
|
10062
|
+
*
|
|
10063
|
+
* Unlike {@link buildForwardingHookData} (an empty version-0 frame), this emits
|
|
10064
|
+
* a version-0 frame whose 32-bit `dataLength` is set to
|
|
10065
|
+
* {@link SOLANA_ATA_FORWARD_PAYLOAD_LENGTH} (33), followed by the payload the
|
|
10066
|
+
* relayer decodes:
|
|
10067
|
+
* - Byte 0: `createAta` flag, always `1`
|
|
10068
|
+
* - Bytes 1-32: the recipient's 32-byte Solana owner public key (`ataOwner`)
|
|
10069
|
+
*
|
|
10070
|
+
* @remarks
|
|
10071
|
+
* `ataOwner` is the recipient's *wallet* public key, not the derived ATA. The
|
|
10072
|
+
* relayer re-derives the ATA from `ataOwner` and the USDC mint and requires it
|
|
10073
|
+
* to equal the burn's `mintRecipient`, so callers must pass the same owner used
|
|
10074
|
+
* to derive `mintRecipient`. The all-zero key is reserved as "absent owner" and
|
|
10075
|
+
* is rejected.
|
|
10076
|
+
*
|
|
10077
|
+
* @param ataOwner - The recipient's 32-byte Solana owner public key.
|
|
10078
|
+
* @returns A 0x-prefixed hex string: the 32-byte frame followed by the 33-byte
|
|
10079
|
+
* Solana ATA payload.
|
|
10080
|
+
* @throws {KitError} If `ataOwner` is not exactly 32 bytes, or is the all-zero
|
|
10081
|
+
* key (INPUT_VALIDATION_FAILED).
|
|
10082
|
+
*
|
|
10083
|
+
* @example
|
|
10084
|
+
* ```typescript
|
|
10085
|
+
* import { PublicKey } from '@solana/web3.js'
|
|
10086
|
+
* import { buildSolanaAtaForwardingHookData } from '@core/utils'
|
|
10087
|
+
*
|
|
10088
|
+
* const owner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM')
|
|
10089
|
+
* const hookData = buildSolanaAtaForwardingHookData(owner.toBytes())
|
|
10090
|
+
*
|
|
10091
|
+
* // Use with the forwarded depositForBurnWithHook action so the relayer
|
|
10092
|
+
* // creates the recipient ATA before minting.
|
|
10093
|
+
* await adapter.prepareAction('cctp.v2.depositForBurnWithHook', {
|
|
10094
|
+
* amount: BigInt('1000000'),
|
|
10095
|
+
* mintRecipient: '0x...',
|
|
10096
|
+
* maxFee: BigInt('50000'),
|
|
10097
|
+
* minFinalityThreshold: 1000,
|
|
10098
|
+
* fromChain: ethereum,
|
|
10099
|
+
* toChain: solana,
|
|
10100
|
+
* hookData,
|
|
10101
|
+
* })
|
|
10102
|
+
* ```
|
|
10103
|
+
*/ function buildSolanaAtaForwardingHookData(ataOwner) {
|
|
10104
|
+
if (!(ataOwner instanceof Uint8Array) || ataOwner.length !== SOLANA_PUBKEY_LENGTH) {
|
|
10105
|
+
throw createValidationFailedError$1('ataOwner', ataOwner, `Expected a ${String(SOLANA_PUBKEY_LENGTH)}-byte Solana owner public key`);
|
|
10106
|
+
}
|
|
10107
|
+
if (ataOwner.every((byte)=>byte === 0)) {
|
|
10108
|
+
throw createValidationFailedError$1('ataOwner', ataOwner, 'Expected a non-zero Solana owner public key; the all-zero key is reserved as "absent owner"');
|
|
10109
|
+
}
|
|
10110
|
+
// Inner payload: createAta(1) + ataOwner(32).
|
|
10111
|
+
const payload = new Uint8Array(SOLANA_ATA_FORWARD_PAYLOAD_LENGTH);
|
|
10112
|
+
payload[0] = 1 // createAta = true
|
|
10113
|
+
;
|
|
10114
|
+
payload.set(ataOwner, 1);
|
|
10115
|
+
// 32-byte header: 24-byte magic + uint32 version(0) + uint32 dataLength(33).
|
|
10116
|
+
// The relayer reads dataLength from the v0 frame to slice the inner payload,
|
|
10117
|
+
// so it MUST reflect the appended byte count (unlike the GenericExecutor path).
|
|
10118
|
+
const frame = new Uint8Array(32);
|
|
10119
|
+
frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
|
|
10120
|
+
const view = new DataView(frame.buffer);
|
|
10121
|
+
view.setUint32(24, CCTP_FORWARD_VERSION, false) // big-endian, 0
|
|
10122
|
+
;
|
|
10123
|
+
view.setUint32(28, SOLANA_ATA_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 33
|
|
10124
|
+
;
|
|
10125
|
+
return bytes.hexlify(bytes.concat([
|
|
10126
|
+
frame,
|
|
10127
|
+
payload
|
|
10128
|
+
]));
|
|
10129
|
+
}
|
|
10130
|
+
|
|
10131
|
+
/**
|
|
10132
|
+
* Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
|
|
10133
|
+
*
|
|
10134
|
+
* Mirrors viem's `pad(address, size 32)` and CCTP's `mintRecipient`
|
|
10135
|
+
* convention. Solana addresses are already 32 bytes and need no padding.
|
|
10136
|
+
*
|
|
10137
|
+
* @param address - A 0x-prefixed 20-byte EVM address.
|
|
10138
|
+
* @returns The address left-zero-padded to a 0x-prefixed 32-byte hex string.
|
|
10139
|
+
* @throws {KitError} If `address` is not a valid EVM address (INPUT_VALIDATION_FAILED).
|
|
10140
|
+
*
|
|
10141
|
+
* @example
|
|
10142
|
+
* ```typescript
|
|
10143
|
+
* import { padAddressToBytes32 } from '@core/utils'
|
|
10144
|
+
*
|
|
10145
|
+
* padAddressToBytes32('0x75275Aff2D01699D922f045b69ed291311209738')
|
|
10146
|
+
* // '0x00000000000000000000000075275aff2d01699d922f045b69ed291311209738'
|
|
10147
|
+
* ```
|
|
10148
|
+
*/ function padAddressToBytes32(address$1) {
|
|
10149
|
+
if (!address.isAddress(address$1)) {
|
|
10150
|
+
throw createValidationFailedError$1('address', address$1, 'Expected a valid 20-byte EVM address');
|
|
10151
|
+
}
|
|
10152
|
+
// bytes32 is raw bytes, not a checksummed address — emit lowercase so it
|
|
10153
|
+
// matches ABI-decoded output.
|
|
10154
|
+
return bytes.hexZeroPad(address.getAddress(address$1), 32).toLowerCase();
|
|
10155
|
+
}
|
|
9299
10156
|
|
|
9300
10157
|
/**
|
|
9301
10158
|
* Configuration for {@link retryAsync}.
|
|
@@ -9769,7 +10626,7 @@ function resolveOptions(options) {
|
|
|
9769
10626
|
}
|
|
9770
10627
|
|
|
9771
10628
|
var name$2 = "@circle-fin/bridge-kit";
|
|
9772
|
-
var version$3 = "1.
|
|
10629
|
+
var version$3 = "1.14.0";
|
|
9773
10630
|
var pkg$3 = {
|
|
9774
10631
|
name: name$2,
|
|
9775
10632
|
version: version$3};
|
|
@@ -10150,7 +11007,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
|
|
|
10150
11007
|
* const result = evmAddressSchema.safeParse(validAddress)
|
|
10151
11008
|
* console.log(result.success) // true
|
|
10152
11009
|
* ```
|
|
10153
|
-
*/ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
|
|
11010
|
+
*/ const evmAddressSchema$1 = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
|
|
10154
11011
|
/**
|
|
10155
11012
|
* Schema for validating transaction hashes.
|
|
10156
11013
|
*
|
|
@@ -10839,6 +11696,10 @@ var TransferSpeed;
|
|
|
10839
11696
|
token: zod.z.literal('USDC').optional(),
|
|
10840
11697
|
config: zod.z.object({
|
|
10841
11698
|
transferSpeed: zod.z.nativeEnum(TransferSpeed).optional(),
|
|
11699
|
+
feePayment: zod.z.enum([
|
|
11700
|
+
'source',
|
|
11701
|
+
'destination'
|
|
11702
|
+
]).optional(),
|
|
10842
11703
|
maxFee: zod.z.string().min(1, 'Required').pipe(createDecimalStringValidator({
|
|
10843
11704
|
allowZero: true,
|
|
10844
11705
|
regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
|
|
@@ -10846,7 +11707,8 @@ var TransferSpeed;
|
|
|
10846
11707
|
maxDecimals: 6
|
|
10847
11708
|
})(zod.z.string())).optional(),
|
|
10848
11709
|
customFee: customFeeSchema.optional()
|
|
10849
|
-
}).optional()
|
|
11710
|
+
}).optional(),
|
|
11711
|
+
quote: zod.z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex').optional()
|
|
10850
11712
|
});
|
|
10851
11713
|
|
|
10852
11714
|
/**
|
|
@@ -12021,16 +12883,73 @@ var TransferSpeed;
|
|
|
12021
12883
|
};
|
|
12022
12884
|
}
|
|
12023
12885
|
|
|
12886
|
+
/**
|
|
12887
|
+
* Dispatch a bridge step event through the provider's action dispatcher.
|
|
12888
|
+
*
|
|
12889
|
+
* Constructs the appropriate action payload and dispatches it to any registered
|
|
12890
|
+
* event listeners. Handles type-safe dispatching for different step types.
|
|
12891
|
+
* When provided, traceId from the invocation context is included for end-to-end correlation.
|
|
12892
|
+
*
|
|
12893
|
+
* @param name - The step name (approve, burn, fetchAttestation, or mint).
|
|
12894
|
+
* @param step - The completed bridge step containing transaction details and explorerUrl.
|
|
12895
|
+
* @param provider - The CCTP v2 provider with action dispatcher.
|
|
12896
|
+
* @param invocation - Optional invocation context containing traceId for correlation.
|
|
12897
|
+
*
|
|
12898
|
+
* @example
|
|
12899
|
+
* ```typescript
|
|
12900
|
+
* const step: BridgeStep = {
|
|
12901
|
+
* name: 'burn',
|
|
12902
|
+
* state: 'success',
|
|
12903
|
+
* txHash: '0xabc...',
|
|
12904
|
+
* explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
|
|
12905
|
+
* data: { ... }
|
|
12906
|
+
* }
|
|
12907
|
+
* dispatchStepEvent('burn', step, provider, invocationContext)
|
|
12908
|
+
* ```
|
|
12909
|
+
*/ function dispatchStepEvent(name, step, provider, invocation) {
|
|
12910
|
+
if (!provider.actionDispatcher) {
|
|
12911
|
+
return;
|
|
12912
|
+
}
|
|
12913
|
+
// Extract traceId from invocation context if provided
|
|
12914
|
+
const traceId = invocation?.traceId;
|
|
12915
|
+
const actionValues = {
|
|
12916
|
+
protocol: 'cctp',
|
|
12917
|
+
version: 'v2',
|
|
12918
|
+
...traceId !== undefined && {
|
|
12919
|
+
traceId
|
|
12920
|
+
},
|
|
12921
|
+
values: step
|
|
12922
|
+
};
|
|
12923
|
+
switch(name){
|
|
12924
|
+
case 'approve':
|
|
12925
|
+
case 'burn':
|
|
12926
|
+
case 'mint':
|
|
12927
|
+
provider.actionDispatcher.dispatch(name, {
|
|
12928
|
+
...actionValues,
|
|
12929
|
+
method: name
|
|
12930
|
+
});
|
|
12931
|
+
break;
|
|
12932
|
+
case 'fetchAttestation':
|
|
12933
|
+
case 'reAttest':
|
|
12934
|
+
provider.actionDispatcher.dispatch(name, {
|
|
12935
|
+
...actionValues,
|
|
12936
|
+
method: name,
|
|
12937
|
+
values: step
|
|
12938
|
+
});
|
|
12939
|
+
break;
|
|
12940
|
+
}
|
|
12941
|
+
}
|
|
12942
|
+
|
|
12024
12943
|
/**
|
|
12025
12944
|
* Base URL for Circle's IRIS API (mainnet/production).
|
|
12026
12945
|
*
|
|
12027
12946
|
* The IRIS API provides attestation services for CCTP cross-chain transfers.
|
|
12028
|
-
*/ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
12947
|
+
*/ const IRIS_API_BASE_URL$1 = 'https://iris-api.circle.com';
|
|
12029
12948
|
/**
|
|
12030
12949
|
* Base URL for Circle's IRIS API (testnet/sandbox).
|
|
12031
12950
|
*
|
|
12032
12951
|
* Used for development and testing on testnet chains.
|
|
12033
|
-
*/ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
12952
|
+
*/ const IRIS_API_SANDBOX_BASE_URL$1 = 'https://iris-api-sandbox.circle.com';
|
|
12034
12953
|
|
|
12035
12954
|
/**
|
|
12036
12955
|
* Type guard to validate the API response structure.
|
|
@@ -12073,7 +12992,7 @@ const isFastBurnFeeResponse = (data)=>{
|
|
|
12073
12992
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
12074
12993
|
* @returns The complete API URL
|
|
12075
12994
|
*/ function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
12076
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
12995
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
12077
12996
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
|
|
12078
12997
|
}
|
|
12079
12998
|
const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
@@ -12284,6 +13203,77 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12284
13203
|
}
|
|
12285
13204
|
};
|
|
12286
13205
|
|
|
13206
|
+
/**
|
|
13207
|
+
* Build the forwarding `hookData` for a forwarded (Orbit-relayed) CCTP v2 burn,
|
|
13208
|
+
* tailored to the destination chain.
|
|
13209
|
+
*
|
|
13210
|
+
* For EVM destinations the recipient already holds ERC-20 USDC directly, so the
|
|
13211
|
+
* empty version-0 `cctp-forward` frame is sufficient. For Solana destinations
|
|
13212
|
+
* USDC is held in an Associated Token Account (ATA) that may not exist for a
|
|
13213
|
+
* fresh wallet, so this emits a frame carrying `createAta` + `ataOwner` that
|
|
13214
|
+
* instructs the relayer to create the recipient ATA (idempotently, at the
|
|
13215
|
+
* relayer's expense) before minting.
|
|
13216
|
+
*
|
|
13217
|
+
* `@solana/web3.js` is imported lazily so EVM-only consumers never load Solana
|
|
13218
|
+
* code, mirroring {@link getMintRecipientAccount}.
|
|
13219
|
+
*
|
|
13220
|
+
* @param chainType - The destination blockchain type ('evm' or 'solana').
|
|
13221
|
+
* @param ownerAddress - The recipient's wallet address on the destination chain
|
|
13222
|
+
* (base58 for Solana). Must be the same owner used to derive `mintRecipient`.
|
|
13223
|
+
* @returns A 0x-prefixed hookData hex string for the forwarded burn.
|
|
13224
|
+
* @throws {KitError} If `chainType` is neither 'evm' nor 'solana', if
|
|
13225
|
+
* `@solana/web3.js` cannot be loaded, or if `ownerAddress` is not a valid
|
|
13226
|
+
* Solana public key (all FATAL).
|
|
13227
|
+
*
|
|
13228
|
+
* @example
|
|
13229
|
+
* ```typescript
|
|
13230
|
+
* import { getForwarderHookData } from './getForwarderHookData'
|
|
13231
|
+
*
|
|
13232
|
+
* // EVM: empty forwarding frame
|
|
13233
|
+
* const evmHook = await getForwarderHookData('evm', '0x742d35Cc...')
|
|
13234
|
+
*
|
|
13235
|
+
* // Solana: frame instructing the relayer to create the recipient ATA
|
|
13236
|
+
* const solanaHook = await getForwarderHookData(
|
|
13237
|
+
* 'solana',
|
|
13238
|
+
* '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
|
|
13239
|
+
* )
|
|
13240
|
+
* ```
|
|
13241
|
+
*/ const getForwarderHookData = async (/** The destination blockchain type - determines the hookData shape */ chainType, /** The recipient's wallet address (hex for EVM, base58 for Solana) */ ownerAddress)=>{
|
|
13242
|
+
if (chainType === 'evm') {
|
|
13243
|
+
// EVM: the recipient holds USDC directly; no ATA setup is needed.
|
|
13244
|
+
return buildForwardingHookData();
|
|
13245
|
+
}
|
|
13246
|
+
// Fail closed: only EVM and Solana forwarding destinations are supported.
|
|
13247
|
+
// Without this guard any future non-EVM chain type would silently fall
|
|
13248
|
+
// through to the Solana path and mis-encode hookData on a money-movement path.
|
|
13249
|
+
if (chainType !== 'solana') {
|
|
13250
|
+
throw new KitError({
|
|
13251
|
+
...InputError.VALIDATION_FAILED,
|
|
13252
|
+
recoverability: 'FATAL',
|
|
13253
|
+
message: `Forwarded burns are not supported for destination chain type "${chainType}"`
|
|
13254
|
+
});
|
|
13255
|
+
}
|
|
13256
|
+
// Solana: encode the owner so the relayer creates the recipient ATA.
|
|
13257
|
+
// Resolve @solana/web3.js lazily so EVM-only consumers never load Solana code.
|
|
13258
|
+
const { PublicKey } = await import('@solana/web3.js').catch(()=>{
|
|
13259
|
+
throw new KitError({
|
|
13260
|
+
...InputError.VALIDATION_FAILED,
|
|
13261
|
+
recoverability: 'FATAL',
|
|
13262
|
+
message: 'Failed to load @solana/web3.js. Please ensure it is installed: npm install @solana/web3.js'
|
|
13263
|
+
});
|
|
13264
|
+
});
|
|
13265
|
+
try {
|
|
13266
|
+
const owner = new PublicKey(ownerAddress);
|
|
13267
|
+
return buildSolanaAtaForwardingHookData(owner.toBytes());
|
|
13268
|
+
} catch (error) {
|
|
13269
|
+
throw new KitError({
|
|
13270
|
+
...InputError.INVALID_ADDRESS,
|
|
13271
|
+
recoverability: 'FATAL',
|
|
13272
|
+
message: `Failed to build Solana forwarder hookData for recipient "${ownerAddress}": ${error instanceof Error ? error.message : String(error)}`
|
|
13273
|
+
});
|
|
13274
|
+
}
|
|
13275
|
+
};
|
|
13276
|
+
|
|
12287
13277
|
/**
|
|
12288
13278
|
* Validates and converts a fee value to bigint.
|
|
12289
13279
|
*
|
|
@@ -12386,7 +13376,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12386
13376
|
|
|
12387
13377
|
/**
|
|
12388
13378
|
* The zero address, denoting a native-currency fee in a signed quote.
|
|
12389
|
-
*/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
13379
|
+
*/ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
|
|
12390
13380
|
/**
|
|
12391
13381
|
* Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
|
|
12392
13382
|
*
|
|
@@ -12427,7 +13417,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12427
13417
|
if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
|
|
12428
13418
|
throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
|
|
12429
13419
|
}
|
|
12430
|
-
const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS;
|
|
13420
|
+
const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
|
|
12431
13421
|
const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
|
|
12432
13422
|
if (isNativeFee) {
|
|
12433
13423
|
return {
|
|
@@ -12565,7 +13555,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
12565
13555
|
* @param isTestnet - Whether the request is for a testnet chain
|
|
12566
13556
|
* @returns The complete API URL with forward=true query parameter
|
|
12567
13557
|
*/ function buildForwardingFeeUrl(sourceDomain, destinationDomain, isTestnet) {
|
|
12568
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
13558
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
12569
13559
|
return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}?forward=true`;
|
|
12570
13560
|
}
|
|
12571
13561
|
/**
|
|
@@ -13626,7 +14616,7 @@ function hasPendingState(analysis, result) {
|
|
|
13626
14616
|
* // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
|
|
13627
14617
|
* ```
|
|
13628
14618
|
*/ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
|
|
13629
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
14619
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
13630
14620
|
const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
|
|
13631
14621
|
url.searchParams.set('transactionHash', transactionHash);
|
|
13632
14622
|
return url.toString();
|
|
@@ -13794,7 +14784,7 @@ function hasPendingState(analysis, result) {
|
|
|
13794
14784
|
* // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
|
|
13795
14785
|
* ```
|
|
13796
14786
|
*/ const buildReAttestUrl = (nonce, isTestnet)=>{
|
|
13797
|
-
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
14787
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
|
|
13798
14788
|
const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
|
|
13799
14789
|
return url.toString();
|
|
13800
14790
|
};
|
|
@@ -14249,7 +15239,8 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14249
15239
|
* - `destinationChain` — present and supports CCTP v2
|
|
14250
15240
|
* - source and destination chains must both be testnet or both mainnet
|
|
14251
15241
|
* - source and destination chains must differ
|
|
14252
|
-
* - `executor
|
|
15242
|
+
* - destination — either `executor`, or both `mintRecipient` and
|
|
15243
|
+
* `destinationCaller`; not both
|
|
14253
15244
|
* - `amount` — bigint or non-empty string coercible to bigint
|
|
14254
15245
|
* - `feeTotalAmount` — bigint or non-empty string coercible to bigint
|
|
14255
15246
|
* - `feeToken` — valid EVM address (`0x` + 40 hex chars)
|
|
@@ -14291,10 +15282,17 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14291
15282
|
if (source.chain.name === dest.name) {
|
|
14292
15283
|
throw createUnsupportedRouteError(source.chain.name, dest.name);
|
|
14293
15284
|
}
|
|
14294
|
-
//
|
|
15285
|
+
// Destination: GenericExecutor shorthand or explicit recipient + caller.
|
|
14295
15286
|
const executor = p['executor'];
|
|
14296
|
-
|
|
14297
|
-
|
|
15287
|
+
const mintRecipient = p['mintRecipient'];
|
|
15288
|
+
const destinationCaller = p['destinationCaller'];
|
|
15289
|
+
const hasExecutor = typeof executor === 'string' && executor !== '';
|
|
15290
|
+
const hasDirectDestination = typeof mintRecipient === 'string' && mintRecipient !== '' && typeof destinationCaller === 'string' && destinationCaller !== '';
|
|
15291
|
+
if (!hasExecutor && !hasDirectDestination) {
|
|
15292
|
+
throw createValidationFailedError$1('destination', undefined, 'Provide executor, or both mintRecipient and destinationCaller');
|
|
15293
|
+
}
|
|
15294
|
+
if (hasExecutor && hasDirectDestination) {
|
|
15295
|
+
throw createValidationFailedError$1('destination', undefined, 'Provide executor or direct destination fields, not both');
|
|
14298
15296
|
}
|
|
14299
15297
|
// amount
|
|
14300
15298
|
const rawAmount = p['amount'];
|
|
@@ -14317,7 +15315,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14317
15315
|
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
|
|
14318
15316
|
}
|
|
14319
15317
|
// feeToken
|
|
14320
|
-
if (!evmAddressSchema.safeParse(p['feeToken']).success) {
|
|
15318
|
+
if (!evmAddressSchema$1.safeParse(p['feeToken']).success) {
|
|
14321
15319
|
throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
|
|
14322
15320
|
}
|
|
14323
15321
|
// claim
|
|
@@ -14329,7 +15327,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
14329
15327
|
if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
|
|
14330
15328
|
throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
|
|
14331
15329
|
}
|
|
14332
|
-
if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
|
|
15330
|
+
if (!evmAddressSchema$1.safeParse(claim['refundAddress']).success) {
|
|
14333
15331
|
throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
|
|
14334
15332
|
}
|
|
14335
15333
|
// hookData (optional)
|
|
@@ -14712,79 +15710,22 @@ const mockAttestationMessage = {
|
|
|
14712
15710
|
}
|
|
14713
15711
|
|
|
14714
15712
|
/**
|
|
14715
|
-
*
|
|
14716
|
-
*
|
|
14717
|
-
* Constructs the appropriate action payload and dispatches it to any registered
|
|
14718
|
-
* event listeners. Handles type-safe dispatching for different step types.
|
|
14719
|
-
* When provided, traceId from the invocation context is included for end-to-end correlation.
|
|
15713
|
+
* Check whether the source adapter supports EIP-5792 atomic batching and
|
|
15714
|
+
* the consumer has not explicitly opted out via `config.batchTransactions`.
|
|
14720
15715
|
*
|
|
14721
|
-
* @param
|
|
14722
|
-
* @
|
|
14723
|
-
* @param provider - The CCTP v2 provider with action dispatcher.
|
|
14724
|
-
* @param invocation - Optional invocation context containing traceId for correlation.
|
|
15716
|
+
* @param params - Bridge parameters (used for adapter and config access).
|
|
15717
|
+
* @returns `true` when batched execution should be attempted.
|
|
14725
15718
|
*
|
|
14726
15719
|
* @example
|
|
14727
15720
|
* ```typescript
|
|
14728
|
-
* const
|
|
14729
|
-
*
|
|
14730
|
-
*
|
|
14731
|
-
* txHash: '0xabc...',
|
|
14732
|
-
* explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
|
|
14733
|
-
* data: { ... }
|
|
15721
|
+
* const useBatched = await shouldUseBatchedExecution(params)
|
|
15722
|
+
* if (useBatched) {
|
|
15723
|
+
* // take the batched approve + burn path
|
|
14734
15724
|
* }
|
|
14735
|
-
* dispatchStepEvent('burn', step, provider, invocationContext)
|
|
14736
15725
|
* ```
|
|
14737
|
-
*/ function
|
|
14738
|
-
if (
|
|
14739
|
-
return;
|
|
14740
|
-
}
|
|
14741
|
-
// Extract traceId from invocation context if provided
|
|
14742
|
-
const traceId = invocation?.traceId;
|
|
14743
|
-
const actionValues = {
|
|
14744
|
-
protocol: 'cctp',
|
|
14745
|
-
version: 'v2',
|
|
14746
|
-
...traceId !== undefined && {
|
|
14747
|
-
traceId
|
|
14748
|
-
},
|
|
14749
|
-
values: step
|
|
14750
|
-
};
|
|
14751
|
-
switch(name){
|
|
14752
|
-
case 'approve':
|
|
14753
|
-
case 'burn':
|
|
14754
|
-
case 'mint':
|
|
14755
|
-
provider.actionDispatcher.dispatch(name, {
|
|
14756
|
-
...actionValues,
|
|
14757
|
-
method: name
|
|
14758
|
-
});
|
|
14759
|
-
break;
|
|
14760
|
-
case 'fetchAttestation':
|
|
14761
|
-
case 'reAttest':
|
|
14762
|
-
provider.actionDispatcher.dispatch(name, {
|
|
14763
|
-
...actionValues,
|
|
14764
|
-
method: name,
|
|
14765
|
-
values: step
|
|
14766
|
-
});
|
|
14767
|
-
break;
|
|
14768
|
-
}
|
|
14769
|
-
}
|
|
14770
|
-
|
|
14771
|
-
/**
|
|
14772
|
-
* Check whether the source adapter supports EIP-5792 atomic batching and
|
|
14773
|
-
* the consumer has not explicitly opted out via `config.batchTransactions`.
|
|
14774
|
-
*
|
|
14775
|
-
* @param params - Bridge parameters (used for adapter and config access).
|
|
14776
|
-
* @returns `true` when batched execution should be attempted.
|
|
14777
|
-
*
|
|
14778
|
-
* @example
|
|
14779
|
-
* ```typescript
|
|
14780
|
-
* const useBatched = await shouldUseBatchedExecution(params)
|
|
14781
|
-
* if (useBatched) {
|
|
14782
|
-
* // take the batched approve + burn path
|
|
14783
|
-
* }
|
|
14784
|
-
* ```
|
|
14785
|
-
*/ async function shouldUseBatchedExecution(params) {
|
|
14786
|
-
if (params.config?.batchTransactions === false) {
|
|
14787
|
-
return false;
|
|
15726
|
+
*/ async function shouldUseBatchedExecution(params) {
|
|
15727
|
+
if (params.config?.batchTransactions === false) {
|
|
15728
|
+
return false;
|
|
14788
15729
|
}
|
|
14789
15730
|
const { chain } = params.source;
|
|
14790
15731
|
if (chain.type !== 'evm') {
|
|
@@ -15014,7 +15955,7 @@ const mockAttestationMessage = {
|
|
|
15014
15955
|
return step;
|
|
15015
15956
|
}
|
|
15016
15957
|
|
|
15017
|
-
var version$2 = "1.
|
|
15958
|
+
var version$2 = "1.12.0";
|
|
15018
15959
|
var pkg$2 = {
|
|
15019
15960
|
version: version$2};
|
|
15020
15961
|
|
|
@@ -15825,6 +16766,34 @@ function assertCCTPV2Config(config) {
|
|
|
15825
16766
|
this.config = config;
|
|
15826
16767
|
}
|
|
15827
16768
|
/**
|
|
16769
|
+
* Emit a bridge step event through the provider's registered action
|
|
16770
|
+
* dispatcher.
|
|
16771
|
+
*
|
|
16772
|
+
* Kit-level orchestration that drives the burn primitives directly instead
|
|
16773
|
+
* of {@link CCTPV2BridgingProvider.bridge} (for example the receive-exact
|
|
16774
|
+
* source-fee flow) uses this to surface the same `approve`/`burn`/`mint`
|
|
16775
|
+
* events as the standard bridge path. It is a no-op when no dispatcher is
|
|
16776
|
+
* registered.
|
|
16777
|
+
*
|
|
16778
|
+
* @param name - The step name (`approve`, `burn`, `mint`, ...).
|
|
16779
|
+
* @param step - The completed bridge step to broadcast.
|
|
16780
|
+
* @param invocation - Optional invocation context carrying a `traceId` for
|
|
16781
|
+
* end-to-end correlation.
|
|
16782
|
+
* @returns Nothing.
|
|
16783
|
+
*
|
|
16784
|
+
* @example
|
|
16785
|
+
* ```typescript
|
|
16786
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
16787
|
+
* provider.emitBridgeStep('burn', {
|
|
16788
|
+
* name: 'burn',
|
|
16789
|
+
* state: 'success',
|
|
16790
|
+
* txHash: '0xabc...',
|
|
16791
|
+
* })
|
|
16792
|
+
* ```
|
|
16793
|
+
*/ emitBridgeStep(name, step, invocation) {
|
|
16794
|
+
dispatchStepEvent(name, step, this, invocation);
|
|
16795
|
+
}
|
|
16796
|
+
/**
|
|
15828
16797
|
* Resolves the effective polling configuration for an attestation request.
|
|
15829
16798
|
*
|
|
15830
16799
|
* Precedence (lowest to highest): provider `config.attestation`, then the
|
|
@@ -16650,8 +17619,11 @@ function assertCCTPV2Config(config) {
|
|
|
16650
17619
|
// 2. Forwarder: Does the user want Circle's relayer to handle attestation/mint?
|
|
16651
17620
|
const useCustomBurn = hasCustomContractSupport(source.chain, 'bridge');
|
|
16652
17621
|
const useForwarder = destination.useForwarder === true;
|
|
16653
|
-
// Build hookData once if forwarder is enabled
|
|
16654
|
-
|
|
17622
|
+
// Build hookData once if forwarder is enabled. EVM destinations get the
|
|
17623
|
+
// empty forwarding frame; Solana destinations get a frame instructing the
|
|
17624
|
+
// relayer to create the recipient's ATA (using the same owner that derived
|
|
17625
|
+
// `mintRecipient`) so the mint succeeds even for a fresh wallet.
|
|
17626
|
+
const hookData = useForwarder ? await getForwarderHookData(destination.chain.type, destinationAddressForMint) : undefined;
|
|
16655
17627
|
if (useCustomBurn) {
|
|
16656
17628
|
// Custom burn path: use bridge contract (with or without hook)
|
|
16657
17629
|
const customBurnParams = {
|
|
@@ -16679,13 +17651,14 @@ function assertCCTPV2Config(config) {
|
|
|
16679
17651
|
/**
|
|
16680
17652
|
* Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
|
|
16681
17653
|
*
|
|
16682
|
-
*
|
|
16683
|
-
*
|
|
16684
|
-
*
|
|
16685
|
-
*
|
|
17654
|
+
* Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
|
|
17655
|
+
* collected up front on the source chain against a signed quote. The
|
|
17656
|
+
* destination may use the GenericExecutor shorthand, or an explicit mint
|
|
17657
|
+
* recipient and destination caller for direct forwarding.
|
|
16686
17658
|
*
|
|
16687
|
-
* This is the low-level on-chain primitive behind the
|
|
16688
|
-
*
|
|
17659
|
+
* This is the low-level on-chain primitive behind the Unified Balance Kit
|
|
17660
|
+
* `fastCrossChainDeposit` and the Bridge Kit source-fee
|
|
17661
|
+
* (`feePayment: 'source'`) flow. The `hookData` and signed-quote
|
|
16689
17662
|
* `claim` are produced elsewhere and passed in here:
|
|
16690
17663
|
* - `hookData`: `buildForwardingHookDataWithPayload(version,
|
|
16691
17664
|
* buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
|
|
@@ -16702,32 +17675,48 @@ function assertCCTPV2Config(config) {
|
|
|
16702
17675
|
* approval covers both; the redundant second approval is skipped.
|
|
16703
17676
|
*
|
|
16704
17677
|
* @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
|
|
16705
|
-
* @param params - The burn amount,
|
|
17678
|
+
* @param params - The burn amount, destination, hook data, signed quote, and fee.
|
|
16706
17679
|
* @returns The prepared approvals, the prepared burn, and the resolved fee plan.
|
|
16707
17680
|
* @throws {KitError} If the wallet context is invalid, `destinationChain` does not
|
|
16708
|
-
* support CCTP v2, the
|
|
16709
|
-
* a bigint or a numeric string coercible to bigint,
|
|
16710
|
-
* `cctp-forward` frame (guaranteed
|
|
16711
|
-
* context cannot be resolved.
|
|
17681
|
+
* support CCTP v2, the destination fields are missing, `amount` or
|
|
17682
|
+
* `feeTotalAmount` is not a bigint or a numeric string coercible to bigint,
|
|
17683
|
+
* the hook data lacks a `cctp-forward` frame (guaranteed
|
|
17684
|
+
* `ForwardFeeWithoutHook`), or the operation context cannot be resolved.
|
|
16712
17685
|
*
|
|
16713
17686
|
* @example
|
|
16714
17687
|
* ```typescript
|
|
17688
|
+
* import {
|
|
17689
|
+
* CCTPV2BridgingProvider,
|
|
17690
|
+
* type BurnWithFeesParams,
|
|
17691
|
+
* } from '@circle-fin/provider-cctp-v2'
|
|
17692
|
+
*
|
|
17693
|
+
* declare const source: BurnWithFeesParams['source']
|
|
17694
|
+
* declare const destinationChain: BurnWithFeesParams['destinationChain']
|
|
17695
|
+
* declare const recipient: string
|
|
17696
|
+
* declare const hookData: string
|
|
17697
|
+
* declare const claim: BurnWithFeesParams['claim']
|
|
17698
|
+
*
|
|
17699
|
+
* const provider = new CCTPV2BridgingProvider()
|
|
16715
17700
|
* const { approvals, burn } = await provider.burnWithFees({
|
|
16716
17701
|
* source,
|
|
16717
|
-
* destinationChain
|
|
17702
|
+
* destinationChain,
|
|
16718
17703
|
* amount: 1_000_000n,
|
|
16719
|
-
*
|
|
16720
|
-
*
|
|
16721
|
-
*
|
|
16722
|
-
*
|
|
16723
|
-
*
|
|
17704
|
+
* mintRecipient: recipient,
|
|
17705
|
+
* destinationCaller: '0x0000000000000000000000000000000000000000',
|
|
17706
|
+
* hookData,
|
|
17707
|
+
* claim,
|
|
17708
|
+
* feeToken: source.chain.usdcAddress,
|
|
17709
|
+
* feeTotalAmount: 10_000n,
|
|
16724
17710
|
* })
|
|
16725
17711
|
* for (const approval of approvals) await approval.execute()
|
|
16726
17712
|
* const txHash = await burn.execute()
|
|
16727
17713
|
* ```
|
|
16728
17714
|
*/ async burnWithFees(params) {
|
|
16729
17715
|
assertBurnWithFeesParams(params);
|
|
16730
|
-
const { source, destinationChain,
|
|
17716
|
+
const { source, destinationChain, hookData, claim, feeToken } = params;
|
|
17717
|
+
const hasExecutor = 'executor' in params && params.executor !== undefined;
|
|
17718
|
+
const mintRecipient = hasExecutor ? params.executor : params.mintRecipient;
|
|
17719
|
+
const destinationCaller = hasExecutor ? params.executor : params.destinationCaller;
|
|
16731
17720
|
const amount = BigInt(params.amount);
|
|
16732
17721
|
const feeTotalAmount = BigInt(params.feeTotalAmount);
|
|
16733
17722
|
// Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
|
|
@@ -16758,13 +17747,14 @@ function assertCCTPV2Config(config) {
|
|
|
16758
17747
|
delegate: wrapperAddress,
|
|
16759
17748
|
amount: approval.amount
|
|
16760
17749
|
}, context)));
|
|
16761
|
-
// Build the burn
|
|
17750
|
+
// Build the burn with either the GenericExecutor shorthand or the explicit
|
|
17751
|
+
// direct-forwarding recipient and caller.
|
|
16762
17752
|
const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
|
|
16763
17753
|
fromChain: source.chain,
|
|
16764
17754
|
toChain: destinationChain,
|
|
16765
17755
|
amount,
|
|
16766
|
-
mintRecipient
|
|
16767
|
-
destinationCaller
|
|
17756
|
+
mintRecipient,
|
|
17757
|
+
destinationCaller,
|
|
16768
17758
|
hookData,
|
|
16769
17759
|
claim,
|
|
16770
17760
|
feeToken,
|
|
@@ -16921,6 +17911,948 @@ function assertCCTPV2Config(config) {
|
|
|
16921
17911
|
]
|
|
16922
17912
|
];
|
|
16923
17913
|
|
|
17914
|
+
/**
|
|
17915
|
+
* Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
|
|
17916
|
+
*/ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
|
|
17917
|
+
/**
|
|
17918
|
+
* Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
|
|
17919
|
+
*/ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
|
|
17920
|
+
/**
|
|
17921
|
+
* Native fee-token sentinel (the zero address).
|
|
17922
|
+
*
|
|
17923
|
+
* When `feeToken` is the zero address the quote prices fees in the source
|
|
17924
|
+
* chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
|
|
17925
|
+
* address instead to denominate fees in USDC.
|
|
17926
|
+
*/ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
|
|
17927
|
+
/**
|
|
17928
|
+
* API path prefix for the CCTP v2 USDC burn quote endpoint.
|
|
17929
|
+
*
|
|
17930
|
+
* The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
|
|
17931
|
+
* `usdc` is a fixed literal, not a token parameter.
|
|
17932
|
+
*/ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
|
|
17933
|
+
/**
|
|
17934
|
+
* API path prefix for the CCTP v2 USDC quote validate endpoint.
|
|
17935
|
+
*
|
|
17936
|
+
* The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
|
|
17937
|
+
* `POST { abiSignature, args }` body and returns whether the signed quote is
|
|
17938
|
+
* currently claimable together with its authoritative expiry status.
|
|
17939
|
+
*/ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
|
|
17940
|
+
/**
|
|
17941
|
+
* Default polling configuration for Quote API calls.
|
|
17942
|
+
*
|
|
17943
|
+
* A signed quote is short-lived (typically ~2 minutes, varying per chain) and
|
|
17944
|
+
* a feature-flag-disabled source chain returns a
|
|
17945
|
+
* permanent `503 SERVICE_NOT_ENABLED`, so retrying buys little and risks
|
|
17946
|
+
* outliving the quote. The client therefore makes a single attempt
|
|
17947
|
+
* (`maxRetries: 1`) with a 15s timeout, mirroring the reference
|
|
17948
|
+
* implementation; callers refresh by requesting a new quote rather than
|
|
17949
|
+
* relying on transport retries.
|
|
17950
|
+
*
|
|
17951
|
+
* No `headers` are set here: `pollApiWithValidation` always injects
|
|
17952
|
+
* `Content-Type: application/json` and adds `User-Agent` in Node. Browser
|
|
17953
|
+
* requests omit a user-agent header to avoid a CORS preflight, so duplicating
|
|
17954
|
+
* either header here would be dead configuration.
|
|
17955
|
+
*/ const FEE_QUOTE_DEFAULT_CONFIG = {
|
|
17956
|
+
timeout: 15_000,
|
|
17957
|
+
maxRetries: 1,
|
|
17958
|
+
retryDelay: 200
|
|
17959
|
+
};
|
|
17960
|
+
|
|
17961
|
+
/** Decimal string in token minor units, constrained to be strictly positive. */ const positiveAmountSchema = zod.z.string().regex(/^\d+$/, 'must be a non-negative integer string')// Re-check the digit shape here: zod still runs this refinement when the
|
|
17962
|
+
// regex check above fails ("dirty"), so guard BigInt() against throwing on a
|
|
17963
|
+
// non-numeric value before comparing.
|
|
17964
|
+
.refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
|
|
17965
|
+
/**
|
|
17966
|
+
* A 20-byte EVM address in `0x` hex.
|
|
17967
|
+
*
|
|
17968
|
+
* The MVP prepaid-`FORWARD` `burn/usdc` path targets EVM contracts
|
|
17969
|
+
* (`TokenMessengerWithFees` / `GenericExecutor`), so `feeToken` and
|
|
17970
|
+
* `destinationCaller` are constrained to EVM addresses by design. This is an
|
|
17971
|
+
* intentional scope limit, not a permanent one: it can be widened to other
|
|
17972
|
+
* address formats as the fee service expands to more chains.
|
|
17973
|
+
*/ const evmAddressSchema = zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'must be a 20-byte 0x address');
|
|
17974
|
+
/** Even-length `0x` hex (the empty `0x` is allowed). */ const hexSchema = zod.z.string().regex(/^0x([a-fA-F0-9]{2})*$/, 'must be even-length 0x hex');
|
|
17975
|
+
/** Non-empty, even-length `0x` hex. */ const nonEmptyHexSchema = zod.z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex');
|
|
17976
|
+
/** A 32-byte `0x` hex hash. */ const bytes32Schema = zod.z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'must be a 32-byte 0x hash');
|
|
17977
|
+
/** Decimal string in minor units, allowing zero. */ const numericStringSchema = zod.z.string().regex(/^\d+$/, 'must be a non-negative integer string');
|
|
17978
|
+
/** An `https:` URL, used for the optional base-URL override. */ const httpsUrlSchema = zod.z.string().refine((value)=>{
|
|
17979
|
+
try {
|
|
17980
|
+
return new URL(value).protocol === 'https:';
|
|
17981
|
+
} catch {
|
|
17982
|
+
return false;
|
|
17983
|
+
}
|
|
17984
|
+
}, 'must be an https URL');
|
|
17985
|
+
const forwardParamsSchema = zod.z.object({
|
|
17986
|
+
hookData: hexSchema.optional(),
|
|
17987
|
+
destinationCaller: evmAddressSchema.optional()
|
|
17988
|
+
}).strict();
|
|
17989
|
+
const forwardRequestSchema = zod.z.object({
|
|
17990
|
+
type: zod.z.literal('FORWARD'),
|
|
17991
|
+
params: forwardParamsSchema.optional()
|
|
17992
|
+
}).strict();
|
|
17993
|
+
const preFinalityRequestSchema = zod.z.object({
|
|
17994
|
+
type: zod.z.literal('PRE_FINALITY')
|
|
17995
|
+
}).strict();
|
|
17996
|
+
/** A single quote request item (`FORWARD` or `PRE_FINALITY`). */ const feeQuoteRequestSchema = zod.z.discriminatedUnion('type', [
|
|
17997
|
+
forwardRequestSchema,
|
|
17998
|
+
preFinalityRequestSchema
|
|
17999
|
+
]);
|
|
18000
|
+
/** A non-empty list of quote request items with unique types. */ const feeQuoteRequestsSchema = zod.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');
|
|
18001
|
+
/**
|
|
18002
|
+
* A structured `Partial<ApiPollingConfig>` polling override.
|
|
18003
|
+
*
|
|
18004
|
+
* Validates the field types callers actually set, so a plain-JS caller passing
|
|
18005
|
+
* `{ timeout: 'soon' }` is rejected at the boundary rather than failing opaquely
|
|
18006
|
+
* inside the transport. Unknown keys pass through so a future `ApiPollingConfig`
|
|
18007
|
+
* field is forwarded rather than silently dropped.
|
|
18008
|
+
*/ const apiPollingConfigSchema = zod.z.object({
|
|
18009
|
+
timeout: zod.z.number().int().positive().optional(),
|
|
18010
|
+
maxRetries: zod.z.number().int().nonnegative().optional(),
|
|
18011
|
+
retryDelay: zod.z.number().int().nonnegative().optional(),
|
|
18012
|
+
backoff: zod.z.enum([
|
|
18013
|
+
'fixed',
|
|
18014
|
+
'exponential'
|
|
18015
|
+
]).optional(),
|
|
18016
|
+
maxRetryDelayMs: zod.z.number().int().positive().optional(),
|
|
18017
|
+
headers: zod.z.record(zod.z.string()).optional()
|
|
18018
|
+
}).passthrough();
|
|
18019
|
+
/**
|
|
18020
|
+
* The validatable input for {@link fetchFeeQuote}.
|
|
18021
|
+
*
|
|
18022
|
+
* This is the single source of truth for input validation, including the CCTP
|
|
18023
|
+
* domains and the `isTestnet` environment flag. Validating `isTestnet` at
|
|
18024
|
+
* runtime matters because a plain-JS caller who omits it would otherwise leave
|
|
18025
|
+
* it `undefined`, which is falsy and silently selects the production base URL.
|
|
18026
|
+
* (`buildFeeQuoteUrl` independently re-validates the domains for standalone
|
|
18027
|
+
* callers.)
|
|
18028
|
+
*/ const fetchFeeQuoteInputSchema = zod.z.object({
|
|
18029
|
+
sourceDomain: zod.z.number().int().nonnegative(),
|
|
18030
|
+
destinationDomain: zod.z.number().int().nonnegative(),
|
|
18031
|
+
amount: positiveAmountSchema,
|
|
18032
|
+
feeToken: evmAddressSchema.optional(),
|
|
18033
|
+
requests: feeQuoteRequestsSchema,
|
|
18034
|
+
isTestnet: zod.z.boolean(),
|
|
18035
|
+
baseUrl: httpsUrlSchema.optional(),
|
|
18036
|
+
config: apiPollingConfigSchema.optional()
|
|
18037
|
+
}).strict();
|
|
18038
|
+
const feeQuoteItemSchema = zod.z.object({
|
|
18039
|
+
type: zod.z.string().min(1),
|
|
18040
|
+
amount: numericStringSchema,
|
|
18041
|
+
args: zod.z.array(zod.z.string()),
|
|
18042
|
+
argsHash: bytes32Schema
|
|
18043
|
+
}).passthrough();
|
|
18044
|
+
const exchangeRatesSchema = zod.z.object({
|
|
18045
|
+
feeTokenUsd: zod.z.string(),
|
|
18046
|
+
destinationTokenUsd: zod.z.string()
|
|
18047
|
+
}).passthrough();
|
|
18048
|
+
const metadataSchema = zod.z.object({
|
|
18049
|
+
destinationGasPrice: zod.z.string().optional(),
|
|
18050
|
+
exchangeRates: exchangeRatesSchema.optional()
|
|
18051
|
+
}).passthrough();
|
|
18052
|
+
/** The `expiry` object the Quote API nests the quote TTL under. */ const expirySchema = zod.z.discriminatedUnion('mode', [
|
|
18053
|
+
zod.z.object({
|
|
18054
|
+
mode: zod.z.literal('TIMESTAMP'),
|
|
18055
|
+
expiresAt: zod.z.number().int().nonnegative()
|
|
18056
|
+
}).passthrough(),
|
|
18057
|
+
zod.z.object({
|
|
18058
|
+
mode: zod.z.literal('BLOCK_NUMBER'),
|
|
18059
|
+
expiresAtBlock: zod.z.number().int().nonnegative(),
|
|
18060
|
+
blockEstimatedAt: zod.z.number().int().nonnegative().optional()
|
|
18061
|
+
}).passthrough()
|
|
18062
|
+
]);
|
|
18063
|
+
/** Schema for a signed fee quote returned by the Quote API. */ const signedFeeQuoteSchema = zod.z.object({
|
|
18064
|
+
// The runtime YAML spec maps signedQuote to a looser `hex` (which allows
|
|
18065
|
+
// an empty `0x`); we keep the stricter non-empty form. Do not relax
|
|
18066
|
+
// without a reason.
|
|
18067
|
+
signedQuote: nonEmptyHexSchema,
|
|
18068
|
+
issuedAt: zod.z.number().int().nonnegative(),
|
|
18069
|
+
// The API returns a mode-specific timestamp or source-block deadline.
|
|
18070
|
+
expiry: expirySchema,
|
|
18071
|
+
feeTotalAmount: numericStringSchema,
|
|
18072
|
+
feeToken: evmAddressSchema,
|
|
18073
|
+
nonce: numericStringSchema,
|
|
18074
|
+
items: zod.z.array(feeQuoteItemSchema),
|
|
18075
|
+
metadata: metadataSchema.optional()
|
|
18076
|
+
}).passthrough();
|
|
18077
|
+
/**
|
|
18078
|
+
* Validate that an unknown value is a signed fee quote.
|
|
18079
|
+
*
|
|
18080
|
+
* @param value - The unknown value to validate.
|
|
18081
|
+
* @returns `true` when the value matches the signed-quote response shape.
|
|
18082
|
+
*
|
|
18083
|
+
* @example
|
|
18084
|
+
* ```typescript
|
|
18085
|
+
* import { isSignedFeeQuote } from '@circle-fin/provider-fee-v1'
|
|
18086
|
+
*
|
|
18087
|
+
* declare const payload: unknown
|
|
18088
|
+
* if (isSignedFeeQuote(payload)) {
|
|
18089
|
+
* console.log(payload.feeTotalAmount)
|
|
18090
|
+
* }
|
|
18091
|
+
* ```
|
|
18092
|
+
*/ function isSignedFeeQuote(value) {
|
|
18093
|
+
return signedFeeQuoteSchema.safeParse(value).success;
|
|
18094
|
+
}
|
|
18095
|
+
/** Validate input to {@link validateQuote}. */ const validateQuoteInputSchema = zod.z.object({
|
|
18096
|
+
sourceDomain: zod.z.number().int().nonnegative(),
|
|
18097
|
+
abiSignature: zod.z.string().min(1),
|
|
18098
|
+
args: zod.z.array(zod.z.union([
|
|
18099
|
+
zod.z.string(),
|
|
18100
|
+
zod.z.array(zod.z.string())
|
|
18101
|
+
])),
|
|
18102
|
+
isTestnet: zod.z.boolean(),
|
|
18103
|
+
baseUrl: httpsUrlSchema.optional(),
|
|
18104
|
+
config: apiPollingConfigSchema.optional()
|
|
18105
|
+
}).strict();
|
|
18106
|
+
const quoteExpiryStatusSchema = zod.z.discriminatedUnion('mode', [
|
|
18107
|
+
zod.z.object({
|
|
18108
|
+
mode: zod.z.literal('TIMESTAMP'),
|
|
18109
|
+
expired: zod.z.boolean(),
|
|
18110
|
+
secondsRemaining: zod.z.number().int().nonnegative(),
|
|
18111
|
+
expiresAt: zod.z.number().int().nonnegative()
|
|
18112
|
+
}).passthrough(),
|
|
18113
|
+
zod.z.object({
|
|
18114
|
+
mode: zod.z.literal('BLOCK_NUMBER'),
|
|
18115
|
+
expired: zod.z.boolean(),
|
|
18116
|
+
secondsRemaining: zod.z.number().int().nonnegative(),
|
|
18117
|
+
expiresAtBlock: zod.z.number().int().nonnegative(),
|
|
18118
|
+
blockEstimatedAt: zod.z.number().int().nonnegative().optional()
|
|
18119
|
+
}).passthrough()
|
|
18120
|
+
]);
|
|
18121
|
+
const validateQuoteItemSchema = zod.z.object({
|
|
18122
|
+
type: zod.z.string().min(1),
|
|
18123
|
+
argsMatch: zod.z.boolean(),
|
|
18124
|
+
amount: numericStringSchema.optional(),
|
|
18125
|
+
args: zod.z.array(zod.z.string()).optional(),
|
|
18126
|
+
argsHash: bytes32Schema.optional(),
|
|
18127
|
+
computedArgsHash: bytes32Schema.optional()
|
|
18128
|
+
}).passthrough();
|
|
18129
|
+
/**
|
|
18130
|
+
* Schema for a response from the Quote API validation endpoint.
|
|
18131
|
+
*
|
|
18132
|
+
* The endpoint takes the source domain as a URL path parameter and does not
|
|
18133
|
+
* return it in the response body, so `sourceDomain` is intentionally not part
|
|
18134
|
+
* of this schema.
|
|
18135
|
+
*/ const validateQuoteResultSchema = zod.z.object({
|
|
18136
|
+
signedQuote: nonEmptyHexSchema,
|
|
18137
|
+
expiry: quoteExpiryStatusSchema,
|
|
18138
|
+
feeTotalAmount: numericStringSchema,
|
|
18139
|
+
feeToken: evmAddressSchema,
|
|
18140
|
+
nonce: numericStringSchema,
|
|
18141
|
+
claimable: zod.z.boolean(),
|
|
18142
|
+
// Preserve newly introduced server-side reasons as opaque strings rather
|
|
18143
|
+
// than rejecting the entire safety response before the SDK is updated.
|
|
18144
|
+
failedChecks: zod.z.array(zod.z.string().min(1)),
|
|
18145
|
+
items: zod.z.array(validateQuoteItemSchema)
|
|
18146
|
+
}).passthrough();
|
|
18147
|
+
/**
|
|
18148
|
+
* Validate a Quote API validation response.
|
|
18149
|
+
*
|
|
18150
|
+
* @param value - The unknown response value.
|
|
18151
|
+
* @returns `true` when the value has the expected validation response shape.
|
|
18152
|
+
*
|
|
18153
|
+
* @example
|
|
18154
|
+
* ```typescript
|
|
18155
|
+
* import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
|
|
18156
|
+
*
|
|
18157
|
+
* declare const response: unknown
|
|
18158
|
+
* if (isValidateQuoteResult(response)) {
|
|
18159
|
+
* console.log(response.claimable)
|
|
18160
|
+
* }
|
|
18161
|
+
* ```
|
|
18162
|
+
*/ function isValidateQuoteResult(value) {
|
|
18163
|
+
return validateQuoteResultSchema.safeParse(value).success;
|
|
18164
|
+
}
|
|
18165
|
+
|
|
18166
|
+
/**
|
|
18167
|
+
* Validate that a CCTP domain id is a non-negative integer.
|
|
18168
|
+
*
|
|
18169
|
+
* @param value - The domain id to validate.
|
|
18170
|
+
* @param label - The parameter name, used in the error message.
|
|
18171
|
+
* @returns Nothing.
|
|
18172
|
+
* @throws {@link KitError} When the value is not a non-negative integer.
|
|
18173
|
+
* @internal
|
|
18174
|
+
*/ function assertDomain(value, label) {
|
|
18175
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
18176
|
+
throw new KitError({
|
|
18177
|
+
...InputError.VALIDATION_FAILED,
|
|
18178
|
+
recoverability: 'FATAL',
|
|
18179
|
+
message: `Quote API getFeeQuote failed: ${label} must be a ` + `non-negative integer, received ${String(value)}`,
|
|
18180
|
+
cause: {
|
|
18181
|
+
trace: {
|
|
18182
|
+
[label]: value
|
|
18183
|
+
}
|
|
18184
|
+
}
|
|
18185
|
+
});
|
|
18186
|
+
}
|
|
18187
|
+
}
|
|
18188
|
+
/**
|
|
18189
|
+
* Build the Quote API URL for a CCTP v2 USDC burn quote.
|
|
18190
|
+
*
|
|
18191
|
+
* Resolves the environment base URL (or an explicit `baseUrl` override) and
|
|
18192
|
+
* appends the burn/usdc path with the source and destination CCTP domains.
|
|
18193
|
+
* `usdc` is a fixed path literal, not a token parameter.
|
|
18194
|
+
*
|
|
18195
|
+
* @param params - The domains and environment selector.
|
|
18196
|
+
* @returns The fully-qualified Quote API URL.
|
|
18197
|
+
* @throws {@link KitError} When either domain is not a non-negative integer.
|
|
18198
|
+
*
|
|
18199
|
+
* @example
|
|
18200
|
+
* ```typescript
|
|
18201
|
+
* import { buildFeeQuoteUrl } from '@circle-fin/provider-fee-v1'
|
|
18202
|
+
*
|
|
18203
|
+
* const url = buildFeeQuoteUrl({
|
|
18204
|
+
* sourceDomain: 3,
|
|
18205
|
+
* destinationDomain: 26,
|
|
18206
|
+
* isTestnet: false,
|
|
18207
|
+
* })
|
|
18208
|
+
* // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
|
|
18209
|
+
* ```
|
|
18210
|
+
*/ function buildFeeQuoteUrl(params) {
|
|
18211
|
+
const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
|
|
18212
|
+
assertDomain(sourceDomain, 'sourceDomain');
|
|
18213
|
+
assertDomain(destinationDomain, 'destinationDomain');
|
|
18214
|
+
const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
|
|
18215
|
+
return new URL(`${QUOTE_BURN_USDC_PATH}/${String(sourceDomain)}/${String(destinationDomain)}`, resolvedBaseUrl).toString();
|
|
18216
|
+
}
|
|
18217
|
+
|
|
18218
|
+
/**
|
|
18219
|
+
* Assert that a quote's per-item fee amounts sum to its `feeTotalAmount`.
|
|
18220
|
+
*
|
|
18221
|
+
* A defensive integrity check on the Quote API response, enforced internally
|
|
18222
|
+
* by `fetchFeeQuote`. It does not compare against the destination-side
|
|
18223
|
+
* `feeExecuted`, which is expected to be zero for prepaid forward-only burns.
|
|
18224
|
+
*
|
|
18225
|
+
* @param quote - The signed fee quote to check.
|
|
18226
|
+
* @returns Nothing.
|
|
18227
|
+
* @throws {@link KitError} When the item amounts do not sum to `feeTotalAmount`.
|
|
18228
|
+
* @internal
|
|
18229
|
+
*/ function assertFeeItemsSumToTotal(quote) {
|
|
18230
|
+
const itemsTotal = quote.items.reduce((sum, item)=>sum + BigInt(item.amount), 0n);
|
|
18231
|
+
const declaredTotal = BigInt(quote.feeTotalAmount);
|
|
18232
|
+
if (itemsTotal !== declaredTotal) {
|
|
18233
|
+
throw new KitError({
|
|
18234
|
+
...InputError.VALIDATION_FAILED,
|
|
18235
|
+
recoverability: 'FATAL',
|
|
18236
|
+
message: `Quote API getFeeQuote failed: fee items sum ` + `(${itemsTotal.toString()}) does not equal feeTotalAmount ` + `(${declaredTotal.toString()})`,
|
|
18237
|
+
cause: {
|
|
18238
|
+
trace: {
|
|
18239
|
+
itemsTotal: itemsTotal.toString(),
|
|
18240
|
+
feeTotalAmount: quote.feeTotalAmount
|
|
18241
|
+
}
|
|
18242
|
+
}
|
|
18243
|
+
});
|
|
18244
|
+
}
|
|
18245
|
+
}
|
|
18246
|
+
|
|
18247
|
+
/**
|
|
18248
|
+
* Determine whether a Quote API error represents a disabled source chain.
|
|
18249
|
+
*
|
|
18250
|
+
* @param error - The error thrown by the HTTP layer.
|
|
18251
|
+
* @returns `true` when the error contains the `SERVICE_NOT_ENABLED` marker.
|
|
18252
|
+
* @internal
|
|
18253
|
+
*/ function isServiceNotEnabled(error) {
|
|
18254
|
+
const body = typeof error === 'object' && error !== null && 'responseBody' in error ? error.responseBody : undefined;
|
|
18255
|
+
if (typeof body === 'object' && body !== null) {
|
|
18256
|
+
const fields = body;
|
|
18257
|
+
const candidates = [
|
|
18258
|
+
fields['errorCode'],
|
|
18259
|
+
fields['code'],
|
|
18260
|
+
fields['message'],
|
|
18261
|
+
fields['externalMessage'],
|
|
18262
|
+
fields['error']
|
|
18263
|
+
];
|
|
18264
|
+
if (candidates.some((value)=>typeof value === 'string' && value.toUpperCase().includes('SERVICE_NOT_ENABLED'))) {
|
|
18265
|
+
return true;
|
|
18266
|
+
}
|
|
18267
|
+
}
|
|
18268
|
+
return getErrorMessage(error).toUpperCase().includes('SERVICE_NOT_ENABLED');
|
|
18269
|
+
}
|
|
18270
|
+
|
|
18271
|
+
const SERVICE$1 = 'Quote API';
|
|
18272
|
+
const OPERATION$1 = 'getFeeQuote';
|
|
18273
|
+
/**
|
|
18274
|
+
* Serialize request items for the wire body.
|
|
18275
|
+
*
|
|
18276
|
+
* `PRE_FINALITY` is emitted with no `params` key, and a `FORWARD` item only
|
|
18277
|
+
* carries the binding fields that are present.
|
|
18278
|
+
*
|
|
18279
|
+
* @param requests - The request items to serialize.
|
|
18280
|
+
* @returns The serialized request items.
|
|
18281
|
+
* @internal
|
|
18282
|
+
*/ function serializeRequests(requests) {
|
|
18283
|
+
return requests.map((request)=>{
|
|
18284
|
+
if (request.type === 'PRE_FINALITY') {
|
|
18285
|
+
return {
|
|
18286
|
+
type: 'PRE_FINALITY'
|
|
18287
|
+
};
|
|
18288
|
+
}
|
|
18289
|
+
const params = request.params;
|
|
18290
|
+
if (params === undefined) {
|
|
18291
|
+
return {
|
|
18292
|
+
type: 'FORWARD'
|
|
18293
|
+
};
|
|
18294
|
+
}
|
|
18295
|
+
const forwardParams = {};
|
|
18296
|
+
if (params.hookData !== undefined) {
|
|
18297
|
+
forwardParams.hookData = params.hookData;
|
|
18298
|
+
}
|
|
18299
|
+
if (params.destinationCaller !== undefined) {
|
|
18300
|
+
forwardParams.destinationCaller = params.destinationCaller;
|
|
18301
|
+
}
|
|
18302
|
+
return {
|
|
18303
|
+
type: 'FORWARD',
|
|
18304
|
+
params: forwardParams
|
|
18305
|
+
};
|
|
18306
|
+
});
|
|
18307
|
+
}
|
|
18308
|
+
/**
|
|
18309
|
+
* Fetch a signed fee quote from Circle's Quote API for a CCTP v2 USDC burn.
|
|
18310
|
+
*
|
|
18311
|
+
* Validates inputs, POSTs to
|
|
18312
|
+
* `/v2/quote/burn/usdc/{sourceDomain}/{destinationDomain}` with a single
|
|
18313
|
+
* attempt (the signed quote is short-lived), and returns the typed quote. A
|
|
18314
|
+
* disabled source chain (`503 SERVICE_NOT_ENABLED`) surfaces as a fatal,
|
|
18315
|
+
* non-retryable error; other failures are mapped to a {@link KitError} via the
|
|
18316
|
+
* shared API error parser.
|
|
18317
|
+
*
|
|
18318
|
+
* @param params - The domains, amount, request items, and environment.
|
|
18319
|
+
* @returns The signed fee quote.
|
|
18320
|
+
* @throws {@link KitError} On invalid input, a disabled source chain, an HTTP
|
|
18321
|
+
* error, or an invalid response shape.
|
|
18322
|
+
*
|
|
18323
|
+
* @example
|
|
18324
|
+
* ```typescript
|
|
18325
|
+
* import { fetchFeeQuote } from '@circle-fin/provider-fee-v1'
|
|
18326
|
+
*
|
|
18327
|
+
* const quote = await fetchFeeQuote({
|
|
18328
|
+
* sourceDomain: 3,
|
|
18329
|
+
* destinationDomain: 26,
|
|
18330
|
+
* amount: '1000000',
|
|
18331
|
+
* requests: [{ type: 'FORWARD' }, { type: 'PRE_FINALITY' }],
|
|
18332
|
+
* isTestnet: false,
|
|
18333
|
+
* })
|
|
18334
|
+
* console.log(quote.feeTotalAmount, quote.expiry)
|
|
18335
|
+
* ```
|
|
18336
|
+
*/ async function fetchFeeQuote(params) {
|
|
18337
|
+
const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
|
|
18338
|
+
const parsed = fetchFeeQuoteInputSchema.safeParse({
|
|
18339
|
+
sourceDomain,
|
|
18340
|
+
destinationDomain,
|
|
18341
|
+
amount,
|
|
18342
|
+
feeToken,
|
|
18343
|
+
requests,
|
|
18344
|
+
isTestnet,
|
|
18345
|
+
baseUrl,
|
|
18346
|
+
config
|
|
18347
|
+
});
|
|
18348
|
+
if (!parsed.success) {
|
|
18349
|
+
const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
18350
|
+
throw new KitError({
|
|
18351
|
+
...InputError.VALIDATION_FAILED,
|
|
18352
|
+
recoverability: 'FATAL',
|
|
18353
|
+
message: `${SERVICE$1} ${OPERATION$1} failed: ${detail}`,
|
|
18354
|
+
cause: {
|
|
18355
|
+
trace: parsed.error.issues
|
|
18356
|
+
}
|
|
18357
|
+
});
|
|
18358
|
+
}
|
|
18359
|
+
const url = baseUrl === undefined ? buildFeeQuoteUrl({
|
|
18360
|
+
sourceDomain,
|
|
18361
|
+
destinationDomain,
|
|
18362
|
+
isTestnet
|
|
18363
|
+
}) : buildFeeQuoteUrl({
|
|
18364
|
+
sourceDomain,
|
|
18365
|
+
destinationDomain,
|
|
18366
|
+
isTestnet,
|
|
18367
|
+
baseUrl
|
|
18368
|
+
});
|
|
18369
|
+
const body = {
|
|
18370
|
+
amount,
|
|
18371
|
+
feeToken: feeToken ?? NATIVE_FEE_TOKEN,
|
|
18372
|
+
requests: serializeRequests(requests)
|
|
18373
|
+
};
|
|
18374
|
+
const pollingConfig = {
|
|
18375
|
+
...FEE_QUOTE_DEFAULT_CONFIG,
|
|
18376
|
+
...config
|
|
18377
|
+
};
|
|
18378
|
+
let quote;
|
|
18379
|
+
try {
|
|
18380
|
+
quote = await pollApiPost(url, body, isSignedFeeQuote, pollingConfig);
|
|
18381
|
+
} catch (error) {
|
|
18382
|
+
// Only one service-specific code (SERVICE_NOT_ENABLED) needs bespoke
|
|
18383
|
+
// mapping, so it is detected inline rather than via a dedicated
|
|
18384
|
+
// `parseFeeQuoteApiError` parser; everything else flows through the shared
|
|
18385
|
+
// `parseApiError`. Promote to a parser if more coded errors appear.
|
|
18386
|
+
if (isServiceNotEnabled(error)) {
|
|
18387
|
+
throw new KitError({
|
|
18388
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
18389
|
+
recoverability: 'FATAL',
|
|
18390
|
+
message: `${SERVICE$1} ${OPERATION$1} failed: source chain not enabled for fee ` + `quotes (SERVICE_NOT_ENABLED)`,
|
|
18391
|
+
cause: {
|
|
18392
|
+
trace: error
|
|
18393
|
+
}
|
|
18394
|
+
});
|
|
18395
|
+
}
|
|
18396
|
+
throw parseApiError(error, {
|
|
18397
|
+
service: SERVICE$1,
|
|
18398
|
+
operation: OPERATION$1
|
|
18399
|
+
});
|
|
18400
|
+
}
|
|
18401
|
+
// Defense-in-depth: a self-consistent quote's per-item fees sum to the
|
|
18402
|
+
// declared total. Enforced here so callers cannot forget the check.
|
|
18403
|
+
assertFeeItemsSumToTotal(quote);
|
|
18404
|
+
return quote;
|
|
18405
|
+
}
|
|
18406
|
+
|
|
18407
|
+
const SERVICE = 'Quote API';
|
|
18408
|
+
const OPERATION = 'validateQuote';
|
|
18409
|
+
/**
|
|
18410
|
+
* Validate a signed quote against a complete source-chain contract call.
|
|
18411
|
+
*
|
|
18412
|
+
* @param params - The source domain, ABI signature, call arguments, and environment.
|
|
18413
|
+
* @returns The claimability, binding checks, and authoritative expiry status.
|
|
18414
|
+
* @throws {@link KitError} When input, transport, or response validation fails.
|
|
18415
|
+
*
|
|
18416
|
+
* @example
|
|
18417
|
+
* ```typescript
|
|
18418
|
+
* import { validateQuote } from '@circle-fin/provider-fee-v1'
|
|
18419
|
+
*
|
|
18420
|
+
* const result = await validateQuote({
|
|
18421
|
+
* sourceDomain: 3,
|
|
18422
|
+
* // The exact function + arguments, in ABI order, that will be burned on-chain.
|
|
18423
|
+
* abiSignature:
|
|
18424
|
+
* 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))',
|
|
18425
|
+
* args: [
|
|
18426
|
+
* '1000000',
|
|
18427
|
+
* '26',
|
|
18428
|
+
* '0x0000000000000000000000001111111111111111111111111111111111111111',
|
|
18429
|
+
* '0x2222222222222222222222222222222222222222',
|
|
18430
|
+
* '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
18431
|
+
* '0x636374702d666f72776172640000000000000000000000000000000000000000',
|
|
18432
|
+
* ['0x01abcd', '0x3333333333333333333333333333333333333333'],
|
|
18433
|
+
* ],
|
|
18434
|
+
* isTestnet: false,
|
|
18435
|
+
* })
|
|
18436
|
+
* console.log(result.claimable, result.expiry.secondsRemaining)
|
|
18437
|
+
* ```
|
|
18438
|
+
*/ async function validateQuote(params) {
|
|
18439
|
+
const parsed = validateQuoteInputSchema.safeParse(params);
|
|
18440
|
+
if (!parsed.success) {
|
|
18441
|
+
const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
18442
|
+
throw new KitError({
|
|
18443
|
+
...InputError.VALIDATION_FAILED,
|
|
18444
|
+
recoverability: 'FATAL',
|
|
18445
|
+
message: `${SERVICE} ${OPERATION} failed: ${detail}`,
|
|
18446
|
+
cause: {
|
|
18447
|
+
trace: parsed.error.issues
|
|
18448
|
+
}
|
|
18449
|
+
});
|
|
18450
|
+
}
|
|
18451
|
+
const { sourceDomain, abiSignature, args, isTestnet, baseUrl } = parsed.data;
|
|
18452
|
+
// `config` was validated by the schema above; spread the caller's original,
|
|
18453
|
+
// precisely typed `Partial<ApiPollingConfig>` so the merged polling config
|
|
18454
|
+
// stays assignable under `exactOptionalPropertyTypes`.
|
|
18455
|
+
const pollingConfig = {
|
|
18456
|
+
...FEE_QUOTE_DEFAULT_CONFIG,
|
|
18457
|
+
...params.config
|
|
18458
|
+
};
|
|
18459
|
+
const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
|
|
18460
|
+
const url = new URL(`${QUOTE_VALIDATE_USDC_PATH}/${String(sourceDomain)}`, resolvedBaseUrl).toString();
|
|
18461
|
+
try {
|
|
18462
|
+
return await pollApiPost(url, {
|
|
18463
|
+
abiSignature,
|
|
18464
|
+
args
|
|
18465
|
+
}, isValidateQuoteResult, pollingConfig);
|
|
18466
|
+
} catch (error) {
|
|
18467
|
+
if (isServiceNotEnabled(error)) {
|
|
18468
|
+
throw new KitError({
|
|
18469
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
18470
|
+
recoverability: 'FATAL',
|
|
18471
|
+
message: `${SERVICE} ${OPERATION} failed: source chain not enabled for ` + `quote validation (SERVICE_NOT_ENABLED)`,
|
|
18472
|
+
cause: {
|
|
18473
|
+
trace: error
|
|
18474
|
+
}
|
|
18475
|
+
});
|
|
18476
|
+
}
|
|
18477
|
+
throw parseApiError(error, {
|
|
18478
|
+
service: SERVICE,
|
|
18479
|
+
operation: OPERATION
|
|
18480
|
+
});
|
|
18481
|
+
}
|
|
18482
|
+
}
|
|
18483
|
+
|
|
18484
|
+
/** Refresh timestamp quotes this many seconds before submission. */ const QUOTE_EXPIRY_SAFETY_SECONDS = 30;
|
|
18485
|
+
/** 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))';
|
|
18486
|
+
/** Unrestricted CCTP destination caller used by the forwarding relayer. */ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
18487
|
+
function isQuoteNearEstimatedExpiry(quote) {
|
|
18488
|
+
const currentSeconds = Math.floor(Date.now() / 1_000);
|
|
18489
|
+
const expiresAt = quote.expiry.mode === 'TIMESTAMP' ? quote.expiry.expiresAt : quote.expiry.blockEstimatedAt;
|
|
18490
|
+
// A BLOCK_NUMBER quote may omit the advisory `blockEstimatedAt` estimate; when
|
|
18491
|
+
// it is absent, skip this wall-clock pre-check and defer to the authoritative
|
|
18492
|
+
// source-chain-tip validation performed downstream.
|
|
18493
|
+
if (expiresAt === undefined) {
|
|
18494
|
+
return false;
|
|
18495
|
+
}
|
|
18496
|
+
return expiresAt <= currentSeconds + QUOTE_EXPIRY_SAFETY_SECONDS;
|
|
18497
|
+
}
|
|
18498
|
+
function assertSourceFeeRoute(params) {
|
|
18499
|
+
const { source, destination, config } = params;
|
|
18500
|
+
if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
|
|
18501
|
+
throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
|
|
18502
|
+
}
|
|
18503
|
+
const useForwarder = destination.useForwarder;
|
|
18504
|
+
if (useForwarder !== true) {
|
|
18505
|
+
throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
|
|
18506
|
+
}
|
|
18507
|
+
if (config.customFee !== undefined) {
|
|
18508
|
+
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.');
|
|
18509
|
+
}
|
|
18510
|
+
}
|
|
18511
|
+
function buildQuoteBinding(params) {
|
|
18512
|
+
assertSourceFeeRoute(params);
|
|
18513
|
+
const mintRecipient = params.destination.recipientAddress ?? params.destination.address;
|
|
18514
|
+
const hookData = buildForwardingHookData();
|
|
18515
|
+
const requests = [
|
|
18516
|
+
{
|
|
18517
|
+
type: 'FORWARD',
|
|
18518
|
+
params: {
|
|
18519
|
+
hookData,
|
|
18520
|
+
destinationCaller: ZERO_ADDRESS
|
|
18521
|
+
}
|
|
18522
|
+
}
|
|
18523
|
+
];
|
|
18524
|
+
if ((params.config.transferSpeed ?? TransferSpeed.FAST) === TransferSpeed.FAST) {
|
|
18525
|
+
requests.push({
|
|
18526
|
+
type: 'PRE_FINALITY'
|
|
18527
|
+
});
|
|
18528
|
+
}
|
|
18529
|
+
return {
|
|
18530
|
+
sourceDomain: params.source.chain.cctp.domain,
|
|
18531
|
+
destinationDomain: params.destination.chain.cctp.domain,
|
|
18532
|
+
isTestnet: params.source.chain.isTestnet,
|
|
18533
|
+
amount: params.amount,
|
|
18534
|
+
mintRecipient,
|
|
18535
|
+
hookData,
|
|
18536
|
+
destinationCaller: ZERO_ADDRESS,
|
|
18537
|
+
feeToken: params.source.chain.usdcAddress,
|
|
18538
|
+
requests
|
|
18539
|
+
};
|
|
18540
|
+
}
|
|
18541
|
+
async function fetchBoundQuote(binding) {
|
|
18542
|
+
try {
|
|
18543
|
+
const quote = await fetchFeeQuote({
|
|
18544
|
+
sourceDomain: binding.sourceDomain,
|
|
18545
|
+
destinationDomain: binding.destinationDomain,
|
|
18546
|
+
amount: binding.amount,
|
|
18547
|
+
feeToken: binding.feeToken,
|
|
18548
|
+
requests: binding.requests,
|
|
18549
|
+
isTestnet: binding.isTestnet
|
|
18550
|
+
});
|
|
18551
|
+
if (quote.feeToken.toLowerCase() !== binding.feeToken.toLowerCase()) {
|
|
18552
|
+
throw createValidationFailedError$1('feeToken', quote.feeToken, 'Fee Service must return source-chain USDC for source-fee bridging');
|
|
18553
|
+
}
|
|
18554
|
+
return quote;
|
|
18555
|
+
} catch (error) {
|
|
18556
|
+
if (isRateLimitError(error)) {
|
|
18557
|
+
throw new KitError({
|
|
18558
|
+
...RateLimitError.RATE_LIMIT_EXCEEDED,
|
|
18559
|
+
recoverability: 'RETRYABLE',
|
|
18560
|
+
message: 'Fee Service rate limit exceeded. Retry with caller-managed exponential backoff; Bridge Kit does not retry signed quote requests automatically.',
|
|
18561
|
+
cause: {
|
|
18562
|
+
trace: error
|
|
18563
|
+
}
|
|
18564
|
+
});
|
|
18565
|
+
}
|
|
18566
|
+
throw error;
|
|
18567
|
+
}
|
|
18568
|
+
}
|
|
18569
|
+
async function fetchSubmissionQuote(binding) {
|
|
18570
|
+
let quote = await fetchBoundQuote(binding);
|
|
18571
|
+
if (isQuoteNearEstimatedExpiry(quote)) {
|
|
18572
|
+
quote = await fetchBoundQuote(binding);
|
|
18573
|
+
}
|
|
18574
|
+
if (isQuoteNearEstimatedExpiry(quote)) {
|
|
18575
|
+
throw createValidationFailedError$1('quote', undefined, 'Fee Service returned a quote too close to expiry for safe submission');
|
|
18576
|
+
}
|
|
18577
|
+
return quote;
|
|
18578
|
+
}
|
|
18579
|
+
async function validateBoundQuote(binding, signedQuote, refundAddress) {
|
|
18580
|
+
return validateQuote({
|
|
18581
|
+
sourceDomain: binding.sourceDomain,
|
|
18582
|
+
abiSignature: BURN_WITH_FEES_ABI_SIGNATURE,
|
|
18583
|
+
args: [
|
|
18584
|
+
binding.amount,
|
|
18585
|
+
String(binding.destinationDomain),
|
|
18586
|
+
padAddressToBytes32(binding.mintRecipient),
|
|
18587
|
+
binding.feeToken,
|
|
18588
|
+
padAddressToBytes32(binding.destinationCaller),
|
|
18589
|
+
binding.hookData,
|
|
18590
|
+
[
|
|
18591
|
+
signedQuote,
|
|
18592
|
+
refundAddress
|
|
18593
|
+
]
|
|
18594
|
+
],
|
|
18595
|
+
isTestnet: binding.isTestnet
|
|
18596
|
+
});
|
|
18597
|
+
}
|
|
18598
|
+
function isValidationSafe(binding, signedQuote, validation, expectedQuote) {
|
|
18599
|
+
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);
|
|
18600
|
+
}
|
|
18601
|
+
function unsafeQuoteError(validation) {
|
|
18602
|
+
const detail = validation.failedChecks.length > 0 ? ` (${validation.failedChecks.join(', ')})` : '';
|
|
18603
|
+
return createValidationFailedError$1('quote', undefined, `The fee quote is not safe for submission${detail}; ` + 'call estimate again to obtain a valid quote');
|
|
18604
|
+
}
|
|
18605
|
+
function toExecutionFeeQuote(quote) {
|
|
18606
|
+
return {
|
|
18607
|
+
signedQuote: quote.signedQuote,
|
|
18608
|
+
feeToken: quote.feeToken,
|
|
18609
|
+
feeTotalAmount: quote.feeTotalAmount
|
|
18610
|
+
};
|
|
18611
|
+
}
|
|
18612
|
+
function toFeeItems(quote) {
|
|
18613
|
+
return quote.items.map((item)=>({
|
|
18614
|
+
type: item.type,
|
|
18615
|
+
amount: formatUnits(item.amount, 6),
|
|
18616
|
+
args: item.args,
|
|
18617
|
+
argsHash: item.argsHash
|
|
18618
|
+
}));
|
|
18619
|
+
}
|
|
18620
|
+
/**
|
|
18621
|
+
* Estimate a source-fee bridge using a source-denominated signed fee quote.
|
|
18622
|
+
*
|
|
18623
|
+
* @internal
|
|
18624
|
+
*/ async function estimateSourceFeeBridge(params) {
|
|
18625
|
+
const binding = buildQuoteBinding(params);
|
|
18626
|
+
const quote = await fetchSubmissionQuote(binding);
|
|
18627
|
+
const feeTotal = formatUnits(quote.feeTotalAmount, 6);
|
|
18628
|
+
return {
|
|
18629
|
+
token: 'USDC',
|
|
18630
|
+
amount: formatUnits(params.amount, 6),
|
|
18631
|
+
source: {
|
|
18632
|
+
address: params.source.address,
|
|
18633
|
+
chain: params.source.chain.chain
|
|
18634
|
+
},
|
|
18635
|
+
destination: {
|
|
18636
|
+
address: params.destination.address,
|
|
18637
|
+
chain: params.destination.chain.chain,
|
|
18638
|
+
...params.destination.recipientAddress !== undefined && {
|
|
18639
|
+
recipientAddress: params.destination.recipientAddress
|
|
18640
|
+
}
|
|
18641
|
+
},
|
|
18642
|
+
gasFees: [],
|
|
18643
|
+
fees: quote.items.map((item)=>({
|
|
18644
|
+
type: item.type === 'FORWARD' ? 'forwarder' : 'provider',
|
|
18645
|
+
token: 'USDC',
|
|
18646
|
+
amount: formatUnits(item.amount, 6)
|
|
18647
|
+
})),
|
|
18648
|
+
amountReceived: formatUnits(params.amount, 6),
|
|
18649
|
+
feeTotal,
|
|
18650
|
+
feeItems: toFeeItems(quote),
|
|
18651
|
+
totalDebit: formatUnits((BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString(), 6),
|
|
18652
|
+
quoteExpiry: quote.expiry,
|
|
18653
|
+
quote: quote.signedQuote
|
|
18654
|
+
};
|
|
18655
|
+
}
|
|
18656
|
+
async function readAllowance(params, delegate) {
|
|
18657
|
+
const operationContext = {
|
|
18658
|
+
chain: params.source.chain,
|
|
18659
|
+
address: params.source.address
|
|
18660
|
+
};
|
|
18661
|
+
const prepared = await params.source.adapter.prepareAction('usdc.allowance', {
|
|
18662
|
+
walletAddress: params.source.address,
|
|
18663
|
+
delegate
|
|
18664
|
+
}, operationContext);
|
|
18665
|
+
return BigInt(String(await prepared.execute()));
|
|
18666
|
+
}
|
|
18667
|
+
async function executeAndConfirm(request, params, provider) {
|
|
18668
|
+
const txHash = await request.execute();
|
|
18669
|
+
const data = await provider.waitForTransaction(params.source.adapter, txHash, params.source.chain);
|
|
18670
|
+
return {
|
|
18671
|
+
txHash,
|
|
18672
|
+
data
|
|
18673
|
+
};
|
|
18674
|
+
}
|
|
18675
|
+
async function prepareAndPreflight(params, binding, quote, provider) {
|
|
18676
|
+
assertSourceFeeRoute(params);
|
|
18677
|
+
const totalDebit = (BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString();
|
|
18678
|
+
const operationContext = {
|
|
18679
|
+
chain: params.source.chain,
|
|
18680
|
+
address: params.source.address
|
|
18681
|
+
};
|
|
18682
|
+
await validateBalanceForTransaction({
|
|
18683
|
+
adapter: params.source.adapter,
|
|
18684
|
+
amount: totalDebit,
|
|
18685
|
+
token: 'USDC',
|
|
18686
|
+
tokenAddress: params.source.chain.usdcAddress,
|
|
18687
|
+
operationContext
|
|
18688
|
+
});
|
|
18689
|
+
const prepared = await provider.burnWithFees({
|
|
18690
|
+
source: params.source,
|
|
18691
|
+
destinationChain: params.destination.chain,
|
|
18692
|
+
amount: params.amount,
|
|
18693
|
+
mintRecipient: binding.mintRecipient,
|
|
18694
|
+
destinationCaller: binding.destinationCaller,
|
|
18695
|
+
hookData: binding.hookData,
|
|
18696
|
+
claim: {
|
|
18697
|
+
signedQuote: quote.signedQuote,
|
|
18698
|
+
refundAddress: params.source.address
|
|
18699
|
+
},
|
|
18700
|
+
feeToken: quote.feeToken,
|
|
18701
|
+
feeTotalAmount: quote.feeTotalAmount
|
|
18702
|
+
});
|
|
18703
|
+
const wrapper = resolveCCTPV2ContractAddress(params.source.chain, 'tokenMessengerWithFees');
|
|
18704
|
+
const allowance = await readAllowance(params, wrapper);
|
|
18705
|
+
if (allowance >= BigInt(totalDebit)) {
|
|
18706
|
+
return {
|
|
18707
|
+
prepared,
|
|
18708
|
+
approveStep: {
|
|
18709
|
+
name: 'approve',
|
|
18710
|
+
state: 'noop'
|
|
18711
|
+
}
|
|
18712
|
+
};
|
|
18713
|
+
}
|
|
18714
|
+
let lastApproval;
|
|
18715
|
+
for (const approval of prepared.approvals){
|
|
18716
|
+
lastApproval = await executeAndConfirm(approval, params, provider);
|
|
18717
|
+
}
|
|
18718
|
+
return {
|
|
18719
|
+
prepared,
|
|
18720
|
+
approveStep: {
|
|
18721
|
+
name: 'approve',
|
|
18722
|
+
state: 'success',
|
|
18723
|
+
data: lastApproval?.data,
|
|
18724
|
+
...lastApproval?.txHash !== undefined && {
|
|
18725
|
+
txHash: lastApproval.txHash,
|
|
18726
|
+
explorerUrl: buildExplorerUrl(params.source.chain, lastApproval.txHash)
|
|
18727
|
+
}
|
|
18728
|
+
}
|
|
18729
|
+
};
|
|
18730
|
+
}
|
|
18731
|
+
async function resolveExecutionQuote(binding, suppliedQuote, refundAddress) {
|
|
18732
|
+
if (suppliedQuote === undefined) {
|
|
18733
|
+
const fetchedQuote = await fetchSubmissionQuote(binding);
|
|
18734
|
+
return {
|
|
18735
|
+
quote: toExecutionFeeQuote(fetchedQuote),
|
|
18736
|
+
fetchedQuote
|
|
18737
|
+
};
|
|
18738
|
+
}
|
|
18739
|
+
const validation = await validateBoundQuote(binding, suppliedQuote, refundAddress);
|
|
18740
|
+
if (!isValidationSafe(binding, suppliedQuote, validation)) {
|
|
18741
|
+
throw unsafeQuoteError(validation);
|
|
18742
|
+
}
|
|
18743
|
+
return {
|
|
18744
|
+
quote: toExecutionFeeQuote(validation),
|
|
18745
|
+
fetchedQuote: undefined
|
|
18746
|
+
};
|
|
18747
|
+
}
|
|
18748
|
+
/**
|
|
18749
|
+
* Execute a source-fee bridge while preserving receive-exact semantics.
|
|
18750
|
+
*
|
|
18751
|
+
* @internal
|
|
18752
|
+
*/ async function executeSourceFeeBridge(rawParams, params, provider) {
|
|
18753
|
+
// Narrows `params` to the CCTP v2 route type for the rest of this function.
|
|
18754
|
+
// buildQuoteBinding asserts too, but that narrows its own scope, not this one.
|
|
18755
|
+
assertSourceFeeRoute(params);
|
|
18756
|
+
const binding = buildQuoteBinding(params);
|
|
18757
|
+
const suppliedQuote = rawParams.quote;
|
|
18758
|
+
let { quote, fetchedQuote } = await resolveExecutionQuote(binding, suppliedQuote, params.source.address);
|
|
18759
|
+
let { prepared, approveStep } = await prepareAndPreflight(params, binding, quote, provider);
|
|
18760
|
+
// Validate against the current source-chain tip after approval confirmation.
|
|
18761
|
+
// BLOCK_NUMBER expiries cannot be checked safely with wall-clock time alone.
|
|
18762
|
+
let validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
|
|
18763
|
+
if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
|
|
18764
|
+
if (suppliedQuote === undefined) {
|
|
18765
|
+
fetchedQuote = await fetchSubmissionQuote(binding);
|
|
18766
|
+
quote = toExecutionFeeQuote(fetchedQuote);
|
|
18767
|
+
const refreshedPreparation = await prepareAndPreflight(params, binding, quote, provider);
|
|
18768
|
+
prepared = refreshedPreparation.prepared;
|
|
18769
|
+
if (refreshedPreparation.approveStep.state !== 'noop') {
|
|
18770
|
+
approveStep = refreshedPreparation.approveStep;
|
|
18771
|
+
}
|
|
18772
|
+
validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
|
|
18773
|
+
if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
|
|
18774
|
+
throw unsafeQuoteError(validation);
|
|
18775
|
+
}
|
|
18776
|
+
} else {
|
|
18777
|
+
throw unsafeQuoteError(validation);
|
|
18778
|
+
}
|
|
18779
|
+
}
|
|
18780
|
+
// Surface the same step events as the standard bridge() path so
|
|
18781
|
+
// kit.on('approve'|'burn'|'mint', ...) handlers fire for source-fee bridges.
|
|
18782
|
+
if (approveStep.state !== 'noop') {
|
|
18783
|
+
provider.emitBridgeStep('approve', approveStep);
|
|
18784
|
+
}
|
|
18785
|
+
const burn = await executeAndConfirm(prepared.burn, params, provider);
|
|
18786
|
+
const burnStep = {
|
|
18787
|
+
name: 'burn',
|
|
18788
|
+
state: 'success',
|
|
18789
|
+
txHash: burn.txHash,
|
|
18790
|
+
data: burn.data,
|
|
18791
|
+
explorerUrl: buildExplorerUrl(params.source.chain, burn.txHash)
|
|
18792
|
+
};
|
|
18793
|
+
provider.emitBridgeStep('burn', burnStep);
|
|
18794
|
+
const resultBase = {
|
|
18795
|
+
amount: params.amount,
|
|
18796
|
+
token: 'USDC',
|
|
18797
|
+
config: params.config,
|
|
18798
|
+
provider: provider.name,
|
|
18799
|
+
source: {
|
|
18800
|
+
address: params.source.address,
|
|
18801
|
+
chain: params.source.chain
|
|
18802
|
+
},
|
|
18803
|
+
destination: {
|
|
18804
|
+
address: params.destination.address,
|
|
18805
|
+
chain: params.destination.chain,
|
|
18806
|
+
...params.destination.recipientAddress !== undefined && {
|
|
18807
|
+
recipientAddress: params.destination.recipientAddress
|
|
18808
|
+
},
|
|
18809
|
+
useForwarder: true
|
|
18810
|
+
}
|
|
18811
|
+
};
|
|
18812
|
+
const attestation = await provider.fetchRelayerMint(params.source, burn.txHash);
|
|
18813
|
+
const forwardTxHash = attestation.forwardTxHash;
|
|
18814
|
+
// The burn already moved funds. If the relayer confirms without a
|
|
18815
|
+
// destination hash, surface an error-state result that preserves the burn
|
|
18816
|
+
// step (so `retry()` can resume the mint) instead of throwing and discarding
|
|
18817
|
+
// the completed burn.
|
|
18818
|
+
if (typeof forwardTxHash !== 'string' || forwardTxHash.trim() === '') {
|
|
18819
|
+
const mintStep = {
|
|
18820
|
+
name: 'mint',
|
|
18821
|
+
state: 'error',
|
|
18822
|
+
forwarded: true,
|
|
18823
|
+
errorCategory: 'failed_offchain',
|
|
18824
|
+
errorMessage: 'Relayer confirmation did not include a destination transaction hash'
|
|
18825
|
+
};
|
|
18826
|
+
provider.emitBridgeStep('mint', mintStep);
|
|
18827
|
+
return {
|
|
18828
|
+
...resultBase,
|
|
18829
|
+
state: 'error',
|
|
18830
|
+
steps: [
|
|
18831
|
+
approveStep,
|
|
18832
|
+
burnStep,
|
|
18833
|
+
mintStep
|
|
18834
|
+
]
|
|
18835
|
+
};
|
|
18836
|
+
}
|
|
18837
|
+
const mintStep = {
|
|
18838
|
+
name: 'mint',
|
|
18839
|
+
state: 'success',
|
|
18840
|
+
forwarded: true,
|
|
18841
|
+
txHash: forwardTxHash,
|
|
18842
|
+
explorerUrl: buildExplorerUrl(params.destination.chain, forwardTxHash)
|
|
18843
|
+
};
|
|
18844
|
+
provider.emitBridgeStep('mint', mintStep);
|
|
18845
|
+
return {
|
|
18846
|
+
...resultBase,
|
|
18847
|
+
state: 'success',
|
|
18848
|
+
steps: [
|
|
18849
|
+
approveStep,
|
|
18850
|
+
burnStep,
|
|
18851
|
+
mintStep
|
|
18852
|
+
]
|
|
18853
|
+
};
|
|
18854
|
+
}
|
|
18855
|
+
|
|
16924
18856
|
/** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg$3.name);
|
|
16925
18857
|
/**
|
|
16926
18858
|
* Pick the most-relevant `txHash` to attach to an error telemetry payload.
|
|
@@ -17148,11 +19080,18 @@ function assertCCTPV2Config(config) {
|
|
|
17148
19080
|
this.validateNetworkCompatibility(resolvedParams);
|
|
17149
19081
|
// Merge the custom fee config into the resolved params
|
|
17150
19082
|
const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
//
|
|
17154
|
-
|
|
17155
|
-
|
|
19083
|
+
let result;
|
|
19084
|
+
// Execute the explicit source-fee path without changing legacy
|
|
19085
|
+
// useForwarder behavior for callers that did not opt in.
|
|
19086
|
+
if (params.config?.feePayment === 'source') {
|
|
19087
|
+
const sourceFeeProvider = this.findSourceFeeProvider(finalResolvedParams);
|
|
19088
|
+
result = formatBridgeResult(await executeSourceFeeBridge(params, finalResolvedParams, sourceFeeProvider), 'to-human-readable');
|
|
19089
|
+
} else {
|
|
19090
|
+
// Find a provider that supports this route
|
|
19091
|
+
const provider = this.findProviderForRoute(finalResolvedParams);
|
|
19092
|
+
// Execute the transfer using the provider and format the result.
|
|
19093
|
+
result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
|
|
19094
|
+
}
|
|
17156
19095
|
// Emit error telemetry when the provider returns an error state
|
|
17157
19096
|
// (provider records step failures in the result instead of throwing).
|
|
17158
19097
|
if (result.state === 'error') {
|
|
@@ -17275,42 +19214,7 @@ function assertCCTPV2Config(config) {
|
|
|
17275
19214
|
tokenIn: result.token
|
|
17276
19215
|
});
|
|
17277
19216
|
}
|
|
17278
|
-
|
|
17279
|
-
* Estimate the cost and fees for a cross-chain USDC bridge operation.
|
|
17280
|
-
*
|
|
17281
|
-
* This method calculates the expected gas fees and protocol costs for bridging
|
|
17282
|
-
* without actually executing the transaction. It performs the same validation
|
|
17283
|
-
* as the bridge method but stops before execution.
|
|
17284
|
-
*
|
|
17285
|
-
* @param params - The bridge parameters for cost estimation, including optional invocation metadata
|
|
17286
|
-
* @returns Promise resolving to detailed cost breakdown including gas estimates
|
|
17287
|
-
* @throws {KitError} When the parameters are invalid.
|
|
17288
|
-
* @throws {UnsupportedRouteError} When the route is not supported.
|
|
17289
|
-
*
|
|
17290
|
-
* @example
|
|
17291
|
-
* ```typescript
|
|
17292
|
-
* // Basic usage
|
|
17293
|
-
* const estimate = await kit.estimate({
|
|
17294
|
-
* from: { adapter: adapter, chain: 'Ethereum' },
|
|
17295
|
-
* to: { adapter: adapter, chain: 'Base' },
|
|
17296
|
-
* amount: '10.50',
|
|
17297
|
-
* token: 'USDC'
|
|
17298
|
-
* })
|
|
17299
|
-
* console.log('Estimated cost:', estimate.totalCost)
|
|
17300
|
-
*
|
|
17301
|
-
* // With custom invocation metadata
|
|
17302
|
-
* const estimate = await kit.estimate({
|
|
17303
|
-
* from: { adapter: adapter, chain: 'Ethereum' },
|
|
17304
|
-
* to: { adapter: adapter, chain: 'Base' },
|
|
17305
|
-
* amount: '10.50',
|
|
17306
|
-
* token: 'USDC',
|
|
17307
|
-
* invocationMeta: {
|
|
17308
|
-
* traceId: 'custom-trace-id',
|
|
17309
|
-
* callers: [{ type: 'app', name: 'MyDApp', version: '1.0.0' }],
|
|
17310
|
-
* },
|
|
17311
|
-
* })
|
|
17312
|
-
* ```
|
|
17313
|
-
*/ async estimate(params) {
|
|
19217
|
+
async estimate(params) {
|
|
17314
19218
|
return withErrorTelemetry(async ()=>{
|
|
17315
19219
|
// First validate the parameters
|
|
17316
19220
|
assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
|
|
@@ -17320,6 +19224,10 @@ function assertCCTPV2Config(config) {
|
|
|
17320
19224
|
this.validateNetworkCompatibility(resolvedParams);
|
|
17321
19225
|
// Merge the custom fee config into the resolved params
|
|
17322
19226
|
const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
|
|
19227
|
+
if (params.config?.feePayment === 'source') {
|
|
19228
|
+
this.findSourceFeeProvider(finalResolvedParams);
|
|
19229
|
+
return estimateSourceFeeBridge(finalResolvedParams);
|
|
19230
|
+
}
|
|
17323
19231
|
// Find a provider that supports this route
|
|
17324
19232
|
const provider = this.findProviderForRoute(finalResolvedParams);
|
|
17325
19233
|
// Estimate the transfer using the provider and format amounts to human-readable strings
|
|
@@ -17368,6 +19276,9 @@ function assertCCTPV2Config(config) {
|
|
|
17368
19276
|
* // Get only chains that support forwarding
|
|
17369
19277
|
* const forwarderChains = kit.getSupportedChains({ forwarderSupported: true })
|
|
17370
19278
|
*
|
|
19279
|
+
* // Get only chains that can pay fees on the source chain (receive-exact)
|
|
19280
|
+
* const sourceFeeChains = kit.getSupportedChains({ sourceFeeSupported: true })
|
|
19281
|
+
*
|
|
17371
19282
|
* console.log('Supported chains:')
|
|
17372
19283
|
* allChains.forEach(chain => {
|
|
17373
19284
|
* console.log(`- ${chain.name} (${chain.type})`)
|
|
@@ -17416,6 +19327,10 @@ function assertCCTPV2Config(config) {
|
|
|
17416
19327
|
return options.forwarderSupported ? fs.source || fs.destination : !fs.source && !fs.destination;
|
|
17417
19328
|
});
|
|
17418
19329
|
}
|
|
19330
|
+
// Apply source-paid ("receive-exact") fee support filter if provided
|
|
19331
|
+
if (options?.sourceFeeSupported !== undefined) {
|
|
19332
|
+
chains = chains.filter((chain)=>hasSourceFeeSupport(chain) === options.sourceFeeSupported);
|
|
19333
|
+
}
|
|
17419
19334
|
return chains;
|
|
17420
19335
|
}
|
|
17421
19336
|
/**
|
|
@@ -17450,6 +19365,20 @@ function assertCCTPV2Config(config) {
|
|
|
17450
19365
|
return provider;
|
|
17451
19366
|
}
|
|
17452
19367
|
/**
|
|
19368
|
+
* Find the default CCTP v2 provider for a source-fee forwarding route.
|
|
19369
|
+
*
|
|
19370
|
+
* @param params - The resolved provider parameters.
|
|
19371
|
+
* @returns The CCTP v2 provider that supports the forwarded route.
|
|
19372
|
+
* @throws {UnsupportedRouteError} When no source-fee provider supports the route.
|
|
19373
|
+
* @internal
|
|
19374
|
+
*/ findSourceFeeProvider(params) {
|
|
19375
|
+
const provider = this.providers.find((candidate)=>candidate instanceof CCTPV2BridgingProvider && candidate.supportsRoute(params.source.chain, params.destination.chain, params.token, true));
|
|
19376
|
+
if (!(provider instanceof CCTPV2BridgingProvider)) {
|
|
19377
|
+
throw createUnsupportedRouteError(params.source.chain.name, params.destination.chain.name);
|
|
19378
|
+
}
|
|
19379
|
+
return provider;
|
|
19380
|
+
}
|
|
19381
|
+
/**
|
|
17453
19382
|
* Merge custom fee configuration into provider parameters.
|
|
17454
19383
|
*
|
|
17455
19384
|
* Prioritizes any custom fee configuration already present on the
|
|
@@ -17658,7 +19587,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
|
|
|
17658
19587
|
};
|
|
17659
19588
|
|
|
17660
19589
|
var name$1 = "@circle-fin/swap-kit";
|
|
17661
|
-
var version$1 = "1.
|
|
19590
|
+
var version$1 = "1.6.0";
|
|
17662
19591
|
var pkg$1 = {
|
|
17663
19592
|
name: name$1,
|
|
17664
19593
|
version: version$1};
|
|
@@ -17672,7 +19601,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17672
19601
|
* Catches obviously malformed addresses at parse time; chain-specific validation
|
|
17673
19602
|
* is performed in buildServiceParams.
|
|
17674
19603
|
*/ const destinationAddressSchema = zod.z.union([
|
|
17675
|
-
evmAddressSchema,
|
|
19604
|
+
evmAddressSchema$1,
|
|
17676
19605
|
solanaAddressSchema
|
|
17677
19606
|
]);
|
|
17678
19607
|
/**
|
|
@@ -17718,9 +19647,16 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17718
19647
|
message: 'stopLimit must be greater than 0'
|
|
17719
19648
|
}).optional(),
|
|
17720
19649
|
customFee: serviceSwapCustomFeeSchema.optional(),
|
|
19650
|
+
apiKey: zod.z.string({
|
|
19651
|
+
invalid_type_error: 'apiKey must be a string'
|
|
19652
|
+
})// Tolerate '' so the `process.env.CIRCLE_API_KEY ?? ''` idiom falls back to
|
|
19653
|
+
// kitKey via resolveApiKey instead of being rejected here.
|
|
19654
|
+
.optional(),
|
|
17721
19655
|
kitKey: zod.z.string({
|
|
17722
19656
|
invalid_type_error: 'kitKey must be a string'
|
|
17723
|
-
})
|
|
19657
|
+
})// Tolerate '' so an unset kit-key env var yields the permissionless path,
|
|
19658
|
+
// matching buildServiceParams (which already omits an empty credential).
|
|
19659
|
+
.optional(),
|
|
17724
19660
|
provider: zod.z.string({
|
|
17725
19661
|
invalid_type_error: 'provider must be a string'
|
|
17726
19662
|
}).min(1, 'provider must be a non-empty string').optional(),
|
|
@@ -17893,6 +19829,41 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17893
19829
|
*
|
|
17894
19830
|
* @internal
|
|
17895
19831
|
*/ const MAX_RATE_ADDRESSES_PER_REQUEST = 100;
|
|
19832
|
+
/**
|
|
19833
|
+
* Environment prefixes carried by Circle platform API keys.
|
|
19834
|
+
*
|
|
19835
|
+
* A Circle API key is `<ENV>_API_KEY:<keyId>:<keySecret>`, where `<ENV>` is one
|
|
19836
|
+
* of these prefixes.
|
|
19837
|
+
*
|
|
19838
|
+
* @internal
|
|
19839
|
+
*/ const API_KEY_ENV_PREFIXES = [
|
|
19840
|
+
'TEST',
|
|
19841
|
+
'LIVE',
|
|
19842
|
+
'SAND',
|
|
19843
|
+
'SANDBOX',
|
|
19844
|
+
'SMOK',
|
|
19845
|
+
'PROD',
|
|
19846
|
+
'STAG',
|
|
19847
|
+
'DEV'
|
|
19848
|
+
];
|
|
19849
|
+
/**
|
|
19850
|
+
* Accepted credential formats for Stablecoin Service authentication.
|
|
19851
|
+
*
|
|
19852
|
+
* Matches a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
|
|
19853
|
+
* the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`). API keys are the
|
|
19854
|
+
* recommended credential; kit keys remain accepted as the legacy path.
|
|
19855
|
+
*
|
|
19856
|
+
* @remarks
|
|
19857
|
+
* This is a local pre-flight check, not the authority — the Stablecoin Service
|
|
19858
|
+
* validates the credential and answers 401 when it rejects one. The prefix list
|
|
19859
|
+
* is therefore deliberately permissive: a valid key carrying a prefix this SDK
|
|
19860
|
+
* has not been taught about should reach the service and be judged there rather
|
|
19861
|
+
* than be refused locally, since refusing locally is indistinguishable from an
|
|
19862
|
+
* outage to the caller. Kept as the single source of truth so the pattern is
|
|
19863
|
+
* not restated per call site.
|
|
19864
|
+
*
|
|
19865
|
+
* @internal
|
|
19866
|
+
*/ const API_KEY_PATTERN = new RegExp(`^(?:KIT_KEY|(?:${API_KEY_ENV_PREFIXES.join('|')})_API_KEY)` + ':[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$');
|
|
17896
19867
|
|
|
17897
19868
|
/**
|
|
17898
19869
|
* Zod schema for validating stop limits.
|
|
@@ -17961,13 +19932,14 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17961
19932
|
/**
|
|
17962
19933
|
* Zod schema for validating API keys.
|
|
17963
19934
|
*
|
|
17964
|
-
*
|
|
19935
|
+
* Accepts a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
|
|
19936
|
+
* the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`).
|
|
17965
19937
|
*
|
|
17966
19938
|
* @example
|
|
17967
19939
|
* ```typescript
|
|
17968
19940
|
* import { apiKeySchema } from '@core/service-client'
|
|
17969
19941
|
*
|
|
17970
|
-
* const result = apiKeySchema.safeParse('
|
|
19942
|
+
* const result = apiKeySchema.safeParse('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
|
|
17971
19943
|
* if (!result.success) {
|
|
17972
19944
|
* console.error('Invalid API key format:', result.error.issues)
|
|
17973
19945
|
* }
|
|
@@ -17975,7 +19947,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
17975
19947
|
*/ const apiKeySchema = zod.z.string({
|
|
17976
19948
|
required_error: 'API key is required',
|
|
17977
19949
|
invalid_type_error: 'Invalid API key format'
|
|
17978
|
-
}).regex(
|
|
19950
|
+
}).regex(API_KEY_PATTERN, 'Invalid API key format');
|
|
17979
19951
|
/**
|
|
17980
19952
|
* Zod schema for platform fees configuration.
|
|
17981
19953
|
*
|
|
@@ -18414,6 +20386,40 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
18414
20386
|
transaction: createSwapTransactionSchema
|
|
18415
20387
|
});
|
|
18416
20388
|
|
|
20389
|
+
/**
|
|
20390
|
+
* Resolve the credential to authenticate a Stablecoin Service request with.
|
|
20391
|
+
*
|
|
20392
|
+
* `apiKey` is the supported field; `kitKey` is the deprecated alias kept for
|
|
20393
|
+
* existing integrations. When both are supplied `apiKey` wins, so a caller
|
|
20394
|
+
* migrating field-by-field cannot be silently pinned to a stale credential.
|
|
20395
|
+
*
|
|
20396
|
+
* An empty-string value is treated as absent on either field. Without this, the
|
|
20397
|
+
* common `process.env.CIRCLE_API_KEY ?? ''` idiom (which yields `''` when the
|
|
20398
|
+
* variable is unset) would either shadow a working `kitKey` or, on a bare
|
|
20399
|
+
* `kitKey: ''`, reach downstream validation as an invalid credential instead of
|
|
20400
|
+
* falling through to the permissionless path.
|
|
20401
|
+
*
|
|
20402
|
+
* @param source - Object carrying either credential field, or neither.
|
|
20403
|
+
* @returns The credential to use, or `undefined` for the permissionless
|
|
20404
|
+
* (keyless) path.
|
|
20405
|
+
*
|
|
20406
|
+
* @example
|
|
20407
|
+
* ```typescript
|
|
20408
|
+
* import { resolveApiKey } from '@core/service-client'
|
|
20409
|
+
*
|
|
20410
|
+
* resolveApiKey({ apiKey: 'TEST_API_KEY:id:secret' }) // 'TEST_API_KEY:id:secret'
|
|
20411
|
+
* resolveApiKey({ kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
|
|
20412
|
+
* resolveApiKey({ apiKey: '', kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
|
|
20413
|
+
* resolveApiKey({ kitKey: '' }) // undefined
|
|
20414
|
+
* resolveApiKey({}) // undefined
|
|
20415
|
+
* ```
|
|
20416
|
+
*/ const resolveApiKey = (source)=>{
|
|
20417
|
+
// Treat an empty-string value as absent on either field so the
|
|
20418
|
+
// `env ?? ''` idiom falls through to the next credential (or permissionless).
|
|
20419
|
+
const normalize = (value)=>value !== undefined && value !== '' ? value : undefined;
|
|
20420
|
+
return normalize(source.apiKey) ?? normalize(source.kitKey);
|
|
20421
|
+
};
|
|
20422
|
+
|
|
18417
20423
|
/**
|
|
18418
20424
|
* Zod schema for validating EVM adapter capabilities.
|
|
18419
20425
|
*
|
|
@@ -18843,7 +20849,7 @@ const abiParameterSchema = zod.z.object({
|
|
|
18843
20849
|
*/ zod.z.object({
|
|
18844
20850
|
type: zod.z.literal('evm'),
|
|
18845
20851
|
abi: abiSchema,
|
|
18846
|
-
address: evmAddressSchema,
|
|
20852
|
+
address: evmAddressSchema$1,
|
|
18847
20853
|
functionName: zod.z.string({
|
|
18848
20854
|
required_error: 'Function name is required',
|
|
18849
20855
|
invalid_type_error: 'Function name must be a string'
|
|
@@ -18880,7 +20886,7 @@ const abiParameterSchema = zod.z.object({
|
|
|
18880
20886
|
* }
|
|
18881
20887
|
* ```
|
|
18882
20888
|
*/ zod.z.object({
|
|
18883
|
-
address: evmAddressSchema,
|
|
20889
|
+
address: evmAddressSchema$1,
|
|
18884
20890
|
value: zod.z.bigint({
|
|
18885
20891
|
required_error: 'Value is required for native transfers',
|
|
18886
20892
|
invalid_type_error: 'Value must be a bigint'
|
|
@@ -18966,7 +20972,7 @@ zod.z.object({
|
|
|
18966
20972
|
signature: evmSignatureSchema,
|
|
18967
20973
|
tokenInputs: zod.z.array(zod.z.object({
|
|
18968
20974
|
permitType: zod.z.nativeEnum(PermitType),
|
|
18969
|
-
token: evmAddressSchema,
|
|
20975
|
+
token: evmAddressSchema$1,
|
|
18970
20976
|
amount: zod.z.bigint().refine((value)=>value >= 0n, {
|
|
18971
20977
|
message: 'amount must be a non-negative bigint'
|
|
18972
20978
|
}),
|
|
@@ -19386,7 +21392,7 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
|
|
|
19386
21392
|
/**
|
|
19387
21393
|
* Fee recipient address (required).
|
|
19388
21394
|
* Must be a valid EVM address or Solana address.
|
|
19389
|
-
*/ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
21395
|
+
*/ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
19390
21396
|
message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
|
|
19391
21397
|
})
|
|
19392
21398
|
}).strict();
|
|
@@ -19398,7 +21404,8 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
|
|
|
19398
21404
|
* - slippageBps: Optional positive number for slippage tolerance
|
|
19399
21405
|
* - stopLimit: Optional decimal string for minimum output
|
|
19400
21406
|
* - customFee: Optional fee configuration
|
|
19401
|
-
* -
|
|
21407
|
+
* - apiKey: Optional credential string
|
|
21408
|
+
* - kitKey: Optional credential string (deprecated alias for apiKey)
|
|
19402
21409
|
*/ const swapConfigSchema = zod.z.object({
|
|
19403
21410
|
allowanceStrategy: allowanceStrategySchema.optional(),
|
|
19404
21411
|
slippageBps: zod.z.number().int().min(0).optional(),
|
|
@@ -19408,11 +21415,12 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
|
|
|
19408
21415
|
attributeName: 'stopLimit'
|
|
19409
21416
|
})(zod.z.string())).optional(),
|
|
19410
21417
|
customFee: swapCustomFeeSchema.optional(),
|
|
21418
|
+
apiKey: zod.z.string().optional(),
|
|
19411
21419
|
kitKey: zod.z.string().optional()
|
|
19412
21420
|
});
|
|
19413
21421
|
const swapDestinationSchema = zod.z.object({
|
|
19414
21422
|
chain: optionalSwapChainIdentifierField,
|
|
19415
|
-
recipientAddress: zod.z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
21423
|
+
recipientAddress: zod.z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
|
|
19416
21424
|
message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
|
|
19417
21425
|
}).optional()
|
|
19418
21426
|
}).strict();
|
|
@@ -19601,7 +21609,7 @@ new Set(Object.values(Blockchain));
|
|
|
19601
21609
|
registerKit(`${pkg$1.name}/${pkg$1.version}`);
|
|
19602
21610
|
|
|
19603
21611
|
var name = "@circle-fin/earn-kit";
|
|
19604
|
-
var version = "1.
|
|
21612
|
+
var version = "1.6.0";
|
|
19605
21613
|
var pkg = {
|
|
19606
21614
|
name: name,
|
|
19607
21615
|
version: version};
|
|
@@ -19673,7 +21681,7 @@ function isNonNegativeBigIntLike(value) {
|
|
|
19673
21681
|
}
|
|
19674
21682
|
}
|
|
19675
21683
|
const hexSignatureSchema = evmSignatureSchema;
|
|
19676
|
-
const hexAddressSchema = evmAddressSchema;
|
|
21684
|
+
const hexAddressSchema = evmAddressSchema$1;
|
|
19677
21685
|
// '0x' prefix + 32 bytes * 2 hex chars.
|
|
19678
21686
|
const BYTES32_HEX_LENGTH = 66;
|
|
19679
21687
|
const bridgeFeeTokenSchema = hexAddressSchema;
|
|
@@ -20578,7 +22586,7 @@ createTokenRegistry();
|
|
|
20578
22586
|
* fast instead of round-tripping to the service.
|
|
20579
22587
|
*
|
|
20580
22588
|
* @internal
|
|
20581
|
-
*/ 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');
|
|
22589
|
+
*/ 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');
|
|
20582
22590
|
/**
|
|
20583
22591
|
* Schema for the adapter context within earn operations.
|
|
20584
22592
|
*
|
|
@@ -20610,19 +22618,39 @@ const sourceAdapterContextSchema = zod.z.object({
|
|
|
20610
22618
|
/**
|
|
20611
22619
|
* Schema for the EarnConfig options.
|
|
20612
22620
|
*
|
|
20613
|
-
* Validate the
|
|
20614
|
-
*
|
|
20615
|
-
*
|
|
20616
|
-
*
|
|
20617
|
-
*
|
|
20618
|
-
*
|
|
22621
|
+
* Validate the *resolved* credential using the standard `apiKeySchema` format
|
|
22622
|
+
* (`<ENV>_API_KEY:<keyId>:<keySecret>`, or a legacy
|
|
22623
|
+
* `KIT_KEY:<keyId>:<keySecret>`). `apiKey` takes precedence over the deprecated
|
|
22624
|
+
* `kitKey`, so a malformed `kitKey` that is being ignored must not fail a config
|
|
22625
|
+
* that supplies a valid `apiKey` (and vice versa) — only the credential that
|
|
22626
|
+
* would actually be sent is format-checked. When neither is supplied the SDK
|
|
22627
|
+
* operates in permissionless mode. `baseUrl` overrides the Earn Service endpoint
|
|
22628
|
+
* (e.g. staging); `batchTransactions: false` opts out of atomic batched
|
|
22629
|
+
* execution. All are forwarded to the provider, so this `.strict()` schema must
|
|
22630
|
+
* accept them or a valid config object is rejected.
|
|
20619
22631
|
*
|
|
20620
22632
|
* @internal
|
|
20621
22633
|
*/ const earnConfigSchema = zod.z.object({
|
|
20622
|
-
|
|
22634
|
+
apiKey: zod.z.string().optional(),
|
|
22635
|
+
kitKey: zod.z.string().optional(),
|
|
20623
22636
|
baseUrl: zod.z.string().optional(),
|
|
20624
22637
|
batchTransactions: zod.z.boolean().optional()
|
|
20625
|
-
}).strict()
|
|
22638
|
+
}).strict().superRefine((config, ctx)=>{
|
|
22639
|
+
const credential = resolveApiKey(config);
|
|
22640
|
+
if (credential === undefined) {
|
|
22641
|
+
return;
|
|
22642
|
+
}
|
|
22643
|
+
const result = apiKeySchema.safeParse(credential);
|
|
22644
|
+
if (!result.success) {
|
|
22645
|
+
ctx.addIssue({
|
|
22646
|
+
code: zod.z.ZodIssueCode.custom,
|
|
22647
|
+
path: [
|
|
22648
|
+
credential === config.apiKey ? 'apiKey' : 'kitKey'
|
|
22649
|
+
],
|
|
22650
|
+
message: result.error.issues[0]?.message ?? 'Invalid API key format'
|
|
22651
|
+
});
|
|
22652
|
+
}
|
|
22653
|
+
});
|
|
20626
22654
|
/**
|
|
20627
22655
|
* Canonical decimal form: a leading digit with no leading zeros (a single
|
|
20628
22656
|
* '0' is only allowed immediately before the decimal point). Rejects the
|
|
@@ -20694,7 +22722,7 @@ const sourceAdapterContextSchema = zod.z.object({
|
|
|
20694
22722
|
* currently supports EVM vault addresses on Arc Testnet.
|
|
20695
22723
|
*
|
|
20696
22724
|
* @internal
|
|
20697
|
-
*/ const vaultAddressSchema = evmAddressSchema.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
|
|
22725
|
+
*/ const vaultAddressSchema = evmAddressSchema$1.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
|
|
20698
22726
|
/**
|
|
20699
22727
|
* Validation schema for VaultQuery.
|
|
20700
22728
|
*
|
|
@@ -20969,7 +22997,7 @@ const sameChainGetDepositQuoteParamsSchema = zod.z.object({
|
|
|
20969
22997
|
const crossChainGetDepositQuoteParamsSchema = zod.z.object({
|
|
20970
22998
|
from: sourceAdapterContextSchema,
|
|
20971
22999
|
chain: earnBridgeDestinationChainIdentifierSchema,
|
|
20972
|
-
address: evmAddressSchema,
|
|
23000
|
+
address: evmAddressSchema$1,
|
|
20973
23001
|
vaultAddress: vaultAddressSchema,
|
|
20974
23002
|
amount: amountSchema,
|
|
20975
23003
|
transferSpeed: zod.z.enum([
|