@circle-fin/app-kit 1.12.1 → 1.14.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.
@@ -33,7 +33,7 @@ var zod = require('zod');
33
33
  var pino = require('pino');
34
34
  var units = require('@ethersproject/units');
35
35
  var bytes = require('@ethersproject/bytes');
36
- require('@ethersproject/abi');
36
+ var abi = require('@ethersproject/abi');
37
37
  var address = require('@ethersproject/address');
38
38
  var bs58 = require('bs58');
39
39
  var web3_js = require('@solana/web3.js');
@@ -792,6 +792,32 @@ class KitError extends Error {
792
792
  type: 'ONCHAIN'
793
793
  }
794
794
  };
795
+ /**
796
+ * Standardized error definitions for LIQUIDITY type errors.
797
+ *
798
+ * LIQUIDITY errors indicate that an upstream provider or AMM cannot fulfill
799
+ * the requested swap size due to insufficient liquidity at the moment.
800
+ * These are typically transient — retrying later or reducing the amount
801
+ * may succeed once liquidity replenishes.
802
+ *
803
+ * @example
804
+ * ```typescript
805
+ * import { LiquidityError } from '@core/errors'
806
+ *
807
+ * const error = new KitError({
808
+ * ...LiquidityError.INSUFFICIENT_LIQUIDITY,
809
+ * recoverability: 'RETRYABLE',
810
+ * message: 'Insufficient liquidity for the requested swap',
811
+ * cause: { trace: { token: '0xA0b86991...' } }
812
+ * })
813
+ * ```
814
+ */ const LiquidityError = {
815
+ /** Upstream provider has a route but cannot fulfill the requested size right now */ INSUFFICIENT_LIQUIDITY: {
816
+ code: 6001,
817
+ name: 'LIQUIDITY_INSUFFICIENT',
818
+ type: 'LIQUIDITY'
819
+ }
820
+ };
795
821
  /**
796
822
  * Standardized error definitions for RPC type errors.
797
823
  *
@@ -839,7 +865,10 @@ class KitError extends Error {
839
865
  type: 'NETWORK'
840
866
  },
841
867
  /** Network request timeout */ TIMEOUT: {
842
- code: 3002},
868
+ code: 3002,
869
+ name: 'NETWORK_TIMEOUT',
870
+ type: 'NETWORK'
871
+ },
843
872
  /** Circle relayer failed to process the forwarding/mint transaction */ RELAYER_FORWARD_FAILED: {
844
873
  code: 3003,
845
874
  name: 'NETWORK_RELAYER_FORWARD_FAILED',
@@ -850,6 +879,58 @@ class KitError extends Error {
850
879
  name: 'NETWORK_RELAYER_PENDING',
851
880
  type: 'NETWORK'
852
881
  }};
882
+ /**
883
+ * Standardized error definitions for RATE_LIMIT type errors.
884
+ *
885
+ * RATE_LIMIT errors indicate API throttling, request frequency limits errors.
886
+ *
887
+ * @example
888
+ * ```typescript
889
+ * import { RateLimitError } from '@core/errors'
890
+ *
891
+ * const error = new KitError({
892
+ * ...RateLimitError.RATE_LIMIT_EXCEEDED,
893
+ * recoverability: 'RETRYABLE',
894
+ * message: 'Rate limit exceeded, please retry later',
895
+ * cause: { trace: { error: '429 Too Many Requests' } }
896
+ * })
897
+ * ```
898
+ */ const RateLimitError = {
899
+ /** Rate limit exceeded */ RATE_LIMIT_EXCEEDED: {
900
+ code: 7001,
901
+ name: 'RATE_LIMIT_EXCEEDED',
902
+ type: 'RATE_LIMIT'
903
+ }
904
+ };
905
+ /**
906
+ * Standardized error definitions for SERVICE type errors.
907
+ *
908
+ * SERVICE errors indicate internal service failures, HTTP 5xx errors,
909
+ * or backend processing issues that are retryable.
910
+ *
911
+ * @example
912
+ * ```typescript
913
+ * import { ServiceError } from '@core/errors'
914
+ *
915
+ * const error = new KitError({
916
+ * ...ServiceError.INTERNAL_ERROR,
917
+ * recoverability: 'RETRYABLE',
918
+ * message: 'Service encountered an internal error (500)',
919
+ * cause: { trace: { statusCode: 500 } }
920
+ * })
921
+ * ```
922
+ */ const ServiceError = {
923
+ /** Internal server error (HTTP 5xx) */ INTERNAL_ERROR: {
924
+ code: 8001,
925
+ name: 'SERVICE_INTERNAL_ERROR',
926
+ type: 'SERVICE'
927
+ },
928
+ /** Unknown or unclassified error that cannot be categorized */ UNKNOWN_ERROR: {
929
+ code: 8002,
930
+ name: 'SERVICE_UNKNOWN_ERROR',
931
+ type: 'SERVICE'
932
+ }
933
+ };
853
934
 
854
935
  /**
855
936
  * Creates error for network type mismatch between source and destination.
@@ -2221,6 +2302,32 @@ class KitError extends Error {
2221
2302
  }
2222
2303
  return false;
2223
2304
  }
2305
+ /**
2306
+ * Type guard to check if error is KitError with RATE_LIMIT type.
2307
+ *
2308
+ * RATE_LIMIT errors indicate API throttling or request frequency limits.
2309
+ * These errors are typically RETRYABLE after a delay.
2310
+ *
2311
+ * @param error - Unknown error to check
2312
+ * @returns True if error is KitError with RATE_LIMIT type
2313
+ *
2314
+ * @example
2315
+ * ```typescript
2316
+ * import { isRateLimitError } from '@core/errors'
2317
+ *
2318
+ * try {
2319
+ * await kit.bridge(params)
2320
+ * } catch (error) {
2321
+ * if (isRateLimitError(error)) {
2322
+ * console.log('Rate limited, retrying in 60s')
2323
+ * await sleep(60000)
2324
+ * retry()
2325
+ * }
2326
+ * }
2327
+ * ```
2328
+ */ function isRateLimitError(error) {
2329
+ return isKitError(error) && error.type === ERROR_TYPES.RATE_LIMIT;
2330
+ }
2224
2331
  /**
2225
2332
  * Safely extracts error message from any error type.
2226
2333
  *
@@ -2470,6 +2577,478 @@ class KitError extends Error {
2470
2577
  return chain;
2471
2578
  }
2472
2579
 
2580
+ /**
2581
+ * Proxy-specific structured error codes carried in `responseBody.code`.
2582
+ *
2583
+ * These are NOT HTTP status codes — they are application-level identifiers
2584
+ * the stablecoin-kits-proxy emits inside JSON error bodies so the kit can
2585
+ * distinguish conditions that share the same HTTP status (e.g. a 400 caused
2586
+ * by an out-of-range swap amount vs. a generic validation failure).
2587
+ *
2588
+ * @internal
2589
+ */ const ProxyErrorCode = {
2590
+ /** Swap amount outside the upstream provider's accepted bounds (HTTP 400) */ INVALID_SWAP_AMOUNT: 331017,
2591
+ /** Upstream liquidity insufficient for the requested size (HTTP 503) */ LOW_LIQUIDITY: 331018
2592
+ };
2593
+ /**
2594
+ * Parses raw HTTP API errors into structured KitError instances.
2595
+ *
2596
+ * This function uses pattern matching to identify common HTTP error types
2597
+ * and converts them into standardized KitError format. It handles errors
2598
+ * from fetch, HTTP status codes, timeouts, and network failures.
2599
+ *
2600
+ * The parser recognizes the following error patterns:
2601
+ * - Client errors (4xx) - validation, authentication, not found
2602
+ * - Server errors (5xx) - service unavailability
2603
+ * - Timeout errors
2604
+ * - Network connectivity errors
2605
+ * - Rate limiting
2606
+ *
2607
+ * Unrecognized errors are treated as fatal `SERVICE` errors, as their
2608
+ * cause and recoverability are unknown.
2609
+ *
2610
+ * @param error - The raw error from the API call
2611
+ * @param context - Context information including operation name
2612
+ * @returns A structured KitError instance
2613
+ *
2614
+ * @example
2615
+ * ```typescript
2616
+ * try {
2617
+ * const response = await fetch(url)
2618
+ * } catch (error) {
2619
+ * throw parseApiError(error, { operation: 'getQuote' })
2620
+ * }
2621
+ * ```
2622
+ */ function parseApiError(error, context) {
2623
+ // If it's already a KitError, return it as-is
2624
+ if (error instanceof KitError) {
2625
+ return error;
2626
+ }
2627
+ const msg = getErrorMessage(error);
2628
+ const statusCode = extractHttpStatusCode(msg);
2629
+ const serviceName = context.service ?? 'Stablecoin Service';
2630
+ const operation = context.operation ?? 'API';
2631
+ const responseBody = extractResponseBody(error);
2632
+ // Rate limit errors (429)
2633
+ if (statusCode === 429 || /too many requests|rate limit exceeded/i.test(msg)) {
2634
+ return handleRateLimitError(serviceName, operation, error);
2635
+ }
2636
+ // HTTP 4xx Client Errors
2637
+ if (statusCode !== null && statusCode >= 400 && statusCode < 500) {
2638
+ return handleClientError(statusCode, serviceName, operation, error, msg, responseBody);
2639
+ }
2640
+ // HTTP 5xx Server Errors
2641
+ if (statusCode !== null && statusCode >= 500 && statusCode < 600) {
2642
+ return handleServerError(statusCode, serviceName, operation, error, responseBody);
2643
+ }
2644
+ // Timeout errors
2645
+ if (/timeout|timed out/i.test(msg)) {
2646
+ return handleTimeoutError(serviceName, operation, error);
2647
+ }
2648
+ // Network connectivity errors
2649
+ if (/connection (refused|failed)|network|ENOTFOUND|ECONNREFUSED|ETIMEDOUT/i.test(msg)) {
2650
+ return handleConnectionError(serviceName, operation, error);
2651
+ }
2652
+ // Fallback: Unknown error - use UNKNOWN_ERROR with fatal recoverability since we don't know what the error is
2653
+ return new KitError({
2654
+ ...ServiceError.UNKNOWN_ERROR,
2655
+ recoverability: 'FATAL',
2656
+ message: `${serviceName} ${operation} failed: ${msg.length > 0 ? msg : 'Unknown error'}`,
2657
+ cause: {
2658
+ trace: error
2659
+ }
2660
+ });
2661
+ }
2662
+ /**
2663
+ * Handles HTTP 4xx client errors and maps them to appropriate KitError instances.
2664
+ *
2665
+ * @param statusCode - The HTTP status code
2666
+ * @param serviceName - The name of the service
2667
+ * @param operation - The operation name
2668
+ * @param error - The raw error object
2669
+ * @param msg - The message extracted from the error
2670
+ * @param responseBody - The parsed JSON response body from the server, if available
2671
+ * @returns A KitError instance
2672
+ */ function handleClientError(statusCode, serviceName, operation, error, msg, responseBody) {
2673
+ const detail = extractDetailFromBody(responseBody) ?? msg;
2674
+ switch(statusCode){
2675
+ // 401/403 - Authentication/Authorization
2676
+ case 401:
2677
+ case 403:
2678
+ return new KitError({
2679
+ ...InputError.VALIDATION_FAILED,
2680
+ recoverability: 'FATAL',
2681
+ message: `${serviceName} ${operation} failed: Invalid or missing API key or authorization`,
2682
+ cause: {
2683
+ trace: error
2684
+ }
2685
+ });
2686
+ // 404 - Not found - unsupported route OR stop-limit / slippage constraint not met
2687
+ case 404:
2688
+ if (isSlippageConstraintFailure(responseBody)) {
2689
+ return new KitError({
2690
+ ...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
2691
+ recoverability: 'RETRYABLE',
2692
+ message: `${serviceName} ${operation} failed: ${detail}. ` + 'Try increasing slippageBps or adjusting stopLimit.',
2693
+ cause: {
2694
+ trace: error
2695
+ }
2696
+ });
2697
+ }
2698
+ return new KitError({
2699
+ ...InputError.UNSUPPORTED_ROUTE,
2700
+ recoverability: 'FATAL',
2701
+ message: `${serviceName} ${operation} failed: Route or resource not found. Details: ${detail}`,
2702
+ cause: {
2703
+ trace: error
2704
+ }
2705
+ });
2706
+ // 422 Unprocessable Entity
2707
+ // Proxy service is mapping 422 to INSUFFICIENT_SWAP_AMOUNT
2708
+ case 422:
2709
+ return new KitError({
2710
+ ...InputError.INSUFFICIENT_SWAP_AMOUNT,
2711
+ recoverability: 'FATAL',
2712
+ message: `${serviceName} ${operation} failed: ${detail}`,
2713
+ cause: {
2714
+ trace: error
2715
+ }
2716
+ });
2717
+ // 400 Bad Request - Invalid token, amount-out-of-range, or validation failed
2718
+ // Proxy maps 400 to UNSUPPORTED_TOKEN | AMOUNT_OUT_OF_RANGE | VALIDATION_FAILED
2719
+ case 400:
2720
+ if (responseBody?.code === ProxyErrorCode.INVALID_SWAP_AMOUNT) {
2721
+ const amountErr = extractAmountError(responseBody);
2722
+ return new KitError({
2723
+ ...InputError.AMOUNT_OUT_OF_RANGE,
2724
+ recoverability: 'FATAL',
2725
+ message: `${serviceName} ${operation} failed: ${detail}`,
2726
+ cause: {
2727
+ trace: {
2728
+ rawError: error,
2729
+ minAmount: amountErr?.minAmount,
2730
+ maxAmount: amountErr?.maxAmount,
2731
+ token: amountErr?.token
2732
+ }
2733
+ }
2734
+ });
2735
+ }
2736
+ return new KitError({
2737
+ ...InputError.VALIDATION_FAILED,
2738
+ recoverability: 'FATAL',
2739
+ message: `${serviceName} ${operation} failed: ${detail}`,
2740
+ cause: {
2741
+ trace: error
2742
+ }
2743
+ });
2744
+ default:
2745
+ // Other 4xx errors - treat as validation failures
2746
+ return new KitError({
2747
+ ...InputError.VALIDATION_FAILED,
2748
+ recoverability: 'FATAL',
2749
+ message: `${serviceName} ${operation} failed: ${detail}`,
2750
+ cause: {
2751
+ trace: error
2752
+ }
2753
+ });
2754
+ }
2755
+ }
2756
+ /**
2757
+ * Pattern that matches proxy response body text indicating the 404 was
2758
+ * caused by a slippage / price constraint rather than a truly unsupported
2759
+ * route. Kept case-insensitive so future proxy wording changes are tolerated.
2760
+ *
2761
+ * @internal
2762
+ */ const SLIPPAGE_BODY_PATTERN = /slippage|stop.?limit|price.?impact|minimum.?output|SLIPPAGE_CONSTRAINT_NOT_MET/i;
2763
+ /**
2764
+ * Determine whether a 404 was caused by an unmet slippage or price
2765
+ * constraint rather than a genuinely unsupported route.
2766
+ *
2767
+ * Detection relies on the proxy response body containing slippage-related
2768
+ * language or a structured reason code. This avoids false positives that
2769
+ * would occur if we guessed based on request parameters alone (a user
2770
+ * can set `slippageBps` and still hit a truly unsupported route).
2771
+ *
2772
+ * @param responseBody - The parsed JSON body returned by the proxy
2773
+ * @returns `true` when the 404 should be treated as a slippage constraint failure
2774
+ * @internal
2775
+ */ function isSlippageConstraintFailure(responseBody) {
2776
+ if (responseBody === undefined) {
2777
+ return false;
2778
+ }
2779
+ const textsToCheck = [
2780
+ responseBody.externalMessage,
2781
+ responseBody.message,
2782
+ extractDetailFromBody(responseBody)
2783
+ ];
2784
+ return textsToCheck.some((t)=>typeof t === 'string' && SLIPPAGE_BODY_PATTERN.test(t));
2785
+ }
2786
+ /**
2787
+ * Handles HTTP 5xx server errors and maps them to appropriate KitError instances.
2788
+ *
2789
+ * Recognizes proxy-specific structured codes in `responseBody.code` and routes
2790
+ * known conditions (e.g. {@link ProxyErrorCode.LOW_LIQUIDITY} on 503) to their
2791
+ * dedicated KitError. Falls back to a generic retryable `SERVICE_INTERNAL_ERROR`
2792
+ * when no specific code is present.
2793
+ *
2794
+ * @param statusCode - The HTTP status code
2795
+ * @param serviceName - The name of the service
2796
+ * @param operation - The operation name
2797
+ * @param error - The raw error object
2798
+ * @param responseBody - The parsed JSON response body from the server, if available
2799
+ * @returns A KitError instance
2800
+ */ function handleServerError(statusCode, serviceName, operation, error, responseBody) {
2801
+ // 503 + 331018 = upstream liquidity insufficient (proxy)
2802
+ if (statusCode === 503 && responseBody?.code === ProxyErrorCode.LOW_LIQUIDITY) {
2803
+ const amountErr = extractAmountError(responseBody);
2804
+ const detail = extractDetailFromBody(responseBody) ?? getErrorMessage(error);
2805
+ return new KitError({
2806
+ ...LiquidityError.INSUFFICIENT_LIQUIDITY,
2807
+ recoverability: 'RETRYABLE',
2808
+ message: `${serviceName} ${operation} failed: ${detail}`,
2809
+ cause: {
2810
+ trace: {
2811
+ rawError: error,
2812
+ minAmount: amountErr?.minAmount,
2813
+ maxAmount: amountErr?.maxAmount,
2814
+ token: amountErr?.token
2815
+ }
2816
+ }
2817
+ });
2818
+ }
2819
+ return new KitError({
2820
+ ...ServiceError.INTERNAL_ERROR,
2821
+ recoverability: 'RETRYABLE',
2822
+ message: `${serviceName} ${operation} failed: Server error (${statusCode.toString()})`,
2823
+ cause: {
2824
+ trace: error
2825
+ }
2826
+ });
2827
+ }
2828
+ /**
2829
+ * Handles network connection errors and maps them to appropriate KitError instances.
2830
+ *
2831
+ * @param serviceName - The name of the service
2832
+ * @param operation - The operation name
2833
+ * @param error - The raw error object
2834
+ * @returns A KitError instance
2835
+ */ function handleConnectionError(serviceName, operation, error) {
2836
+ return new KitError({
2837
+ ...NetworkError.CONNECTION_FAILED,
2838
+ recoverability: 'RETRYABLE',
2839
+ message: `${serviceName} ${operation} failed: Network connection error`,
2840
+ cause: {
2841
+ trace: error
2842
+ }
2843
+ });
2844
+ }
2845
+ /**
2846
+ * Handles rate limit errors and maps them to appropriate KitError instances.
2847
+ *
2848
+ * @param serviceName - The name of the service
2849
+ * @param operation - The operation name
2850
+ * @param error - The raw error object
2851
+ * @returns A KitError instance
2852
+ */ function handleRateLimitError(serviceName, operation, error) {
2853
+ return new KitError({
2854
+ ...RateLimitError.RATE_LIMIT_EXCEEDED,
2855
+ recoverability: 'RETRYABLE',
2856
+ message: `${serviceName} ${operation} failed: Too many requests, please retry later`,
2857
+ cause: {
2858
+ trace: error
2859
+ }
2860
+ });
2861
+ }
2862
+ /**
2863
+ * Handles timeout errors and maps them to appropriate KitError instances.
2864
+ *
2865
+ * @param serviceName - The name of the service
2866
+ * @param operation - The operation name
2867
+ * @param error - The raw error object
2868
+ * @returns A KitError instance
2869
+ */ function handleTimeoutError(serviceName, operation, error) {
2870
+ return new KitError({
2871
+ ...NetworkError.TIMEOUT,
2872
+ recoverability: 'RETRYABLE',
2873
+ message: `${serviceName} ${operation} failed: Request timeout`,
2874
+ cause: {
2875
+ trace: error
2876
+ }
2877
+ });
2878
+ }
2879
+ /**
2880
+ * Type guard that narrows an unknown error to one carrying a non-null
2881
+ * object `responseBody` property (attached by `makeApiRequest`).
2882
+ *
2883
+ * @param error - The raw error from the HTTP layer
2884
+ * @returns `true` when `error.responseBody` is a non-null object
2885
+ * @internal
2886
+ */ function hasResponseBody(error) {
2887
+ return typeof error === 'object' && error !== null && 'responseBody' in error && typeof error['responseBody'] === 'object' && error['responseBody'] !== null;
2888
+ }
2889
+ /**
2890
+ * Extract the `responseBody` property that `makeApiRequest` attaches to
2891
+ * HTTP error instances when the server returns a JSON body.
2892
+ *
2893
+ * @param error - The raw error from the HTTP layer
2894
+ * @returns The parsed body cast to {@link ApiErrorResponseBody}, or undefined
2895
+ * @throws Never. Returns `undefined` when the error does not carry a valid
2896
+ * `responseBody`.
2897
+ * @internal
2898
+ */ function extractResponseBody(error) {
2899
+ if (!hasResponseBody(error)) {
2900
+ return undefined;
2901
+ }
2902
+ return error.responseBody;
2903
+ }
2904
+ /**
2905
+ * Extract a field name from an {@link ApiFieldError}.
2906
+ *
2907
+ * The proxy service may provide the field name as a plain `field` string
2908
+ * or as a `path` array (e.g. `["tokenInChain"]`). This helper resolves
2909
+ * whichever is available, preferring `field` when both exist.
2910
+ *
2911
+ * @param entry - A single error entry from the `errors` array
2912
+ * @returns The field name, or `undefined` when neither is available
2913
+ * @internal
2914
+ */ function extractFieldName(entry) {
2915
+ if (typeof entry.field === 'string' && entry.field.length > 0) {
2916
+ return entry.field;
2917
+ }
2918
+ if (Array.isArray(entry.path) && entry.path.length > 0) {
2919
+ const first = entry.path[0];
2920
+ if (typeof first === 'string' && first.length > 0) {
2921
+ return first;
2922
+ }
2923
+ }
2924
+ return undefined;
2925
+ }
2926
+ /**
2927
+ * Join field-level error entries into a single human-readable string.
2928
+ *
2929
+ * Each entry is formatted as `"field: message"` when a field name is
2930
+ * available (via `field` or `path`), or just the message otherwise.
2931
+ * Entries without a usable message (including amount-bound entries that
2932
+ * only carry `minAmount`/`maxAmount`/`token`) are skipped.
2933
+ *
2934
+ * @param errors - The `errors` array from the response body
2935
+ * @returns A joined string, or `undefined` when no usable entries exist
2936
+ * @internal
2937
+ */ function joinFieldErrors(errors) {
2938
+ const parts = errors.map((entry)=>{
2939
+ if (!isFieldEntry(entry)) {
2940
+ return undefined;
2941
+ }
2942
+ const fieldMsg = typeof entry.message === 'string' && entry.message.length > 0 ? entry.message : undefined;
2943
+ if (fieldMsg === undefined) {
2944
+ return undefined;
2945
+ }
2946
+ const fieldName = extractFieldName(entry);
2947
+ return fieldName === undefined ? fieldMsg : `${fieldName}: ${fieldMsg}`;
2948
+ }).filter((s)=>s !== undefined);
2949
+ return parts.length > 0 ? parts.join('; ') : undefined;
2950
+ }
2951
+ /**
2952
+ * Narrow an {@link ApiErrorItem} to the field-error shape used for
2953
+ * validation messages. Amount-bound entries (which carry no `message`)
2954
+ * are excluded.
2955
+ *
2956
+ * @internal
2957
+ */ function isFieldEntry(entry) {
2958
+ return 'message' in entry || 'field' in entry || 'path' in entry;
2959
+ }
2960
+ /**
2961
+ * Narrow an {@link ApiErrorItem} to the amount-bound shape emitted by the
2962
+ * proxy for {@link ProxyErrorCode.INVALID_SWAP_AMOUNT}.
2963
+ *
2964
+ * @internal
2965
+ */ function isAmountEntry(entry) {
2966
+ return 'minAmount' in entry || 'maxAmount' in entry || 'token' in entry;
2967
+ }
2968
+ /**
2969
+ * Extract the first amount-bound entry from a response body, if any.
2970
+ *
2971
+ * @internal
2972
+ */ function extractAmountError(body) {
2973
+ if (body === undefined || !Array.isArray(body.errors)) {
2974
+ return undefined;
2975
+ }
2976
+ return body.errors.find(isAmountEntry);
2977
+ }
2978
+ /**
2979
+ * Derive a human-readable detail string from an {@link ApiErrorResponseBody}.
2980
+ *
2981
+ * Resolution order:
2982
+ * 1. `body.externalMessage` -- the user-facing string the proxy intends
2983
+ * consumers to display (e.g. "No route found that satisfies the
2984
+ * requested stop limit"). Preferred when available.
2985
+ * 2. `body.message` **and** `body.errors` -- when both are present the
2986
+ * top-level message is combined with the field-level detail so
2987
+ * developers see the full picture
2988
+ * (e.g. `"Validation error: tokenInChain: Invalid input; amount: …"`).
2989
+ * 3. `body.message` alone -- used as-is.
2990
+ * 4. `body.errors` alone -- field-level entries joined with "; ".
2991
+ * 5. `undefined` -- caller should fall back to the raw HTTP status text.
2992
+ *
2993
+ * @param body - The parsed response body, may be undefined
2994
+ * @returns A detail string, or undefined when no useful info is available
2995
+ * @internal
2996
+ */ function extractDetailFromBody(body) {
2997
+ if (body === undefined) {
2998
+ return undefined;
2999
+ }
3000
+ const externalMessage = typeof body.externalMessage === 'string' && body.externalMessage.length > 0 ? body.externalMessage : undefined;
3001
+ if (externalMessage !== undefined) {
3002
+ return externalMessage;
3003
+ }
3004
+ const topMessage = typeof body.message === 'string' && body.message.length > 0 ? body.message : undefined;
3005
+ const fieldDetail = Array.isArray(body.errors) && body.errors.length > 0 ? joinFieldErrors(body.errors) : undefined;
3006
+ if (topMessage !== undefined && fieldDetail !== undefined) {
3007
+ return `${topMessage}: ${fieldDetail}`;
3008
+ }
3009
+ return topMessage ?? fieldDetail;
3010
+ }
3011
+ /**
3012
+ * Extracts the HTTP status code from an error message.
3013
+ *
3014
+ * Attempts to parse HTTP status codes from common error message formats,
3015
+ * such as "HTTP 404" or "Status: 500".
3016
+ *
3017
+ * @param msg - The error message to extract from
3018
+ * @returns The extracted HTTP status code, or null if not found
3019
+ *
3020
+ * @example
3021
+ * ```typescript
3022
+ * const code = extractHttpStatusCode('HTTP 404')
3023
+ * // Returns: 404
3024
+ * ```
3025
+ *
3026
+ * @example
3027
+ * ```typescript
3028
+ * const code = extractHttpStatusCode('Status: 500 Internal Server Error')
3029
+ * // Returns: 500
3030
+ * ```
3031
+ */ function extractHttpStatusCode(msg) {
3032
+ // Pattern: "HTTP 404" or "HTTP 404 - some message" or "Status: 404"
3033
+ const patterns = [
3034
+ /HTTP (\d{3})/i,
3035
+ /Status:\s*(\d{3})/i,
3036
+ /^(\d{3}) -/
3037
+ ];
3038
+ for (const pattern of patterns){
3039
+ const match = pattern.exec(msg);
3040
+ const codeStr = match?.at(1);
3041
+ if (codeStr !== undefined) {
3042
+ const code = Number.parseInt(codeStr, 10);
3043
+ // Validate it's a valid HTTP status code
3044
+ if (code >= 100 && code < 600) {
3045
+ return code;
3046
+ }
3047
+ }
3048
+ }
3049
+ return null;
3050
+ }
3051
+
2473
3052
  /**
2474
3053
  * @packageDocumentation
2475
3054
  * @module ChainDefinitions
@@ -2541,6 +3120,8 @@ class KitError extends Error {
2541
3120
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
2542
3121
  Blockchain["Pharos"] = "Pharos";
2543
3122
  Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
3123
+ Blockchain["Plasma"] = "Plasma";
3124
+ Blockchain["Plasma_Testnet"] = "Plasma_Testnet";
2544
3125
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
2545
3126
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
2546
3127
  Blockchain["Plume"] = "Plume";
@@ -2610,6 +3191,7 @@ var BridgeChain;
2610
3191
  BridgeChain["Morph"] = "Morph";
2611
3192
  BridgeChain["Optimism"] = "Optimism";
2612
3193
  BridgeChain["Pharos"] = "Pharos";
3194
+ BridgeChain["Plasma"] = "Plasma";
2613
3195
  BridgeChain["Plume"] = "Plume";
2614
3196
  BridgeChain["Polygon"] = "Polygon";
2615
3197
  BridgeChain["Sei"] = "Sei";
@@ -2636,6 +3218,7 @@ var BridgeChain;
2636
3218
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
2637
3219
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
2638
3220
  BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
3221
+ BridgeChain["Plasma_Testnet"] = "Plasma_Testnet";
2639
3222
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
2640
3223
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
2641
3224
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -3082,17 +3665,56 @@ var EarnChain;
3082
3665
  * This program handles minting operations for Gateway transactions
3083
3666
  * on Solana devnet.
3084
3667
  */ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
