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