@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.
@@ -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
- * Plume Mainnet chain definition
5566
+ * Plasma Mainnet chain definition
4962
5567
  * @remarks
4963
- * This represents the official production network for the Plume blockchain.
4964
- * Plume is a Layer 1 blockchain specialized for DeFi and trading applications
4965
- * with native orderbook and matching engine.
4966
- */ const Plume = defineChain({
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.Plume,
4969
- name: 'Plume',
4970
- title: 'Plume Mainnet',
5573
+ chain: Blockchain.Plasma,
5574
+ name: 'Plasma',
5575
+ title: 'Plasma Mainnet',
4971
5576
  nativeCurrency: {
4972
- name: 'Plume',
4973
- symbol: 'PLUME',
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.13.0";
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` non-empty string
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
- // executor
15285
+ // Destination: GenericExecutor shorthand or explicit recipient + caller.
14295
15286
  const executor = p['executor'];
14296
- if (typeof executor !== 'string' || executor === '') {
14297
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
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
- * Dispatch a bridge step event through the provider's action dispatcher.
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 name - The step name (approve, burn, fetchAttestation, or mint).
14722
- * @param step - The completed bridge step containing transaction details and explorerUrl.
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 step: BridgeStep = {
14729
- * name: 'burn',
14730
- * state: 'success',
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 dispatchStepEvent(name, step, provider, invocation) {
14738
- if (!provider.actionDispatcher) {
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.11.0";
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 (memoized internally)
16654
- const hookData = useForwarder ? buildForwardingHookData() : undefined;
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
- * Builds the source-chain `depositForBurnWithHookAndFees` call for the
16683
- * GenericExecutor FORWARD path: fees are collected up front on the source chain
16684
- * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
16685
- * the GenericExecutor, and the GE `hookData` is passed through unchanged.
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 UBK `fastCrossChainDeposit`
16688
- * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
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, executor, hookData, signed-quote claim, and fee.
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 executor is missing, `amount` or `feeTotalAmount` is not
16709
- * a bigint or a numeric string coercible to bigint, the hookData lacks a
16710
- * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
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: Arc,
17702
+ * destinationChain,
16718
17703
  * amount: 1_000_000n,
16719
- * executor: genericExecutorAddress,
16720
- * hookData: geForwardHookData,
16721
- * claim: { signedQuote: '0x01...', refundAddress: userAddress },
16722
- * feeToken: '0x0000000000000000000000000000000000000000', // native
16723
- * feeTotalAmount: 3_500_000n,
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, executor, hookData, claim, feeToken } = params;
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: mintRecipient AND destinationCaller are both the executor.
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: executor,
16767
- destinationCaller: executor,
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
- // Find a provider that supports this route
17152
- const provider = this.findProviderForRoute(finalResolvedParams);
17153
- // Execute the transfer using the provider
17154
- // Format the bridge result into human-readable string values for the user
17155
- const result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
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.5.2";
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
- }).min(1, 'kitKey must be a non-empty string').optional(),
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
- * Validates that the API key is a valid API key format.
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('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
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(/^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/, 'Invalid API key format');
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
- * - kitKey: Optional string identifier
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.5.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 optional Kit Key field using the standard `apiKeySchema`
20614
- * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
20615
- * operates in permissionless mode. `baseUrl` overrides the Earn Service
20616
- * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
20617
- * batched execution. Both are forwarded to the provider, so this `.strict()`
20618
- * schema must accept them or a valid config object is rejected.
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
- kitKey: apiKeySchema.optional(),
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([