3085
-
3086
3668
  /**
3087
- * Arc Testnet chain definition
3088
- * @remarks
3089
- * This represents the test network for the Arc blockchain,
3090
- * Circle's EVM-compatible Layer-1 designed for stablecoin finance
3091
- * and asset tokenization. Arc uses USDC as the native gas token and
3092
- * features the Malachite Byzantine Fault Tolerant (BFT) consensus
3093
- * engine for sub-second finality.
3094
- */ const ArcTestnet = defineChain({
3095
- type: 'evm',
3669
+ * The `TokenMessengerWithFees` proxy contract address for EVM mainnet networks
3670
+ * (all chains except Edge).
3671
+ *
3672
+ * Deployed at a CREATE3-derived address; identical across all mainnet EVM
3673
+ * source chains. Present on any chain that supports the prepaid FORWARD path
3674
+ * via `depositForBurnWithHookAndFees`.
3675
+ */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
3676
+ /**
3677
+ * The `TokenMessengerWithFees` proxy contract address for EVM testnet networks.
3678
+ *
3679
+ * Identical across all testnet EVM source chains. Present on any testnet chain
3680
+ * that supports the prepaid FORWARD path via `depositForBurnWithHookAndFees`.
3681
+ */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
3682
+ /**
3683
+ * The `DepositForHandler` proxy contract address for EVM mainnet networks.
3684
+ *
3685
+ * The handler the GenericExecutor calls on a fast-deposit destination chain to
3686
+ * run a cross-chain deposit into the GatewayWallet. Deployed at the same
3687
+ * address across all mainnet EVM destination chains.
3688
+ */ const DEPOSIT_FOR_HANDLER_EVM_MAINNET = '0x16529813203f77E036576666336554a1210dce4D';
3689
+ /**
3690
+ * The `DepositForHandler` proxy contract address for EVM testnet networks.
3691
+ *
3692
+ * Identical across all testnet EVM destination chains.
3693
+ */ const DEPOSIT_FOR_HANDLER_EVM_TESTNET = '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48';
3694
+ /**
3695
+ * The `GenericExecutor` proxy contract address for EVM mainnet networks.
3696
+ *
3697
+ * The GenericExecutor is the `mintRecipient` and `destinationCaller` on the
3698
+ * destination chain for the CCTP v2 prepaid FORWARD path. It receives the CCTP
3699
+ * mint and calls the `DepositForHandler` to complete the fast deposit.
3700
+ * Deployed at the same address across all mainnet EVM destination chains.
3701
+ */ const GENERIC_EXECUTOR_EVM_MAINNET = '0xFa7be2f04F3Ad4ca969260729c6d45B5625984A7';
3702
+ /**
3703
+ * The `GenericExecutor` proxy contract address for EVM testnet networks.
3704
+ *
3705
+ * Identical across all testnet EVM destination chains.
3706
+ */ const GENERIC_EXECUTOR_EVM_TESTNET = '0xEdC81040756AcCfF070c21D37b265b9D0b5Ba45e';
3707
+
3708
+ /**
3709
+ * Arc Testnet chain definition
3710
+ * @remarks
3711
+ * This represents the test network for the Arc blockchain,
3712
+ * Circle's EVM-compatible Layer-1 designed for stablecoin finance
3713
+ * and asset tokenization. Arc uses USDC as the native gas token and
3714
+ * features the Malachite Byzantine Fault Tolerant (BFT) consensus
3715
+ * engine for sub-second finality.
3716
+ */ const ArcTestnet = defineChain({
3717
+ type: 'evm',
3096
3718
  chain: Blockchain.Arc_Testnet,
3097
3719
  name: 'Arc Testnet',
3098
3720
  title: 'ArcTestnet',
@@ -3119,6 +3741,7 @@ var EarnChain;
3119
3741
  v2: {
3120
3742
  type: 'split',
3121
3743
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
3744
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3122
3745
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3123
3746
  confirmations: 1,
3124
3747
  fastConfirmations: 1
@@ -3139,9 +3762,8 @@ var EarnChain;
3139
3762
  v1: {
3140
3763
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3141
3764
  minter: GATEWAY_MINTER_EVM_TESTNET,
3142
- // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3143
- // deposit into the GatewayWallet above.
3144
- depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3765
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
3766
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
3145
3767
  }
3146
3768
  },
3147
3769
  forwarderSupported: {
@@ -3186,6 +3808,7 @@ var EarnChain;
3186
3808
  v2: {
3187
3809
  type: 'split',
3188
3810
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
3811
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3189
3812
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3190
3813
  confirmations: 65,
3191
3814
  fastConfirmations: 1
@@ -3250,6 +3873,7 @@ var EarnChain;
3250
3873
  v2: {
3251
3874
  type: 'split',
3252
3875
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
3876
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3253
3877
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3254
3878
  confirmations: 65,
3255
3879
  fastConfirmations: 1
@@ -3314,6 +3938,7 @@ var EarnChain;
3314
3938
  v2: {
3315
3939
  type: 'split',
3316
3940
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
3941
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3317
3942
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3318
3943
  confirmations: 1,
3319
3944
  fastConfirmations: 1
@@ -3333,7 +3958,9 @@ var EarnChain;
3333
3958
  contracts: {
3334
3959
  v1: {
3335
3960
  wallet: GATEWAY_WALLET_EVM_MAINNET,
3336
- minter: GATEWAY_MINTER_EVM_MAINNET
3961
+ minter: GATEWAY_MINTER_EVM_MAINNET,
3962
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_MAINNET,
3963
+ genericExecutor: GENERIC_EXECUTOR_EVM_MAINNET
3337
3964
  }
3338
3965
  },
3339
3966
  forwarderSupported: {
@@ -3375,6 +4002,7 @@ var EarnChain;
3375
4002
  v2: {
3376
4003
  type: 'split',
3377
4004
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4005
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3378
4006
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3379
4007
  confirmations: 1,
3380
4008
  fastConfirmations: 1
@@ -3396,7 +4024,9 @@ var EarnChain;
3396
4024
  contracts: {
3397
4025
  v1: {
3398
4026
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3399
- minter: GATEWAY_MINTER_EVM_TESTNET
4027
+ minter: GATEWAY_MINTER_EVM_TESTNET,
4028
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
4029
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
3400
4030
  }
3401
4031
  },
3402
4032
  forwarderSupported: {
@@ -3442,6 +4072,7 @@ var EarnChain;
3442
4072
  v2: {
3443
4073
  type: 'split',
3444
4074
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4075
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3445
4076
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3446
4077
  confirmations: 65,
3447
4078
  fastConfirmations: 1
@@ -3506,6 +4137,7 @@ var EarnChain;
3506
4137
  v2: {
3507
4138
  type: 'split',
3508
4139
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4140
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3509
4141
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3510
4142
  confirmations: 65,
3511
4143
  fastConfirmations: 1
@@ -3616,6 +4248,7 @@ var EarnChain;
3616
4248
  v2: {
3617
4249
  type: 'split',
3618
4250
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4251
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3619
4252
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3620
4253
  confirmations: 65,
3621
4254
  fastConfirmations: 1
@@ -3660,6 +4293,7 @@ var EarnChain;
3660
4293
  v2: {
3661
4294
  type: 'split',
3662
4295
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4296
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3663
4297
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3664
4298
  confirmations: 65,
3665
4299
  fastConfirmations: 1
@@ -3705,6 +4339,7 @@ var EarnChain;
3705
4339
  v2: {
3706
4340
  type: 'split',
3707
4341
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4342
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3708
4343
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3709
4344
  confirmations: 1,
3710
4345
  fastConfirmations: 1
@@ -3750,6 +4385,7 @@ var EarnChain;
3750
4385
  v2: {
3751
4386
  type: 'split',
3752
4387
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4388
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3753
4389
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3754
4390
  confirmations: 1,
3755
4391
  fastConfirmations: 1
@@ -3795,6 +4431,7 @@ var EarnChain;
3795
4431
  v2: {
3796
4432
  type: 'split',
3797
4433
  tokenMessenger: '0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c',
4434
+ tokenMessengerWithFees: '0x3Ac96675F9a3E6922713e041645D82f3561d3686',
3798
4435
  messageTransmitter: '0x5b61381Fc9e58E70EfC13a4A97516997019198ee',
3799
4436
  confirmations: 65,
3800
4437
  fastConfirmations: 1
@@ -3840,6 +4477,7 @@ var EarnChain;
3840
4477
  v2: {
3841
4478
  type: 'split',
3842
4479
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4480
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3843
4481
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3844
4482
  confirmations: 65,
3845
4483
  fastConfirmations: 1
@@ -3891,6 +4529,7 @@ var EarnChain;
3891
4529
  v2: {
3892
4530
  type: 'split',
3893
4531
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4532
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3894
4533
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3895
4534
  confirmations: 65,
3896
4535
  fastConfirmations: 2
@@ -3955,6 +4594,7 @@ var EarnChain;
3955
4594
  v2: {
3956
4595
  type: 'split',
3957
4596
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4597
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3958
4598
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3959
4599
  confirmations: 65,
3960
4600
  fastConfirmations: 2
@@ -4065,6 +4705,7 @@ var EarnChain;
4065
4705
  v2: {
4066
4706
  type: 'split',
4067
4707
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4708
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4068
4709
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4069
4710
  confirmations: 1,
4070
4711
  fastConfirmations: 1
@@ -4124,6 +4765,7 @@ var EarnChain;
4124
4765
  v2: {
4125
4766
  type: 'split',
4126
4767
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4768
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4127
4769
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4128
4770
  confirmations: 1,
4129
4771
  fastConfirmations: 1
@@ -4184,6 +4826,7 @@ var EarnChain;
4184
4826
  v2: {
4185
4827
  type: 'split',
4186
4828
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4829
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4187
4830
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4188
4831
  confirmations: 1,
4189
4832
  fastConfirmations: 1
@@ -4231,6 +4874,7 @@ var EarnChain;
4231
4874
  v2: {
4232
4875
  type: 'split',
4233
4876
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4877
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4234
4878
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4235
4879
  confirmations: 1,
4236
4880
  fastConfirmations: 1
@@ -4278,6 +4922,7 @@ var EarnChain;
4278
4922
  v2: {
4279
4923
  type: 'split',
4280
4924
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4925
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4281
4926
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4282
4927
  confirmations: 65,
4283
4928
  fastConfirmations: 1
@@ -4325,6 +4970,7 @@ var EarnChain;
4325
4970
  v2: {
4326
4971
  type: 'split',
4327
4972
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4973
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4328
4974
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4329
4975
  confirmations: 65,
4330
4976
  fastConfirmations: 1
@@ -4369,6 +5015,7 @@ var EarnChain;
4369
5015
  v2: {
4370
5016
  type: 'split',
4371
5017
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5018
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4372
5019
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4373
5020
  confirmations: 1,
4374
5021
  fastConfirmations: 1
@@ -4414,6 +5061,7 @@ var EarnChain;
4414
5061
  v2: {
4415
5062
  type: 'split',
4416
5063
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5064
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4417
5065
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
4418
5066
  confirmations: 1,
4419
5067
  fastConfirmations: 1
@@ -4460,6 +5108,7 @@ var EarnChain;
4460
5108
  v2: {
4461
5109
  type: 'split',
4462
5110
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5111
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4463
5112
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4464
5113
  confirmations: 1,
4465
5114
  fastConfirmations: 1
@@ -4507,6 +5156,7 @@ var EarnChain;
4507
5156
  v2: {
4508
5157
  type: 'split',
4509
5158
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5159
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4510
5160
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4511
5161
  confirmations: 1,
4512
5162
  fastConfirmations: 1
@@ -4552,6 +5202,7 @@ var EarnChain;
4552
5202
  v2: {
4553
5203
  type: 'split',
4554
5204
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5205
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4555
5206
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4556
5207
  confirmations: 64,
4557
5208
  fastConfirmations: 1
@@ -4597,6 +5248,7 @@ var EarnChain;
4597
5248
  v2: {
4598
5249
  type: 'split',
4599
5250
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5251
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4600
5252
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4601
5253
  confirmations: 64,
4602
5254
  fastConfirmations: 1
@@ -4773,6 +5425,7 @@ var EarnChain;
4773
5425
  v2: {
4774
5426
  type: 'split',
4775
5427
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5428
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4776
5429
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4777
5430
  confirmations: 65,
4778
5431
  fastConfirmations: 1
@@ -4837,6 +5490,7 @@ var EarnChain;
4837
5490
  v2: {
4838
5491
  type: 'split',
4839
5492
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5493
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4840
5494
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
4841
5495
  confirmations: 65,
4842
5496
  fastConfirmations: 1
@@ -4896,6 +5550,7 @@ var EarnChain;
4896
5550
  v2: {
4897
5551
  type: 'split',
4898
5552
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5553
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4899
5554
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4900
5555
  confirmations: 1,
4901
5556
  fastConfirmations: 1
@@ -4942,6 +5597,7 @@ var EarnChain;
4942
5597
  v2: {
4943
5598
  type: 'split',
4944
5599
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5600
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4945
5601
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4946
5602
  confirmations: 1,
4947
5603
  fastConfirmations: 1
@@ -4957,6 +5613,98 @@ var EarnChain;
4957
5613
  }
4958
5614
  });
4959
5615
 
5616
+ /**
5617
+ * Plasma Mainnet chain definition
5618
+ * @remarks
5619
+ * This represents the official production network for the Plasma blockchain.
5620
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
5621
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
5622
+ */ const Plasma = defineChain({
5623
+ type: 'evm',
5624
+ chain: Blockchain.Plasma,
5625
+ name: 'Plasma',
5626
+ title: 'Plasma Mainnet',
5627
+ nativeCurrency: {
5628
+ name: 'Plasma',
5629
+ symbol: 'XPL',
5630
+ decimals: 18
5631
+ },
5632
+ chainId: 9745,
5633
+ isTestnet: false,
5634
+ explorerUrl: 'https://plasmascan.to/tx/{hash}',
5635
+ rpcEndpoints: [
5636
+ 'https://rpc.plasma.to'
5637
+ ],
5638
+ eurcAddress: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
5639
+ usdcAddress: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
5640
+ usdtAddress: null,
5641
+ cctp: {
5642
+ domain: 33,
5643
+ contracts: {
5644
+ v2: {
5645
+ type: 'split',
5646
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5647
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5648
+ confirmations: 3,
5649
+ fastConfirmations: 1
5650
+ }
5651
+ },
5652
+ forwarderSupported: {
5653
+ source: false,
5654
+ destination: false
5655
+ }
5656
+ },
5657
+ kitContracts: {
5658
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5659
+ }
5660
+ });
5661
+
5662
+ /**
5663
+ * Plasma Testnet chain definition
5664
+ * @remarks
5665
+ * This represents the official test network for the Plasma blockchain.
5666
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
5667
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
5668
+ */ const PlasmaTestnet = defineChain({
5669
+ type: 'evm',
5670
+ chain: Blockchain.Plasma_Testnet,
5671
+ name: 'Plasma Testnet',
5672
+ title: 'Plasma Testnet',
5673
+ nativeCurrency: {
5674
+ name: 'Plasma',
5675
+ symbol: 'XPL',
5676
+ decimals: 18
5677
+ },
5678
+ chainId: 9746,
5679
+ isTestnet: true,
5680
+ explorerUrl: 'https://testnet.plasmascan.to/tx/{hash}',
5681
+ rpcEndpoints: [
5682
+ 'https://testnet-rpc.plasma.to'
5683
+ ],
5684
+ eurcAddress: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330',
5685
+ usdcAddress: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
5686
+ usdtAddress: null,
5687
+ cctp: {
5688
+ domain: 33,
5689
+ contracts: {
5690
+ v2: {
5691
+ type: 'split',
5692
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5693
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5694
+ confirmations: 3,
5695
+ fastConfirmations: 1
5696
+ }
5697
+ },
5698
+ forwarderSupported: {
5699
+ source: false,
5700
+ destination: false
5701
+ }
5702
+ },
5703
+ kitContracts: {
5704
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5705
+ }
5706
+ });
5707
+
4960
5708
  /**
4961
5709
  * Plume Mainnet chain definition
4962
5710
  * @remarks
@@ -4988,6 +5736,7 @@ var EarnChain;
4988
5736
  v2: {
4989
5737
  type: 'split',
4990
5738
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5739
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4991
5740
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4992
5741
  confirmations: 65,
4993
5742
  fastConfirmations: 1
@@ -5034,6 +5783,7 @@ var EarnChain;
5034
5783
  v2: {
5035
5784
  type: 'split',
5036
5785
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5786
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5037
5787
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5038
5788
  confirmations: 65,
5039
5789
  fastConfirmations: 1
@@ -5135,6 +5885,7 @@ var EarnChain;
5135
5885
  v2: {
5136
5886
  type: 'split',
5137
5887
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5888
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5138
5889
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5139
5890
  confirmations: 33,
5140
5891
  fastConfirmations: 13
@@ -5154,7 +5905,9 @@ var EarnChain;
5154
5905
  contracts: {
5155
5906
  v1: {
5156
5907
  wallet: GATEWAY_WALLET_EVM_MAINNET,
5157
- minter: GATEWAY_MINTER_EVM_MAINNET
5908
+ minter: GATEWAY_MINTER_EVM_MAINNET,
5909
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_MAINNET,
5910
+ genericExecutor: GENERIC_EXECUTOR_EVM_MAINNET
5158
5911
  }
5159
5912
  },
5160
5913
  forwarderSupported: {
@@ -5200,6 +5953,7 @@ var EarnChain;
5200
5953
  v2: {
5201
5954
  type: 'split',
5202
5955
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5956
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5203
5957
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5204
5958
  confirmations: 33,
5205
5959
  fastConfirmations: 13
@@ -5218,7 +5972,9 @@ var EarnChain;
5218
5972
  contracts: {
5219
5973
  v1: {
5220
5974
  wallet: GATEWAY_WALLET_EVM_TESTNET,
5221
- minter: GATEWAY_MINTER_EVM_TESTNET
5975
+ minter: GATEWAY_MINTER_EVM_TESTNET,
5976
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
5977
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
5222
5978
  }
5223
5979
  },
5224
5980
  forwarderSupported: {
@@ -5259,6 +6015,7 @@ var EarnChain;
5259
6015
  v2: {
5260
6016
  type: 'split',
5261
6017
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6018
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5262
6019
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5263
6020
  confirmations: 1,
5264
6021
  fastConfirmations: 1
@@ -5318,6 +6075,7 @@ var EarnChain;
5318
6075
  v2: {
5319
6076
  type: 'split',
5320
6077
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6078
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5321
6079
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5322
6080
  confirmations: 1,
5323
6081
  fastConfirmations: 1
@@ -5375,6 +6133,7 @@ var EarnChain;
5375
6133
  v2: {
5376
6134
  type: 'split',
5377
6135
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6136
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5378
6137
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5379
6138
  confirmations: 1,
5380
6139
  fastConfirmations: 1
@@ -5433,6 +6192,7 @@ var EarnChain;
5433
6192
  v2: {
5434
6193
  type: 'split',
5435
6194
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6195
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5436
6196
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5437
6197
  confirmations: 1,
5438
6198
  fastConfirmations: 1
@@ -5748,6 +6508,7 @@ var EarnChain;
5748
6508
  v2: {
5749
6509
  type: 'split',
5750
6510
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6511
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5751
6512
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5752
6513
  confirmations: 65,
5753
6514
  fastConfirmations: 1
@@ -5812,6 +6573,7 @@ var EarnChain;
5812
6573
  v2: {
5813
6574
  type: 'split',
5814
6575
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6576
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5815
6577
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5816
6578
  confirmations: 65,
5817
6579
  fastConfirmations: 1
@@ -5869,6 +6631,7 @@ var EarnChain;
5869
6631
  v2: {
5870
6632
  type: 'split',
5871
6633
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cF5d',
6634
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5872
6635
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5873
6636
  confirmations: 65,
5874
6637
  fastConfirmations: 1
@@ -5928,6 +6691,7 @@ var EarnChain;
5928
6691
  v2: {
5929
6692
  type: 'split',
5930
6693
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
6694
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5931
6695
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5932
6696
  confirmations: 65,
5933
6697
  fastConfirmations: 1
@@ -5988,6 +6752,7 @@ var EarnChain;
5988
6752
  v2: {
5989
6753
  type: 'split',
5990
6754
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6755
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5991
6756
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5992
6757
  confirmations: 3,
5993
6758
  fastConfirmations: 3
@@ -6033,6 +6798,7 @@ var EarnChain;
6033
6798
  v2: {
6034
6799
  type: 'split',
6035
6800
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6801
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6036
6802
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6037
6803
  confirmations: 3,
6038
6804
  fastConfirmations: 1
@@ -6243,6 +7009,8 @@ var Chains = {
6243
7009
  OptimismSepolia: OptimismSepolia,
6244
7010
  Pharos: Pharos,
6245
7011
  PharosTestnet: PharosTestnet,
7012
+ Plasma: Plasma,
7013
+ PlasmaTestnet: PlasmaTestnet,
6246
7014
  Plume: Plume,
6247
7015
  PlumeTestnet: PlumeTestnet,
6248
7016
  PolkadotAssetHub: PolkadotAssetHub,
@@ -6292,6 +7060,71 @@ var Chains = {
6292
7060
  return chain.cctp?.contracts.v2 !== undefined;
6293
7061
  }
6294
7062
 
7063
+ /**
7064
+ * Chains the Fee Service accepts as a SOURCE for source-paid ("receive-exact")
7065
+ * CCTP v2 fees. An explicit allowlist is required because the
7066
+ * `TokenMessengerWithFees` wrapper address is now shared with the fast-deposit
7067
+ * forwarder path, so wrapper presence no longer implies source-fee support.
7068
+ * Keep in sync with backend coverage.
7069
+ */ const SOURCE_FEE_SUPPORTED_ALLOWLIST = new Set([
7070
+ // Mainnet
7071
+ Blockchain.Ethereum,
7072
+ Blockchain.Base,
7073
+ Blockchain.Arbitrum,
7074
+ Blockchain.Unichain,
7075
+ Blockchain.Optimism,
7076
+ Blockchain.Codex,
7077
+ Blockchain.Ink,
7078
+ Blockchain.Plume,
7079
+ Blockchain.Linea,
7080
+ Blockchain.World_Chain,
7081
+ // Testnet counterparts
7082
+ Blockchain.Ethereum_Sepolia,
7083
+ Blockchain.Base_Sepolia,
7084
+ Blockchain.Arbitrum_Sepolia,
7085
+ Blockchain.Unichain_Sepolia,
7086
+ Blockchain.Optimism_Sepolia,
7087
+ Blockchain.Codex_Testnet,
7088
+ Blockchain.Ink_Testnet,
7089
+ Blockchain.Plume_Testnet,
7090
+ Blockchain.Linea_Sepolia,
7091
+ Blockchain.World_Chain_Sepolia
7092
+ ]);
7093
+ /**
7094
+ * Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
7095
+ *
7096
+ * A chain qualifies when it supports CCTP v2, carries a `TokenMessengerWithFees`
7097
+ * wrapper, and is in {@link SOURCE_FEE_SUPPORTED_ALLOWLIST}.
7098
+ *
7099
+ * @param chain - The chain definition to check. A nullish or non-object value
7100
+ * returns `false` rather than throwing, since consumers may call from plain
7101
+ * JavaScript.
7102
+ * @returns `true` when the chain supports CCTP v2, carries a
7103
+ * `TokenMessengerWithFees` wrapper, and is on the source-fee allowlist;
7104
+ * `false` otherwise.
7105
+ *
7106
+ * @example
7107
+ * ```typescript
7108
+ * import { Chains, hasSourceFeeSupport } from '@core/chains'
7109
+ *
7110
+ * hasSourceFeeSupport(Chains.Optimism) // true
7111
+ * hasSourceFeeSupport(Chains.Solana) // false
7112
+ * ```
7113
+ */ function hasSourceFeeSupport(chain) {
7114
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for nullish/non-object input from plain JS
7115
+ if (chain === null || typeof chain !== 'object') {
7116
+ return false;
7117
+ }
7118
+ if (!isCCTPV2Supported(chain)) {
7119
+ return false;
7120
+ }
7121
+ const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
7122
+ if (typeof wrapper !== 'string' || wrapper.length === 0) {
7123
+ return false;
7124
+ }
7125
+ return SOURCE_FEE_SUPPORTED_ALLOWLIST.has(chain.chain);
7126
+ }
7127
+
6295
7128
  /**
6296
7129
  * Check if a chain supports a specific type of custom smart contract logic.
6297
7130
  *
@@ -6342,6 +7175,73 @@ var Chains = {
6342
7175
  return typeof contractAddress === 'string' && contractAddress.trim().length > 0;
6343
7176
  }
6344
7177
 
7178
+ /**
7179
+ * Check whether a given chain supports Gateway protocol version 1.
7180
+ *
7181
+ * This type guard function examines a chain definition to determine if it has Gateway v1
7182
+ * contract configurations. It checks that the chain has a gateway object with a
7183
+ * `contracts.v1` entry present.
7184
+ *
7185
+ * @param chain - The chain definition to check for Gateway v1 support
7186
+ * @returns `true` if `chain.gateway?.contracts?.v1` is defined, `false` otherwise
7187
+ *
7188
+ * @example
7189
+ * ```typescript
7190
+ * import { isGatewayV1Supported, Base } from '@core/chains'
7191
+ *
7192
+ * if (isGatewayV1Supported(Base)) {
7193
+ * // TypeScript knows Base.gateway is defined here
7194
+ * console.log('Gateway domain:', Base.gateway.domain)
7195
+ * console.log('Wallet address:', Base.gateway.contracts.v1.wallet)
7196
+ * console.log('Minter address:', Base.gateway.contracts.v1.minter)
7197
+ * }
7198
+ * ```
7199
+ *
7200
+ * @example
7201
+ * ```typescript
7202
+ * // Usage in conditional flow
7203
+ * function getGatewayWalletAddress(chain: ChainDefinition): string | null {
7204
+ * if (isGatewayV1Supported(chain)) {
7205
+ * return chain.gateway.contracts.v1.wallet
7206
+ * }
7207
+ * return null
7208
+ * }
7209
+ * ```
7210
+ */ function isGatewayV1Supported(chain) {
7211
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS consumers may pass a gateway object without contracts
7212
+ return chain.gateway?.contracts?.v1 !== undefined;
7213
+ }
7214
+
7215
+ /**
7216
+ * Temporary allowlist of chains permitted to initiate Gateway fast deposits.
7217
+ * Only chains keyed here are eligible; all others are rejected. Using the
7218
+ * {@link Blockchain} enum keeps entries type-safe and catches typos at compile
7219
+ * time. Remove this allowlist once roll-out is complete.
7220
+ */ new Set([
7221
+ // Mainnet
7222
+ Blockchain.Ethereum,
7223
+ Blockchain.Base,
7224
+ Blockchain.Arbitrum,
7225
+ Blockchain.Unichain,
7226
+ Blockchain.Optimism,
7227
+ Blockchain.Codex,
7228
+ Blockchain.Ink,
7229
+ Blockchain.Plume,
7230
+ Blockchain.Linea,
7231
+ Blockchain.World_Chain,
7232
+ // Testnet counterparts
7233
+ Blockchain.Ethereum_Sepolia,
7234
+ Blockchain.Base_Sepolia,
7235
+ Blockchain.Arbitrum_Sepolia,
7236
+ Blockchain.Unichain_Sepolia,
7237
+ Blockchain.Optimism_Sepolia,
7238
+ Blockchain.Codex_Testnet,
7239
+ Blockchain.Ink_Testnet,
7240
+ Blockchain.Plume_Testnet,
7241
+ Blockchain.Linea_Sepolia,
7242
+ Blockchain.World_Chain_Sepolia
7243
+ ]);
7244
+
6345
7245
  /**
6346
7246
  * Zod schema for validating Gateway v1 contract addresses.
6347
7247
  *
@@ -6363,7 +7263,10 @@ var Chains = {
6363
7263
  }).min(1, 'Gateway minter address cannot be empty.'),
6364
7264
  depositForHandler: zod.z.string({
6365
7265
  invalid_type_error: 'Gateway depositForHandler address must be a string.'
6366
- }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
7266
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional(),
7267
+ genericExecutor: zod.z.string({
7268
+ invalid_type_error: 'Gateway genericExecutor address must be a string.'
7269
+ }).min(1, 'Gateway genericExecutor address cannot be empty.').optional()
6367
7270
  }).strict() // Reject any additional properties not defined in the schema
6368
7271
  ;
6369
7272
  /**
@@ -8310,6 +9213,7 @@ const swapTokenEnumSchema = zod.z.enum([
8310
9213
  [Blockchain.Noble]: 'uusdc',
8311
9214
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
8312
9215
  [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
9216
+ [Blockchain.Plasma]: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
8313
9217
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
8314
9218
  [Blockchain.Polkadot_Asset_Hub]: '1337',
8315
9219
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -8346,6 +9250,7 @@ const swapTokenEnumSchema = zod.z.enum([
8346
9250
  [Blockchain.Noble_Testnet]: 'uusdc',
8347
9251
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
8348
9252
  [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
9253
+ [Blockchain.Plasma_Testnet]: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
8349
9254
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
8350
9255
  [Blockchain.Polkadot_Westmint]: '31337',
8351
9256
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -8408,6 +9313,7 @@ const swapTokenEnumSchema = zod.z.enum([
8408
9313
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
8409
9314
  [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
8410
9315
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
9316
+ [Blockchain.Plasma]: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
8411
9317
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
8412
9318
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
8413
9319
  // =========================================================================
@@ -8416,7 +9322,8 @@ const swapTokenEnumSchema = zod.z.enum([
8416
9322
  [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
8417
9323
  [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
8418
9324
  [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
8419
- [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
9325
+ [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
9326
+ [Blockchain.Plasma_Testnet]: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330'
8420
9327
  }
8421
9328
  };
8422
9329
 
@@ -9230,6 +10137,9 @@ const swapTokenEnumSchema = zod.z.enum([
9230
10137
  * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9231
10138
  * This prefix is right-padded to 24 bytes in the final hookData.
9232
10139
  */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
10140
+ /**
10141
+ * Maximum value of the 32-bit `version` field in a `cctp-forward` frame.
10142
+ */ const MAX_UINT32 = 0xffffffff;
9233
10143
  /**
9234
10144
  * CCTP forwarding version number.
9235
10145
  *
@@ -9240,6 +10150,16 @@ const swapTokenEnumSchema = zod.z.enum([
9240
10150
  *
9241
10151
  * Set to 0 when no additional Circle-reserved data is needed.
9242
10152
  */ const CCTP_FORWARD_PAYLOAD_LENGTH = 0;
10153
+ /**
10154
+ * Length in bytes of a Solana owner (ed25519 / PDA) public key.
10155
+ */ const SOLANA_PUBKEY_LENGTH = 32;
10156
+ /**
10157
+ * Byte length of the Solana ATA-creation forwarding payload appended after the
10158
+ * `cctp-forward` frame: `createAta` (1 byte) + `ataOwner` (32 bytes).
10159
+ *
10160
+ * Circle's Orbit relayer decodes exactly this many bytes; see
10161
+ * {@link buildSolanaAtaForwardingHookData}.
10162
+ */ const SOLANA_ATA_FORWARD_PAYLOAD_LENGTH = 1 + SOLANA_PUBKEY_LENGTH;
9243
10163
  /**
9244
10164
  * Build the hookData bytes for CCTP forwarding.
9245
10165
  *
@@ -9296,6 +10216,518 @@ function buildForwardingHookData() {
9296
10216
  cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
9297
10217
  return cachedHookDataHex;
9298
10218
  }
10219
+ /**
10220
+ * Build a `cctp-forward` hookData frame with a versioned header and an appended
10221
+ * opaque payload.
10222
+ *
10223
+ * Produces the 32-byte `cctp-forward` header (24-byte ASCII magic + `uint32`
10224
+ * version + `uint32` `dataLength = 0`) followed by `payload` appended verbatim.
10225
+ * Unlike {@link buildForwardingHookData} — which emits only the fixed,
10226
+ * version-0 empty frame — this lets the caller set the frame `version` and
10227
+ * carry an inner payload such as a GenericExecutor blob.
10228
+ *
10229
+ * @remarks
10230
+ * The forwarder reads only the 32-byte header to decide that a hook is
10231
+ * forwardable, then strips it before the inner payload is consumed downstream
10232
+ * (e.g. the GenericExecutor `abi.decode`s the appended blob, never the frame).
10233
+ * `dataLength` stays `0` because the appended bytes are opaque to the forwarder
10234
+ * — it is not the payload's length.
10235
+ *
10236
+ * @param version - The `uint32` frame version (e.g. `1` for the GenericExecutor
10237
+ * FORWARD path). Must be an integer in `[0, 0xFFFFFFFF]`.
10238
+ * @param payload - A 0x-prefixed hex string appended after the header (e.g. the
10239
+ * bare GenericExecutor blob from `buildDepositForGenericExecutorPayload`).
10240
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by `payload`.
10241
+ * @throws {KitError} If `version` is out of `uint32` range or `payload` is not
10242
+ * a 0x-prefixed hex string (INPUT_VALIDATION_FAILED).
10243
+ *
10244
+ * @example
10245
+ * ```typescript
10246
+ * import {
10247
+ * buildDepositForGenericExecutorPayload,
10248
+ * buildForwardingHookDataWithPayload,
10249
+ * padAddressToBytes32,
10250
+ * } from '@core/utils'
10251
+ *
10252
+ * const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
10253
+ * dappId: 'gateway_deposit',
10254
+ * domainId: 26,
10255
+ * handler: '0xHandlerAddressOnDestinationChain',
10256
+ * params: [USDC_ARC, user, 0],
10257
+ * recoveryAddress: padAddressToBytes32(user),
10258
+ * })
10259
+ *
10260
+ * // Wrap for the prepaid Quote-API FORWARD path (frame version 1).
10261
+ * const hookData = buildForwardingHookDataWithPayload(1, geBlob)
10262
+ * ```
10263
+ */ function buildForwardingHookDataWithPayload(version, payload) {
10264
+ if (!Number.isInteger(version) || version < 0 || version > MAX_UINT32) {
10265
+ throw createValidationFailedError$1('version', version, 'Expected an integer in the uint32 range [0, 4294967295]');
10266
+ }
10267
+ if (!bytes.isHexString(payload)) {
10268
+ throw createValidationFailedError$1('payload', payload, 'Expected a 0x-prefixed hex string');
10269
+ }
10270
+ // `isHexString` accepts odd-length hex (e.g. '0xabc'); catch it here so it
10271
+ // surfaces as a KitError rather than ethers' raw "hex data is odd-length"
10272
+ // from `concat` below.
10273
+ if (payload.length % 2 !== 0) {
10274
+ throw createValidationFailedError$1('payload', payload, 'Expected an even-length (whole-byte) hex string');
10275
+ }
10276
+ // 32-byte header: 24-byte magic + uint32 version + uint32 dataLength (0).
10277
+ const frame = new Uint8Array(32);
10278
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
10279
+ const view = new DataView(frame.buffer);
10280
+ view.setUint32(24, version, false) // big-endian
10281
+ ;
10282
+ view.setUint32(28, CCTP_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 0
10283
+ ;
10284
+ return bytes.hexlify(bytes.concat([
10285
+ frame,
10286
+ payload
10287
+ ]));
10288
+ }
10289
+ /**
10290
+ * Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
10291
+ * create the recipient's Associated Token Account (ATA) before minting on Solana.
10292
+ *
10293
+ * When an EVM→Solana bridge is forwarded, the destination mint targets the
10294
+ * recipient's USDC ATA — which does not exist for a fresh wallet. This frame
10295
+ * tells the relayer to prepend an idempotent `createAssociatedTokenAccount`
10296
+ * instruction (the relayer pays the rent) so the mint always succeeds.
10297
+ *
10298
+ * Unlike {@link buildForwardingHookData} (an empty version-0 frame), this emits
10299
+ * a version-0 frame whose 32-bit `dataLength` is set to
10300
+ * {@link SOLANA_ATA_FORWARD_PAYLOAD_LENGTH} (33), followed by the payload the
10301
+ * relayer decodes:
10302
+ * - Byte 0: `createAta` flag, always `1`
10303
+ * - Bytes 1-32: the recipient's 32-byte Solana owner public key (`ataOwner`)
10304
+ *
10305
+ * @remarks
10306
+ * `ataOwner` is the recipient's *wallet* public key, not the derived ATA. The
10307
+ * relayer re-derives the ATA from `ataOwner` and the USDC mint and requires it
10308
+ * to equal the burn's `mintRecipient`, so callers must pass the same owner used
10309
+ * to derive `mintRecipient`. The all-zero key is reserved as "absent owner" and
10310
+ * is rejected.
10311
+ *
10312
+ * @param ataOwner - The recipient's 32-byte Solana owner public key.
10313
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by the 33-byte
10314
+ * Solana ATA payload.
10315
+ * @throws {KitError} If `ataOwner` is not exactly 32 bytes, or is the all-zero
10316
+ * key (INPUT_VALIDATION_FAILED).
10317
+ *
10318
+ * @example
10319
+ * ```typescript
10320
+ * import { PublicKey } from '@solana/web3.js'
10321
+ * import { buildSolanaAtaForwardingHookData } from '@core/utils'
10322
+ *
10323
+ * const owner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM')
10324
+ * const hookData = buildSolanaAtaForwardingHookData(owner.toBytes())
10325
+ *
10326
+ * // Use with the forwarded depositForBurnWithHook action so the relayer
10327
+ * // creates the recipient ATA before minting.
10328
+ * await adapter.prepareAction('cctp.v2.depositForBurnWithHook', {
10329
+ * amount: BigInt('1000000'),
10330
+ * mintRecipient: '0x...',
10331
+ * maxFee: BigInt('50000'),
10332
+ * minFinalityThreshold: 1000,
10333
+ * fromChain: ethereum,
10334
+ * toChain: solana,
10335
+ * hookData,
10336
+ * })
10337
+ * ```
10338
+ */ function buildSolanaAtaForwardingHookData(ataOwner) {
10339
+ if (!(ataOwner instanceof Uint8Array) || ataOwner.length !== SOLANA_PUBKEY_LENGTH) {
10340
+ throw createValidationFailedError$1('ataOwner', ataOwner, `Expected a ${String(SOLANA_PUBKEY_LENGTH)}-byte Solana owner public key`);
10341
+ }
10342
+ if (ataOwner.every((byte)=>byte === 0)) {
10343
+ throw createValidationFailedError$1('ataOwner', ataOwner, 'Expected a non-zero Solana owner public key; the all-zero key is reserved as "absent owner"');
10344
+ }
10345
+ // Inner payload: createAta(1) + ataOwner(32).
10346
+ const payload = new Uint8Array(SOLANA_ATA_FORWARD_PAYLOAD_LENGTH);
10347
+ payload[0] = 1 // createAta = true
10348
+ ;
10349
+ payload.set(ataOwner, 1);
10350
+ // 32-byte header: 24-byte magic + uint32 version(0) + uint32 dataLength(33).
10351
+ // The relayer reads dataLength from the v0 frame to slice the inner payload,
10352
+ // so it MUST reflect the appended byte count (unlike the GenericExecutor path).
10353
+ const frame = new Uint8Array(32);
10354
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
10355
+ const view = new DataView(frame.buffer);
10356
+ view.setUint32(24, CCTP_FORWARD_VERSION, false) // big-endian, 0
10357
+ ;
10358
+ view.setUint32(28, SOLANA_ATA_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 33
10359
+ ;
10360
+ return bytes.hexlify(bytes.concat([
10361
+ frame,
10362
+ payload
10363
+ ]));
10364
+ }
10365
+
10366
+ /**
10367
+ * `version` field of the GenericExecutor hookData, in both the
10368
+ * `circle-generic-executor` header (`uint32`) and the ABI tuple (`uint8`). The
10369
+ * executor reverts if it is not `1`.
10370
+ *
10371
+ * @see https://circlepay.atlassian.net/wiki/spaces/~712020cd79585b52ea4353b4720c277fbfcca6/pages/3049291839
10372
+ */ const GENERIC_EXECUTOR_HOOK_DATA_VERSION = 1;
10373
+ /**
10374
+ * ASCII magic that prefixes a GenericExecutor hookData blob.
10375
+ *
10376
+ * The executor auto-detects its payload by this string. It is left-aligned and
10377
+ * zero-padded to 24 bytes in the header, mirroring the `cctp-forward` frame
10378
+ * layout (magic + `uint32` version + `uint32` dataLength).
10379
+ */ const GENERIC_EXECUTOR_MAGIC_PREFIX = 'circle-generic-executor';
10380
+ /**
10381
+ * Prepend the 32-byte `circle-generic-executor` header to the ABI tuple.
10382
+ *
10383
+ * Header layout (mirrors the `cctp-forward` frame): 24-byte zero-padded ASCII
10384
+ * magic + `uint32` version + `uint32` dataLength. Unlike the `cctp-forward`
10385
+ * frame (which the forwarder strips and so carries `dataLength = 0`), this
10386
+ * header's dataLength is the byte length of the ABI tuple that follows, since
10387
+ * the executor consumes both.
10388
+ */ function prependGenericExecutorHeader(abiTuple) {
10389
+ const header = new Uint8Array(32);
10390
+ header.set(new TextEncoder().encode(GENERIC_EXECUTOR_MAGIC_PREFIX), 0);
10391
+ // Byte length of the ABI tuple that the header announces.
10392
+ const tupleByteLength = (abiTuple.length - 2) / 2;
10393
+ const view = new DataView(header.buffer);
10394
+ view.setUint32(24, GENERIC_EXECUTOR_HOOK_DATA_VERSION, false) // big-endian
10395
+ ;
10396
+ view.setUint32(28, tupleByteLength, false) // big-endian
10397
+ ;
10398
+ return bytes.hexlify(bytes.concat([
10399
+ header,
10400
+ abiTuple
10401
+ ]));
10402
+ }
10403
+ /**
10404
+ * Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
10405
+ *
10406
+ * Mirrors viem's `pad(address, size 32)` and CCTP's `mintRecipient`
10407
+ * convention. Solana addresses are already 32 bytes and need no padding.
10408
+ *
10409
+ * @param address - A 0x-prefixed 20-byte EVM address.
10410
+ * @returns The address left-zero-padded to a 0x-prefixed 32-byte hex string.
10411
+ * @throws {KitError} If `address` is not a valid EVM address (INPUT_VALIDATION_FAILED).
10412
+ *
10413
+ * @example
10414
+ * ```typescript
10415
+ * import { padAddressToBytes32 } from '@core/utils'
10416
+ *
10417
+ * padAddressToBytes32('0x75275Aff2D01699D922f045b69ed291311209738')
10418
+ * // '0x00000000000000000000000075275aff2d01699d922f045b69ed291311209738'
10419
+ * ```
10420
+ */ function padAddressToBytes32(address$1) {
10421
+ if (!address.isAddress(address$1)) {
10422
+ throw createValidationFailedError$1('address', address$1, 'Expected a valid 20-byte EVM address');
10423
+ }
10424
+ // bytes32 is raw bytes, not a checksummed address — emit lowercase so it
10425
+ // matches ABI-decoded output.
10426
+ return bytes.hexZeroPad(address.getAddress(address$1), 32).toLowerCase();
10427
+ }
10428
+ /**
10429
+ * Encode the bare GenericExecutor + DepositForHandler payload for a CCTP v2
10430
+ * fast-transfer deposit into a dApp.
10431
+ *
10432
+ * Builds the layers inner→outer:
10433
+ * 1. dApp calldata — the dApp function selector + ABI params, with each amount
10434
+ * slot left as the caller-supplied placeholder.
10435
+ * 2. handler calldata — `(depositContract, approvalTarget, depositCalldata, amountIndices)`
10436
+ * for `DepositForHandler`.
10437
+ * 3. ABI tuple — `(uint8 version, bytes32 recoveryAddress, address handler, bytes handlerCalldata)`.
10438
+ * No handler selector travels on the wire; the executor applies a fixed one.
10439
+ * 4. header — the 32-byte `circle-generic-executor` magic frame prepended to the
10440
+ * tuple, by which the executor auto-detects the payload.
10441
+ *
10442
+ * The returned `hookData` is the bare GenericExecutor blob (header ‖ tuple) — the
10443
+ * exact bytes the executor consumes. It carries no `cctp-forward` envelope. For
10444
+ * the prepaid Quote-API FORWARD path, wrap it with
10445
+ * {@link buildForwardingHookDataWithPayload}; the forwarder strips that envelope
10446
+ * before the executor reads the blob.
10447
+ *
10448
+ * @param options - See {@link BuildDepositForGenericExecutorPayloadParams}.
10449
+ * @returns The encoded {@link DepositForGenericExecutorPayload} layers.
10450
+ * @throws {KitError} If `options` is not an object, `dappId` is unknown,
10451
+ * `config.deployments` is not an array, neither `domainId` nor
10452
+ * `destinationChain` resolves a domain (or the two disagree), no deployment
10453
+ * exists for the resolved domain, the deposit contract cannot be resolved (a
10454
+ * built-in deployment supplied without a `destinationChain`), no `handler` is
10455
+ * supplied and it cannot be resolved from `destinationChain`,
10456
+ * `recoveryAddress`/`handler`/contract addresses are malformed,
10457
+ * `config.function` is not a valid Solidity function signature, `params`
10458
+ * length does not match the dApp signature, `params` values fail ABI encoding
10459
+ * (type mismatch), `config.dynamicAmountIndices` is not an array, or an amount
10460
+ * index is out of range (all INPUT_VALIDATION_FAILED).
10461
+ *
10462
+ * @example Encode a 1-click cross-chain Circle Gateway deposit
10463
+ * ```typescript
10464
+ * import { buildDepositForGenericExecutorPayload, padAddressToBytes32 } from '@core/utils'
10465
+ * import { ArcTestnet } from '@core/chains'
10466
+ *
10467
+ * const user = '0x75275Aff2D01699D922f045b69ed291311209738'
10468
+ * const usdcArc = '0x3600000000000000000000000000000000000000'
10469
+ * const { hookData } = buildDepositForGenericExecutorPayload({
10470
+ * dappId: 'gateway_deposit',
10471
+ * destinationChain: ArcTestnet, // resolves the GatewayWallet + DepositForHandler
10472
+ * // depositFor(address token, address depositor, uint256 value), amount idx [2]
10473
+ * params: [usdcArc, user, 0],
10474
+ * recoveryAddress: padAddressToBytes32(user),
10475
+ * })
10476
+ *
10477
+ * // Pass hookData straight into the CCTP v2 fast transfer.
10478
+ * console.log(hookData)
10479
+ * ```
10480
+ */ function buildDepositForGenericExecutorPayload(options) {
10481
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for plain-JS callers
10482
+ if (options === null || typeof options !== 'object') {
10483
+ throw createValidationFailedError$1('options', options, 'Expected an options object');
10484
+ }
10485
+ const { dappId, params: dappParams } = options;
10486
+ const registry = options.config ?? DAPP_CONFIG;
10487
+ // Look up the dApp before resolving the deposit contract or handler, so an
10488
+ // unknown dApp reports the actionable "Unknown dApp" error rather than an
10489
+ // unresolved-handler error.
10490
+ const config = registry[dappId];
10491
+ if (config === undefined) {
10492
+ throw createValidationFailedError$1('dappId', dappId, `Unknown dApp. Known dApps: ${Object.keys(registry).join(', ')}`);
10493
+ }
10494
+ if (!Array.isArray(config.deployments)) {
10495
+ throw createValidationFailedError$1('deployments', config.deployments, `Expected an array of deployments for dApp '${dappId}'`);
10496
+ }
10497
+ // Array.isArray narrows to `any[]`; re-assert the concrete type.
10498
+ const deployments = config.deployments;
10499
+ // Destination CCTP domain: taken from `destinationChain` when supplied (the
10500
+ // chain is the (network, domain) key), else the explicit `domainId`.
10501
+ const domainId = resolveDomainId(options);
10502
+ const deployment = deployments.find((d)=>d.domainId === domainId);
10503
+ if (deployment === undefined) {
10504
+ throw createValidationFailedError$1('domainId', domainId, `No '${dappId}' deployment for domain ${String(domainId)}`);
10505
+ }
10506
+ if (!Array.isArray(dappParams)) {
10507
+ throw createValidationFailedError$1('params', dappParams, 'Expected an array of ABI-ordered parameters');
10508
+ }
10509
+ if (!bytes.isHexString(options.recoveryAddress, 32)) {
10510
+ throw createValidationFailedError$1('recoveryAddress', options.recoveryAddress, 'Expected a 0x-prefixed 32-byte (bytes32) hex string');
10511
+ }
10512
+ // Deposit contract: the deployment's own address, or — for the built-in
10513
+ // gateway_deposit deployment, which carries none — the destination chain's
10514
+ // GatewayWallet, so the address lives only in @core/chains.
10515
+ const resolvedDepositContract = resolveDepositContract(deployment, options, dappId);
10516
+ // Handler: an explicit `handler` always wins; otherwise resolve the
10517
+ // DepositForHandler from `destinationChain`. The chain is the (network, domain)
10518
+ // key, so a shared CCTP domain (Arc is 26 on both testnet and mainnet) can
10519
+ // never resolve the wrong network's handler and strand funds.
10520
+ const handler = resolveDepositForHandler(options, domainId);
10521
+ if (!address.isAddress(handler)) {
10522
+ throw createValidationFailedError$1('handler', handler, 'Expected a valid EVM address');
10523
+ }
10524
+ const depositContract = assertAddress(resolvedDepositContract, 'depositContract');
10525
+ const approvalTarget = assertAddress(deployment.approvalTarget ?? resolvedDepositContract, 'approvalTarget');
10526
+ // 1. dApp calldata: selector + ABI-encoded params (amount slots stay as placeholders).
10527
+ let dappInterface;
10528
+ try {
10529
+ dappInterface = new abi.Interface([
10530
+ `function ${config.function}`
10531
+ ]);
10532
+ } catch {
10533
+ throw createValidationFailedError$1('function', config.function, 'Expected a valid Solidity function signature');
10534
+ }
10535
+ const rawFragment = dappInterface.fragments[0];
10536
+ /* v8 ignore start -- defensive: guards against unexpected library behavior */ if (rawFragment === undefined || rawFragment.type !== 'function') {
10537
+ throw createValidationFailedError$1('function', config.function, 'Expected a valid Solidity function signature');
10538
+ }
10539
+ /* v8 ignore stop */ const fragment = rawFragment;
10540
+ if (dappParams.length !== fragment.inputs.length) {
10541
+ throw createValidationFailedError$1('params', dappParams, `'${config.function}' expects ${String(fragment.inputs.length)} params, got ${String(dappParams.length)}`);
10542
+ }
10543
+ let depositCalldata;
10544
+ try {
10545
+ depositCalldata = dappInterface.encodeFunctionData(fragment, dappParams);
10546
+ } catch {
10547
+ throw createValidationFailedError$1('params', dappParams, 'ABI encoding failed — check that each param matches the expected Solidity type');
10548
+ }
10549
+ if (!Array.isArray(config.dynamicAmountIndices)) {
10550
+ throw createValidationFailedError$1('dynamicAmountIndices', config.dynamicAmountIndices, `Expected an array of amount indices for dApp '${dappId}'`);
10551
+ }
10552
+ // Array.isArray narrows to `any[]`; re-assert the concrete type.
10553
+ const dynamicAmountIndices = config.dynamicAmountIndices;
10554
+ // Amount byte offsets in depositCalldata: 4 (selector) + paramIndex * 32.
10555
+ const amountIndices = dynamicAmountIndices.map((paramIndex)=>{
10556
+ if (!Number.isInteger(paramIndex) || paramIndex < 0 || paramIndex >= fragment.inputs.length) {
10557
+ throw createValidationFailedError$1('dynamicAmountIndices', paramIndex, `Index out of range for '${config.function}' (${String(fragment.inputs.length)} params)`);
10558
+ }
10559
+ return BigInt(4 + paramIndex * 32);
10560
+ });
10561
+ // 2. Handler layer.
10562
+ const handlerCalldata = abi.defaultAbiCoder.encode([
10563
+ 'address',
10564
+ 'address',
10565
+ 'bytes',
10566
+ 'uint256[]'
10567
+ ], [
10568
+ depositContract,
10569
+ approvalTarget,
10570
+ depositCalldata,
10571
+ amountIndices
10572
+ ]);
10573
+ // 3. Executor ABI tuple. No handler selector travels on the wire — the
10574
+ // executor applies a fixed selector internally.
10575
+ const executorTuple = abi.defaultAbiCoder.encode([
10576
+ 'uint8',
10577
+ 'bytes32',
10578
+ 'address',
10579
+ 'bytes'
10580
+ ], [
10581
+ GENERIC_EXECUTOR_HOOK_DATA_VERSION,
10582
+ options.recoveryAddress,
10583
+ address.getAddress(handler),
10584
+ handlerCalldata
10585
+ ]);
10586
+ // 4. Prepend the circle-generic-executor magic header; this is the final
10587
+ // bare GE blob the executor consumes.
10588
+ const hookData = prependGenericExecutorHeader(executorTuple);
10589
+ return {
10590
+ hookData,
10591
+ handlerCalldata,
10592
+ depositCalldata,
10593
+ amountIndices,
10594
+ depositContract,
10595
+ approvalTarget
10596
+ };
10597
+ }
10598
+ /**
10599
+ * Config key of the built-in Circle Gateway deposit dApp. Its deposit contract
10600
+ * is the destination chain's GatewayWallet, resolved from `destinationChain`
10601
+ * (not a hardcoded address), so this is the only dApp whose deployment may omit
10602
+ * `depositContract`.
10603
+ */ const GATEWAY_DEPOSIT_DAPP_ID = 'gateway_deposit';
10604
+ /**
10605
+ * Built-in dApp registry. Adding a new `depositFor`-style dApp is a config entry
10606
+ * here (or via {@link BuildDepositForGenericExecutorPayloadParams.config}).
10607
+ *
10608
+ * @remarks
10609
+ * Only dApps with confirmed deployment addresses and active callers are included.
10610
+ * The built-in `gateway_deposit` entry omits `depositContract` — it is resolved
10611
+ * from the destination chain's GatewayWallet (`@core/chains`) rather than
10612
+ * duplicated here. Pass a custom registry via `config` for unlisted dApps.
10613
+ */ const DAPP_CONFIG = {
10614
+ // Circle Gateway: depositFor(address token, address depositor, uint256 value)
10615
+ [GATEWAY_DEPOSIT_DAPP_ID]: {
10616
+ function: 'depositFor(address,address,uint256)',
10617
+ dynamicAmountIndices: [
10618
+ 2
10619
+ ],
10620
+ deployments: [
10621
+ // Arc Testnet (CCTP domain 26). The deposit contract is the chain's
10622
+ // GatewayWallet, resolved from `destinationChain` (@core/chains) rather
10623
+ // than duplicated here.
10624
+ {
10625
+ domainId: 26
10626
+ }
10627
+ ]
10628
+ }
10629
+ };
10630
+ /**
10631
+ * Resolve the destination CCTP domain for an encode request.
10632
+ *
10633
+ * Prefers {@link BuildDepositForGenericExecutorPayloadParams.destinationChain}
10634
+ * (`chain.cctp.domain`) — the chain is the (network, domain) key. Falls back to
10635
+ * an explicit `domainId`. When both are supplied they must agree.
10636
+ *
10637
+ * @param options - The encode request.
10638
+ * @returns The destination CCTP domain.
10639
+ * @throws {KitError} If no domain is available, or `domainId` disagrees with
10640
+ * `destinationChain` (INPUT_VALIDATION_FAILED).
10641
+ * @internal
10642
+ */ function resolveDomainId(options) {
10643
+ const chain = options.destinationChain;
10644
+ if (chain !== undefined) {
10645
+ const chainDomain = chain.cctp?.domain;
10646
+ if (chainDomain !== undefined) {
10647
+ if (options.domainId !== undefined && options.domainId !== chainDomain) {
10648
+ throw createValidationFailedError$1('domainId', options.domainId, `does not match destinationChain '${chain.name}' CCTP domain ` + String(chainDomain));
10649
+ }
10650
+ return chainDomain;
10651
+ }
10652
+ }
10653
+ if (options.domainId !== undefined) {
10654
+ return options.domainId;
10655
+ }
10656
+ throw createValidationFailedError$1('domainId', options.domainId, "Provide 'domainId', or a 'destinationChain' with a CCTP domain");
10657
+ }
10658
+ /**
10659
+ * Resolve the deposit contract the handler calls.
10660
+ *
10661
+ * Uses the deployment's own `depositContract` when present. Only the built-in
10662
+ * {@link GATEWAY_DEPOSIT_DAPP_ID} may omit it: its deposit contract is the
10663
+ * destination chain's Gateway v1 wallet, resolved from
10664
+ * {@link BuildDepositForGenericExecutorPayloadParams.destinationChain} so the
10665
+ * address is owned once in `@core/chains`. Any other dApp that omits
10666
+ * `depositContract` is a config error and fails here rather than silently
10667
+ * targeting the GatewayWallet.
10668
+ *
10669
+ * @param deployment - The resolved dApp deployment.
10670
+ * @param options - The encode request.
10671
+ * @param dappId - The dApp key, checked against {@link GATEWAY_DEPOSIT_DAPP_ID}.
10672
+ * @returns The deposit contract address (unvalidated; the caller checks it).
10673
+ * @throws {KitError} If a non-`gateway_deposit` deployment omits
10674
+ * `depositContract`, or if `gateway_deposit` has no Gateway v1
10675
+ * `destinationChain` to resolve one (INPUT_VALIDATION_FAILED).
10676
+ * @internal
10677
+ */ function resolveDepositContract(deployment, options, dappId) {
10678
+ if (deployment.depositContract !== undefined) {
10679
+ return deployment.depositContract;
10680
+ }
10681
+ // Only gateway_deposit may omit its address (it targets the chain's
10682
+ // GatewayWallet). Any other addressless deployment is a config mistake and
10683
+ // must fail rather than silently resolve to the GatewayWallet.
10684
+ if (dappId !== GATEWAY_DEPOSIT_DAPP_ID) {
10685
+ throw createValidationFailedError$1('depositContract', dappId, `dApp '${dappId}' must declare a 'depositContract'; only the built-in ` + `'${GATEWAY_DEPOSIT_DAPP_ID}' resolves its address from the ` + "destination chain's GatewayWallet");
10686
+ }
10687
+ const chain = options.destinationChain;
10688
+ if (chain === undefined || !isGatewayV1Supported(chain)) {
10689
+ throw createValidationFailedError$1('depositContract', dappId, `'${GATEWAY_DEPOSIT_DAPP_ID}' needs a 'destinationChain' with Gateway v1 ` + 'support to resolve its GatewayWallet, or an explicit deployment address');
10690
+ }
10691
+ return chain.gateway.contracts.v1.wallet;
10692
+ }
10693
+ /**
10694
+ * Resolve the `DepositForHandler` address for an encode request.
10695
+ *
10696
+ * An explicit `options.handler` always wins. Otherwise the handler is resolved
10697
+ * from {@link BuildDepositForGenericExecutorPayloadParams.destinationChain}
10698
+ * (`chain.gateway.contracts.v1.depositForHandler`). The chain is the (network,
10699
+ * domain) key, so a CCTP domain shared across a chain's testnet and mainnet
10700
+ * cannot resolve the wrong network's handler; a missing chain or an unregistered
10701
+ * handler throws rather than guessing.
10702
+ *
10703
+ * @param options - The encode request.
10704
+ * @param domainId - The resolved destination domain, reported in the error.
10705
+ * @returns The resolved handler address (unvalidated; the caller checks it).
10706
+ * @throws {KitError} If `handler` is omitted and cannot be resolved
10707
+ * (INPUT_VALIDATION_FAILED).
10708
+ * @internal
10709
+ */ function resolveDepositForHandler(options, domainId) {
10710
+ if (options.handler !== undefined) {
10711
+ return options.handler;
10712
+ }
10713
+ const chain = options.destinationChain;
10714
+ if (chain === undefined) {
10715
+ throw createValidationFailedError$1('handler', domainId, "No 'handler' supplied; pass 'handler' explicitly, or a " + "'destinationChain' whose Gateway config registers a DepositForHandler");
10716
+ }
10717
+ const registered = isGatewayV1Supported(chain) ? chain.gateway.contracts.v1.depositForHandler : undefined;
10718
+ if (registered === undefined) {
10719
+ throw createValidationFailedError$1('handler', domainId, `No DepositForHandler registered for domain ${String(domainId)} on ` + `chain '${chain.name}'; pass 'handler' explicitly`);
10720
+ }
10721
+ return registered;
10722
+ }
10723
+ /**
10724
+ * Validate and checksum an EVM address, throwing a consistent validation error.
10725
+ */ function assertAddress(address$1, field) {
10726
+ if (!address.isAddress(address$1)) {
10727
+ throw createValidationFailedError$1(field, address$1, 'Expected a valid EVM address');
10728
+ }
10729
+ return address.getAddress(address$1);
10730
+ }
9299
10731
 
9300
10732
  /**
9301
10733
  * Configuration for {@link retryAsync}.
@@ -9385,7 +10817,7 @@ function resolveOptions(options) {
9385
10817
  * allowlisted {@link ClientLogPayload} fields (and the allowlisted
9386
10818
  * sub-fields of `errorDetails` / `clientContext`) are copied across.
9387
10819
  * A regressing upstream mapper — or a plain-JS caller that bypasses the
9388
- * type — therefore cannot exfiltrate stray properties (secrets, PII,
10820
+ * type — therefore cannot exfiltrate stray properties (secrets,
9389
10821
  * raw error stacks) through the analytics channel. Optional fields are
9390
10822
  * only included when present so the serialised shape matches the
9391
10823
  * server's strict schema.
@@ -9408,6 +10840,9 @@ function resolveOptions(options) {
9408
10840
  if (payload.destinationChain !== undefined) safe['destinationChain'] = payload.destinationChain;
9409
10841
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
9410
10842
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
10843
+ if (payload.amountIn !== undefined) safe['amountIn'] = payload.amountIn;
10844
+ if (payload.durationMs !== undefined) safe['durationMs'] = payload.durationMs;
10845
+ if (payload.sourceAddress !== undefined) safe['sourceAddress'] = payload.sourceAddress;
9411
10846
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
9412
10847
  if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
9413
10848
  if (payload.errorDetails !== undefined) {
@@ -9604,22 +11039,36 @@ function resolveOptions(options) {
9604
11039
  }
9605
11040
 
9606
11041
  /**
9607
- * Soft signal for the case where building or emitting a telemetry payload
9608
- * threw — for example, a buggy `TelemetryContextResolver`, a regression in
11042
+ * Emit a stable console warning when building or emitting a telemetry payload
11043
+ * throws — for example, a buggy `TelemetryContextResolver`, a regression in
9609
11044
  * `extractErrorDetails`, or a synchronous failure inside `emitAnalyticsLog`
9610
- * before it could swallow the error itself. Logged with a stable prefix so
9611
- * consumers can grep for it. We deliberately do not re-throw: the caller's
9612
- * original operation error must always win.
11045
+ * before it could swallow the error itself. Uses a stable prefix so the
11046
+ * drop is discoverable via grep. Never re-throws: the caller's original
11047
+ * operation error must always win.
9613
11048
  *
9614
11049
  * @internal
9615
- */ function warnTelemetryDrop(eventType, cause) {
9616
- try {
9617
- // Pass `cause` as the second console.warn argument rather than
9618
- // string-coercing it. `String(err)` (and `err.message` alone)
9619
- // discards the stack trace, nested `cause`, and any custom Error
9620
- // properties — exactly the context an on-call needs when a
9621
- // resolver-closure regression triggers this path.
9622
- console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
11050
+ *
11051
+ * @param eventType - The telemetry event type that was being emitted.
11052
+ * @param cause - The error or value that caused the drop.
11053
+ *
11054
+ * @example
11055
+ * ```typescript
11056
+ * import { warnTelemetryDrop } from '@core/utils'
11057
+ *
11058
+ * try {
11059
+ * void emitAnalyticsLog(payload)
11060
+ * } catch (err) {
11061
+ * warnTelemetryDrop('my_event', err)
11062
+ * }
11063
+ * ```
11064
+ */ function warnTelemetryDrop(eventType, cause) {
11065
+ try {
11066
+ // Pass `cause` as the second console.warn argument rather than
11067
+ // string-coercing it. `String(err)` (and `err.message` alone)
11068
+ // discards the stack trace, nested `cause`, and any custom Error
11069
+ // properties — exactly the context an on-call needs when a
11070
+ // resolver-closure regression triggers this path.
11071
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
9623
11072
  } catch {
9624
11073
  // console.warn itself throwing is the user's environment; nothing more we
9625
11074
  // can do without risking the original operation error.
@@ -9651,6 +11100,9 @@ function resolveOptions(options) {
9651
11100
  ...context?.tokenOut != null && {
9652
11101
  tokenOut: context.tokenOut
9653
11102
  },
11103
+ ...context?.amountIn != null && {
11104
+ amountIn: context.amountIn
11105
+ },
9654
11106
  ...context?.txHash != null && {
9655
11107
  txHash: context.txHash
9656
11108
  },
@@ -9752,7 +11204,7 @@ function resolveOptions(options) {
9752
11204
  const stepEntry = stepEventMap.find(([name])=>name === failedStep?.name);
9753
11205
  // `failedStep.errorMessage` is intentionally **not** copied into the payload.
9754
11206
  // Provider messages are unbounded and frequently contain operator data
9755
- // (addresses, signatures, partial intent payloads, raw RPC responses). The
11207
+ // (signatures, partial intent payloads, raw RPC responses). The
9756
11208
  // step name plus the surrounding context fields already identify *which*
9757
11209
  // phase failed; the *why* is left to the corresponding step-level logs that
9758
11210
  // the provider emits separately. The thrown-error path (`extractErrorDetails`
@@ -9769,7 +11221,7 @@ function resolveOptions(options) {
9769
11221
  }
9770
11222
 
9771
11223
  var name$2 = "@circle-fin/bridge-kit";
9772
- var version$3 = "1.13.0";
11224
+ var version$3 = "1.14.1";
9773
11225
  var pkg$3 = {
9774
11226
  name: name$2,
9775
11227
  version: version$3};
@@ -10150,7 +11602,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
10150
11602
  * const result = evmAddressSchema.safeParse(validAddress)
10151
11603
  * console.log(result.success) // true
10152
11604
  * ```
10153
- */ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
11605
+ */ const evmAddressSchema$1 = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
10154
11606
  /**
10155
11607
  * Schema for validating transaction hashes.
10156
11608
  *
@@ -10839,6 +12291,10 @@ var TransferSpeed;
10839
12291
  token: zod.z.literal('USDC').optional(),
10840
12292
  config: zod.z.object({
10841
12293
  transferSpeed: zod.z.nativeEnum(TransferSpeed).optional(),
12294
+ feePayment: zod.z.enum([
12295
+ 'source',
12296
+ 'destination'
12297
+ ]).optional(),
10842
12298
  maxFee: zod.z.string().min(1, 'Required').pipe(createDecimalStringValidator({
10843
12299
  allowZero: true,
10844
12300
  regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
@@ -10846,7 +12302,8 @@ var TransferSpeed;
10846
12302
  maxDecimals: 6
10847
12303
  })(zod.z.string())).optional(),
10848
12304
  customFee: customFeeSchema.optional()
10849
- }).optional()
12305
+ }).optional(),
12306
+ quote: zod.z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex').optional()
10850
12307
  });
10851
12308
 
10852
12309
  /**
@@ -11458,7 +12915,7 @@ var TransferSpeed;
11458
12915
  * })
11459
12916
  * ```
11460
12917
  */ function createLogger(options, stream) {
11461
- const { redact, ...pinoOptions } = {};
12918
+ const { redact, ...pinoOptions } = options ?? {};
11462
12919
  // Build redaction config
11463
12920
  const redactConfig = buildRedactConfig(redact);
11464
12921
  // Build final pino options, only include redact if defined
@@ -12021,16 +13478,73 @@ var TransferSpeed;
12021
13478
  };
12022
13479
  }
12023
13480
 
13481
+ /**
13482
+ * Dispatch a bridge step event through the provider's action dispatcher.
13483
+ *
13484
+ * Constructs the appropriate action payload and dispatches it to any registered
13485
+ * event listeners. Handles type-safe dispatching for different step types.
13486
+ * When provided, traceId from the invocation context is included for end-to-end correlation.
13487
+ *
13488
+ * @param name - The step name (approve, burn, fetchAttestation, or mint).
13489
+ * @param step - The completed bridge step containing transaction details and explorerUrl.
13490
+ * @param provider - The CCTP v2 provider with action dispatcher.
13491
+ * @param invocation - Optional invocation context containing traceId for correlation.
13492
+ *
13493
+ * @example
13494
+ * ```typescript
13495
+ * const step: BridgeStep = {
13496
+ * name: 'burn',
13497
+ * state: 'success',
13498
+ * txHash: '0xabc...',
13499
+ * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
13500
+ * data: { ... }
13501
+ * }
13502
+ * dispatchStepEvent('burn', step, provider, invocationContext)
13503
+ * ```
13504
+ */ function dispatchStepEvent(name, step, provider, invocation) {
13505
+ if (!provider.actionDispatcher) {
13506
+ return;
13507
+ }
13508
+ // Extract traceId from invocation context if provided
13509
+ const traceId = invocation?.traceId;
13510
+ const actionValues = {
13511
+ protocol: 'cctp',
13512
+ version: 'v2',
13513
+ ...traceId !== undefined && {
13514
+ traceId
13515
+ },
13516
+ values: step
13517
+ };
13518
+ switch(name){
13519
+ case 'approve':
13520
+ case 'burn':
13521
+ case 'mint':
13522
+ provider.actionDispatcher.dispatch(name, {
13523
+ ...actionValues,
13524
+ method: name
13525
+ });
13526
+ break;
13527
+ case 'fetchAttestation':
13528
+ case 'reAttest':
13529
+ provider.actionDispatcher.dispatch(name, {
13530
+ ...actionValues,
13531
+ method: name,
13532
+ values: step
13533
+ });
13534
+ break;
13535
+ }
13536
+ }
13537
+
12024
13538
  /**
12025
13539
  * Base URL for Circle's IRIS API (mainnet/production).
12026
13540
  *
12027
13541
  * The IRIS API provides attestation services for CCTP cross-chain transfers.
12028
- */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
13542
+ */ const IRIS_API_BASE_URL$1 = 'https://iris-api.circle.com';
12029
13543
  /**
12030
13544
  * Base URL for Circle's IRIS API (testnet/sandbox).
12031
13545
  *
12032
13546
  * Used for development and testing on testnet chains.
12033
- */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
13547
+ */ const IRIS_API_SANDBOX_BASE_URL$1 = 'https://iris-api-sandbox.circle.com';
12034
13548
 
12035
13549
  /**
12036
13550
  * Type guard to validate the API response structure.
@@ -12073,7 +13587,7 @@ const isFastBurnFeeResponse = (data)=>{
12073
13587
  * @param isTestnet - Whether the request is for a testnet chain
12074
13588
  * @returns The complete API URL
12075
13589
  */ function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
12076
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
13590
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
12077
13591
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
12078
13592
  }
12079
13593
  const FAST_TIER_FINALITY_THRESHOLD = 1000;
@@ -12284,6 +13798,77 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12284
13798
  }
12285
13799
  };
12286
13800
 
13801
+ /**
13802
+ * Build the forwarding `hookData` for a forwarded (Orbit-relayed) CCTP v2 burn,
13803
+ * tailored to the destination chain.
13804
+ *
13805
+ * For EVM destinations the recipient already holds ERC-20 USDC directly, so the
13806
+ * empty version-0 `cctp-forward` frame is sufficient. For Solana destinations
13807
+ * USDC is held in an Associated Token Account (ATA) that may not exist for a
13808
+ * fresh wallet, so this emits a frame carrying `createAta` + `ataOwner` that
13809
+ * instructs the relayer to create the recipient ATA (idempotently, at the
13810
+ * relayer's expense) before minting.
13811
+ *
13812
+ * `@solana/web3.js` is imported lazily so EVM-only consumers never load Solana
13813
+ * code, mirroring {@link getMintRecipientAccount}.
13814
+ *
13815
+ * @param chainType - The destination blockchain type ('evm' or 'solana').
13816
+ * @param ownerAddress - The recipient's wallet address on the destination chain
13817
+ * (base58 for Solana). Must be the same owner used to derive `mintRecipient`.
13818
+ * @returns A 0x-prefixed hookData hex string for the forwarded burn.
13819
+ * @throws {KitError} If `chainType` is neither 'evm' nor 'solana', if
13820
+ * `@solana/web3.js` cannot be loaded, or if `ownerAddress` is not a valid
13821
+ * Solana public key (all FATAL).
13822
+ *
13823
+ * @example
13824
+ * ```typescript
13825
+ * import { getForwarderHookData } from './getForwarderHookData'
13826
+ *
13827
+ * // EVM: empty forwarding frame
13828
+ * const evmHook = await getForwarderHookData('evm', '0x742d35Cc...')
13829
+ *
13830
+ * // Solana: frame instructing the relayer to create the recipient ATA
13831
+ * const solanaHook = await getForwarderHookData(
13832
+ * 'solana',
13833
+ * '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
13834
+ * )
13835
+ * ```
13836
+ */ const getForwarderHookData = async (/** The destination blockchain type - determines the hookData shape */ chainType, /** The recipient's wallet address (hex for EVM, base58 for Solana) */ ownerAddress)=>{
13837
+ if (chainType === 'evm') {
13838
+ // EVM: the recipient holds USDC directly; no ATA setup is needed.
13839
+ return buildForwardingHookData();
13840
+ }
13841
+ // Fail closed: only EVM and Solana forwarding destinations are supported.
13842
+ // Without this guard any future non-EVM chain type would silently fall
13843
+ // through to the Solana path and mis-encode hookData on a money-movement path.
13844
+ if (chainType !== 'solana') {
13845
+ throw new KitError({
13846
+ ...InputError.VALIDATION_FAILED,
13847
+ recoverability: 'FATAL',
13848
+ message: `Forwarded burns are not supported for destination chain type "${chainType}"`
13849
+ });
13850
+ }
13851
+ // Solana: encode the owner so the relayer creates the recipient ATA.
13852
+ // Resolve @solana/web3.js lazily so EVM-only consumers never load Solana code.
13853
+ const { PublicKey } = await import('@solana/web3.js').catch(()=>{
13854
+ throw new KitError({
13855
+ ...InputError.VALIDATION_FAILED,
13856
+ recoverability: 'FATAL',
13857
+ message: 'Failed to load @solana/web3.js. Please ensure it is installed: npm install @solana/web3.js'
13858
+ });
13859
+ });
13860
+ try {
13861
+ const owner = new PublicKey(ownerAddress);
13862
+ return buildSolanaAtaForwardingHookData(owner.toBytes());
13863
+ } catch (error) {
13864
+ throw new KitError({
13865
+ ...InputError.INVALID_ADDRESS,
13866
+ recoverability: 'FATAL',
13867
+ message: `Failed to build Solana forwarder hookData for recipient "${ownerAddress}": ${error instanceof Error ? error.message : String(error)}`
13868
+ });
13869
+ }
13870
+ };
13871
+
12287
13872
  /**
12288
13873
  * Validates and converts a fee value to bigint.
12289
13874
  *
@@ -12386,7 +13971,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12386
13971
 
12387
13972
  /**
12388
13973
  * The zero address, denoting a native-currency fee in a signed quote.
12389
- */ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
13974
+ */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
12390
13975
  /**
12391
13976
  * Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
12392
13977
  *
@@ -12427,7 +14012,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12427
14012
  if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
12428
14013
  throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
12429
14014
  }
12430
- const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS;
14015
+ const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
12431
14016
  const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
12432
14017
  if (isNativeFee) {
12433
14018
  return {
@@ -12565,7 +14150,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12565
14150
  * @param isTestnet - Whether the request is for a testnet chain
12566
14151
  * @returns The complete API URL with forward=true query parameter
12567
14152
  */ function buildForwardingFeeUrl(sourceDomain, destinationDomain, isTestnet) {
12568
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
14153
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
12569
14154
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}?forward=true`;
12570
14155
  }
12571
14156
  /**
@@ -12684,6 +14269,8 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
12684
14269
  ;
12685
14270
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
12686
14271
  ;
14272
+ /** Gas units consumed by `depositForBurnWithHookAndFees` on an EVM chain (prepaid-FORWARD path). */ const DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM = 626_584n // avg of the last 10 txns
14273
+ ;
12687
14274
  // Gas FLOORS, not ceilings — kept separate from the fee-estimate averages
12688
14275
  // above. `executePreparedChainRequest` submits
12689
14276
  // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
@@ -13626,7 +15213,7 @@ function hasPendingState(analysis, result) {
13626
15213
  * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
13627
15214
  * ```
13628
15215
  */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
13629
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15216
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
13630
15217
  const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
13631
15218
  url.searchParams.set('transactionHash', transactionHash);
13632
15219
  return url.toString();
@@ -13794,7 +15381,7 @@ function hasPendingState(analysis, result) {
13794
15381
  * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
13795
15382
  * ```
13796
15383
  */ const buildReAttestUrl = (nonce, isTestnet)=>{
13797
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15384
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
13798
15385
  const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
13799
15386
  return url.toString();
13800
15387
  };
@@ -14249,7 +15836,8 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14249
15836
  * - `destinationChain` — present and supports CCTP v2
14250
15837
  * - source and destination chains must both be testnet or both mainnet
14251
15838
  * - source and destination chains must differ
14252
- * - `executor` non-empty string
15839
+ * - destination — either `executor`, or both `mintRecipient` and
15840
+ * `destinationCaller`; not both
14253
15841
  * - `amount` — bigint or non-empty string coercible to bigint
14254
15842
  * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
14255
15843
  * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
@@ -14291,10 +15879,17 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14291
15879
  if (source.chain.name === dest.name) {
14292
15880
  throw createUnsupportedRouteError(source.chain.name, dest.name);
14293
15881
  }
14294
- // executor
15882
+ // Destination: GenericExecutor shorthand or explicit recipient + caller.
14295
15883
  const executor = p['executor'];
14296
- if (typeof executor !== 'string' || executor === '') {
14297
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
15884
+ const mintRecipient = p['mintRecipient'];
15885
+ const destinationCaller = p['destinationCaller'];
15886
+ const hasExecutor = typeof executor === 'string' && executor !== '';
15887
+ const hasDirectDestination = typeof mintRecipient === 'string' && mintRecipient !== '' && typeof destinationCaller === 'string' && destinationCaller !== '';
15888
+ if (!hasExecutor && !hasDirectDestination) {
15889
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor, or both mintRecipient and destinationCaller');
15890
+ }
15891
+ if (hasExecutor && hasDirectDestination) {
15892
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor or direct destination fields, not both');
14298
15893
  }
14299
15894
  // amount
14300
15895
  const rawAmount = p['amount'];
@@ -14317,7 +15912,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14317
15912
  throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
14318
15913
  }
14319
15914
  // feeToken
14320
- if (!evmAddressSchema.safeParse(p['feeToken']).success) {
15915
+ if (!evmAddressSchema$1.safeParse(p['feeToken']).success) {
14321
15916
  throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
14322
15917
  }
14323
15918
  // claim
@@ -14329,7 +15924,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14329
15924
  if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
14330
15925
  throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
14331
15926
  }
14332
- if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
15927
+ if (!evmAddressSchema$1.safeParse(claim['refundAddress']).success) {
14333
15928
  throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
14334
15929
  }
14335
15930
  // hookData (optional)
@@ -14711,63 +16306,6 @@ const mockAttestationMessage = {
14711
16306
  });
14712
16307
  }
14713
16308
 
14714
- /**
14715
- * Dispatch a bridge step event through the provider's action dispatcher.
14716
- *
14717
- * Constructs the appropriate action payload and dispatches it to any registered
14718
- * event listeners. Handles type-safe dispatching for different step types.
14719
- * When provided, traceId from the invocation context is included for end-to-end correlation.
14720
- *
14721
- * @param name - The step name (approve, burn, fetchAttestation, or mint).
14722
- * @param step - The completed bridge step containing transaction details and explorerUrl.
14723
- * @param provider - The CCTP v2 provider with action dispatcher.
14724
- * @param invocation - Optional invocation context containing traceId for correlation.
14725
- *
14726
- * @example
14727
- * ```typescript
14728
- * const step: BridgeStep = {
14729
- * name: 'burn',
14730
- * state: 'success',
14731
- * txHash: '0xabc...',
14732
- * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
14733
- * data: { ... }
14734
- * }
14735
- * dispatchStepEvent('burn', step, provider, invocationContext)
14736
- * ```
14737
- */ function dispatchStepEvent(name, step, provider, invocation) {
14738
- if (!provider.actionDispatcher) {
14739
- return;
14740
- }
14741
- // Extract traceId from invocation context if provided
14742
- const traceId = invocation?.traceId;
14743
- const actionValues = {
14744
- protocol: 'cctp',
14745
- version: 'v2',
14746
- ...traceId !== undefined && {
14747
- traceId
14748
- },
14749
- values: step
14750
- };
14751
- switch(name){
14752
- case 'approve':
14753
- case 'burn':
14754
- case 'mint':
14755
- provider.actionDispatcher.dispatch(name, {
14756
- ...actionValues,
14757
- method: name
14758
- });
14759
- break;
14760
- case 'fetchAttestation':
14761
- case 'reAttest':
14762
- provider.actionDispatcher.dispatch(name, {
14763
- ...actionValues,
14764
- method: name,
14765
- values: step
14766
- });
14767
- break;
14768
- }
14769
- }
14770
-
14771
16309
  /**
14772
16310
  * Check whether the source adapter supports EIP-5792 atomic batching and
14773
16311
  * the consumer has not explicitly opted out via `config.batchTransactions`.
@@ -15014,7 +16552,7 @@ const mockAttestationMessage = {
15014
16552
  return step;
15015
16553
  }
15016
16554
 
15017
- var version$2 = "1.11.0";
16555
+ var version$2 = "1.13.0";
15018
16556
  var pkg$2 = {
15019
16557
  version: version$2};
15020
16558
 
@@ -15720,6 +17258,9 @@ var pkg$2 = {
15720
17258
  }
15721
17259
  }
15722
17260
 
17261
+ const logger = createLogger({
17262
+ name: 'provider-cctp-v2'
17263
+ });
15723
17264
  function isPlainObject(value) {
15724
17265
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
15725
17266
  return false;
@@ -15825,6 +17366,34 @@ function assertCCTPV2Config(config) {
15825
17366
  this.config = config;
15826
17367
  }
15827
17368
  /**
17369
+ * Emit a bridge step event through the provider's registered action
17370
+ * dispatcher.
17371
+ *
17372
+ * Kit-level orchestration that drives the burn primitives directly instead
17373
+ * of {@link CCTPV2BridgingProvider.bridge} (for example the receive-exact
17374
+ * source-fee flow) uses this to surface the same `approve`/`burn`/`mint`
17375
+ * events as the standard bridge path. It is a no-op when no dispatcher is
17376
+ * registered.
17377
+ *
17378
+ * @param name - The step name (`approve`, `burn`, `mint`, ...).
17379
+ * @param step - The completed bridge step to broadcast.
17380
+ * @param invocation - Optional invocation context carrying a `traceId` for
17381
+ * end-to-end correlation.
17382
+ * @returns Nothing.
17383
+ *
17384
+ * @example
17385
+ * ```typescript
17386
+ * const provider = new CCTPV2BridgingProvider()
17387
+ * provider.emitBridgeStep('burn', {
17388
+ * name: 'burn',
17389
+ * state: 'success',
17390
+ * txHash: '0xabc...',
17391
+ * })
17392
+ * ```
17393
+ */ emitBridgeStep(name, step, invocation) {
17394
+ dispatchStepEvent(name, step, this, invocation);
17395
+ }
17396
+ /**
15828
17397
  * Resolves the effective polling configuration for an attestation request.
15829
17398
  *
15830
17399
  * Precedence (lowest to highest): provider `config.attestation`, then the
@@ -16054,6 +17623,116 @@ function assertCCTPV2Config(config) {
16054
17623
  return estimateResult;
16055
17624
  }
16056
17625
  /**
17626
+ * Estimate source-chain gas for a prepaid-FORWARD deposit burn via
17627
+ * `TokenMessengerWithFees.depositForBurnWithHookAndFees`.
17628
+ *
17629
+ * Builds a size-correct GenericExecutor hookData placeholder — using the
17630
+ * pre-validated `contracts.executor` and `contracts.depositForHandler` — so
17631
+ * the EVM calldata length matches production. Attempts a live
17632
+ * `eth_estimateGas` via the adapter's `cctp.v2.depositForBurnWithFees`
17633
+ * action and falls back to the static
17634
+ * {@link DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM} constant when the live
17635
+ * RPC call fails.
17636
+ *
17637
+ * Gateway eligibility is the caller's responsibility: validate the
17638
+ * destination chain with `resolveGatewayExecutorContracts` (UBK) before
17639
+ * calling this method.
17640
+ *
17641
+ * @param params - Estimation parameters including adapter, chains, amount,
17642
+ * the signed fee quote returned by the Quote API, and the pre-validated
17643
+ * Gateway executor contracts resolved by the caller.
17644
+ * @returns Promise resolving to the estimated (or fallback) gas cost.
17645
+ * @throws KitError `SERVICE_INTERNAL_ERROR` (FATAL) if `dstChain`'s
17646
+ * `usdcAddress` is `null`.
17647
+ *
17648
+ * @example
17649
+ * ```typescript
17650
+ * const contracts = resolveGatewayExecutorContracts(srcChain, dstChain)
17651
+ * const gasEstimate = await CCTPV2BridgingProvider.estimateDepositBurn({
17652
+ * adapter: evmAdapter,
17653
+ * srcChain: Ethereum,
17654
+ * dstChain: ArcTestnet,
17655
+ * amountMinorUnits: 100_000_000n,
17656
+ * refundAddress: '0xUserWallet',
17657
+ * signedQuote: quote.signedQuote,
17658
+ * feeToken: quote.feeToken,
17659
+ * feeTotalAmount: BigInt(quote.feeTotalAmount),
17660
+ * resolvedContext,
17661
+ * contracts,
17662
+ * })
17663
+ * ```
17664
+ */ static async estimateDepositBurn(params) {
17665
+ const { adapter, srcChain, dstChain, amountMinorUnits, refundAddress, signedQuote, feeToken, feeTotalAmount, resolvedContext, contracts } = params;
17666
+ const { executor, depositForHandler } = contracts;
17667
+ if (dstChain.usdcAddress === null) {
17668
+ throw new KitError({
17669
+ ...ServiceError.INTERNAL_ERROR,
17670
+ recoverability: 'FATAL',
17671
+ message: `Destination chain ${dstChain.name} has no USDC address configured`
17672
+ });
17673
+ }
17674
+ // Build a size-correct hookData for eth_estimateGas. The EVM uses calldata
17675
+ // length to compute gas, so the byte layout must match production even
17676
+ // though field values are not final.
17677
+ const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
17678
+ dappId: 'gateway_deposit',
17679
+ destinationChain: dstChain,
17680
+ handler: depositForHandler,
17681
+ params: [
17682
+ dstChain.usdcAddress,
17683
+ refundAddress,
17684
+ 0n
17685
+ ],
17686
+ recoveryAddress: padAddressToBytes32(refundAddress),
17687
+ // Override deployments only — spread the canonical function signature
17688
+ // and amount indices from DAPP_CONFIG so they stay in sync.
17689
+ config: {
17690
+ gateway_deposit: {
17691
+ ...DAPP_CONFIG.gateway_deposit,
17692
+ deployments: [
17693
+ {
17694
+ domainId: dstChain.cctp.domain
17695
+ }
17696
+ ]
17697
+ }
17698
+ }
17699
+ });
17700
+ const hookData = buildForwardingHookDataWithPayload(GENERIC_EXECUTOR_HOOK_DATA_VERSION, geBlob);
17701
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee
17702
+ // item, so the hookData must carry a `cctp-forward` frame — matches the
17703
+ // assertion in `prepareDepositForBurn`.
17704
+ assertForwardHookData(hookData);
17705
+ try {
17706
+ const prepared = await adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
17707
+ fromChain: srcChain,
17708
+ toChain: dstChain,
17709
+ amount: amountMinorUnits,
17710
+ mintRecipient: executor,
17711
+ destinationCaller: executor,
17712
+ claim: {
17713
+ signedQuote,
17714
+ refundAddress
17715
+ },
17716
+ feeToken,
17717
+ feeTotalAmount,
17718
+ hookData
17719
+ }, resolvedContext);
17720
+ return await prepared.estimate(undefined);
17721
+ } catch (err) {
17722
+ logger.debug('estimateDepositBurn: live eth_estimateGas failed, using static fallback', {
17723
+ err,
17724
+ chain: srcChain.name
17725
+ });
17726
+ try {
17727
+ return await adapter.calculateTransactionFee(DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM, undefined, srcChain);
17728
+ } catch (feeErr) {
17729
+ throw createRpcEndpointError(srcChain.name, {
17730
+ rawError: feeErr
17731
+ });
17732
+ }
17733
+ }
17734
+ }
17735
+ /**
16057
17736
  * Extracts OperationContext from bridge parameters for a given wallet context.
16058
17737
  *
16059
17738
  * This method extracts the chain and address information from the wallet context
@@ -16650,8 +18329,11 @@ function assertCCTPV2Config(config) {
16650
18329
  // 2. Forwarder: Does the user want Circle's relayer to handle attestation/mint?
16651
18330
  const useCustomBurn = hasCustomContractSupport(source.chain, 'bridge');
16652
18331
  const useForwarder = destination.useForwarder === true;
16653
- // Build hookData once if forwarder is enabled (memoized internally)
16654
- const hookData = useForwarder ? buildForwardingHookData() : undefined;
18332
+ // Build hookData once if forwarder is enabled. EVM destinations get the
18333
+ // empty forwarding frame; Solana destinations get a frame instructing the
18334
+ // relayer to create the recipient's ATA (using the same owner that derived
18335
+ // `mintRecipient`) so the mint succeeds even for a fresh wallet.
18336
+ const hookData = useForwarder ? await getForwarderHookData(destination.chain.type, destinationAddressForMint) : undefined;
16655
18337
  if (useCustomBurn) {
16656
18338
  // Custom burn path: use bridge contract (with or without hook)
16657
18339
  const customBurnParams = {
@@ -16677,15 +18359,109 @@ function assertCCTPV2Config(config) {
16677
18359
  return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
16678
18360
  }
16679
18361
  /**
18362
+ * Prepare the source-chain `depositForBurnWithHookAndFees` call for the
18363
+ * GenericExecutor FORWARD path.
18364
+ *
18365
+ * Exposed as a public static method so the UBK fast-deposit flow can invoke
18366
+ * it directly without holding a provider instance. The byte layout mirrors
18367
+ * {@link CCTPV2BridgingProvider.estimateDepositBurn} so gas estimates and the
18368
+ * executed call agree.
18369
+ *
18370
+ * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18371
+ * @param params - Burn parameters including adapter, chains, deposit action,
18372
+ * amount, signer address, signed fee quote, and pre-resolved adapter context.
18373
+ * @returns The prepared `depositForBurnWithHookAndFees` burn transaction.
18374
+ * @throws {KitError} `UNSUPPORTED_ROUTE` when `dstChain` lacks a
18375
+ * GenericExecutor or DepositForHandler, or the `deposit.dappId` is unknown.
18376
+ *
18377
+ * @example
18378
+ * ```typescript
18379
+ * import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
18380
+ * import { Ethereum, ArcTestnet } from '@core/chains'
18381
+ *
18382
+ * const prepared = await CCTPV2BridgingProvider.prepareDepositForBurn({
18383
+ * adapter,
18384
+ * srcChain: Ethereum,
18385
+ * dstChain: ArcTestnet,
18386
+ * deposit: { dappId: 'gateway_deposit', params: [usdcAddress, recipient, 0n] },
18387
+ * amountMinorUnits: 100_000_000n,
18388
+ * refundAddress: '0xSender...',
18389
+ * signedQuote: quote.signedQuote,
18390
+ * feeToken: quote.feeToken,
18391
+ * feeTotalAmount: BigInt(quote.feeTotalAmount),
18392
+ * resolvedContext,
18393
+ * })
18394
+ * const txHash = await prepared.execute()
18395
+ * ```
18396
+ */ static async prepareDepositForBurn(params) {
18397
+ const { adapter, srcChain, dstChain, deposit, amountMinorUnits, refundAddress, signedQuote, feeToken, feeTotalAmount, resolvedContext } = params;
18398
+ // Resolve executor and depositForHandler from the destination chain's
18399
+ // gateway config. Throw an unsupported-route error if either is absent.
18400
+ const executor = dstChain.gateway?.contracts?.v1?.genericExecutor;
18401
+ const depositForHandler = dstChain.gateway?.contracts?.v1?.depositForHandler;
18402
+ if (!executor || !depositForHandler) {
18403
+ throw createUnsupportedRouteError(srcChain.name, dstChain.name);
18404
+ }
18405
+ // Resolve the canonical dApp config (function signature + amount indices)
18406
+ // for THIS deposit's `dappId`. Reject an unknown `dappId` rather than
18407
+ // encoding the wrong ABI selector, which would burn on the source but
18408
+ // revert in the executor call on the destination.
18409
+ const dappConfig = DAPP_CONFIG[deposit.dappId];
18410
+ if (dappConfig === undefined) {
18411
+ throw createUnsupportedRouteError(srcChain.name, dstChain.name);
18412
+ }
18413
+ // Build the bare GenericExecutor payload, then wrap it in the `cctp-forward`
18414
+ // frame required by the prepaid-FORWARD wrapper.
18415
+ const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
18416
+ dappId: deposit.dappId,
18417
+ destinationChain: dstChain,
18418
+ handler: depositForHandler,
18419
+ params: deposit.params,
18420
+ recoveryAddress: padAddressToBytes32(refundAddress),
18421
+ config: {
18422
+ [deposit.dappId]: {
18423
+ ...dappConfig,
18424
+ deployments: [
18425
+ {
18426
+ domainId: dstChain.cctp.domain
18427
+ }
18428
+ ]
18429
+ }
18430
+ }
18431
+ });
18432
+ const hookData = buildForwardingHookDataWithPayload(GENERIC_EXECUTOR_HOOK_DATA_VERSION, geBlob);
18433
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee
18434
+ // item, so the hookData must carry a `cctp-forward` frame; otherwise the
18435
+ // wrapper reverts `ForwardFeeWithoutHook`.
18436
+ assertForwardHookData(hookData);
18437
+ // Source-chain burn: `mintRecipient` AND `destinationCaller` are both the
18438
+ // executor; fees are prepaid against the signed quote.
18439
+ return adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18440
+ fromChain: srcChain,
18441
+ toChain: dstChain,
18442
+ amount: amountMinorUnits,
18443
+ mintRecipient: executor,
18444
+ destinationCaller: executor,
18445
+ hookData,
18446
+ claim: {
18447
+ signedQuote,
18448
+ refundAddress
18449
+ },
18450
+ feeToken,
18451
+ feeTotalAmount
18452
+ }, resolvedContext);
18453
+ }
18454
+ /**
16680
18455
  * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
16681
18456
  *
16682
- * Builds the source-chain `depositForBurnWithHookAndFees` call for the
16683
- * GenericExecutor FORWARD path: fees are collected up front on the source chain
16684
- * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
16685
- * the GenericExecutor, and the GE `hookData` is passed through unchanged.
18457
+ * Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
18458
+ * collected up front on the source chain against a signed quote. The
18459
+ * destination may use the GenericExecutor shorthand, or an explicit mint
18460
+ * recipient and destination caller for direct forwarding.
16686
18461
  *
16687
- * This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
16688
- * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
18462
+ * This is the low-level on-chain primitive behind the Unified Balance Kit
18463
+ * `fastCrossChainDeposit` and the Bridge Kit source-fee
18464
+ * (`feePayment: 'source'`) flow. The `hookData` and signed-quote
16689
18465
  * `claim` are produced elsewhere and passed in here:
16690
18466
  * - `hookData`: `buildForwardingHookDataWithPayload(version,
16691
18467
  * buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
@@ -16702,32 +18478,48 @@ function assertCCTPV2Config(config) {
16702
18478
  * approval covers both; the redundant second approval is skipped.
16703
18479
  *
16704
18480
  * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
16705
- * @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
18481
+ * @param params - The burn amount, destination, hook data, signed quote, and fee.
16706
18482
  * @returns The prepared approvals, the prepared burn, and the resolved fee plan.
16707
18483
  * @throws {KitError} If the wallet context is invalid, `destinationChain` does not
16708
- * support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
16709
- * a bigint or a numeric string coercible to bigint, the hookData lacks a
16710
- * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
16711
- * context cannot be resolved.
18484
+ * support CCTP v2, the destination fields are missing, `amount` or
18485
+ * `feeTotalAmount` is not a bigint or a numeric string coercible to bigint,
18486
+ * the hook data lacks a `cctp-forward` frame (guaranteed
18487
+ * `ForwardFeeWithoutHook`), or the operation context cannot be resolved.
16712
18488
  *
16713
18489
  * @example
16714
18490
  * ```typescript
18491
+ * import {
18492
+ * CCTPV2BridgingProvider,
18493
+ * type BurnWithFeesParams,
18494
+ * } from '@circle-fin/provider-cctp-v2'
18495
+ *
18496
+ * declare const source: BurnWithFeesParams['source']
18497
+ * declare const destinationChain: BurnWithFeesParams['destinationChain']
18498
+ * declare const recipient: string
18499
+ * declare const hookData: string
18500
+ * declare const claim: BurnWithFeesParams['claim']
18501
+ *
18502
+ * const provider = new CCTPV2BridgingProvider()
16715
18503
  * const { approvals, burn } = await provider.burnWithFees({
16716
18504
  * source,
16717
- * destinationChain: Arc,
18505
+ * destinationChain,
16718
18506
  * amount: 1_000_000n,
16719
- * executor: genericExecutorAddress,
16720
- * hookData: geForwardHookData,
16721
- * claim: { signedQuote: '0x01...', refundAddress: userAddress },
16722
- * feeToken: '0x0000000000000000000000000000000000000000', // native
16723
- * feeTotalAmount: 3_500_000n,
18507
+ * mintRecipient: recipient,
18508
+ * destinationCaller: '0x0000000000000000000000000000000000000000',
18509
+ * hookData,
18510
+ * claim,
18511
+ * feeToken: source.chain.usdcAddress,
18512
+ * feeTotalAmount: 10_000n,
16724
18513
  * })
16725
18514
  * for (const approval of approvals) await approval.execute()
16726
18515
  * const txHash = await burn.execute()
16727
18516
  * ```
16728
18517
  */ async burnWithFees(params) {
16729
18518
  assertBurnWithFeesParams(params);
16730
- const { source, destinationChain, executor, hookData, claim, feeToken } = params;
18519
+ const { source, destinationChain, hookData, claim, feeToken } = params;
18520
+ const hasExecutor = 'executor' in params && params.executor !== undefined;
18521
+ const mintRecipient = hasExecutor ? params.executor : params.mintRecipient;
18522
+ const destinationCaller = hasExecutor ? params.executor : params.destinationCaller;
16731
18523
  const amount = BigInt(params.amount);
16732
18524
  const feeTotalAmount = BigInt(params.feeTotalAmount);
16733
18525
  // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
@@ -16758,168 +18550,1171 @@ function assertCCTPV2Config(config) {
16758
18550
  delegate: wrapperAddress,
16759
18551
  amount: approval.amount
16760
18552
  }, context)));
16761
- // Build the burn: mintRecipient AND destinationCaller are both the executor.
18553
+ // Build the burn with either the GenericExecutor shorthand or the explicit
18554
+ // direct-forwarding recipient and caller.
16762
18555
  const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
16763
18556
  fromChain: source.chain,
16764
18557
  toChain: destinationChain,
16765
18558
  amount,
16766
- mintRecipient: executor,
16767
- destinationCaller: executor,
18559
+ mintRecipient,
18560
+ destinationCaller,
16768
18561
  hookData,
16769
18562
  claim,
16770
18563
  feeToken,
16771
18564
  feeTotalAmount
16772
18565
  }, context);
16773
18566
  return {
16774
- approvals,
16775
- burn,
16776
- feePayment
18567
+ approvals,
18568
+ burn,
18569
+ feePayment
18570
+ };
18571
+ }
18572
+ /**
18573
+ * Waits for a transaction to be mined and confirmed on the blockchain.
18574
+ *
18575
+ * This method should block until the transaction is confirmed on the blockchain.
18576
+ *
18577
+ * @param adapter - The adapter to use for transaction waiting
18578
+ * @param txHash - The hash of the transaction to wait for
18579
+ * @param chain - The chain definition where the transaction was executed
18580
+ * @param config - Optional configuration for transaction waiting (confirmations, timeout)
18581
+ * @returns The hash of the confirmed transaction
18582
+ * @example
18583
+ * ```typescript
18584
+ * const provider = new CCTPV2BridgingProvider()
18585
+ * const txHash = await provider.waitForTransaction(
18586
+ * adapter,
18587
+ * '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
18588
+ * Ethereum,
18589
+ * )
18590
+ * console.log('Transaction confirmed:', txHash)
18591
+ * ```
18592
+ */ async waitForTransaction(adapter, txHash, chain, config) {
18593
+ return adapter.waitForTransaction(txHash, config, chain);
18594
+ }
18595
+ }
18596
+
18597
+ /**
18598
+ * The default providers that will be used in addition to the providers provided
18599
+ * to the BridgeKit constructor.
18600
+ *
18601
+ * @param config - Optional configuration forwarded to the default providers
18602
+ * @returns The default bridging providers
18603
+ */ const getDefaultProviders = (config = {})=>[
18604
+ new CCTPV2BridgingProvider(config.headers ? {
18605
+ headers: config.headers
18606
+ } : {})
18607
+ ];
18608
+
18609
+ /**
18610
+ * A helper function to get a function that transforms an amount into a human-readable string or a bigint string.
18611
+ * @param formatDirection - The direction to format the amount in.
18612
+ * @returns A function that transforms an amount into a human-readable string or a bigint string.
18613
+ */ const getAmountTransformer = (formatDirection)=>formatDirection === 'to-human-readable' ? (params)=>formatAmount(params) : (params)=>parseAmount(params).toString();
18614
+ /**
18615
+ * Format the bridge result into human-readable string values for the user or bigint string values for internal use.
18616
+ *
18617
+ * @typeParam T - The specific result type (must extend BridgeResult or EstimateResult). Preserves the exact type passed in.
18618
+ * @param result - The bridge result to format.
18619
+ * @param formatDirection - The direction to format the result in.
18620
+ * - If 'to-human-readable', the result will be converted to human-readable string values.
18621
+ * - If 'to-internal', the result will be converted to bigint string values (usually for internal use).
18622
+ * @returns The formatted bridge result.
18623
+ *
18624
+ * @example
18625
+ * ```typescript
18626
+ * const result = await kit.bridge({
18627
+ * amount: '1000000',
18628
+ * token: 'USDC',
18629
+ * from: { adapter: adapter, chain: 'Ethereum' },
18630
+ * to: { adapter: adapter, chain: 'Base' },
18631
+ * })
18632
+ *
18633
+ * // Format the bridge result into human-readable string values for the user
18634
+ * const formattedResultHumanReadable = formatBridgeResult(result, 'to-human-readable')
18635
+ * console.log(formattedResultHumanReadable)
18636
+ *
18637
+ * // Format the bridge result into bigint string values for internal use
18638
+ * const formattedResultInternal = formatBridgeResult(result, 'to-internal')
18639
+ * console.log(formattedResultInternal)
18640
+ * ```
18641
+ */ const formatBridgeResult = (result, formatDirection)=>{
18642
+ const transform = getAmountTransformer(formatDirection);
18643
+ return {
18644
+ ...result,
18645
+ amount: transform({
18646
+ value: result.amount,
18647
+ token: result.token
18648
+ }),
18649
+ ...'config' in result && result.config && Object.keys(result.config).length > 0 && {
18650
+ config: {
18651
+ ...result.config,
18652
+ ...result.config.maxFee && {
18653
+ maxFee: transform({
18654
+ value: result.config.maxFee,
18655
+ token: result.token
18656
+ })
18657
+ },
18658
+ ...result.config.customFee && {
18659
+ customFee: {
18660
+ ...result.config.customFee,
18661
+ ...result.config.customFee.value && {
18662
+ value: transform({
18663
+ value: result.config.customFee.value,
18664
+ token: result.token
18665
+ })
18666
+ }
18667
+ }
18668
+ }
18669
+ }
18670
+ }
18671
+ };
18672
+ };
18673
+
18674
+ /**
18675
+ * Register all bridge-kit event type strings with the shared registry so
18676
+ * callers of `withErrorTelemetry` / `emitResultStepErrorTelemetry` are
18677
+ * compile-time checked.
18678
+ *
18679
+ * @internal
18680
+ */ /**
18681
+ * Telemetry event type identifiers for bridge-kit operations.
18682
+ *
18683
+ * @internal
18684
+ */ const BRIDGE_EVENT_TYPES = {
18685
+ BRIDGE: 'bridge_bridge',
18686
+ RETRY: 'bridge_retry',
18687
+ ESTIMATE: 'bridge_estimate'
18688
+ };
18689
+ /**
18690
+ * Ordered mapping from provider step event names to telemetry event types.
18691
+ *
18692
+ * @remarks
18693
+ * The order matches the CCTP v2 bridge execution sequence. During
18694
+ * `bridge()`, completed step events are counted so the failing step
18695
+ * can be identified by its index.
18696
+ *
18697
+ * @internal
18698
+ */ const BRIDGE_STEP_EVENT_MAP = [
18699
+ [
18700
+ 'approve',
18701
+ 'bridge_approve'
18702
+ ],
18703
+ [
18704
+ 'burn',
18705
+ 'bridge_burn'
18706
+ ],
18707
+ [
18708
+ 'fetchAttestation',
18709
+ 'bridge_fetch_attestation'
18710
+ ],
18711
+ [
18712
+ 'mint',
18713
+ 'bridge_mint'
18714
+ ]
18715
+ ];
18716
+
18717
+ /**
18718
+ * Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
18719
+ *
18720
+ * @internal
18721
+ */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
18722
+ /**
18723
+ * Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
18724
+ *
18725
+ * @internal
18726
+ */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
18727
+ /**
18728
+ * Native fee-token sentinel (the zero address).
18729
+ *
18730
+ * When `feeToken` is the zero address the quote prices fees in the source
18731
+ * chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
18732
+ * address instead to denominate fees in USDC.
18733
+ *
18734
+ * @internal
18735
+ */ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
18736
+ /**
18737
+ * API path prefix for the CCTP v2 USDC burn quote endpoint.
18738
+ *
18739
+ * The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
18740
+ * `usdc` is a fixed literal, not a token parameter.
18741
+ *
18742
+ * @internal
18743
+ */ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
18744
+ /**
18745
+ * API path prefix for the CCTP v2 USDC quote validate endpoint.
18746
+ *
18747
+ * The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
18748
+ * `POST { abiSignature, args }` body and returns `claimable`, `failedChecks`,
18749
+ * and the decoded `expiry`, `feeToken`, and `feeTotalAmount`.
18750
+ *
18751
+ * @internal
18752
+ */ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
18753
+ /**
18754
+ * Default polling configuration for Quote API calls.
18755
+ *
18756
+ * A signed quote is short-lived (typically ~2 minutes, varying per chain) and
18757
+ * a feature-flag-disabled source chain returns a
18758
+ * permanent `503 SERVICE_NOT_ENABLED`, so retrying buys little and risks
18759
+ * outliving the quote. The client therefore makes a single attempt
18760
+ * (`maxRetries: 1`) with a 15s timeout, mirroring the reference
18761
+ * implementation; callers refresh by requesting a new quote rather than
18762
+ * relying on transport retries.
18763
+ *
18764
+ * No `headers` are set here: `pollApiWithValidation` always injects
18765
+ * `Content-Type: application/json` and adds `User-Agent` in Node. Browser
18766
+ * requests omit a user-agent header to avoid a CORS preflight, so duplicating
18767
+ * either header here would be dead configuration.
18768
+ *
18769
+ * @internal
18770
+ */ const FEE_QUOTE_DEFAULT_CONFIG = {
18771
+ timeout: 15_000,
18772
+ maxRetries: 1,
18773
+ retryDelay: 200
18774
+ };
18775
+
18776
+ /**
18777
+ * Decimal string in token minor units, constrained to be strictly positive.
18778
+ *
18779
+ * @internal
18780
+ */ const positiveAmountSchema = zod.z.string().regex(/^\d+$/, 'must be a non-negative integer string')// Re-check the digit shape here: zod still runs this refinement when the
18781
+ // regex check above fails ("dirty"), so guard BigInt() against throwing on a
18782
+ // non-numeric value before comparing.
18783
+ .refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
18784
+ /**
18785
+ * A 20-byte EVM address in `0x` hex.
18786
+ *
18787
+ * The MVP prepaid-`FORWARD` `burn/usdc` path targets EVM contracts
18788
+ * (`TokenMessengerWithFees` / `GenericExecutor`), so `feeToken` and
18789
+ * `destinationCaller` are constrained to EVM addresses by design. This is an
18790
+ * intentional scope limit, not a permanent one: it can be widened to other
18791
+ * address formats as the fee service expands to more chains.
18792
+ */ const evmAddressSchema = zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'must be a 20-byte 0x address');
18793
+ /** Even-length `0x` hex (the empty `0x` is allowed). */ const hexSchema = zod.z.string().regex(/^0x([a-fA-F0-9]{2})*$/, 'must be even-length 0x hex');
18794
+ /** Non-empty, even-length `0x` hex. */ const nonEmptyHexSchema = zod.z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex');
18795
+ /** A 32-byte `0x` hex hash. */ const bytes32Schema = zod.z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'must be a 32-byte 0x hash');
18796
+ /** Decimal string in minor units, allowing zero. */ const numericStringSchema = zod.z.string().regex(/^\d+$/, 'must be a non-negative integer string');
18797
+ /** An `https:` URL, used for the optional base-URL override. */ const httpsUrlSchema = zod.z.string().refine((value)=>{
18798
+ try {
18799
+ return new URL(value).protocol === 'https:';
18800
+ } catch {
18801
+ return false;
18802
+ }
18803
+ }, 'must be an https URL');
18804
+ const forwardParamsSchema = zod.z.object({
18805
+ hookData: hexSchema.optional(),
18806
+ destinationCaller: evmAddressSchema.optional()
18807
+ }).strict();
18808
+ const forwardRequestSchema = zod.z.object({
18809
+ type: zod.z.literal('FORWARD'),
18810
+ params: forwardParamsSchema.optional()
18811
+ }).strict();
18812
+ const preFinalityRequestSchema = zod.z.object({
18813
+ type: zod.z.literal('PRE_FINALITY')
18814
+ }).strict();
18815
+ /**
18816
+ * A single quote request item (`FORWARD` or `PRE_FINALITY`).
18817
+ *
18818
+ * @internal
18819
+ */ const feeQuoteRequestSchema = zod.z.discriminatedUnion('type', [
18820
+ forwardRequestSchema,
18821
+ preFinalityRequestSchema
18822
+ ]);
18823
+ /**
18824
+ * A non-empty list of quote request items with unique types.
18825
+ *
18826
+ * @internal
18827
+ */ const feeQuoteRequestsSchema = zod.z.array(feeQuoteRequestSchema).min(1, 'at least one request item is required').refine((items)=>new Set(items.map((item)=>item.type)).size === items.length, 'request item types must be unique');
18828
+ /**
18829
+ * A structured `Partial<ApiPollingConfig>` polling override.
18830
+ *
18831
+ * Validates the field types callers actually set, so a plain-JS caller passing
18832
+ * `{ timeout: 'soon' }` is rejected at the boundary rather than failing opaquely
18833
+ * inside the transport. Unknown keys pass through so a future `ApiPollingConfig`
18834
+ * field is forwarded rather than silently dropped.
18835
+ */ const apiPollingConfigSchema = zod.z.object({
18836
+ timeout: zod.z.number().int().positive().optional(),
18837
+ maxRetries: zod.z.number().int().nonnegative().optional(),
18838
+ retryDelay: zod.z.number().int().nonnegative().optional(),
18839
+ backoff: zod.z.enum([
18840
+ 'fixed',
18841
+ 'exponential'
18842
+ ]).optional(),
18843
+ maxRetryDelayMs: zod.z.number().int().positive().optional(),
18844
+ headers: zod.z.record(zod.z.string()).optional()
18845
+ }).passthrough();
18846
+ /**
18847
+ * The validatable input for {@link fetchFeeQuote}.
18848
+ *
18849
+ * This is the single source of truth for input validation, including the CCTP
18850
+ * domains and the `isTestnet` environment flag. Validating `isTestnet` at
18851
+ * runtime matters because a plain-JS caller who omits it would otherwise leave
18852
+ * it `undefined`, which is falsy and silently selects the production base URL.
18853
+ * (`buildFeeQuoteUrl` independently re-validates the domains for standalone
18854
+ * callers.)
18855
+ *
18856
+ * @internal
18857
+ */ const fetchFeeQuoteInputSchema = zod.z.object({
18858
+ sourceDomain: zod.z.number().int().nonnegative(),
18859
+ destinationDomain: zod.z.number().int().nonnegative(),
18860
+ amount: positiveAmountSchema,
18861
+ feeToken: evmAddressSchema.optional(),
18862
+ requests: feeQuoteRequestsSchema,
18863
+ isTestnet: zod.z.boolean(),
18864
+ baseUrl: httpsUrlSchema.optional(),
18865
+ config: apiPollingConfigSchema.optional()
18866
+ }).strict();
18867
+ const feeQuoteItemSchema = zod.z.object({
18868
+ type: zod.z.string().min(1),
18869
+ amount: numericStringSchema,
18870
+ args: zod.z.array(zod.z.string()),
18871
+ argsHash: bytes32Schema
18872
+ }).passthrough();
18873
+ const exchangeRatesSchema = zod.z.object({
18874
+ feeTokenUsd: zod.z.string(),
18875
+ destinationTokenUsd: zod.z.string()
18876
+ }).passthrough();
18877
+ const metadataSchema = zod.z.object({
18878
+ destinationGasPrice: zod.z.string().optional(),
18879
+ exchangeRates: exchangeRatesSchema.optional()
18880
+ }).passthrough();
18881
+ /** The `expiry` object the Quote API nests the quote TTL under. */ const expirySchema = zod.z.discriminatedUnion('mode', [
18882
+ zod.z.object({
18883
+ mode: zod.z.literal('TIMESTAMP'),
18884
+ expiresAt: zod.z.number().int().nonnegative()
18885
+ }).passthrough(),
18886
+ zod.z.object({
18887
+ mode: zod.z.literal('BLOCK_NUMBER'),
18888
+ expiresAtBlock: zod.z.number().int().nonnegative(),
18889
+ blockEstimatedAt: zod.z.number().int().nonnegative().optional()
18890
+ }).passthrough()
18891
+ ]);
18892
+ /**
18893
+ * Schema for a signed fee quote returned by the Quote API.
18894
+ *
18895
+ * @internal
18896
+ */ const signedFeeQuoteSchema = zod.z.object({
18897
+ // The runtime YAML spec maps signedQuote to a looser `hex` (which allows
18898
+ // an empty `0x`); we keep the stricter non-empty form. Do not relax
18899
+ // without a reason.
18900
+ signedQuote: nonEmptyHexSchema,
18901
+ issuedAt: zod.z.number().int().nonnegative(),
18902
+ // The API returns a mode-specific timestamp or source-block deadline.
18903
+ expiry: expirySchema,
18904
+ feeTotalAmount: numericStringSchema,
18905
+ feeToken: evmAddressSchema,
18906
+ nonce: numericStringSchema,
18907
+ items: zod.z.array(feeQuoteItemSchema),
18908
+ metadata: metadataSchema.optional()
18909
+ }).passthrough();
18910
+ /**
18911
+ * Validate that an unknown value is a signed fee quote.
18912
+ *
18913
+ * @param value - The unknown value to validate.
18914
+ * @returns `true` when the value matches the signed-quote response shape.
18915
+ *
18916
+ * @example
18917
+ * ```typescript
18918
+ * import { isSignedFeeQuote } from '@circle-fin/provider-fee-v1'
18919
+ *
18920
+ * declare const payload: unknown
18921
+ * if (isSignedFeeQuote(payload)) {
18922
+ * console.log(payload.feeTotalAmount)
18923
+ * }
18924
+ * ```
18925
+ *
18926
+ * @internal
18927
+ */ function isSignedFeeQuote(value) {
18928
+ return signedFeeQuoteSchema.safeParse(value).success;
18929
+ }
18930
+ /**
18931
+ * Validates input to {@link validateQuote}.
18932
+ *
18933
+ * @internal
18934
+ */ const validateQuoteInputSchema = zod.z.object({
18935
+ sourceDomain: zod.z.number().int().nonnegative(),
18936
+ abiSignature: zod.z.string().min(1),
18937
+ args: zod.z.array(zod.z.union([
18938
+ zod.z.string(),
18939
+ zod.z.array(zod.z.string())
18940
+ ])),
18941
+ isTestnet: zod.z.boolean(),
18942
+ baseUrl: httpsUrlSchema.optional(),
18943
+ config: apiPollingConfigSchema.optional()
18944
+ }).strict();
18945
+ const quoteExpiryStatusSchema = zod.z.discriminatedUnion('mode', [
18946
+ zod.z.object({
18947
+ mode: zod.z.literal('TIMESTAMP'),
18948
+ expired: zod.z.boolean(),
18949
+ secondsRemaining: zod.z.number().int().nonnegative(),
18950
+ expiresAt: zod.z.number().int().nonnegative()
18951
+ }).passthrough(),
18952
+ zod.z.object({
18953
+ mode: zod.z.literal('BLOCK_NUMBER'),
18954
+ expired: zod.z.boolean(),
18955
+ secondsRemaining: zod.z.number().int().nonnegative(),
18956
+ expiresAtBlock: zod.z.number().int().nonnegative(),
18957
+ blockEstimatedAt: zod.z.number().int().nonnegative().optional()
18958
+ }).passthrough()
18959
+ ]);
18960
+ const validateQuoteItemSchema = zod.z.object({
18961
+ type: zod.z.string().min(1),
18962
+ argsMatch: zod.z.boolean(),
18963
+ amount: numericStringSchema.optional(),
18964
+ args: zod.z.array(zod.z.string()).optional(),
18965
+ argsHash: bytes32Schema.optional(),
18966
+ computedArgsHash: bytes32Schema.optional()
18967
+ }).passthrough();
18968
+ /**
18969
+ * Schema for a validate-quote result returned by the Iris `/validate/usdc/:sourceDomain` endpoint.
18970
+ *
18971
+ * The endpoint takes the source domain as a URL path parameter and does not
18972
+ * return it in the response body, so `sourceDomain` is intentionally not part
18973
+ * of this schema.
18974
+ *
18975
+ * @internal
18976
+ */ const validateQuoteResultSchema = zod.z.object({
18977
+ signedQuote: nonEmptyHexSchema,
18978
+ expiry: quoteExpiryStatusSchema,
18979
+ feeTotalAmount: numericStringSchema,
18980
+ feeToken: evmAddressSchema,
18981
+ nonce: numericStringSchema,
18982
+ claimable: zod.z.boolean(),
18983
+ // Preserve newly introduced server-side reasons as opaque strings rather
18984
+ // than rejecting the entire safety response before the SDK is updated.
18985
+ failedChecks: zod.z.array(zod.z.string().min(1)),
18986
+ items: zod.z.array(validateQuoteItemSchema)
18987
+ }).passthrough();
18988
+ /**
18989
+ * Validate that an unknown value is a validate-quote result.
18990
+ *
18991
+ * @param value - The unknown value to validate.
18992
+ * @returns `true` when the value matches the validate-quote response shape.
18993
+ *
18994
+ * @example
18995
+ * ```typescript
18996
+ * import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
18997
+ *
18998
+ * declare const payload: unknown
18999
+ * if (isValidateQuoteResult(payload)) {
19000
+ * console.log(payload.claimable, payload.failedChecks)
19001
+ * }
19002
+ * ```
19003
+ *
19004
+ * @internal
19005
+ */ function isValidateQuoteResult(value) {
19006
+ return validateQuoteResultSchema.safeParse(value).success;
19007
+ }
19008
+
19009
+ /**
19010
+ * Validate that a CCTP domain id is a non-negative integer.
19011
+ *
19012
+ * @param value - The domain id to validate.
19013
+ * @param label - The parameter name, used in the error message.
19014
+ * @returns Nothing.
19015
+ * @throws {@link KitError} When the value is not a non-negative integer.
19016
+ * @internal
19017
+ */ function assertDomain(value, label) {
19018
+ if (!Number.isInteger(value) || value < 0) {
19019
+ throw new KitError({
19020
+ ...InputError.VALIDATION_FAILED,
19021
+ recoverability: 'FATAL',
19022
+ message: `Quote API getFeeQuote failed: ${label} must be a ` + `non-negative integer, received ${String(value)}`,
19023
+ cause: {
19024
+ trace: {
19025
+ [label]: value
19026
+ }
19027
+ }
19028
+ });
19029
+ }
19030
+ }
19031
+ /**
19032
+ * Build the Quote API URL for a CCTP v2 USDC burn quote.
19033
+ *
19034
+ * Resolves the environment base URL (or an explicit `baseUrl` override) and
19035
+ * appends the burn/usdc path with the source and destination CCTP domains.
19036
+ * `usdc` is a fixed path literal, not a token parameter.
19037
+ *
19038
+ * @param params - The domains and environment selector.
19039
+ * @returns The fully-qualified Quote API URL.
19040
+ * @throws {@link KitError} When either domain is not a non-negative integer.
19041
+ *
19042
+ * @example
19043
+ * ```typescript
19044
+ * import { buildFeeQuoteUrl } from '@circle-fin/provider-fee-v1'
19045
+ *
19046
+ * const url = buildFeeQuoteUrl({
19047
+ * sourceDomain: 3,
19048
+ * destinationDomain: 26,
19049
+ * isTestnet: false,
19050
+ * })
19051
+ * // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
19052
+ * ```
19053
+ *
19054
+ * @internal
19055
+ */ function buildFeeQuoteUrl(params) {
19056
+ const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
19057
+ assertDomain(sourceDomain, 'sourceDomain');
19058
+ assertDomain(destinationDomain, 'destinationDomain');
19059
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
19060
+ return new URL(`${QUOTE_BURN_USDC_PATH}/${String(sourceDomain)}/${String(destinationDomain)}`, resolvedBaseUrl).toString();
19061
+ }
19062
+
19063
+ /**
19064
+ * Assert that a quote's per-item fee amounts sum to its `feeTotalAmount`.
19065
+ *
19066
+ * A defensive integrity check on the Quote API response, enforced internally
19067
+ * by `fetchFeeQuote`. It does not compare against the destination-side
19068
+ * `feeExecuted`, which is expected to be zero for prepaid forward-only burns.
19069
+ *
19070
+ * @param quote - The signed fee quote to check.
19071
+ * @returns Nothing.
19072
+ * @throws {@link KitError} When the item amounts do not sum to `feeTotalAmount`.
19073
+ * @internal
19074
+ */ function assertFeeItemsSumToTotal(quote) {
19075
+ const itemsTotal = quote.items.reduce((sum, item)=>sum + BigInt(item.amount), 0n);
19076
+ const declaredTotal = BigInt(quote.feeTotalAmount);
19077
+ if (itemsTotal !== declaredTotal) {
19078
+ throw new KitError({
19079
+ ...InputError.VALIDATION_FAILED,
19080
+ recoverability: 'FATAL',
19081
+ message: `Quote API getFeeQuote failed: fee items sum ` + `(${itemsTotal.toString()}) does not equal feeTotalAmount ` + `(${declaredTotal.toString()})`,
19082
+ cause: {
19083
+ trace: {
19084
+ itemsTotal: itemsTotal.toString(),
19085
+ feeTotalAmount: quote.feeTotalAmount
19086
+ }
19087
+ }
19088
+ });
19089
+ }
19090
+ }
19091
+
19092
+ /**
19093
+ * Determine whether a Quote API error represents a disabled source chain.
19094
+ *
19095
+ * @param error - The error thrown by the HTTP layer.
19096
+ * @returns `true` when the error contains the `SERVICE_NOT_ENABLED` marker.
19097
+ * @internal
19098
+ */ function isServiceNotEnabled(error) {
19099
+ const body = typeof error === 'object' && error !== null && 'responseBody' in error ? error.responseBody : undefined;
19100
+ if (typeof body === 'object' && body !== null) {
19101
+ const fields = body;
19102
+ const candidates = [
19103
+ fields['errorCode'],
19104
+ fields['code'],
19105
+ fields['message'],
19106
+ fields['externalMessage'],
19107
+ fields['error']
19108
+ ];
19109
+ if (candidates.some((value)=>typeof value === 'string' && value.toUpperCase().includes('SERVICE_NOT_ENABLED'))) {
19110
+ return true;
19111
+ }
19112
+ }
19113
+ return getErrorMessage(error).toUpperCase().includes('SERVICE_NOT_ENABLED');
19114
+ }
19115
+
19116
+ const SERVICE$1 = 'Quote API';
19117
+ const OPERATION$1 = 'getFeeQuote';
19118
+ /**
19119
+ * Serialize request items for the wire body.
19120
+ *
19121
+ * `PRE_FINALITY` is emitted with no `params` key, and a `FORWARD` item only
19122
+ * carries the binding fields that are present.
19123
+ *
19124
+ * @param requests - The request items to serialize.
19125
+ * @returns The serialized request items.
19126
+ * @internal
19127
+ */ function serializeRequests(requests) {
19128
+ return requests.map((request)=>{
19129
+ if (request.type === 'PRE_FINALITY') {
19130
+ return {
19131
+ type: 'PRE_FINALITY'
19132
+ };
19133
+ }
19134
+ const params = request.params;
19135
+ if (params === undefined) {
19136
+ return {
19137
+ type: 'FORWARD'
19138
+ };
19139
+ }
19140
+ const forwardParams = {};
19141
+ if (params.hookData !== undefined) {
19142
+ forwardParams.hookData = params.hookData;
19143
+ }
19144
+ if (params.destinationCaller !== undefined) {
19145
+ forwardParams.destinationCaller = params.destinationCaller;
19146
+ }
19147
+ return {
19148
+ type: 'FORWARD',
19149
+ params: forwardParams
19150
+ };
19151
+ });
19152
+ }
19153
+ /**
19154
+ * Fetch a signed fee quote from Circle's Quote API for a CCTP v2 USDC burn.
19155
+ *
19156
+ * Validates inputs, POSTs to
19157
+ * `/v2/quote/burn/usdc/{sourceDomain}/{destinationDomain}` with a single
19158
+ * attempt (the signed quote is short-lived), and returns the typed quote. A
19159
+ * disabled source chain (`503 SERVICE_NOT_ENABLED`) surfaces as a fatal,
19160
+ * non-retryable error; other failures are mapped to a {@link KitError} via the
19161
+ * shared API error parser.
19162
+ *
19163
+ * @param params - The domains, amount, request items, and environment.
19164
+ * @returns The signed fee quote.
19165
+ * @throws {@link KitError} On invalid input, a disabled source chain, an HTTP
19166
+ * error, or an invalid response shape.
19167
+ *
19168
+ * @example
19169
+ * ```typescript
19170
+ * import { fetchFeeQuote } from '@circle-fin/provider-fee-v1'
19171
+ *
19172
+ * const quote = await fetchFeeQuote({
19173
+ * sourceDomain: 3,
19174
+ * destinationDomain: 26,
19175
+ * amount: '1000000',
19176
+ * requests: [{ type: 'FORWARD' }, { type: 'PRE_FINALITY' }],
19177
+ * isTestnet: false,
19178
+ * })
19179
+ * console.log(quote.feeTotalAmount, quote.expiry)
19180
+ * ```
19181
+ *
19182
+ * @internal
19183
+ */ async function fetchFeeQuote(params) {
19184
+ const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
19185
+ const parsed = fetchFeeQuoteInputSchema.safeParse({
19186
+ sourceDomain,
19187
+ destinationDomain,
19188
+ amount,
19189
+ feeToken,
19190
+ requests,
19191
+ isTestnet,
19192
+ baseUrl,
19193
+ config
19194
+ });
19195
+ if (!parsed.success) {
19196
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
19197
+ throw new KitError({
19198
+ ...InputError.VALIDATION_FAILED,
19199
+ recoverability: 'FATAL',
19200
+ message: `${SERVICE$1} ${OPERATION$1} failed: ${detail}`,
19201
+ cause: {
19202
+ trace: parsed.error.issues
19203
+ }
19204
+ });
19205
+ }
19206
+ const url = baseUrl === undefined ? buildFeeQuoteUrl({
19207
+ sourceDomain,
19208
+ destinationDomain,
19209
+ isTestnet
19210
+ }) : buildFeeQuoteUrl({
19211
+ sourceDomain,
19212
+ destinationDomain,
19213
+ isTestnet,
19214
+ baseUrl
19215
+ });
19216
+ const body = {
19217
+ amount,
19218
+ feeToken: feeToken ?? NATIVE_FEE_TOKEN,
19219
+ requests: serializeRequests(requests)
19220
+ };
19221
+ const pollingConfig = {
19222
+ ...FEE_QUOTE_DEFAULT_CONFIG,
19223
+ ...config
19224
+ };
19225
+ let quote;
19226
+ try {
19227
+ quote = await pollApiPost(url, body, isSignedFeeQuote, pollingConfig);
19228
+ } catch (error) {
19229
+ // Only one service-specific code (SERVICE_NOT_ENABLED) needs bespoke
19230
+ // mapping, so it is detected inline rather than via a dedicated
19231
+ // `parseFeeQuoteApiError` parser; everything else flows through the shared
19232
+ // `parseApiError`. Promote to a parser if more coded errors appear.
19233
+ if (isServiceNotEnabled(error)) {
19234
+ throw new KitError({
19235
+ ...InputError.UNSUPPORTED_ROUTE,
19236
+ recoverability: 'FATAL',
19237
+ message: `${SERVICE$1} ${OPERATION$1} failed: source chain not enabled for fee ` + `quotes (SERVICE_NOT_ENABLED)`,
19238
+ cause: {
19239
+ trace: error
19240
+ }
19241
+ });
19242
+ }
19243
+ throw parseApiError(error, {
19244
+ service: SERVICE$1,
19245
+ operation: OPERATION$1
19246
+ });
19247
+ }
19248
+ // Defense-in-depth: a self-consistent quote's per-item fees sum to the
19249
+ // declared total. Enforced here so callers cannot forget the check.
19250
+ assertFeeItemsSumToTotal(quote);
19251
+ return quote;
19252
+ }
19253
+
19254
+ const SERVICE = 'Quote API';
19255
+ const OPERATION = 'validateQuote';
19256
+ /**
19257
+ * Validate a signed fee quote against a full on-chain call via Circle's Iris
19258
+ * `/v2/quote/validate/usdc/:sourceDomain` endpoint.
19259
+ *
19260
+ * POSTs the ABI function signature and the encoded call arguments to Iris,
19261
+ * which verifies the signature, checks expiry, confirms the argsHash committed
19262
+ * in the quote matches the submitted args, and returns `claimable` plus the
19263
+ * decoded `expiry`, `feeToken`, and `feeTotalAmount`.
19264
+ *
19265
+ * @param params - The source domain, ABI signature, call arguments, and environment.
19266
+ * @returns The claimability, binding checks, and authoritative expiry status.
19267
+ * @throws {@link KitError} When input, transport, or response validation fails.
19268
+ *
19269
+ * @example
19270
+ * ```typescript
19271
+ * import { validateQuote } from '@circle-fin/provider-fee-v1'
19272
+ *
19273
+ * const result = await validateQuote({
19274
+ * sourceDomain: 3,
19275
+ * // The exact function + arguments, in ABI order, that will be burned on-chain.
19276
+ * abiSignature:
19277
+ * 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))',
19278
+ * args: [
19279
+ * '1000000',
19280
+ * '26',
19281
+ * '0x0000000000000000000000001111111111111111111111111111111111111111',
19282
+ * '0x2222222222222222222222222222222222222222',
19283
+ * '0x0000000000000000000000000000000000000000000000000000000000000000',
19284
+ * '0x636374702d666f72776172640000000000000000000000000000000000000000',
19285
+ * ['0x01abcd', '0x3333333333333333333333333333333333333333'],
19286
+ * ],
19287
+ * isTestnet: false,
19288
+ * })
19289
+ * console.log(result.claimable, result.expiry.secondsRemaining)
19290
+ * if (!result.claimable) {
19291
+ * console.error('Quote rejected:', result.failedChecks)
19292
+ * }
19293
+ * ```
19294
+ *
19295
+ * @internal
19296
+ */ async function validateQuote(params) {
19297
+ const parsed = validateQuoteInputSchema.safeParse(params);
19298
+ if (!parsed.success) {
19299
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
19300
+ throw new KitError({
19301
+ ...InputError.VALIDATION_FAILED,
19302
+ recoverability: 'FATAL',
19303
+ message: `${SERVICE} ${OPERATION} failed: ${detail}`,
19304
+ cause: {
19305
+ trace: parsed.error.issues
19306
+ }
19307
+ });
19308
+ }
19309
+ const { sourceDomain, abiSignature, args, isTestnet, baseUrl } = parsed.data;
19310
+ // `config` was validated by the schema above; spread the caller's original,
19311
+ // precisely typed `Partial<ApiPollingConfig>` so the merged polling config
19312
+ // stays assignable under `exactOptionalPropertyTypes`.
19313
+ const pollingConfig = {
19314
+ ...FEE_QUOTE_DEFAULT_CONFIG,
19315
+ ...params.config
19316
+ };
19317
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
19318
+ const url = new URL(`${QUOTE_VALIDATE_USDC_PATH}/${String(sourceDomain)}`, resolvedBaseUrl).toString();
19319
+ try {
19320
+ return await pollApiPost(url, {
19321
+ abiSignature,
19322
+ args
19323
+ }, isValidateQuoteResult, pollingConfig);
19324
+ } catch (error) {
19325
+ if (isServiceNotEnabled(error)) {
19326
+ throw new KitError({
19327
+ ...InputError.UNSUPPORTED_ROUTE,
19328
+ recoverability: 'FATAL',
19329
+ message: `${SERVICE} ${OPERATION} failed: source chain not enabled for ` + `quote validation (SERVICE_NOT_ENABLED)`,
19330
+ cause: {
19331
+ trace: error
19332
+ }
19333
+ });
19334
+ }
19335
+ throw parseApiError(error, {
19336
+ service: SERVICE,
19337
+ operation: OPERATION
19338
+ });
19339
+ }
19340
+ }
19341
+
19342
+ /** Refresh timestamp quotes this many seconds before submission. */ const QUOTE_EXPIRY_SAFETY_SECONDS = 30;
19343
+ /** 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))';
19344
+ /** Unrestricted CCTP destination caller used by the forwarding relayer. */ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
19345
+ function isQuoteNearEstimatedExpiry(quote) {
19346
+ const currentSeconds = Math.floor(Date.now() / 1_000);
19347
+ const expiresAt = quote.expiry.mode === 'TIMESTAMP' ? quote.expiry.expiresAt : quote.expiry.blockEstimatedAt;
19348
+ // A BLOCK_NUMBER quote may omit the advisory `blockEstimatedAt` estimate; when
19349
+ // it is absent, skip this wall-clock pre-check and defer to the authoritative
19350
+ // source-chain-tip validation performed downstream.
19351
+ if (expiresAt === undefined) {
19352
+ return false;
19353
+ }
19354
+ return expiresAt <= currentSeconds + QUOTE_EXPIRY_SAFETY_SECONDS;
19355
+ }
19356
+ function assertSourceFeeRoute(params) {
19357
+ const { source, destination, config } = params;
19358
+ // Keep these checks explicit so a successful assertion guarantees the CCTP
19359
+ // v2 narrowing; hasSourceFeeSupport returns a plain boolean.
19360
+ if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
19361
+ throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
19362
+ }
19363
+ if (!hasSourceFeeSupport(source.chain)) {
19364
+ throw createValidationFailedError$1('config.feePayment', 'source', `Source-paid ("receive-exact") fees are not supported from ${source.chain.name}. ` + 'Use the default destination-paid fees for this source chain');
19365
+ }
19366
+ const useForwarder = destination.useForwarder;
19367
+ if (useForwarder !== true) {
19368
+ throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
19369
+ }
19370
+ if (config.customFee !== undefined) {
19371
+ 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.');
19372
+ }
19373
+ }
19374
+ function buildQuoteBinding(params) {
19375
+ assertSourceFeeRoute(params);
19376
+ const mintRecipient = params.destination.recipientAddress ?? params.destination.address;
19377
+ const hookData = buildForwardingHookData();
19378
+ const requests = [
19379
+ {
19380
+ type: 'FORWARD',
19381
+ params: {
19382
+ hookData,
19383
+ destinationCaller: ZERO_ADDRESS
19384
+ }
19385
+ }
19386
+ ];
19387
+ if ((params.config.transferSpeed ?? TransferSpeed.FAST) === TransferSpeed.FAST) {
19388
+ requests.push({
19389
+ type: 'PRE_FINALITY'
19390
+ });
19391
+ }
19392
+ return {
19393
+ sourceDomain: params.source.chain.cctp.domain,
19394
+ destinationDomain: params.destination.chain.cctp.domain,
19395
+ isTestnet: params.source.chain.isTestnet,
19396
+ amount: params.amount,
19397
+ mintRecipient,
19398
+ hookData,
19399
+ destinationCaller: ZERO_ADDRESS,
19400
+ feeToken: params.source.chain.usdcAddress,
19401
+ requests
19402
+ };
19403
+ }
19404
+ async function fetchBoundQuote(binding) {
19405
+ try {
19406
+ const quote = await fetchFeeQuote({
19407
+ sourceDomain: binding.sourceDomain,
19408
+ destinationDomain: binding.destinationDomain,
19409
+ amount: binding.amount,
19410
+ feeToken: binding.feeToken,
19411
+ requests: binding.requests,
19412
+ isTestnet: binding.isTestnet
19413
+ });
19414
+ if (quote.feeToken.toLowerCase() !== binding.feeToken.toLowerCase()) {
19415
+ throw createValidationFailedError$1('feeToken', quote.feeToken, 'Fee Service must return source-chain USDC for source-fee bridging');
19416
+ }
19417
+ return quote;
19418
+ } catch (error) {
19419
+ if (isRateLimitError(error)) {
19420
+ throw new KitError({
19421
+ ...RateLimitError.RATE_LIMIT_EXCEEDED,
19422
+ recoverability: 'RETRYABLE',
19423
+ message: 'Fee Service rate limit exceeded. Retry with caller-managed exponential backoff; Bridge Kit does not retry signed quote requests automatically.',
19424
+ cause: {
19425
+ trace: error
19426
+ }
19427
+ });
19428
+ }
19429
+ throw error;
19430
+ }
19431
+ }
19432
+ async function fetchSubmissionQuote(binding) {
19433
+ let quote = await fetchBoundQuote(binding);
19434
+ if (isQuoteNearEstimatedExpiry(quote)) {
19435
+ quote = await fetchBoundQuote(binding);
19436
+ }
19437
+ if (isQuoteNearEstimatedExpiry(quote)) {
19438
+ throw createValidationFailedError$1('quote', undefined, 'Fee Service returned a quote too close to expiry for safe submission');
19439
+ }
19440
+ return quote;
19441
+ }
19442
+ async function validateBoundQuote(binding, signedQuote, refundAddress) {
19443
+ return validateQuote({
19444
+ sourceDomain: binding.sourceDomain,
19445
+ abiSignature: BURN_WITH_FEES_ABI_SIGNATURE,
19446
+ args: [
19447
+ binding.amount,
19448
+ String(binding.destinationDomain),
19449
+ padAddressToBytes32(binding.mintRecipient),
19450
+ binding.feeToken,
19451
+ padAddressToBytes32(binding.destinationCaller),
19452
+ binding.hookData,
19453
+ [
19454
+ signedQuote,
19455
+ refundAddress
19456
+ ]
19457
+ ],
19458
+ isTestnet: binding.isTestnet
19459
+ });
19460
+ }
19461
+ function isValidationSafe(binding, signedQuote, validation, expectedQuote) {
19462
+ 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);
19463
+ }
19464
+ function unsafeQuoteError(validation) {
19465
+ const detail = validation.failedChecks.length > 0 ? ` (${validation.failedChecks.join(', ')})` : '';
19466
+ return createValidationFailedError$1('quote', undefined, `The fee quote is not safe for submission${detail}; ` + 'call estimate again to obtain a valid quote');
19467
+ }
19468
+ function toExecutionFeeQuote(quote) {
19469
+ return {
19470
+ signedQuote: quote.signedQuote,
19471
+ feeToken: quote.feeToken,
19472
+ feeTotalAmount: quote.feeTotalAmount
19473
+ };
19474
+ }
19475
+ function toFeeItems(quote) {
19476
+ return quote.items.map((item)=>({
19477
+ type: item.type,
19478
+ amount: formatUnits(item.amount, 6),
19479
+ args: item.args,
19480
+ argsHash: item.argsHash
19481
+ }));
19482
+ }
19483
+ /**
19484
+ * Estimate a source-fee bridge using a source-denominated signed fee quote.
19485
+ *
19486
+ * @internal
19487
+ */ async function estimateSourceFeeBridge(params) {
19488
+ const binding = buildQuoteBinding(params);
19489
+ const quote = await fetchSubmissionQuote(binding);
19490
+ const feeTotal = formatUnits(quote.feeTotalAmount, 6);
19491
+ return {
19492
+ token: 'USDC',
19493
+ amount: formatUnits(params.amount, 6),
19494
+ source: {
19495
+ address: params.source.address,
19496
+ chain: params.source.chain.chain
19497
+ },
19498
+ destination: {
19499
+ address: params.destination.address,
19500
+ chain: params.destination.chain.chain,
19501
+ ...params.destination.recipientAddress !== undefined && {
19502
+ recipientAddress: params.destination.recipientAddress
19503
+ }
19504
+ },
19505
+ gasFees: [],
19506
+ fees: quote.items.map((item)=>({
19507
+ type: item.type === 'FORWARD' ? 'forwarder' : 'provider',
19508
+ token: 'USDC',
19509
+ amount: formatUnits(item.amount, 6)
19510
+ })),
19511
+ amountReceived: formatUnits(params.amount, 6),
19512
+ feeTotal,
19513
+ feeItems: toFeeItems(quote),
19514
+ totalDebit: formatUnits((BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString(), 6),
19515
+ quoteExpiry: quote.expiry,
19516
+ quote: quote.signedQuote
19517
+ };
19518
+ }
19519
+ async function readAllowance(params, delegate) {
19520
+ const operationContext = {
19521
+ chain: params.source.chain,
19522
+ address: params.source.address
19523
+ };
19524
+ const prepared = await params.source.adapter.prepareAction('usdc.allowance', {
19525
+ walletAddress: params.source.address,
19526
+ delegate
19527
+ }, operationContext);
19528
+ return BigInt(String(await prepared.execute()));
19529
+ }
19530
+ async function executeAndConfirm(request, params, provider) {
19531
+ const txHash = await request.execute();
19532
+ const data = await provider.waitForTransaction(params.source.adapter, txHash, params.source.chain);
19533
+ return {
19534
+ txHash,
19535
+ data
19536
+ };
19537
+ }
19538
+ async function prepareAndPreflight(params, binding, quote, provider) {
19539
+ assertSourceFeeRoute(params);
19540
+ const totalDebit = (BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString();
19541
+ const operationContext = {
19542
+ chain: params.source.chain,
19543
+ address: params.source.address
19544
+ };
19545
+ await validateBalanceForTransaction({
19546
+ adapter: params.source.adapter,
19547
+ amount: totalDebit,
19548
+ token: 'USDC',
19549
+ tokenAddress: params.source.chain.usdcAddress,
19550
+ operationContext
19551
+ });
19552
+ const prepared = await provider.burnWithFees({
19553
+ source: params.source,
19554
+ destinationChain: params.destination.chain,
19555
+ amount: params.amount,
19556
+ mintRecipient: binding.mintRecipient,
19557
+ destinationCaller: binding.destinationCaller,
19558
+ hookData: binding.hookData,
19559
+ claim: {
19560
+ signedQuote: quote.signedQuote,
19561
+ refundAddress: params.source.address
19562
+ },
19563
+ feeToken: quote.feeToken,
19564
+ feeTotalAmount: quote.feeTotalAmount
19565
+ });
19566
+ const wrapper = resolveCCTPV2ContractAddress(params.source.chain, 'tokenMessengerWithFees');
19567
+ const allowance = await readAllowance(params, wrapper);
19568
+ if (allowance >= BigInt(totalDebit)) {
19569
+ return {
19570
+ prepared,
19571
+ approveStep: {
19572
+ name: 'approve',
19573
+ state: 'noop'
19574
+ }
16777
19575
  };
16778
19576
  }
16779
- /**
16780
- * Waits for a transaction to be mined and confirmed on the blockchain.
16781
- *
16782
- * This method should block until the transaction is confirmed on the blockchain.
16783
- *
16784
- * @param adapter - The adapter to use for transaction waiting
16785
- * @param txHash - The hash of the transaction to wait for
16786
- * @param chain - The chain definition where the transaction was executed
16787
- * @param config - Optional configuration for transaction waiting (confirmations, timeout)
16788
- * @returns The hash of the confirmed transaction
16789
- * @example
16790
- * ```typescript
16791
- * const provider = new CCTPV2BridgingProvider()
16792
- * const txHash = await provider.waitForTransaction(
16793
- * adapter,
16794
- * '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
16795
- * Ethereum,
16796
- * )
16797
- * console.log('Transaction confirmed:', txHash)
16798
- * ```
16799
- */ async waitForTransaction(adapter, txHash, chain, config) {
16800
- return adapter.waitForTransaction(txHash, config, chain);
19577
+ let lastApproval;
19578
+ for (const approval of prepared.approvals){
19579
+ lastApproval = await executeAndConfirm(approval, params, provider);
16801
19580
  }
16802
- }
16803
-
16804
- /**
16805
- * The default providers that will be used in addition to the providers provided
16806
- * to the BridgeKit constructor.
16807
- *
16808
- * @param config - Optional configuration forwarded to the default providers
16809
- * @returns The default bridging providers
16810
- */ const getDefaultProviders = (config = {})=>[
16811
- new CCTPV2BridgingProvider(config.headers ? {
16812
- headers: config.headers
16813
- } : {})
16814
- ];
16815
-
16816
- /**
16817
- * A helper function to get a function that transforms an amount into a human-readable string or a bigint string.
16818
- * @param formatDirection - The direction to format the amount in.
16819
- * @returns A function that transforms an amount into a human-readable string or a bigint string.
16820
- */ const getAmountTransformer = (formatDirection)=>formatDirection === 'to-human-readable' ? (params)=>formatAmount(params) : (params)=>parseAmount(params).toString();
16821
- /**
16822
- * Format the bridge result into human-readable string values for the user or bigint string values for internal use.
16823
- *
16824
- * @typeParam T - The specific result type (must extend BridgeResult or EstimateResult). Preserves the exact type passed in.
16825
- * @param result - The bridge result to format.
16826
- * @param formatDirection - The direction to format the result in.
16827
- * - If 'to-human-readable', the result will be converted to human-readable string values.
16828
- * - If 'to-internal', the result will be converted to bigint string values (usually for internal use).
16829
- * @returns The formatted bridge result.
16830
- *
16831
- * @example
16832
- * ```typescript
16833
- * const result = await kit.bridge({
16834
- * amount: '1000000',
16835
- * token: 'USDC',
16836
- * from: { adapter: adapter, chain: 'Ethereum' },
16837
- * to: { adapter: adapter, chain: 'Base' },
16838
- * })
16839
- *
16840
- * // Format the bridge result into human-readable string values for the user
16841
- * const formattedResultHumanReadable = formatBridgeResult(result, 'to-human-readable')
16842
- * console.log(formattedResultHumanReadable)
16843
- *
16844
- * // Format the bridge result into bigint string values for internal use
16845
- * const formattedResultInternal = formatBridgeResult(result, 'to-internal')
16846
- * console.log(formattedResultInternal)
16847
- * ```
16848
- */ const formatBridgeResult = (result, formatDirection)=>{
16849
- const transform = getAmountTransformer(formatDirection);
16850
19581
  return {
16851
- ...result,
16852
- amount: transform({
16853
- value: result.amount,
16854
- token: result.token
16855
- }),
16856
- ...'config' in result && result.config && Object.keys(result.config).length > 0 && {
16857
- config: {
16858
- ...result.config,
16859
- ...result.config.maxFee && {
16860
- maxFee: transform({
16861
- value: result.config.maxFee,
16862
- token: result.token
16863
- })
16864
- },
16865
- ...result.config.customFee && {
16866
- customFee: {
16867
- ...result.config.customFee,
16868
- ...result.config.customFee.value && {
16869
- value: transform({
16870
- value: result.config.customFee.value,
16871
- token: result.token
16872
- })
16873
- }
16874
- }
16875
- }
19582
+ prepared,
19583
+ approveStep: {
19584
+ name: 'approve',
19585
+ state: 'success',
19586
+ data: lastApproval?.data,
19587
+ ...lastApproval?.txHash !== undefined && {
19588
+ txHash: lastApproval.txHash,
19589
+ explorerUrl: buildExplorerUrl(params.source.chain, lastApproval.txHash)
16876
19590
  }
16877
19591
  }
16878
19592
  };
16879
- };
16880
-
16881
- /**
16882
- * Register all bridge-kit event type strings with the shared registry so
16883
- * callers of `withErrorTelemetry` / `emitResultStepErrorTelemetry` are
16884
- * compile-time checked.
16885
- *
16886
- * @internal
16887
- */ /**
16888
- * Telemetry event type identifiers for bridge-kit operations.
16889
- *
16890
- * @internal
16891
- */ const BRIDGE_EVENT_TYPES = {
16892
- BRIDGE: 'bridge_bridge',
16893
- RETRY: 'bridge_retry',
16894
- ESTIMATE: 'bridge_estimate'
16895
- };
19593
+ }
19594
+ async function resolveExecutionQuote(binding, suppliedQuote, refundAddress) {
19595
+ if (suppliedQuote === undefined) {
19596
+ const fetchedQuote = await fetchSubmissionQuote(binding);
19597
+ return {
19598
+ quote: toExecutionFeeQuote(fetchedQuote),
19599
+ fetchedQuote
19600
+ };
19601
+ }
19602
+ const validation = await validateBoundQuote(binding, suppliedQuote, refundAddress);
19603
+ if (!isValidationSafe(binding, suppliedQuote, validation)) {
19604
+ throw unsafeQuoteError(validation);
19605
+ }
19606
+ return {
19607
+ quote: toExecutionFeeQuote(validation),
19608
+ fetchedQuote: undefined
19609
+ };
19610
+ }
16896
19611
  /**
16897
- * Ordered mapping from provider step event names to telemetry event types.
16898
- *
16899
- * @remarks
16900
- * The order matches the CCTP v2 bridge execution sequence. During
16901
- * `bridge()`, completed step events are counted so the failing step
16902
- * can be identified by its index.
19612
+ * Execute a source-fee bridge while preserving receive-exact semantics.
16903
19613
  *
16904
19614
  * @internal
16905
- */ const BRIDGE_STEP_EVENT_MAP = [
16906
- [
16907
- 'approve',
16908
- 'bridge_approve'
16909
- ],
16910
- [
16911
- 'burn',
16912
- 'bridge_burn'
16913
- ],
16914
- [
16915
- 'fetchAttestation',
16916
- 'bridge_fetch_attestation'
16917
- ],
16918
- [
16919
- 'mint',
16920
- 'bridge_mint'
16921
- ]
16922
- ];
19615
+ */ async function executeSourceFeeBridge(rawParams, params, provider) {
19616
+ // Narrows `params` to the CCTP v2 route type for the rest of this function.
19617
+ // buildQuoteBinding asserts too, but that narrows its own scope, not this one.
19618
+ assertSourceFeeRoute(params);
19619
+ const binding = buildQuoteBinding(params);
19620
+ const suppliedQuote = rawParams.quote;
19621
+ let { quote, fetchedQuote } = await resolveExecutionQuote(binding, suppliedQuote, params.source.address);
19622
+ let { prepared, approveStep } = await prepareAndPreflight(params, binding, quote, provider);
19623
+ // Validate against the current source-chain tip after approval confirmation.
19624
+ // BLOCK_NUMBER expiries cannot be checked safely with wall-clock time alone.
19625
+ let validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
19626
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
19627
+ if (suppliedQuote === undefined) {
19628
+ fetchedQuote = await fetchSubmissionQuote(binding);
19629
+ quote = toExecutionFeeQuote(fetchedQuote);
19630
+ const refreshedPreparation = await prepareAndPreflight(params, binding, quote, provider);
19631
+ prepared = refreshedPreparation.prepared;
19632
+ if (refreshedPreparation.approveStep.state !== 'noop') {
19633
+ approveStep = refreshedPreparation.approveStep;
19634
+ }
19635
+ validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
19636
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
19637
+ throw unsafeQuoteError(validation);
19638
+ }
19639
+ } else {
19640
+ throw unsafeQuoteError(validation);
19641
+ }
19642
+ }
19643
+ // Surface the same step events as the standard bridge() path so
19644
+ // kit.on('approve'|'burn'|'mint', ...) handlers fire for source-fee bridges.
19645
+ if (approveStep.state !== 'noop') {
19646
+ provider.emitBridgeStep('approve', approveStep);
19647
+ }
19648
+ const burn = await executeAndConfirm(prepared.burn, params, provider);
19649
+ const burnStep = {
19650
+ name: 'burn',
19651
+ state: 'success',
19652
+ txHash: burn.txHash,
19653
+ data: burn.data,
19654
+ explorerUrl: buildExplorerUrl(params.source.chain, burn.txHash)
19655
+ };
19656
+ provider.emitBridgeStep('burn', burnStep);
19657
+ const resultBase = {
19658
+ amount: params.amount,
19659
+ token: 'USDC',
19660
+ config: params.config,
19661
+ provider: provider.name,
19662
+ source: {
19663
+ address: params.source.address,
19664
+ chain: params.source.chain
19665
+ },
19666
+ destination: {
19667
+ address: params.destination.address,
19668
+ chain: params.destination.chain,
19669
+ ...params.destination.recipientAddress !== undefined && {
19670
+ recipientAddress: params.destination.recipientAddress
19671
+ },
19672
+ useForwarder: true
19673
+ }
19674
+ };
19675
+ const attestation = await provider.fetchRelayerMint(params.source, burn.txHash);
19676
+ const forwardTxHash = attestation.forwardTxHash;
19677
+ // The burn already moved funds. If the relayer confirms without a
19678
+ // destination hash, surface an error-state result that preserves the burn
19679
+ // step (so `retry()` can resume the mint) instead of throwing and discarding
19680
+ // the completed burn.
19681
+ if (typeof forwardTxHash !== 'string' || forwardTxHash.trim() === '') {
19682
+ const mintStep = {
19683
+ name: 'mint',
19684
+ state: 'error',
19685
+ forwarded: true,
19686
+ errorCategory: 'failed_offchain',
19687
+ errorMessage: 'Relayer confirmation did not include a destination transaction hash'
19688
+ };
19689
+ provider.emitBridgeStep('mint', mintStep);
19690
+ return {
19691
+ ...resultBase,
19692
+ state: 'error',
19693
+ steps: [
19694
+ approveStep,
19695
+ burnStep,
19696
+ mintStep
19697
+ ]
19698
+ };
19699
+ }
19700
+ const mintStep = {
19701
+ name: 'mint',
19702
+ state: 'success',
19703
+ forwarded: true,
19704
+ txHash: forwardTxHash,
19705
+ explorerUrl: buildExplorerUrl(params.destination.chain, forwardTxHash)
19706
+ };
19707
+ provider.emitBridgeStep('mint', mintStep);
19708
+ return {
19709
+ ...resultBase,
19710
+ state: 'success',
19711
+ steps: [
19712
+ approveStep,
19713
+ burnStep,
19714
+ mintStep
19715
+ ]
19716
+ };
19717
+ }
16923
19718
 
16924
19719
  /** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg$3.name);
16925
19720
  /**
@@ -17148,11 +19943,18 @@ function assertCCTPV2Config(config) {
17148
19943
  this.validateNetworkCompatibility(resolvedParams);
17149
19944
  // Merge the custom fee config into the resolved params
17150
19945
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
17151
- // Find a provider that supports this route
17152
- const provider = this.findProviderForRoute(finalResolvedParams);
17153
- // Execute the transfer using the provider
17154
- // Format the bridge result into human-readable string values for the user
17155
- const result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
19946
+ let result;
19947
+ // Execute the explicit source-fee path without changing legacy
19948
+ // useForwarder behavior for callers that did not opt in.
19949
+ if (params.config?.feePayment === 'source') {
19950
+ const sourceFeeProvider = this.findSourceFeeProvider(finalResolvedParams);
19951
+ result = formatBridgeResult(await executeSourceFeeBridge(params, finalResolvedParams, sourceFeeProvider), 'to-human-readable');
19952
+ } else {
19953
+ // Find a provider that supports this route
19954
+ const provider = this.findProviderForRoute(finalResolvedParams);
19955
+ // Execute the transfer using the provider and format the result.
19956
+ result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
19957
+ }
17156
19958
  // Emit error telemetry when the provider returns an error state
17157
19959
  // (provider records step failures in the result instead of throwing).
17158
19960
  if (result.state === 'error') {
@@ -17275,42 +20077,7 @@ function assertCCTPV2Config(config) {
17275
20077
  tokenIn: result.token
17276
20078
  });
17277
20079
  }
17278
- /**
17279
- * Estimate the cost and fees for a cross-chain USDC bridge operation.
17280
- *
17281
- * This method calculates the expected gas fees and protocol costs for bridging
17282
- * without actually executing the transaction. It performs the same validation
17283
- * as the bridge method but stops before execution.
17284
- *
17285
- * @param params - The bridge parameters for cost estimation, including optional invocation metadata
17286
- * @returns Promise resolving to detailed cost breakdown including gas estimates
17287
- * @throws {KitError} When the parameters are invalid.
17288
- * @throws {UnsupportedRouteError} When the route is not supported.
17289
- *
17290
- * @example
17291
- * ```typescript
17292
- * // Basic usage
17293
- * const estimate = await kit.estimate({
17294
- * from: { adapter: adapter, chain: 'Ethereum' },
17295
- * to: { adapter: adapter, chain: 'Base' },
17296
- * amount: '10.50',
17297
- * token: 'USDC'
17298
- * })
17299
- * console.log('Estimated cost:', estimate.totalCost)
17300
- *
17301
- * // With custom invocation metadata
17302
- * const estimate = await kit.estimate({
17303
- * from: { adapter: adapter, chain: 'Ethereum' },
17304
- * to: { adapter: adapter, chain: 'Base' },
17305
- * amount: '10.50',
17306
- * token: 'USDC',
17307
- * invocationMeta: {
17308
- * traceId: 'custom-trace-id',
17309
- * callers: [{ type: 'app', name: 'MyDApp', version: '1.0.0' }],
17310
- * },
17311
- * })
17312
- * ```
17313
- */ async estimate(params) {
20080
+ async estimate(params) {
17314
20081
  return withErrorTelemetry(async ()=>{
17315
20082
  // First validate the parameters
17316
20083
  assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
@@ -17320,6 +20087,10 @@ function assertCCTPV2Config(config) {
17320
20087
  this.validateNetworkCompatibility(resolvedParams);
17321
20088
  // Merge the custom fee config into the resolved params
17322
20089
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
20090
+ if (params.config?.feePayment === 'source') {
20091
+ this.findSourceFeeProvider(finalResolvedParams);
20092
+ return estimateSourceFeeBridge(finalResolvedParams);
20093
+ }
17323
20094
  // Find a provider that supports this route
17324
20095
  const provider = this.findProviderForRoute(finalResolvedParams);
17325
20096
  // Estimate the transfer using the provider and format amounts to human-readable strings
@@ -17368,6 +20139,9 @@ function assertCCTPV2Config(config) {
17368
20139
  * // Get only chains that support forwarding
17369
20140
  * const forwarderChains = kit.getSupportedChains({ forwarderSupported: true })
17370
20141
  *
20142
+ * // Get only chains that can pay fees on the source chain (receive-exact)
20143
+ * const sourceFeeChains = kit.getSupportedChains({ sourceFeeSupported: true })
20144
+ *
17371
20145
  * console.log('Supported chains:')
17372
20146
  * allChains.forEach(chain => {
17373
20147
  * console.log(`- ${chain.name} (${chain.type})`)
@@ -17416,6 +20190,10 @@ function assertCCTPV2Config(config) {
17416
20190
  return options.forwarderSupported ? fs.source || fs.destination : !fs.source && !fs.destination;
17417
20191
  });
17418
20192
  }
20193
+ // Apply source-paid ("receive-exact") fee support filter if provided
20194
+ if (options?.sourceFeeSupported !== undefined) {
20195
+ chains = chains.filter((chain)=>hasSourceFeeSupport(chain) === options.sourceFeeSupported);
20196
+ }
17419
20197
  return chains;
17420
20198
  }
17421
20199
  /**
@@ -17450,6 +20228,20 @@ function assertCCTPV2Config(config) {
17450
20228
  return provider;
17451
20229
  }
17452
20230
  /**
20231
+ * Find the default CCTP v2 provider for a source-fee forwarding route.
20232
+ *
20233
+ * @param params - The resolved provider parameters.
20234
+ * @returns The CCTP v2 provider that supports the forwarded route.
20235
+ * @throws {UnsupportedRouteError} When no source-fee provider supports the route.
20236
+ * @internal
20237
+ */ findSourceFeeProvider(params) {
20238
+ const provider = this.providers.find((candidate)=>candidate instanceof CCTPV2BridgingProvider && candidate.supportsRoute(params.source.chain, params.destination.chain, params.token, true));
20239
+ if (!(provider instanceof CCTPV2BridgingProvider)) {
20240
+ throw createUnsupportedRouteError(params.source.chain.name, params.destination.chain.name);
20241
+ }
20242
+ return provider;
20243
+ }
20244
+ /**
17453
20245
  * Merge custom fee configuration into provider parameters.
17454
20246
  *
17455
20247
  * Prioritizes any custom fee configuration already present on the
@@ -17658,7 +20450,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
17658
20450
  };
17659
20451
 
17660
20452
  var name$1 = "@circle-fin/swap-kit";
17661
- var version$1 = "1.5.2";
20453
+ var version$1 = "1.6.1";
17662
20454
  var pkg$1 = {
17663
20455
  name: name$1,
17664
20456
  version: version$1};
@@ -17672,7 +20464,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17672
20464
  * Catches obviously malformed addresses at parse time; chain-specific validation
17673
20465
  * is performed in buildServiceParams.
17674
20466
  */ const destinationAddressSchema = zod.z.union([
17675
- evmAddressSchema,
20467
+ evmAddressSchema$1,
17676
20468
  solanaAddressSchema
17677
20469
  ]);
17678
20470
  /**
@@ -17718,9 +20510,16 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17718
20510
  message: 'stopLimit must be greater than 0'
17719
20511
  }).optional(),
17720
20512
  customFee: serviceSwapCustomFeeSchema.optional(),
20513
+ apiKey: zod.z.string({
20514
+ invalid_type_error: 'apiKey must be a string'
20515
+ })// Tolerate '' so the `process.env.CIRCLE_API_KEY ?? ''` idiom falls back to
20516
+ // kitKey via resolveApiKey instead of being rejected here.
20517
+ .optional(),
17721
20518
  kitKey: zod.z.string({
17722
20519
  invalid_type_error: 'kitKey must be a string'
17723
- }).min(1, 'kitKey must be a non-empty string').optional(),
20520
+ })// Tolerate '' so an unset kit-key env var yields the permissionless path,
20521
+ // matching buildServiceParams (which already omits an empty credential).
20522
+ .optional(),
17724
20523
  provider: zod.z.string({
17725
20524
  invalid_type_error: 'provider must be a string'
17726
20525
  }).min(1, 'provider must be a non-empty string').optional(),
@@ -17893,6 +20692,41 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17893
20692
  *
17894
20693
  * @internal
17895
20694
  */ const MAX_RATE_ADDRESSES_PER_REQUEST = 100;
20695
+ /**
20696
+ * Environment prefixes carried by Circle platform API keys.
20697
+ *
20698
+ * A Circle API key is `<ENV>_API_KEY:<keyId>:<keySecret>`, where `<ENV>` is one
20699
+ * of these prefixes.
20700
+ *
20701
+ * @internal
20702
+ */ const API_KEY_ENV_PREFIXES = [
20703
+ 'TEST',
20704
+ 'LIVE',
20705
+ 'SAND',
20706
+ 'SANDBOX',
20707
+ 'SMOK',
20708
+ 'PROD',
20709
+ 'STAG',
20710
+ 'DEV'
20711
+ ];
20712
+ /**
20713
+ * Accepted credential formats for Stablecoin Service authentication.
20714
+ *
20715
+ * Matches a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
20716
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`). API keys are the
20717
+ * recommended credential; kit keys remain accepted as the legacy path.
20718
+ *
20719
+ * @remarks
20720
+ * This is a local pre-flight check, not the authority — the Stablecoin Service
20721
+ * validates the credential and answers 401 when it rejects one. The prefix list
20722
+ * is therefore deliberately permissive: a valid key carrying a prefix this SDK
20723
+ * has not been taught about should reach the service and be judged there rather
20724
+ * than be refused locally, since refusing locally is indistinguishable from an
20725
+ * outage to the caller. Kept as the single source of truth so the pattern is
20726
+ * not restated per call site.
20727
+ *
20728
+ * @internal
20729
+ */ const API_KEY_PATTERN = new RegExp(`^(?:KIT_KEY|(?:${API_KEY_ENV_PREFIXES.join('|')})_API_KEY)` + ':[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$');
17896
20730
 
17897
20731
  /**
17898
20732
  * Zod schema for validating stop limits.
@@ -17961,13 +20795,14 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17961
20795
  /**
17962
20796
  * Zod schema for validating API keys.
17963
20797
  *
17964
- * Validates that the API key is a valid API key format.
20798
+ * Accepts a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
20799
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`).
17965
20800
  *
17966
20801
  * @example
17967
20802
  * ```typescript
17968
20803
  * import { apiKeySchema } from '@core/service-client'
17969
20804
  *
17970
- * const result = apiKeySchema.safeParse('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
20805
+ * const result = apiKeySchema.safeParse('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
17971
20806
  * if (!result.success) {
17972
20807
  * console.error('Invalid API key format:', result.error.issues)
17973
20808
  * }
@@ -17975,7 +20810,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17975
20810
  */ const apiKeySchema = zod.z.string({
17976
20811
  required_error: 'API key is required',
17977
20812
  invalid_type_error: 'Invalid API key format'
17978
- }).regex(/^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/, 'Invalid API key format');
20813
+ }).regex(API_KEY_PATTERN, 'Invalid API key format');
17979
20814
  /**
17980
20815
  * Zod schema for platform fees configuration.
17981
20816
  *
@@ -18414,6 +21249,40 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
18414
21249
  transaction: createSwapTransactionSchema
18415
21250
  });
18416
21251
 
21252
+ /**
21253
+ * Resolve the credential to authenticate a Stablecoin Service request with.
21254
+ *
21255
+ * `apiKey` is the supported field; `kitKey` is the deprecated alias kept for
21256
+ * existing integrations. When both are supplied `apiKey` wins, so a caller
21257
+ * migrating field-by-field cannot be silently pinned to a stale credential.
21258
+ *
21259
+ * An empty-string value is treated as absent on either field. Without this, the
21260
+ * common `process.env.CIRCLE_API_KEY ?? ''` idiom (which yields `''` when the
21261
+ * variable is unset) would either shadow a working `kitKey` or, on a bare
21262
+ * `kitKey: ''`, reach downstream validation as an invalid credential instead of
21263
+ * falling through to the permissionless path.
21264
+ *
21265
+ * @param source - Object carrying either credential field, or neither.
21266
+ * @returns The credential to use, or `undefined` for the permissionless
21267
+ * (keyless) path.
21268
+ *
21269
+ * @example
21270
+ * ```typescript
21271
+ * import { resolveApiKey } from '@core/service-client'
21272
+ *
21273
+ * resolveApiKey({ apiKey: 'TEST_API_KEY:id:secret' }) // 'TEST_API_KEY:id:secret'
21274
+ * resolveApiKey({ kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
21275
+ * resolveApiKey({ apiKey: '', kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
21276
+ * resolveApiKey({ kitKey: '' }) // undefined
21277
+ * resolveApiKey({}) // undefined
21278
+ * ```
21279
+ */ const resolveApiKey = (source)=>{
21280
+ // Treat an empty-string value as absent on either field so the
21281
+ // `env ?? ''` idiom falls through to the next credential (or permissionless).
21282
+ const normalize = (value)=>value !== undefined && value !== '' ? value : undefined;
21283
+ return normalize(source.apiKey) ?? normalize(source.kitKey);
21284
+ };
21285
+
18417
21286
  /**
18418
21287
  * Zod schema for validating EVM adapter capabilities.
18419
21288
  *
@@ -18843,7 +21712,7 @@ const abiParameterSchema = zod.z.object({
18843
21712
  */ zod.z.object({
18844
21713
  type: zod.z.literal('evm'),
18845
21714
  abi: abiSchema,
18846
- address: evmAddressSchema,
21715
+ address: evmAddressSchema$1,
18847
21716
  functionName: zod.z.string({
18848
21717
  required_error: 'Function name is required',
18849
21718
  invalid_type_error: 'Function name must be a string'
@@ -18880,7 +21749,7 @@ const abiParameterSchema = zod.z.object({
18880
21749
  * }
18881
21750
  * ```
18882
21751
  */ zod.z.object({
18883
- address: evmAddressSchema,
21752
+ address: evmAddressSchema$1,
18884
21753
  value: zod.z.bigint({
18885
21754
  required_error: 'Value is required for native transfers',
18886
21755
  invalid_type_error: 'Value must be a bigint'
@@ -18966,7 +21835,7 @@ zod.z.object({
18966
21835
  signature: evmSignatureSchema,
18967
21836
  tokenInputs: zod.z.array(zod.z.object({
18968
21837
  permitType: zod.z.nativeEnum(PermitType),
18969
- token: evmAddressSchema,
21838
+ token: evmAddressSchema$1,
18970
21839
  amount: zod.z.bigint().refine((value)=>value >= 0n, {
18971
21840
  message: 'amount must be a non-negative bigint'
18972
21841
  }),
@@ -19386,7 +22255,7 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
19386
22255
  /**
19387
22256
  * Fee recipient address (required).
19388
22257
  * Must be a valid EVM address or Solana address.
19389
- */ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
22258
+ */ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
19390
22259
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
19391
22260
  })
19392
22261
  }).strict();
@@ -19398,7 +22267,8 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
19398
22267
  * - slippageBps: Optional positive number for slippage tolerance
19399
22268
  * - stopLimit: Optional decimal string for minimum output
19400
22269
  * - customFee: Optional fee configuration
19401
- * - kitKey: Optional string identifier
22270
+ * - apiKey: Optional credential string
22271
+ * - kitKey: Optional credential string (deprecated alias for apiKey)
19402
22272
  */ const swapConfigSchema = zod.z.object({
19403
22273
  allowanceStrategy: allowanceStrategySchema.optional(),
19404
22274
  slippageBps: zod.z.number().int().min(0).optional(),
@@ -19408,11 +22278,12 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
19408
22278
  attributeName: 'stopLimit'
19409
22279
  })(zod.z.string())).optional(),
19410
22280
  customFee: swapCustomFeeSchema.optional(),
22281
+ apiKey: zod.z.string().optional(),
19411
22282
  kitKey: zod.z.string().optional()
19412
22283
  });
19413
22284
  const swapDestinationSchema = zod.z.object({
19414
22285
  chain: optionalSwapChainIdentifierField,
19415
- recipientAddress: zod.z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
22286
+ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
19416
22287
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
19417
22288
  }).optional()
19418
22289
  }).strict();
@@ -19601,7 +22472,7 @@ new Set(Object.values(Blockchain));
19601
22472
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
19602
22473
 
19603
22474
  var name = "@circle-fin/earn-kit";
19604
- var version = "1.5.1";
22475
+ var version = "1.6.1";
19605
22476
  var pkg = {
19606
22477
  name: name,
19607
22478
  version: version};
@@ -19673,7 +22544,7 @@ function isNonNegativeBigIntLike(value) {
19673
22544
  }
19674
22545
  }
19675
22546
  const hexSignatureSchema = evmSignatureSchema;
19676
- const hexAddressSchema = evmAddressSchema;
22547
+ const hexAddressSchema = evmAddressSchema$1;
19677
22548
  // '0x' prefix + 32 bytes * 2 hex chars.
19678
22549
  const BYTES32_HEX_LENGTH = 66;
19679
22550
  const bridgeFeeTokenSchema = hexAddressSchema;
@@ -20578,7 +23449,7 @@ createTokenRegistry();
20578
23449
  * fast instead of round-tripping to the service.
20579
23450
  *
20580
23451
  * @internal
20581
- */ const recipientEvmAddressSchema = evmAddressSchema.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
23452
+ */ const recipientEvmAddressSchema = evmAddressSchema$1.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
20582
23453
  /**
20583
23454
  * Schema for the adapter context within earn operations.
20584
23455
  *
@@ -20610,19 +23481,39 @@ const sourceAdapterContextSchema = zod.z.object({
20610
23481
  /**
20611
23482
  * Schema for the EarnConfig options.
20612
23483
  *
20613
- * Validate the optional Kit Key field using the standard `apiKeySchema`
20614
- * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
20615
- * operates in permissionless mode. `baseUrl` overrides the Earn Service
20616
- * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
20617
- * batched execution. Both are forwarded to the provider, so this `.strict()`
20618
- * schema must accept them or a valid config object is rejected.
23484
+ * Validate the *resolved* credential using the standard `apiKeySchema` format
23485
+ * (`<ENV>_API_KEY:<keyId>:<keySecret>`, or a legacy
23486
+ * `KIT_KEY:<keyId>:<keySecret>`). `apiKey` takes precedence over the deprecated
23487
+ * `kitKey`, so a malformed `kitKey` that is being ignored must not fail a config
23488
+ * that supplies a valid `apiKey` (and vice versa) only the credential that
23489
+ * would actually be sent is format-checked. When neither is supplied the SDK
23490
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service endpoint
23491
+ * (e.g. staging); `batchTransactions: false` opts out of atomic batched
23492
+ * execution. All are forwarded to the provider, so this `.strict()` schema must
23493
+ * accept them or a valid config object is rejected.
20619
23494
  *
20620
23495
  * @internal
20621
23496
  */ const earnConfigSchema = zod.z.object({
20622
- kitKey: apiKeySchema.optional(),
23497
+ apiKey: zod.z.string().optional(),
23498
+ kitKey: zod.z.string().optional(),
20623
23499
  baseUrl: zod.z.string().optional(),
20624
23500
  batchTransactions: zod.z.boolean().optional()
20625
- }).strict();
23501
+ }).strict().superRefine((config, ctx)=>{
23502
+ const credential = resolveApiKey(config);
23503
+ if (credential === undefined) {
23504
+ return;
23505
+ }
23506
+ const result = apiKeySchema.safeParse(credential);
23507
+ if (!result.success) {
23508
+ ctx.addIssue({
23509
+ code: zod.z.ZodIssueCode.custom,
23510
+ path: [
23511
+ credential === config.apiKey ? 'apiKey' : 'kitKey'
23512
+ ],
23513
+ message: result.error.issues[0]?.message ?? 'Invalid API key format'
23514
+ });
23515
+ }
23516
+ });
20626
23517
  /**
20627
23518
  * Canonical decimal form: a leading digit with no leading zeros (a single
20628
23519
  * '0' is only allowed immediately before the decimal point). Rejects the
@@ -20694,7 +23585,7 @@ const sourceAdapterContextSchema = zod.z.object({
20694
23585
  * currently supports EVM vault addresses on Arc Testnet.
20695
23586
  *
20696
23587
  * @internal
20697
- */ const vaultAddressSchema = evmAddressSchema.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
23588
+ */ const vaultAddressSchema = evmAddressSchema$1.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
20698
23589
  /**
20699
23590
  * Validation schema for VaultQuery.
20700
23591
  *
@@ -20969,7 +23860,7 @@ const sameChainGetDepositQuoteParamsSchema = zod.z.object({
20969
23860
  const crossChainGetDepositQuoteParamsSchema = zod.z.object({
20970
23861
  from: sourceAdapterContextSchema,
20971
23862
  chain: earnBridgeDestinationChainIdentifierSchema,
20972
- address: evmAddressSchema,
23863
+ address: evmAddressSchema$1,
20973
23864
  vaultAddress: vaultAddressSchema,
20974
23865
  amount: amountSchema,
20975
23866
  transferSpeed: zod.z.enum([