@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.
package/bridge.mjs CHANGED
@@ -30,9 +30,9 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
30
30
  import { z } from 'zod';
31
31
  import pino from 'pino';
32
32
  import { formatUnits as formatUnits$1, parseUnits as parseUnits$1 } from '@ethersproject/units';
33
- import { hexlify, hexZeroPad } from '@ethersproject/bytes';
34
- import '@ethersproject/abi';
35
- import { getAddress } from '@ethersproject/address';
33
+ import { hexlify, hexZeroPad, isHexString, concat } from '@ethersproject/bytes';
34
+ import { Interface, defaultAbiCoder } from '@ethersproject/abi';
35
+ import { getAddress, isAddress } from '@ethersproject/address';
36
36
  import bs58 from 'bs58';
37
37
  import { PublicKey } from '@solana/web3.js';
38
38
  import 'bn.js';
@@ -785,6 +785,32 @@ class KitError extends Error {
785
785
  type: 'ONCHAIN'
786
786
  }
787
787
  };
788
+ /**
789
+ * Standardized error definitions for LIQUIDITY type errors.
790
+ *
791
+ * LIQUIDITY errors indicate that an upstream provider or AMM cannot fulfill
792
+ * the requested swap size due to insufficient liquidity at the moment.
793
+ * These are typically transient — retrying later or reducing the amount
794
+ * may succeed once liquidity replenishes.
795
+ *
796
+ * @example
797
+ * ```typescript
798
+ * import { LiquidityError } from '@core/errors'
799
+ *
800
+ * const error = new KitError({
801
+ * ...LiquidityError.INSUFFICIENT_LIQUIDITY,
802
+ * recoverability: 'RETRYABLE',
803
+ * message: 'Insufficient liquidity for the requested swap',
804
+ * cause: { trace: { token: '0xA0b86991...' } }
805
+ * })
806
+ * ```
807
+ */ const LiquidityError = {
808
+ /** Upstream provider has a route but cannot fulfill the requested size right now */ INSUFFICIENT_LIQUIDITY: {
809
+ code: 6001,
810
+ name: 'LIQUIDITY_INSUFFICIENT',
811
+ type: 'LIQUIDITY'
812
+ }
813
+ };
788
814
  /**
789
815
  * Standardized error definitions for RPC type errors.
790
816
  *
@@ -832,7 +858,10 @@ class KitError extends Error {
832
858
  type: 'NETWORK'
833
859
  },
834
860
  /** Network request timeout */ TIMEOUT: {
835
- code: 3002},
861
+ code: 3002,
862
+ name: 'NETWORK_TIMEOUT',
863
+ type: 'NETWORK'
864
+ },
836
865
  /** Circle relayer failed to process the forwarding/mint transaction */ RELAYER_FORWARD_FAILED: {
837
866
  code: 3003,
838
867
  name: 'NETWORK_RELAYER_FORWARD_FAILED',
@@ -843,6 +872,58 @@ class KitError extends Error {
843
872
  name: 'NETWORK_RELAYER_PENDING',
844
873
  type: 'NETWORK'
845
874
  }};
875
+ /**
876
+ * Standardized error definitions for RATE_LIMIT type errors.
877
+ *
878
+ * RATE_LIMIT errors indicate API throttling, request frequency limits errors.
879
+ *
880
+ * @example
881
+ * ```typescript
882
+ * import { RateLimitError } from '@core/errors'
883
+ *
884
+ * const error = new KitError({
885
+ * ...RateLimitError.RATE_LIMIT_EXCEEDED,
886
+ * recoverability: 'RETRYABLE',
887
+ * message: 'Rate limit exceeded, please retry later',
888
+ * cause: { trace: { error: '429 Too Many Requests' } }
889
+ * })
890
+ * ```
891
+ */ const RateLimitError = {
892
+ /** Rate limit exceeded */ RATE_LIMIT_EXCEEDED: {
893
+ code: 7001,
894
+ name: 'RATE_LIMIT_EXCEEDED',
895
+ type: 'RATE_LIMIT'
896
+ }
897
+ };
898
+ /**
899
+ * Standardized error definitions for SERVICE type errors.
900
+ *
901
+ * SERVICE errors indicate internal service failures, HTTP 5xx errors,
902
+ * or backend processing issues that are retryable.
903
+ *
904
+ * @example
905
+ * ```typescript
906
+ * import { ServiceError } from '@core/errors'
907
+ *
908
+ * const error = new KitError({
909
+ * ...ServiceError.INTERNAL_ERROR,
910
+ * recoverability: 'RETRYABLE',
911
+ * message: 'Service encountered an internal error (500)',
912
+ * cause: { trace: { statusCode: 500 } }
913
+ * })
914
+ * ```
915
+ */ const ServiceError = {
916
+ /** Internal server error (HTTP 5xx) */ INTERNAL_ERROR: {
917
+ code: 8001,
918
+ name: 'SERVICE_INTERNAL_ERROR',
919
+ type: 'SERVICE'
920
+ },
921
+ /** Unknown or unclassified error that cannot be categorized */ UNKNOWN_ERROR: {
922
+ code: 8002,
923
+ name: 'SERVICE_UNKNOWN_ERROR',
924
+ type: 'SERVICE'
925
+ }
926
+ };
846
927
 
847
928
  /**
848
929
  * Creates error for network type mismatch between source and destination.
@@ -2214,6 +2295,32 @@ class KitError extends Error {
2214
2295
  }
2215
2296
  return false;
2216
2297
  }
2298
+ /**
2299
+ * Type guard to check if error is KitError with RATE_LIMIT type.
2300
+ *
2301
+ * RATE_LIMIT errors indicate API throttling or request frequency limits.
2302
+ * These errors are typically RETRYABLE after a delay.
2303
+ *
2304
+ * @param error - Unknown error to check
2305
+ * @returns True if error is KitError with RATE_LIMIT type
2306
+ *
2307
+ * @example
2308
+ * ```typescript
2309
+ * import { isRateLimitError } from '@core/errors'
2310
+ *
2311
+ * try {
2312
+ * await kit.bridge(params)
2313
+ * } catch (error) {
2314
+ * if (isRateLimitError(error)) {
2315
+ * console.log('Rate limited, retrying in 60s')
2316
+ * await sleep(60000)
2317
+ * retry()
2318
+ * }
2319
+ * }
2320
+ * ```
2321
+ */ function isRateLimitError(error) {
2322
+ return isKitError(error) && error.type === ERROR_TYPES.RATE_LIMIT;
2323
+ }
2217
2324
  /**
2218
2325
  * Safely extracts error message from any error type.
2219
2326
  *
@@ -2463,6 +2570,478 @@ class KitError extends Error {
2463
2570
  return chain;
2464
2571
  }
2465
2572
 
2573
+ /**
2574
+ * Proxy-specific structured error codes carried in `responseBody.code`.
2575
+ *
2576
+ * These are NOT HTTP status codes — they are application-level identifiers
2577
+ * the stablecoin-kits-proxy emits inside JSON error bodies so the kit can
2578
+ * distinguish conditions that share the same HTTP status (e.g. a 400 caused
2579
+ * by an out-of-range swap amount vs. a generic validation failure).
2580
+ *
2581
+ * @internal
2582
+ */ const ProxyErrorCode = {
2583
+ /** Swap amount outside the upstream provider's accepted bounds (HTTP 400) */ INVALID_SWAP_AMOUNT: 331017,
2584
+ /** Upstream liquidity insufficient for the requested size (HTTP 503) */ LOW_LIQUIDITY: 331018
2585
+ };
2586
+ /**
2587
+ * Parses raw HTTP API errors into structured KitError instances.
2588
+ *
2589
+ * This function uses pattern matching to identify common HTTP error types
2590
+ * and converts them into standardized KitError format. It handles errors
2591
+ * from fetch, HTTP status codes, timeouts, and network failures.
2592
+ *
2593
+ * The parser recognizes the following error patterns:
2594
+ * - Client errors (4xx) - validation, authentication, not found
2595
+ * - Server errors (5xx) - service unavailability
2596
+ * - Timeout errors
2597
+ * - Network connectivity errors
2598
+ * - Rate limiting
2599
+ *
2600
+ * Unrecognized errors are treated as fatal `SERVICE` errors, as their
2601
+ * cause and recoverability are unknown.
2602
+ *
2603
+ * @param error - The raw error from the API call
2604
+ * @param context - Context information including operation name
2605
+ * @returns A structured KitError instance
2606
+ *
2607
+ * @example
2608
+ * ```typescript
2609
+ * try {
2610
+ * const response = await fetch(url)
2611
+ * } catch (error) {
2612
+ * throw parseApiError(error, { operation: 'getQuote' })
2613
+ * }
2614
+ * ```
2615
+ */ function parseApiError(error, context) {
2616
+ // If it's already a KitError, return it as-is
2617
+ if (error instanceof KitError) {
2618
+ return error;
2619
+ }
2620
+ const msg = getErrorMessage(error);
2621
+ const statusCode = extractHttpStatusCode(msg);
2622
+ const serviceName = context.service ?? 'Stablecoin Service';
2623
+ const operation = context.operation ?? 'API';
2624
+ const responseBody = extractResponseBody(error);
2625
+ // Rate limit errors (429)
2626
+ if (statusCode === 429 || /too many requests|rate limit exceeded/i.test(msg)) {
2627
+ return handleRateLimitError(serviceName, operation, error);
2628
+ }
2629
+ // HTTP 4xx Client Errors
2630
+ if (statusCode !== null && statusCode >= 400 && statusCode < 500) {
2631
+ return handleClientError(statusCode, serviceName, operation, error, msg, responseBody);
2632
+ }
2633
+ // HTTP 5xx Server Errors
2634
+ if (statusCode !== null && statusCode >= 500 && statusCode < 600) {
2635
+ return handleServerError(statusCode, serviceName, operation, error, responseBody);
2636
+ }
2637
+ // Timeout errors
2638
+ if (/timeout|timed out/i.test(msg)) {
2639
+ return handleTimeoutError(serviceName, operation, error);
2640
+ }
2641
+ // Network connectivity errors
2642
+ if (/connection (refused|failed)|network|ENOTFOUND|ECONNREFUSED|ETIMEDOUT/i.test(msg)) {
2643
+ return handleConnectionError(serviceName, operation, error);
2644
+ }
2645
+ // Fallback: Unknown error - use UNKNOWN_ERROR with fatal recoverability since we don't know what the error is
2646
+ return new KitError({
2647
+ ...ServiceError.UNKNOWN_ERROR,
2648
+ recoverability: 'FATAL',
2649
+ message: `${serviceName} ${operation} failed: ${msg.length > 0 ? msg : 'Unknown error'}`,
2650
+ cause: {
2651
+ trace: error
2652
+ }
2653
+ });
2654
+ }
2655
+ /**
2656
+ * Handles HTTP 4xx client errors and maps them to appropriate KitError instances.
2657
+ *
2658
+ * @param statusCode - The HTTP status code
2659
+ * @param serviceName - The name of the service
2660
+ * @param operation - The operation name
2661
+ * @param error - The raw error object
2662
+ * @param msg - The message extracted from the error
2663
+ * @param responseBody - The parsed JSON response body from the server, if available
2664
+ * @returns A KitError instance
2665
+ */ function handleClientError(statusCode, serviceName, operation, error, msg, responseBody) {
2666
+ const detail = extractDetailFromBody(responseBody) ?? msg;
2667
+ switch(statusCode){
2668
+ // 401/403 - Authentication/Authorization
2669
+ case 401:
2670
+ case 403:
2671
+ return new KitError({
2672
+ ...InputError.VALIDATION_FAILED,
2673
+ recoverability: 'FATAL',
2674
+ message: `${serviceName} ${operation} failed: Invalid or missing API key or authorization`,
2675
+ cause: {
2676
+ trace: error
2677
+ }
2678
+ });
2679
+ // 404 - Not found - unsupported route OR stop-limit / slippage constraint not met
2680
+ case 404:
2681
+ if (isSlippageConstraintFailure(responseBody)) {
2682
+ return new KitError({
2683
+ ...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
2684
+ recoverability: 'RETRYABLE',
2685
+ message: `${serviceName} ${operation} failed: ${detail}. ` + 'Try increasing slippageBps or adjusting stopLimit.',
2686
+ cause: {
2687
+ trace: error
2688
+ }
2689
+ });
2690
+ }
2691
+ return new KitError({
2692
+ ...InputError.UNSUPPORTED_ROUTE,
2693
+ recoverability: 'FATAL',
2694
+ message: `${serviceName} ${operation} failed: Route or resource not found. Details: ${detail}`,
2695
+ cause: {
2696
+ trace: error
2697
+ }
2698
+ });
2699
+ // 422 Unprocessable Entity
2700
+ // Proxy service is mapping 422 to INSUFFICIENT_SWAP_AMOUNT
2701
+ case 422:
2702
+ return new KitError({
2703
+ ...InputError.INSUFFICIENT_SWAP_AMOUNT,
2704
+ recoverability: 'FATAL',
2705
+ message: `${serviceName} ${operation} failed: ${detail}`,
2706
+ cause: {
2707
+ trace: error
2708
+ }
2709
+ });
2710
+ // 400 Bad Request - Invalid token, amount-out-of-range, or validation failed
2711
+ // Proxy maps 400 to UNSUPPORTED_TOKEN | AMOUNT_OUT_OF_RANGE | VALIDATION_FAILED
2712
+ case 400:
2713
+ if (responseBody?.code === ProxyErrorCode.INVALID_SWAP_AMOUNT) {
2714
+ const amountErr = extractAmountError(responseBody);
2715
+ return new KitError({
2716
+ ...InputError.AMOUNT_OUT_OF_RANGE,
2717
+ recoverability: 'FATAL',
2718
+ message: `${serviceName} ${operation} failed: ${detail}`,
2719
+ cause: {
2720
+ trace: {
2721
+ rawError: error,
2722
+ minAmount: amountErr?.minAmount,
2723
+ maxAmount: amountErr?.maxAmount,
2724
+ token: amountErr?.token
2725
+ }
2726
+ }
2727
+ });
2728
+ }
2729
+ return new KitError({
2730
+ ...InputError.VALIDATION_FAILED,
2731
+ recoverability: 'FATAL',
2732
+ message: `${serviceName} ${operation} failed: ${detail}`,
2733
+ cause: {
2734
+ trace: error
2735
+ }
2736
+ });
2737
+ default:
2738
+ // Other 4xx errors - treat as validation failures
2739
+ return new KitError({
2740
+ ...InputError.VALIDATION_FAILED,
2741
+ recoverability: 'FATAL',
2742
+ message: `${serviceName} ${operation} failed: ${detail}`,
2743
+ cause: {
2744
+ trace: error
2745
+ }
2746
+ });
2747
+ }
2748
+ }
2749
+ /**
2750
+ * Pattern that matches proxy response body text indicating the 404 was
2751
+ * caused by a slippage / price constraint rather than a truly unsupported
2752
+ * route. Kept case-insensitive so future proxy wording changes are tolerated.
2753
+ *
2754
+ * @internal
2755
+ */ const SLIPPAGE_BODY_PATTERN = /slippage|stop.?limit|price.?impact|minimum.?output|SLIPPAGE_CONSTRAINT_NOT_MET/i;
2756
+ /**
2757
+ * Determine whether a 404 was caused by an unmet slippage or price
2758
+ * constraint rather than a genuinely unsupported route.
2759
+ *
2760
+ * Detection relies on the proxy response body containing slippage-related
2761
+ * language or a structured reason code. This avoids false positives that
2762
+ * would occur if we guessed based on request parameters alone (a user
2763
+ * can set `slippageBps` and still hit a truly unsupported route).
2764
+ *
2765
+ * @param responseBody - The parsed JSON body returned by the proxy
2766
+ * @returns `true` when the 404 should be treated as a slippage constraint failure
2767
+ * @internal
2768
+ */ function isSlippageConstraintFailure(responseBody) {
2769
+ if (responseBody === undefined) {
2770
+ return false;
2771
+ }
2772
+ const textsToCheck = [
2773
+ responseBody.externalMessage,
2774
+ responseBody.message,
2775
+ extractDetailFromBody(responseBody)
2776
+ ];
2777
+ return textsToCheck.some((t)=>typeof t === 'string' && SLIPPAGE_BODY_PATTERN.test(t));
2778
+ }
2779
+ /**
2780
+ * Handles HTTP 5xx server errors and maps them to appropriate KitError instances.
2781
+ *
2782
+ * Recognizes proxy-specific structured codes in `responseBody.code` and routes
2783
+ * known conditions (e.g. {@link ProxyErrorCode.LOW_LIQUIDITY} on 503) to their
2784
+ * dedicated KitError. Falls back to a generic retryable `SERVICE_INTERNAL_ERROR`
2785
+ * when no specific code is present.
2786
+ *
2787
+ * @param statusCode - The HTTP status code
2788
+ * @param serviceName - The name of the service
2789
+ * @param operation - The operation name
2790
+ * @param error - The raw error object
2791
+ * @param responseBody - The parsed JSON response body from the server, if available
2792
+ * @returns A KitError instance
2793
+ */ function handleServerError(statusCode, serviceName, operation, error, responseBody) {
2794
+ // 503 + 331018 = upstream liquidity insufficient (proxy)
2795
+ if (statusCode === 503 && responseBody?.code === ProxyErrorCode.LOW_LIQUIDITY) {
2796
+ const amountErr = extractAmountError(responseBody);
2797
+ const detail = extractDetailFromBody(responseBody) ?? getErrorMessage(error);
2798
+ return new KitError({
2799
+ ...LiquidityError.INSUFFICIENT_LIQUIDITY,
2800
+ recoverability: 'RETRYABLE',
2801
+ message: `${serviceName} ${operation} failed: ${detail}`,
2802
+ cause: {
2803
+ trace: {
2804
+ rawError: error,
2805
+ minAmount: amountErr?.minAmount,
2806
+ maxAmount: amountErr?.maxAmount,
2807
+ token: amountErr?.token
2808
+ }
2809
+ }
2810
+ });
2811
+ }
2812
+ return new KitError({
2813
+ ...ServiceError.INTERNAL_ERROR,
2814
+ recoverability: 'RETRYABLE',
2815
+ message: `${serviceName} ${operation} failed: Server error (${statusCode.toString()})`,
2816
+ cause: {
2817
+ trace: error
2818
+ }
2819
+ });
2820
+ }
2821
+ /**
2822
+ * Handles network connection errors and maps them to appropriate KitError instances.
2823
+ *
2824
+ * @param serviceName - The name of the service
2825
+ * @param operation - The operation name
2826
+ * @param error - The raw error object
2827
+ * @returns A KitError instance
2828
+ */ function handleConnectionError(serviceName, operation, error) {
2829
+ return new KitError({
2830
+ ...NetworkError.CONNECTION_FAILED,
2831
+ recoverability: 'RETRYABLE',
2832
+ message: `${serviceName} ${operation} failed: Network connection error`,
2833
+ cause: {
2834
+ trace: error
2835
+ }
2836
+ });
2837
+ }
2838
+ /**
2839
+ * Handles rate limit errors and maps them to appropriate KitError instances.
2840
+ *
2841
+ * @param serviceName - The name of the service
2842
+ * @param operation - The operation name
2843
+ * @param error - The raw error object
2844
+ * @returns A KitError instance
2845
+ */ function handleRateLimitError(serviceName, operation, error) {
2846
+ return new KitError({
2847
+ ...RateLimitError.RATE_LIMIT_EXCEEDED,
2848
+ recoverability: 'RETRYABLE',
2849
+ message: `${serviceName} ${operation} failed: Too many requests, please retry later`,
2850
+ cause: {
2851
+ trace: error
2852
+ }
2853
+ });
2854
+ }
2855
+ /**
2856
+ * Handles timeout errors and maps them to appropriate KitError instances.
2857
+ *
2858
+ * @param serviceName - The name of the service
2859
+ * @param operation - The operation name
2860
+ * @param error - The raw error object
2861
+ * @returns A KitError instance
2862
+ */ function handleTimeoutError(serviceName, operation, error) {
2863
+ return new KitError({
2864
+ ...NetworkError.TIMEOUT,
2865
+ recoverability: 'RETRYABLE',
2866
+ message: `${serviceName} ${operation} failed: Request timeout`,
2867
+ cause: {
2868
+ trace: error
2869
+ }
2870
+ });
2871
+ }
2872
+ /**
2873
+ * Type guard that narrows an unknown error to one carrying a non-null
2874
+ * object `responseBody` property (attached by `makeApiRequest`).
2875
+ *
2876
+ * @param error - The raw error from the HTTP layer
2877
+ * @returns `true` when `error.responseBody` is a non-null object
2878
+ * @internal
2879
+ */ function hasResponseBody(error) {
2880
+ return typeof error === 'object' && error !== null && 'responseBody' in error && typeof error['responseBody'] === 'object' && error['responseBody'] !== null;
2881
+ }
2882
+ /**
2883
+ * Extract the `responseBody` property that `makeApiRequest` attaches to
2884
+ * HTTP error instances when the server returns a JSON body.
2885
+ *
2886
+ * @param error - The raw error from the HTTP layer
2887
+ * @returns The parsed body cast to {@link ApiErrorResponseBody}, or undefined
2888
+ * @throws Never. Returns `undefined` when the error does not carry a valid
2889
+ * `responseBody`.
2890
+ * @internal
2891
+ */ function extractResponseBody(error) {
2892
+ if (!hasResponseBody(error)) {
2893
+ return undefined;
2894
+ }
2895
+ return error.responseBody;
2896
+ }
2897
+ /**
2898
+ * Extract a field name from an {@link ApiFieldError}.
2899
+ *
2900
+ * The proxy service may provide the field name as a plain `field` string
2901
+ * or as a `path` array (e.g. `["tokenInChain"]`). This helper resolves
2902
+ * whichever is available, preferring `field` when both exist.
2903
+ *
2904
+ * @param entry - A single error entry from the `errors` array
2905
+ * @returns The field name, or `undefined` when neither is available
2906
+ * @internal
2907
+ */ function extractFieldName(entry) {
2908
+ if (typeof entry.field === 'string' && entry.field.length > 0) {
2909
+ return entry.field;
2910
+ }
2911
+ if (Array.isArray(entry.path) && entry.path.length > 0) {
2912
+ const first = entry.path[0];
2913
+ if (typeof first === 'string' && first.length > 0) {
2914
+ return first;
2915
+ }
2916
+ }
2917
+ return undefined;
2918
+ }
2919
+ /**
2920
+ * Join field-level error entries into a single human-readable string.
2921
+ *
2922
+ * Each entry is formatted as `"field: message"` when a field name is
2923
+ * available (via `field` or `path`), or just the message otherwise.
2924
+ * Entries without a usable message (including amount-bound entries that
2925
+ * only carry `minAmount`/`maxAmount`/`token`) are skipped.
2926
+ *
2927
+ * @param errors - The `errors` array from the response body
2928
+ * @returns A joined string, or `undefined` when no usable entries exist
2929
+ * @internal
2930
+ */ function joinFieldErrors(errors) {
2931
+ const parts = errors.map((entry)=>{
2932
+ if (!isFieldEntry(entry)) {
2933
+ return undefined;
2934
+ }
2935
+ const fieldMsg = typeof entry.message === 'string' && entry.message.length > 0 ? entry.message : undefined;
2936
+ if (fieldMsg === undefined) {
2937
+ return undefined;
2938
+ }
2939
+ const fieldName = extractFieldName(entry);
2940
+ return fieldName === undefined ? fieldMsg : `${fieldName}: ${fieldMsg}`;
2941
+ }).filter((s)=>s !== undefined);
2942
+ return parts.length > 0 ? parts.join('; ') : undefined;
2943
+ }
2944
+ /**
2945
+ * Narrow an {@link ApiErrorItem} to the field-error shape used for
2946
+ * validation messages. Amount-bound entries (which carry no `message`)
2947
+ * are excluded.
2948
+ *
2949
+ * @internal
2950
+ */ function isFieldEntry(entry) {
2951
+ return 'message' in entry || 'field' in entry || 'path' in entry;
2952
+ }
2953
+ /**
2954
+ * Narrow an {@link ApiErrorItem} to the amount-bound shape emitted by the
2955
+ * proxy for {@link ProxyErrorCode.INVALID_SWAP_AMOUNT}.
2956
+ *
2957
+ * @internal
2958
+ */ function isAmountEntry(entry) {
2959
+ return 'minAmount' in entry || 'maxAmount' in entry || 'token' in entry;
2960
+ }
2961
+ /**
2962
+ * Extract the first amount-bound entry from a response body, if any.
2963
+ *
2964
+ * @internal
2965
+ */ function extractAmountError(body) {
2966
+ if (body === undefined || !Array.isArray(body.errors)) {
2967
+ return undefined;
2968
+ }
2969
+ return body.errors.find(isAmountEntry);
2970
+ }
2971
+ /**
2972
+ * Derive a human-readable detail string from an {@link ApiErrorResponseBody}.
2973
+ *
2974
+ * Resolution order:
2975
+ * 1. `body.externalMessage` -- the user-facing string the proxy intends
2976
+ * consumers to display (e.g. "No route found that satisfies the
2977
+ * requested stop limit"). Preferred when available.
2978
+ * 2. `body.message` **and** `body.errors` -- when both are present the
2979
+ * top-level message is combined with the field-level detail so
2980
+ * developers see the full picture
2981
+ * (e.g. `"Validation error: tokenInChain: Invalid input; amount: …"`).
2982
+ * 3. `body.message` alone -- used as-is.
2983
+ * 4. `body.errors` alone -- field-level entries joined with "; ".
2984
+ * 5. `undefined` -- caller should fall back to the raw HTTP status text.
2985
+ *
2986
+ * @param body - The parsed response body, may be undefined
2987
+ * @returns A detail string, or undefined when no useful info is available
2988
+ * @internal
2989
+ */ function extractDetailFromBody(body) {
2990
+ if (body === undefined) {
2991
+ return undefined;
2992
+ }
2993
+ const externalMessage = typeof body.externalMessage === 'string' && body.externalMessage.length > 0 ? body.externalMessage : undefined;
2994
+ if (externalMessage !== undefined) {
2995
+ return externalMessage;
2996
+ }
2997
+ const topMessage = typeof body.message === 'string' && body.message.length > 0 ? body.message : undefined;
2998
+ const fieldDetail = Array.isArray(body.errors) && body.errors.length > 0 ? joinFieldErrors(body.errors) : undefined;
2999
+ if (topMessage !== undefined && fieldDetail !== undefined) {
3000
+ return `${topMessage}: ${fieldDetail}`;
3001
+ }
3002
+ return topMessage ?? fieldDetail;
3003
+ }
3004
+ /**
3005
+ * Extracts the HTTP status code from an error message.
3006
+ *
3007
+ * Attempts to parse HTTP status codes from common error message formats,
3008
+ * such as "HTTP 404" or "Status: 500".
3009
+ *
3010
+ * @param msg - The error message to extract from
3011
+ * @returns The extracted HTTP status code, or null if not found
3012
+ *
3013
+ * @example
3014
+ * ```typescript
3015
+ * const code = extractHttpStatusCode('HTTP 404')
3016
+ * // Returns: 404
3017
+ * ```
3018
+ *
3019
+ * @example
3020
+ * ```typescript
3021
+ * const code = extractHttpStatusCode('Status: 500 Internal Server Error')
3022
+ * // Returns: 500
3023
+ * ```
3024
+ */ function extractHttpStatusCode(msg) {
3025
+ // Pattern: "HTTP 404" or "HTTP 404 - some message" or "Status: 404"
3026
+ const patterns = [
3027
+ /HTTP (\d{3})/i,
3028
+ /Status:\s*(\d{3})/i,
3029
+ /^(\d{3}) -/
3030
+ ];
3031
+ for (const pattern of patterns){
3032
+ const match = pattern.exec(msg);
3033
+ const codeStr = match?.at(1);
3034
+ if (codeStr !== undefined) {
3035
+ const code = Number.parseInt(codeStr, 10);
3036
+ // Validate it's a valid HTTP status code
3037
+ if (code >= 100 && code < 600) {
3038
+ return code;
3039
+ }
3040
+ }
3041
+ }
3042
+ return null;
3043
+ }
3044
+
2466
3045
  /**
2467
3046
  * @packageDocumentation
2468
3047
  * @module ChainDefinitions
@@ -2534,6 +3113,8 @@ class KitError extends Error {
2534
3113
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
2535
3114
  Blockchain["Pharos"] = "Pharos";
2536
3115
  Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
3116
+ Blockchain["Plasma"] = "Plasma";
3117
+ Blockchain["Plasma_Testnet"] = "Plasma_Testnet";
2537
3118
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
2538
3119
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
2539
3120
  Blockchain["Plume"] = "Plume";
@@ -2603,6 +3184,7 @@ var BridgeChain;
2603
3184
  BridgeChain["Morph"] = "Morph";
2604
3185
  BridgeChain["Optimism"] = "Optimism";
2605
3186
  BridgeChain["Pharos"] = "Pharos";
3187
+ BridgeChain["Plasma"] = "Plasma";
2606
3188
  BridgeChain["Plume"] = "Plume";
2607
3189
  BridgeChain["Polygon"] = "Polygon";
2608
3190
  BridgeChain["Sei"] = "Sei";
@@ -2629,6 +3211,7 @@ var BridgeChain;
2629
3211
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
2630
3212
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
2631
3213
  BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
3214
+ BridgeChain["Plasma_Testnet"] = "Plasma_Testnet";
2632
3215
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
2633
3216
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
2634
3217
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -3075,17 +3658,56 @@ var EarnChain;
3075
3658
  * This program handles minting operations for Gateway transactions
3076
3659
  * on Solana devnet.
3077
3660
  */ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
3078
-
3079
3661
  /**
3080
- * Arc Testnet chain definition
3081
- * @remarks
3082
- * This represents the test network for the Arc blockchain,
3083
- * Circle's EVM-compatible Layer-1 designed for stablecoin finance
3084
- * and asset tokenization. Arc uses USDC as the native gas token and
3085
- * features the Malachite Byzantine Fault Tolerant (BFT) consensus
3086
- * engine for sub-second finality.
3087
- */ const ArcTestnet = defineChain({
3088
- type: 'evm',
3662
+ * The `TokenMessengerWithFees` proxy contract address for EVM mainnet networks
3663
+ * (all chains except Edge).
3664
+ *
3665
+ * Deployed at a CREATE3-derived address; identical across all mainnet EVM
3666
+ * source chains. Present on any chain that supports the prepaid FORWARD path
3667
+ * via `depositForBurnWithHookAndFees`.
3668
+ */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
3669
+ /**
3670
+ * The `TokenMessengerWithFees` proxy contract address for EVM testnet networks.
3671
+ *
3672
+ * Identical across all testnet EVM source chains. Present on any testnet chain
3673
+ * that supports the prepaid FORWARD path via `depositForBurnWithHookAndFees`.
3674
+ */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
3675
+ /**
3676
+ * The `DepositForHandler` proxy contract address for EVM mainnet networks.
3677
+ *
3678
+ * The handler the GenericExecutor calls on a fast-deposit destination chain to
3679
+ * run a cross-chain deposit into the GatewayWallet. Deployed at the same
3680
+ * address across all mainnet EVM destination chains.
3681
+ */ const DEPOSIT_FOR_HANDLER_EVM_MAINNET = '0x16529813203f77E036576666336554a1210dce4D';
3682
+ /**
3683
+ * The `DepositForHandler` proxy contract address for EVM testnet networks.
3684
+ *
3685
+ * Identical across all testnet EVM destination chains.
3686
+ */ const DEPOSIT_FOR_HANDLER_EVM_TESTNET = '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48';
3687
+ /**
3688
+ * The `GenericExecutor` proxy contract address for EVM mainnet networks.
3689
+ *
3690
+ * The GenericExecutor is the `mintRecipient` and `destinationCaller` on the
3691
+ * destination chain for the CCTP v2 prepaid FORWARD path. It receives the CCTP
3692
+ * mint and calls the `DepositForHandler` to complete the fast deposit.
3693
+ * Deployed at the same address across all mainnet EVM destination chains.
3694
+ */ const GENERIC_EXECUTOR_EVM_MAINNET = '0xFa7be2f04F3Ad4ca969260729c6d45B5625984A7';
3695
+ /**
3696
+ * The `GenericExecutor` proxy contract address for EVM testnet networks.
3697
+ *
3698
+ * Identical across all testnet EVM destination chains.
3699
+ */ const GENERIC_EXECUTOR_EVM_TESTNET = '0xEdC81040756AcCfF070c21D37b265b9D0b5Ba45e';
3700
+
3701
+ /**
3702
+ * Arc Testnet chain definition
3703
+ * @remarks
3704
+ * This represents the test network for the Arc blockchain,
3705
+ * Circle's EVM-compatible Layer-1 designed for stablecoin finance
3706
+ * and asset tokenization. Arc uses USDC as the native gas token and
3707
+ * features the Malachite Byzantine Fault Tolerant (BFT) consensus
3708
+ * engine for sub-second finality.
3709
+ */ const ArcTestnet = defineChain({
3710
+ type: 'evm',
3089
3711
  chain: Blockchain.Arc_Testnet,
3090
3712
  name: 'Arc Testnet',
3091
3713
  title: 'ArcTestnet',
@@ -3112,6 +3734,7 @@ var EarnChain;
3112
3734
  v2: {
3113
3735
  type: 'split',
3114
3736
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
3737
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3115
3738
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3116
3739
  confirmations: 1,
3117
3740
  fastConfirmations: 1
@@ -3132,9 +3755,8 @@ var EarnChain;
3132
3755
  v1: {
3133
3756
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3134
3757
  minter: GATEWAY_MINTER_EVM_TESTNET,
3135
- // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3136
- // deposit into the GatewayWallet above.
3137
- depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3758
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
3759
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
3138
3760
  }
3139
3761
  },
3140
3762
  forwarderSupported: {
@@ -3179,6 +3801,7 @@ var EarnChain;
3179
3801
  v2: {
3180
3802
  type: 'split',
3181
3803
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
3804
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3182
3805
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3183
3806
  confirmations: 65,
3184
3807
  fastConfirmations: 1
@@ -3243,6 +3866,7 @@ var EarnChain;
3243
3866
  v2: {
3244
3867
  type: 'split',
3245
3868
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
3869
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3246
3870
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3247
3871
  confirmations: 65,
3248
3872
  fastConfirmations: 1
@@ -3307,6 +3931,7 @@ var EarnChain;
3307
3931
  v2: {
3308
3932
  type: 'split',
3309
3933
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
3934
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3310
3935
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3311
3936
  confirmations: 1,
3312
3937
  fastConfirmations: 1
@@ -3326,7 +3951,9 @@ var EarnChain;
3326
3951
  contracts: {
3327
3952
  v1: {
3328
3953
  wallet: GATEWAY_WALLET_EVM_MAINNET,
3329
- minter: GATEWAY_MINTER_EVM_MAINNET
3954
+ minter: GATEWAY_MINTER_EVM_MAINNET,
3955
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_MAINNET,
3956
+ genericExecutor: GENERIC_EXECUTOR_EVM_MAINNET
3330
3957
  }
3331
3958
  },
3332
3959
  forwarderSupported: {
@@ -3368,6 +3995,7 @@ var EarnChain;
3368
3995
  v2: {
3369
3996
  type: 'split',
3370
3997
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
3998
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3371
3999
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3372
4000
  confirmations: 1,
3373
4001
  fastConfirmations: 1
@@ -3389,7 +4017,9 @@ var EarnChain;
3389
4017
  contracts: {
3390
4018
  v1: {
3391
4019
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3392
- minter: GATEWAY_MINTER_EVM_TESTNET
4020
+ minter: GATEWAY_MINTER_EVM_TESTNET,
4021
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
4022
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
3393
4023
  }
3394
4024
  },
3395
4025
  forwarderSupported: {
@@ -3435,6 +4065,7 @@ var EarnChain;
3435
4065
  v2: {
3436
4066
  type: 'split',
3437
4067
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4068
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3438
4069
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3439
4070
  confirmations: 65,
3440
4071
  fastConfirmations: 1
@@ -3499,6 +4130,7 @@ var EarnChain;
3499
4130
  v2: {
3500
4131
  type: 'split',
3501
4132
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4133
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3502
4134
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3503
4135
  confirmations: 65,
3504
4136
  fastConfirmations: 1
@@ -3609,6 +4241,7 @@ var EarnChain;
3609
4241
  v2: {
3610
4242
  type: 'split',
3611
4243
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4244
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3612
4245
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3613
4246
  confirmations: 65,
3614
4247
  fastConfirmations: 1
@@ -3653,6 +4286,7 @@ var EarnChain;
3653
4286
  v2: {
3654
4287
  type: 'split',
3655
4288
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4289
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3656
4290
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3657
4291
  confirmations: 65,
3658
4292
  fastConfirmations: 1
@@ -3698,6 +4332,7 @@ var EarnChain;
3698
4332
  v2: {
3699
4333
  type: 'split',
3700
4334
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4335
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3701
4336
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3702
4337
  confirmations: 1,
3703
4338
  fastConfirmations: 1
@@ -3743,6 +4378,7 @@ var EarnChain;
3743
4378
  v2: {
3744
4379
  type: 'split',
3745
4380
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4381
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3746
4382
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3747
4383
  confirmations: 1,
3748
4384
  fastConfirmations: 1
@@ -3788,6 +4424,7 @@ var EarnChain;
3788
4424
  v2: {
3789
4425
  type: 'split',
3790
4426
  tokenMessenger: '0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c',
4427
+ tokenMessengerWithFees: '0x3Ac96675F9a3E6922713e041645D82f3561d3686',
3791
4428
  messageTransmitter: '0x5b61381Fc9e58E70EfC13a4A97516997019198ee',
3792
4429
  confirmations: 65,
3793
4430
  fastConfirmations: 1
@@ -3833,6 +4470,7 @@ var EarnChain;
3833
4470
  v2: {
3834
4471
  type: 'split',
3835
4472
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4473
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3836
4474
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3837
4475
  confirmations: 65,
3838
4476
  fastConfirmations: 1
@@ -3884,6 +4522,7 @@ var EarnChain;
3884
4522
  v2: {
3885
4523
  type: 'split',
3886
4524
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4525
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
3887
4526
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3888
4527
  confirmations: 65,
3889
4528
  fastConfirmations: 2
@@ -3948,6 +4587,7 @@ var EarnChain;
3948
4587
  v2: {
3949
4588
  type: 'split',
3950
4589
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4590
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
3951
4591
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
3952
4592
  confirmations: 65,
3953
4593
  fastConfirmations: 2
@@ -4058,6 +4698,7 @@ var EarnChain;
4058
4698
  v2: {
4059
4699
  type: 'split',
4060
4700
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4701
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4061
4702
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4062
4703
  confirmations: 1,
4063
4704
  fastConfirmations: 1
@@ -4117,6 +4758,7 @@ var EarnChain;
4117
4758
  v2: {
4118
4759
  type: 'split',
4119
4760
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4761
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4120
4762
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4121
4763
  confirmations: 1,
4122
4764
  fastConfirmations: 1
@@ -4177,6 +4819,7 @@ var EarnChain;
4177
4819
  v2: {
4178
4820
  type: 'split',
4179
4821
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4822
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4180
4823
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4181
4824
  confirmations: 1,
4182
4825
  fastConfirmations: 1
@@ -4224,6 +4867,7 @@ var EarnChain;
4224
4867
  v2: {
4225
4868
  type: 'split',
4226
4869
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4870
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4227
4871
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4228
4872
  confirmations: 1,
4229
4873
  fastConfirmations: 1
@@ -4271,6 +4915,7 @@ var EarnChain;
4271
4915
  v2: {
4272
4916
  type: 'split',
4273
4917
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4918
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4274
4919
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4275
4920
  confirmations: 65,
4276
4921
  fastConfirmations: 1
@@ -4318,6 +4963,7 @@ var EarnChain;
4318
4963
  v2: {
4319
4964
  type: 'split',
4320
4965
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4966
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4321
4967
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4322
4968
  confirmations: 65,
4323
4969
  fastConfirmations: 1
@@ -4362,6 +5008,7 @@ var EarnChain;
4362
5008
  v2: {
4363
5009
  type: 'split',
4364
5010
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5011
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4365
5012
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4366
5013
  confirmations: 1,
4367
5014
  fastConfirmations: 1
@@ -4407,6 +5054,7 @@ var EarnChain;
4407
5054
  v2: {
4408
5055
  type: 'split',
4409
5056
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5057
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4410
5058
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
4411
5059
  confirmations: 1,
4412
5060
  fastConfirmations: 1
@@ -4453,6 +5101,7 @@ var EarnChain;
4453
5101
  v2: {
4454
5102
  type: 'split',
4455
5103
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5104
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4456
5105
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4457
5106
  confirmations: 1,
4458
5107
  fastConfirmations: 1
@@ -4500,6 +5149,7 @@ var EarnChain;
4500
5149
  v2: {
4501
5150
  type: 'split',
4502
5151
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5152
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4503
5153
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4504
5154
  confirmations: 1,
4505
5155
  fastConfirmations: 1
@@ -4545,6 +5195,7 @@ var EarnChain;
4545
5195
  v2: {
4546
5196
  type: 'split',
4547
5197
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5198
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4548
5199
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4549
5200
  confirmations: 64,
4550
5201
  fastConfirmations: 1
@@ -4590,6 +5241,7 @@ var EarnChain;
4590
5241
  v2: {
4591
5242
  type: 'split',
4592
5243
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5244
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4593
5245
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4594
5246
  confirmations: 64,
4595
5247
  fastConfirmations: 1
@@ -4766,6 +5418,7 @@ var EarnChain;
4766
5418
  v2: {
4767
5419
  type: 'split',
4768
5420
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5421
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4769
5422
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4770
5423
  confirmations: 65,
4771
5424
  fastConfirmations: 1
@@ -4830,6 +5483,7 @@ var EarnChain;
4830
5483
  v2: {
4831
5484
  type: 'split',
4832
5485
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5486
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4833
5487
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
4834
5488
  confirmations: 65,
4835
5489
  fastConfirmations: 1
@@ -4889,6 +5543,7 @@ var EarnChain;
4889
5543
  v2: {
4890
5544
  type: 'split',
4891
5545
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5546
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4892
5547
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4893
5548
  confirmations: 1,
4894
5549
  fastConfirmations: 1
@@ -4935,6 +5590,7 @@ var EarnChain;
4935
5590
  v2: {
4936
5591
  type: 'split',
4937
5592
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5593
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4938
5594
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4939
5595
  confirmations: 1,
4940
5596
  fastConfirmations: 1
@@ -4950,6 +5606,98 @@ var EarnChain;
4950
5606
  }
4951
5607
  });
4952
5608
 
5609
+ /**
5610
+ * Plasma Mainnet chain definition
5611
+ * @remarks
5612
+ * This represents the official production network for the Plasma blockchain.
5613
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
5614
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
5615
+ */ const Plasma = defineChain({
5616
+ type: 'evm',
5617
+ chain: Blockchain.Plasma,
5618
+ name: 'Plasma',
5619
+ title: 'Plasma Mainnet',
5620
+ nativeCurrency: {
5621
+ name: 'Plasma',
5622
+ symbol: 'XPL',
5623
+ decimals: 18
5624
+ },
5625
+ chainId: 9745,
5626
+ isTestnet: false,
5627
+ explorerUrl: 'https://plasmascan.to/tx/{hash}',
5628
+ rpcEndpoints: [
5629
+ 'https://rpc.plasma.to'
5630
+ ],
5631
+ eurcAddress: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
5632
+ usdcAddress: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
5633
+ usdtAddress: null,
5634
+ cctp: {
5635
+ domain: 33,
5636
+ contracts: {
5637
+ v2: {
5638
+ type: 'split',
5639
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5640
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5641
+ confirmations: 3,
5642
+ fastConfirmations: 1
5643
+ }
5644
+ },
5645
+ forwarderSupported: {
5646
+ source: false,
5647
+ destination: false
5648
+ }
5649
+ },
5650
+ kitContracts: {
5651
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5652
+ }
5653
+ });
5654
+
5655
+ /**
5656
+ * Plasma Testnet chain definition
5657
+ * @remarks
5658
+ * This represents the official test network for the Plasma blockchain.
5659
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
5660
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
5661
+ */ const PlasmaTestnet = defineChain({
5662
+ type: 'evm',
5663
+ chain: Blockchain.Plasma_Testnet,
5664
+ name: 'Plasma Testnet',
5665
+ title: 'Plasma Testnet',
5666
+ nativeCurrency: {
5667
+ name: 'Plasma',
5668
+ symbol: 'XPL',
5669
+ decimals: 18
5670
+ },
5671
+ chainId: 9746,
5672
+ isTestnet: true,
5673
+ explorerUrl: 'https://testnet.plasmascan.to/tx/{hash}',
5674
+ rpcEndpoints: [
5675
+ 'https://testnet-rpc.plasma.to'
5676
+ ],
5677
+ eurcAddress: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330',
5678
+ usdcAddress: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
5679
+ usdtAddress: null,
5680
+ cctp: {
5681
+ domain: 33,
5682
+ contracts: {
5683
+ v2: {
5684
+ type: 'split',
5685
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5686
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5687
+ confirmations: 3,
5688
+ fastConfirmations: 1
5689
+ }
5690
+ },
5691
+ forwarderSupported: {
5692
+ source: false,
5693
+ destination: false
5694
+ }
5695
+ },
5696
+ kitContracts: {
5697
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5698
+ }
5699
+ });
5700
+
4953
5701
  /**
4954
5702
  * Plume Mainnet chain definition
4955
5703
  * @remarks
@@ -4981,6 +5729,7 @@ var EarnChain;
4981
5729
  v2: {
4982
5730
  type: 'split',
4983
5731
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5732
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4984
5733
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4985
5734
  confirmations: 65,
4986
5735
  fastConfirmations: 1
@@ -5027,6 +5776,7 @@ var EarnChain;
5027
5776
  v2: {
5028
5777
  type: 'split',
5029
5778
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5779
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5030
5780
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5031
5781
  confirmations: 65,
5032
5782
  fastConfirmations: 1
@@ -5128,6 +5878,7 @@ var EarnChain;
5128
5878
  v2: {
5129
5879
  type: 'split',
5130
5880
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5881
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5131
5882
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5132
5883
  confirmations: 33,
5133
5884
  fastConfirmations: 13
@@ -5147,7 +5898,9 @@ var EarnChain;
5147
5898
  contracts: {
5148
5899
  v1: {
5149
5900
  wallet: GATEWAY_WALLET_EVM_MAINNET,
5150
- minter: GATEWAY_MINTER_EVM_MAINNET
5901
+ minter: GATEWAY_MINTER_EVM_MAINNET,
5902
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_MAINNET,
5903
+ genericExecutor: GENERIC_EXECUTOR_EVM_MAINNET
5151
5904
  }
5152
5905
  },
5153
5906
  forwarderSupported: {
@@ -5193,6 +5946,7 @@ var EarnChain;
5193
5946
  v2: {
5194
5947
  type: 'split',
5195
5948
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5949
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5196
5950
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5197
5951
  confirmations: 33,
5198
5952
  fastConfirmations: 13
@@ -5211,7 +5965,9 @@ var EarnChain;
5211
5965
  contracts: {
5212
5966
  v1: {
5213
5967
  wallet: GATEWAY_WALLET_EVM_TESTNET,
5214
- minter: GATEWAY_MINTER_EVM_TESTNET
5968
+ minter: GATEWAY_MINTER_EVM_TESTNET,
5969
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
5970
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
5215
5971
  }
5216
5972
  },
5217
5973
  forwarderSupported: {
@@ -5252,6 +6008,7 @@ var EarnChain;
5252
6008
  v2: {
5253
6009
  type: 'split',
5254
6010
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6011
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5255
6012
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5256
6013
  confirmations: 1,
5257
6014
  fastConfirmations: 1
@@ -5311,6 +6068,7 @@ var EarnChain;
5311
6068
  v2: {
5312
6069
  type: 'split',
5313
6070
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6071
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5314
6072
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5315
6073
  confirmations: 1,
5316
6074
  fastConfirmations: 1
@@ -5368,6 +6126,7 @@ var EarnChain;
5368
6126
  v2: {
5369
6127
  type: 'split',
5370
6128
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6129
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5371
6130
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5372
6131
  confirmations: 1,
5373
6132
  fastConfirmations: 1
@@ -5426,6 +6185,7 @@ var EarnChain;
5426
6185
  v2: {
5427
6186
  type: 'split',
5428
6187
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6188
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5429
6189
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5430
6190
  confirmations: 1,
5431
6191
  fastConfirmations: 1
@@ -5741,6 +6501,7 @@ var EarnChain;
5741
6501
  v2: {
5742
6502
  type: 'split',
5743
6503
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6504
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5744
6505
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5745
6506
  confirmations: 65,
5746
6507
  fastConfirmations: 1
@@ -5805,6 +6566,7 @@ var EarnChain;
5805
6566
  v2: {
5806
6567
  type: 'split',
5807
6568
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6569
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5808
6570
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5809
6571
  confirmations: 65,
5810
6572
  fastConfirmations: 1
@@ -5862,6 +6624,7 @@ var EarnChain;
5862
6624
  v2: {
5863
6625
  type: 'split',
5864
6626
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cF5d',
6627
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5865
6628
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5866
6629
  confirmations: 65,
5867
6630
  fastConfirmations: 1
@@ -5921,6 +6684,7 @@ var EarnChain;
5921
6684
  v2: {
5922
6685
  type: 'split',
5923
6686
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
6687
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5924
6688
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5925
6689
  confirmations: 65,
5926
6690
  fastConfirmations: 1
@@ -5981,6 +6745,7 @@ var EarnChain;
5981
6745
  v2: {
5982
6746
  type: 'split',
5983
6747
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6748
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5984
6749
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5985
6750
  confirmations: 3,
5986
6751
  fastConfirmations: 3
@@ -6026,6 +6791,7 @@ var EarnChain;
6026
6791
  v2: {
6027
6792
  type: 'split',
6028
6793
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6794
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6029
6795
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6030
6796
  confirmations: 3,
6031
6797
  fastConfirmations: 1
@@ -6236,6 +7002,8 @@ var Chains = /*#__PURE__*/Object.freeze({
6236
7002
  OptimismSepolia: OptimismSepolia,
6237
7003
  Pharos: Pharos,
6238
7004
  PharosTestnet: PharosTestnet,
7005
+ Plasma: Plasma,
7006
+ PlasmaTestnet: PlasmaTestnet,
6239
7007
  Plume: Plume,
6240
7008
  PlumeTestnet: PlumeTestnet,
6241
7009
  PolkadotAssetHub: PolkadotAssetHub,
@@ -6285,6 +7053,71 @@ var Chains = /*#__PURE__*/Object.freeze({
6285
7053
  return chain.cctp?.contracts.v2 !== undefined;
6286
7054
  }
6287
7055
 
7056
+ /**
7057
+ * Chains the Fee Service accepts as a SOURCE for source-paid ("receive-exact")
7058
+ * CCTP v2 fees. An explicit allowlist is required because the
7059
+ * `TokenMessengerWithFees` wrapper address is now shared with the fast-deposit
7060
+ * forwarder path, so wrapper presence no longer implies source-fee support.
7061
+ * Keep in sync with backend coverage.
7062
+ */ const SOURCE_FEE_SUPPORTED_ALLOWLIST = new Set([
7063
+ // Mainnet
7064
+ Blockchain.Ethereum,
7065
+ Blockchain.Base,
7066
+ Blockchain.Arbitrum,
7067
+ Blockchain.Unichain,
7068
+ Blockchain.Optimism,
7069
+ Blockchain.Codex,
7070
+ Blockchain.Ink,
7071
+ Blockchain.Plume,
7072
+ Blockchain.Linea,
7073
+ Blockchain.World_Chain,
7074
+ // Testnet counterparts
7075
+ Blockchain.Ethereum_Sepolia,
7076
+ Blockchain.Base_Sepolia,
7077
+ Blockchain.Arbitrum_Sepolia,
7078
+ Blockchain.Unichain_Sepolia,
7079
+ Blockchain.Optimism_Sepolia,
7080
+ Blockchain.Codex_Testnet,
7081
+ Blockchain.Ink_Testnet,
7082
+ Blockchain.Plume_Testnet,
7083
+ Blockchain.Linea_Sepolia,
7084
+ Blockchain.World_Chain_Sepolia
7085
+ ]);
7086
+ /**
7087
+ * Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
7088
+ *
7089
+ * A chain qualifies when it supports CCTP v2, carries a `TokenMessengerWithFees`
7090
+ * wrapper, and is in {@link SOURCE_FEE_SUPPORTED_ALLOWLIST}.
7091
+ *
7092
+ * @param chain - The chain definition to check. A nullish or non-object value
7093
+ * returns `false` rather than throwing, since consumers may call from plain
7094
+ * JavaScript.
7095
+ * @returns `true` when the chain supports CCTP v2, carries a
7096
+ * `TokenMessengerWithFees` wrapper, and is on the source-fee allowlist;
7097
+ * `false` otherwise.
7098
+ *
7099
+ * @example
7100
+ * ```typescript
7101
+ * import { Chains, hasSourceFeeSupport } from '@core/chains'
7102
+ *
7103
+ * hasSourceFeeSupport(Chains.Optimism) // true
7104
+ * hasSourceFeeSupport(Chains.Solana) // false
7105
+ * ```
7106
+ */ function hasSourceFeeSupport(chain) {
7107
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for nullish/non-object input from plain JS
7108
+ if (chain === null || typeof chain !== 'object') {
7109
+ return false;
7110
+ }
7111
+ if (!isCCTPV2Supported(chain)) {
7112
+ return false;
7113
+ }
7114
+ const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
7115
+ if (typeof wrapper !== 'string' || wrapper.length === 0) {
7116
+ return false;
7117
+ }
7118
+ return SOURCE_FEE_SUPPORTED_ALLOWLIST.has(chain.chain);
7119
+ }
7120
+
6288
7121
  /**
6289
7122
  * Check if a chain supports a specific type of custom smart contract logic.
6290
7123
  *
@@ -6335,6 +7168,73 @@ var Chains = /*#__PURE__*/Object.freeze({
6335
7168
  return typeof contractAddress === 'string' && contractAddress.trim().length > 0;
6336
7169
  }
6337
7170
 
7171
+ /**
7172
+ * Check whether a given chain supports Gateway protocol version 1.
7173
+ *
7174
+ * This type guard function examines a chain definition to determine if it has Gateway v1
7175
+ * contract configurations. It checks that the chain has a gateway object with a
7176
+ * `contracts.v1` entry present.
7177
+ *
7178
+ * @param chain - The chain definition to check for Gateway v1 support
7179
+ * @returns `true` if `chain.gateway?.contracts?.v1` is defined, `false` otherwise
7180
+ *
7181
+ * @example
7182
+ * ```typescript
7183
+ * import { isGatewayV1Supported, Base } from '@core/chains'
7184
+ *
7185
+ * if (isGatewayV1Supported(Base)) {
7186
+ * // TypeScript knows Base.gateway is defined here
7187
+ * console.log('Gateway domain:', Base.gateway.domain)
7188
+ * console.log('Wallet address:', Base.gateway.contracts.v1.wallet)
7189
+ * console.log('Minter address:', Base.gateway.contracts.v1.minter)
7190
+ * }
7191
+ * ```
7192
+ *
7193
+ * @example
7194
+ * ```typescript
7195
+ * // Usage in conditional flow
7196
+ * function getGatewayWalletAddress(chain: ChainDefinition): string | null {
7197
+ * if (isGatewayV1Supported(chain)) {
7198
+ * return chain.gateway.contracts.v1.wallet
7199
+ * }
7200
+ * return null
7201
+ * }
7202
+ * ```
7203
+ */ function isGatewayV1Supported(chain) {
7204
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS consumers may pass a gateway object without contracts
7205
+ return chain.gateway?.contracts?.v1 !== undefined;
7206
+ }
7207
+
7208
+ /**
7209
+ * Temporary allowlist of chains permitted to initiate Gateway fast deposits.
7210
+ * Only chains keyed here are eligible; all others are rejected. Using the
7211
+ * {@link Blockchain} enum keeps entries type-safe and catches typos at compile
7212
+ * time. Remove this allowlist once roll-out is complete.
7213
+ */ new Set([
7214
+ // Mainnet
7215
+ Blockchain.Ethereum,
7216
+ Blockchain.Base,
7217
+ Blockchain.Arbitrum,
7218
+ Blockchain.Unichain,
7219
+ Blockchain.Optimism,
7220
+ Blockchain.Codex,
7221
+ Blockchain.Ink,
7222
+ Blockchain.Plume,
7223
+ Blockchain.Linea,
7224
+ Blockchain.World_Chain,
7225
+ // Testnet counterparts
7226
+ Blockchain.Ethereum_Sepolia,
7227
+ Blockchain.Base_Sepolia,
7228
+ Blockchain.Arbitrum_Sepolia,
7229
+ Blockchain.Unichain_Sepolia,
7230
+ Blockchain.Optimism_Sepolia,
7231
+ Blockchain.Codex_Testnet,
7232
+ Blockchain.Ink_Testnet,
7233
+ Blockchain.Plume_Testnet,
7234
+ Blockchain.Linea_Sepolia,
7235
+ Blockchain.World_Chain_Sepolia
7236
+ ]);
7237
+
6338
7238
  /**
6339
7239
  * Zod schema for validating Gateway v1 contract addresses.
6340
7240
  *
@@ -6356,7 +7256,10 @@ var Chains = /*#__PURE__*/Object.freeze({
6356
7256
  }).min(1, 'Gateway minter address cannot be empty.'),
6357
7257
  depositForHandler: z.string({
6358
7258
  invalid_type_error: 'Gateway depositForHandler address must be a string.'
6359
- }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
7259
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional(),
7260
+ genericExecutor: z.string({
7261
+ invalid_type_error: 'Gateway genericExecutor address must be a string.'
7262
+ }).min(1, 'Gateway genericExecutor address cannot be empty.').optional()
6360
7263
  }).strict() // Reject any additional properties not defined in the schema
6361
7264
  ;
6362
7265
  /**
@@ -8303,6 +9206,7 @@ const swapTokenEnumSchema = z.enum([
8303
9206
  [Blockchain.Noble]: 'uusdc',
8304
9207
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
8305
9208
  [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
9209
+ [Blockchain.Plasma]: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
8306
9210
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
8307
9211
  [Blockchain.Polkadot_Asset_Hub]: '1337',
8308
9212
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -8339,6 +9243,7 @@ const swapTokenEnumSchema = z.enum([
8339
9243
  [Blockchain.Noble_Testnet]: 'uusdc',
8340
9244
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
8341
9245
  [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
9246
+ [Blockchain.Plasma_Testnet]: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
8342
9247
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
8343
9248
  [Blockchain.Polkadot_Westmint]: '31337',
8344
9249
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -8401,6 +9306,7 @@ const swapTokenEnumSchema = z.enum([
8401
9306
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
8402
9307
  [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
8403
9308
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
9309
+ [Blockchain.Plasma]: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
8404
9310
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
8405
9311
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
8406
9312
  // =========================================================================
@@ -8409,7 +9315,8 @@ const swapTokenEnumSchema = z.enum([
8409
9315
  [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
8410
9316
  [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
8411
9317
  [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
8412
- [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
9318
+ [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
9319
+ [Blockchain.Plasma_Testnet]: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330'
8413
9320
  }
8414
9321
  };
8415
9322
 
@@ -9223,6 +10130,9 @@ const swapTokenEnumSchema = z.enum([
9223
10130
  * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9224
10131
  * This prefix is right-padded to 24 bytes in the final hookData.
9225
10132
  */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
10133
+ /**
10134
+ * Maximum value of the 32-bit `version` field in a `cctp-forward` frame.
10135
+ */ const MAX_UINT32 = 0xffffffff;
9226
10136
  /**
9227
10137
  * CCTP forwarding version number.
9228
10138
  *
@@ -9233,6 +10143,16 @@ const swapTokenEnumSchema = z.enum([
9233
10143
  *
9234
10144
  * Set to 0 when no additional Circle-reserved data is needed.
9235
10145
  */ const CCTP_FORWARD_PAYLOAD_LENGTH = 0;
10146
+ /**
10147
+ * Length in bytes of a Solana owner (ed25519 / PDA) public key.
10148
+ */ const SOLANA_PUBKEY_LENGTH = 32;
10149
+ /**
10150
+ * Byte length of the Solana ATA-creation forwarding payload appended after the
10151
+ * `cctp-forward` frame: `createAta` (1 byte) + `ataOwner` (32 bytes).
10152
+ *
10153
+ * Circle's Orbit relayer decodes exactly this many bytes; see
10154
+ * {@link buildSolanaAtaForwardingHookData}.
10155
+ */ const SOLANA_ATA_FORWARD_PAYLOAD_LENGTH = 1 + SOLANA_PUBKEY_LENGTH;
9236
10156
  /**
9237
10157
  * Build the hookData bytes for CCTP forwarding.
9238
10158
  *
@@ -9289,6 +10209,518 @@ function buildForwardingHookData() {
9289
10209
  cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
9290
10210
  return cachedHookDataHex;
9291
10211
  }
10212
+ /**
10213
+ * Build a `cctp-forward` hookData frame with a versioned header and an appended
10214
+ * opaque payload.
10215
+ *
10216
+ * Produces the 32-byte `cctp-forward` header (24-byte ASCII magic + `uint32`
10217
+ * version + `uint32` `dataLength = 0`) followed by `payload` appended verbatim.
10218
+ * Unlike {@link buildForwardingHookData} — which emits only the fixed,
10219
+ * version-0 empty frame — this lets the caller set the frame `version` and
10220
+ * carry an inner payload such as a GenericExecutor blob.
10221
+ *
10222
+ * @remarks
10223
+ * The forwarder reads only the 32-byte header to decide that a hook is
10224
+ * forwardable, then strips it before the inner payload is consumed downstream
10225
+ * (e.g. the GenericExecutor `abi.decode`s the appended blob, never the frame).
10226
+ * `dataLength` stays `0` because the appended bytes are opaque to the forwarder
10227
+ * — it is not the payload's length.
10228
+ *
10229
+ * @param version - The `uint32` frame version (e.g. `1` for the GenericExecutor
10230
+ * FORWARD path). Must be an integer in `[0, 0xFFFFFFFF]`.
10231
+ * @param payload - A 0x-prefixed hex string appended after the header (e.g. the
10232
+ * bare GenericExecutor blob from `buildDepositForGenericExecutorPayload`).
10233
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by `payload`.
10234
+ * @throws {KitError} If `version` is out of `uint32` range or `payload` is not
10235
+ * a 0x-prefixed hex string (INPUT_VALIDATION_FAILED).
10236
+ *
10237
+ * @example
10238
+ * ```typescript
10239
+ * import {
10240
+ * buildDepositForGenericExecutorPayload,
10241
+ * buildForwardingHookDataWithPayload,
10242
+ * padAddressToBytes32,
10243
+ * } from '@core/utils'
10244
+ *
10245
+ * const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
10246
+ * dappId: 'gateway_deposit',
10247
+ * domainId: 26,
10248
+ * handler: '0xHandlerAddressOnDestinationChain',
10249
+ * params: [USDC_ARC, user, 0],
10250
+ * recoveryAddress: padAddressToBytes32(user),
10251
+ * })
10252
+ *
10253
+ * // Wrap for the prepaid Quote-API FORWARD path (frame version 1).
10254
+ * const hookData = buildForwardingHookDataWithPayload(1, geBlob)
10255
+ * ```
10256
+ */ function buildForwardingHookDataWithPayload(version, payload) {
10257
+ if (!Number.isInteger(version) || version < 0 || version > MAX_UINT32) {
10258
+ throw createValidationFailedError$1('version', version, 'Expected an integer in the uint32 range [0, 4294967295]');
10259
+ }
10260
+ if (!isHexString(payload)) {
10261
+ throw createValidationFailedError$1('payload', payload, 'Expected a 0x-prefixed hex string');
10262
+ }
10263
+ // `isHexString` accepts odd-length hex (e.g. '0xabc'); catch it here so it
10264
+ // surfaces as a KitError rather than ethers' raw "hex data is odd-length"
10265
+ // from `concat` below.
10266
+ if (payload.length % 2 !== 0) {
10267
+ throw createValidationFailedError$1('payload', payload, 'Expected an even-length (whole-byte) hex string');
10268
+ }
10269
+ // 32-byte header: 24-byte magic + uint32 version + uint32 dataLength (0).
10270
+ const frame = new Uint8Array(32);
10271
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
10272
+ const view = new DataView(frame.buffer);
10273
+ view.setUint32(24, version, false) // big-endian
10274
+ ;
10275
+ view.setUint32(28, CCTP_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 0
10276
+ ;
10277
+ return hexlify(concat([
10278
+ frame,
10279
+ payload
10280
+ ]));
10281
+ }
10282
+ /**
10283
+ * Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
10284
+ * create the recipient's Associated Token Account (ATA) before minting on Solana.
10285
+ *
10286
+ * When an EVM→Solana bridge is forwarded, the destination mint targets the
10287
+ * recipient's USDC ATA — which does not exist for a fresh wallet. This frame
10288
+ * tells the relayer to prepend an idempotent `createAssociatedTokenAccount`
10289
+ * instruction (the relayer pays the rent) so the mint always succeeds.
10290
+ *
10291
+ * Unlike {@link buildForwardingHookData} (an empty version-0 frame), this emits
10292
+ * a version-0 frame whose 32-bit `dataLength` is set to
10293
+ * {@link SOLANA_ATA_FORWARD_PAYLOAD_LENGTH} (33), followed by the payload the
10294
+ * relayer decodes:
10295
+ * - Byte 0: `createAta` flag, always `1`
10296
+ * - Bytes 1-32: the recipient's 32-byte Solana owner public key (`ataOwner`)
10297
+ *
10298
+ * @remarks
10299
+ * `ataOwner` is the recipient's *wallet* public key, not the derived ATA. The
10300
+ * relayer re-derives the ATA from `ataOwner` and the USDC mint and requires it
10301
+ * to equal the burn's `mintRecipient`, so callers must pass the same owner used
10302
+ * to derive `mintRecipient`. The all-zero key is reserved as "absent owner" and
10303
+ * is rejected.
10304
+ *
10305
+ * @param ataOwner - The recipient's 32-byte Solana owner public key.
10306
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by the 33-byte
10307
+ * Solana ATA payload.
10308
+ * @throws {KitError} If `ataOwner` is not exactly 32 bytes, or is the all-zero
10309
+ * key (INPUT_VALIDATION_FAILED).
10310
+ *
10311
+ * @example
10312
+ * ```typescript
10313
+ * import { PublicKey } from '@solana/web3.js'
10314
+ * import { buildSolanaAtaForwardingHookData } from '@core/utils'
10315
+ *
10316
+ * const owner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM')
10317
+ * const hookData = buildSolanaAtaForwardingHookData(owner.toBytes())
10318
+ *
10319
+ * // Use with the forwarded depositForBurnWithHook action so the relayer
10320
+ * // creates the recipient ATA before minting.
10321
+ * await adapter.prepareAction('cctp.v2.depositForBurnWithHook', {
10322
+ * amount: BigInt('1000000'),
10323
+ * mintRecipient: '0x...',
10324
+ * maxFee: BigInt('50000'),
10325
+ * minFinalityThreshold: 1000,
10326
+ * fromChain: ethereum,
10327
+ * toChain: solana,
10328
+ * hookData,
10329
+ * })
10330
+ * ```
10331
+ */ function buildSolanaAtaForwardingHookData(ataOwner) {
10332
+ if (!(ataOwner instanceof Uint8Array) || ataOwner.length !== SOLANA_PUBKEY_LENGTH) {
10333
+ throw createValidationFailedError$1('ataOwner', ataOwner, `Expected a ${String(SOLANA_PUBKEY_LENGTH)}-byte Solana owner public key`);
10334
+ }
10335
+ if (ataOwner.every((byte)=>byte === 0)) {
10336
+ throw createValidationFailedError$1('ataOwner', ataOwner, 'Expected a non-zero Solana owner public key; the all-zero key is reserved as "absent owner"');
10337
+ }
10338
+ // Inner payload: createAta(1) + ataOwner(32).
10339
+ const payload = new Uint8Array(SOLANA_ATA_FORWARD_PAYLOAD_LENGTH);
10340
+ payload[0] = 1 // createAta = true
10341
+ ;
10342
+ payload.set(ataOwner, 1);
10343
+ // 32-byte header: 24-byte magic + uint32 version(0) + uint32 dataLength(33).
10344
+ // The relayer reads dataLength from the v0 frame to slice the inner payload,
10345
+ // so it MUST reflect the appended byte count (unlike the GenericExecutor path).
10346
+ const frame = new Uint8Array(32);
10347
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
10348
+ const view = new DataView(frame.buffer);
10349
+ view.setUint32(24, CCTP_FORWARD_VERSION, false) // big-endian, 0
10350
+ ;
10351
+ view.setUint32(28, SOLANA_ATA_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 33
10352
+ ;
10353
+ return hexlify(concat([
10354
+ frame,
10355
+ payload
10356
+ ]));
10357
+ }
10358
+
10359
+ /**
10360
+ * `version` field of the GenericExecutor hookData, in both the
10361
+ * `circle-generic-executor` header (`uint32`) and the ABI tuple (`uint8`). The
10362
+ * executor reverts if it is not `1`.
10363
+ *
10364
+ * @see https://circlepay.atlassian.net/wiki/spaces/~712020cd79585b52ea4353b4720c277fbfcca6/pages/3049291839
10365
+ */ const GENERIC_EXECUTOR_HOOK_DATA_VERSION = 1;
10366
+ /**
10367
+ * ASCII magic that prefixes a GenericExecutor hookData blob.
10368
+ *
10369
+ * The executor auto-detects its payload by this string. It is left-aligned and
10370
+ * zero-padded to 24 bytes in the header, mirroring the `cctp-forward` frame
10371
+ * layout (magic + `uint32` version + `uint32` dataLength).
10372
+ */ const GENERIC_EXECUTOR_MAGIC_PREFIX = 'circle-generic-executor';
10373
+ /**
10374
+ * Prepend the 32-byte `circle-generic-executor` header to the ABI tuple.
10375
+ *
10376
+ * Header layout (mirrors the `cctp-forward` frame): 24-byte zero-padded ASCII
10377
+ * magic + `uint32` version + `uint32` dataLength. Unlike the `cctp-forward`
10378
+ * frame (which the forwarder strips and so carries `dataLength = 0`), this
10379
+ * header's dataLength is the byte length of the ABI tuple that follows, since
10380
+ * the executor consumes both.
10381
+ */ function prependGenericExecutorHeader(abiTuple) {
10382
+ const header = new Uint8Array(32);
10383
+ header.set(new TextEncoder().encode(GENERIC_EXECUTOR_MAGIC_PREFIX), 0);
10384
+ // Byte length of the ABI tuple that the header announces.
10385
+ const tupleByteLength = (abiTuple.length - 2) / 2;
10386
+ const view = new DataView(header.buffer);
10387
+ view.setUint32(24, GENERIC_EXECUTOR_HOOK_DATA_VERSION, false) // big-endian
10388
+ ;
10389
+ view.setUint32(28, tupleByteLength, false) // big-endian
10390
+ ;
10391
+ return hexlify(concat([
10392
+ header,
10393
+ abiTuple
10394
+ ]));
10395
+ }
10396
+ /**
10397
+ * Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
10398
+ *
10399
+ * Mirrors viem's `pad(address, size 32)` and CCTP's `mintRecipient`
10400
+ * convention. Solana addresses are already 32 bytes and need no padding.
10401
+ *
10402
+ * @param address - A 0x-prefixed 20-byte EVM address.
10403
+ * @returns The address left-zero-padded to a 0x-prefixed 32-byte hex string.
10404
+ * @throws {KitError} If `address` is not a valid EVM address (INPUT_VALIDATION_FAILED).
10405
+ *
10406
+ * @example
10407
+ * ```typescript
10408
+ * import { padAddressToBytes32 } from '@core/utils'
10409
+ *
10410
+ * padAddressToBytes32('0x75275Aff2D01699D922f045b69ed291311209738')
10411
+ * // '0x00000000000000000000000075275aff2d01699d922f045b69ed291311209738'
10412
+ * ```
10413
+ */ function padAddressToBytes32(address) {
10414
+ if (!isAddress(address)) {
10415
+ throw createValidationFailedError$1('address', address, 'Expected a valid 20-byte EVM address');
10416
+ }
10417
+ // bytes32 is raw bytes, not a checksummed address — emit lowercase so it
10418
+ // matches ABI-decoded output.
10419
+ return hexZeroPad(getAddress(address), 32).toLowerCase();
10420
+ }
10421
+ /**
10422
+ * Encode the bare GenericExecutor + DepositForHandler payload for a CCTP v2
10423
+ * fast-transfer deposit into a dApp.
10424
+ *
10425
+ * Builds the layers inner→outer:
10426
+ * 1. dApp calldata — the dApp function selector + ABI params, with each amount
10427
+ * slot left as the caller-supplied placeholder.
10428
+ * 2. handler calldata — `(depositContract, approvalTarget, depositCalldata, amountIndices)`
10429
+ * for `DepositForHandler`.
10430
+ * 3. ABI tuple — `(uint8 version, bytes32 recoveryAddress, address handler, bytes handlerCalldata)`.
10431
+ * No handler selector travels on the wire; the executor applies a fixed one.
10432
+ * 4. header — the 32-byte `circle-generic-executor` magic frame prepended to the
10433
+ * tuple, by which the executor auto-detects the payload.
10434
+ *
10435
+ * The returned `hookData` is the bare GenericExecutor blob (header ‖ tuple) — the
10436
+ * exact bytes the executor consumes. It carries no `cctp-forward` envelope. For
10437
+ * the prepaid Quote-API FORWARD path, wrap it with
10438
+ * {@link buildForwardingHookDataWithPayload}; the forwarder strips that envelope
10439
+ * before the executor reads the blob.
10440
+ *
10441
+ * @param options - See {@link BuildDepositForGenericExecutorPayloadParams}.
10442
+ * @returns The encoded {@link DepositForGenericExecutorPayload} layers.
10443
+ * @throws {KitError} If `options` is not an object, `dappId` is unknown,
10444
+ * `config.deployments` is not an array, neither `domainId` nor
10445
+ * `destinationChain` resolves a domain (or the two disagree), no deployment
10446
+ * exists for the resolved domain, the deposit contract cannot be resolved (a
10447
+ * built-in deployment supplied without a `destinationChain`), no `handler` is
10448
+ * supplied and it cannot be resolved from `destinationChain`,
10449
+ * `recoveryAddress`/`handler`/contract addresses are malformed,
10450
+ * `config.function` is not a valid Solidity function signature, `params`
10451
+ * length does not match the dApp signature, `params` values fail ABI encoding
10452
+ * (type mismatch), `config.dynamicAmountIndices` is not an array, or an amount
10453
+ * index is out of range (all INPUT_VALIDATION_FAILED).
10454
+ *
10455
+ * @example Encode a 1-click cross-chain Circle Gateway deposit
10456
+ * ```typescript
10457
+ * import { buildDepositForGenericExecutorPayload, padAddressToBytes32 } from '@core/utils'
10458
+ * import { ArcTestnet } from '@core/chains'
10459
+ *
10460
+ * const user = '0x75275Aff2D01699D922f045b69ed291311209738'
10461
+ * const usdcArc = '0x3600000000000000000000000000000000000000'
10462
+ * const { hookData } = buildDepositForGenericExecutorPayload({
10463
+ * dappId: 'gateway_deposit',
10464
+ * destinationChain: ArcTestnet, // resolves the GatewayWallet + DepositForHandler
10465
+ * // depositFor(address token, address depositor, uint256 value), amount idx [2]
10466
+ * params: [usdcArc, user, 0],
10467
+ * recoveryAddress: padAddressToBytes32(user),
10468
+ * })
10469
+ *
10470
+ * // Pass hookData straight into the CCTP v2 fast transfer.
10471
+ * console.log(hookData)
10472
+ * ```
10473
+ */ function buildDepositForGenericExecutorPayload(options) {
10474
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for plain-JS callers
10475
+ if (options === null || typeof options !== 'object') {
10476
+ throw createValidationFailedError$1('options', options, 'Expected an options object');
10477
+ }
10478
+ const { dappId, params: dappParams } = options;
10479
+ const registry = options.config ?? DAPP_CONFIG;
10480
+ // Look up the dApp before resolving the deposit contract or handler, so an
10481
+ // unknown dApp reports the actionable "Unknown dApp" error rather than an
10482
+ // unresolved-handler error.
10483
+ const config = registry[dappId];
10484
+ if (config === undefined) {
10485
+ throw createValidationFailedError$1('dappId', dappId, `Unknown dApp. Known dApps: ${Object.keys(registry).join(', ')}`);
10486
+ }
10487
+ if (!Array.isArray(config.deployments)) {
10488
+ throw createValidationFailedError$1('deployments', config.deployments, `Expected an array of deployments for dApp '${dappId}'`);
10489
+ }
10490
+ // Array.isArray narrows to `any[]`; re-assert the concrete type.
10491
+ const deployments = config.deployments;
10492
+ // Destination CCTP domain: taken from `destinationChain` when supplied (the
10493
+ // chain is the (network, domain) key), else the explicit `domainId`.
10494
+ const domainId = resolveDomainId(options);
10495
+ const deployment = deployments.find((d)=>d.domainId === domainId);
10496
+ if (deployment === undefined) {
10497
+ throw createValidationFailedError$1('domainId', domainId, `No '${dappId}' deployment for domain ${String(domainId)}`);
10498
+ }
10499
+ if (!Array.isArray(dappParams)) {
10500
+ throw createValidationFailedError$1('params', dappParams, 'Expected an array of ABI-ordered parameters');
10501
+ }
10502
+ if (!isHexString(options.recoveryAddress, 32)) {
10503
+ throw createValidationFailedError$1('recoveryAddress', options.recoveryAddress, 'Expected a 0x-prefixed 32-byte (bytes32) hex string');
10504
+ }
10505
+ // Deposit contract: the deployment's own address, or — for the built-in
10506
+ // gateway_deposit deployment, which carries none — the destination chain's
10507
+ // GatewayWallet, so the address lives only in @core/chains.
10508
+ const resolvedDepositContract = resolveDepositContract(deployment, options, dappId);
10509
+ // Handler: an explicit `handler` always wins; otherwise resolve the
10510
+ // DepositForHandler from `destinationChain`. The chain is the (network, domain)
10511
+ // key, so a shared CCTP domain (Arc is 26 on both testnet and mainnet) can
10512
+ // never resolve the wrong network's handler and strand funds.
10513
+ const handler = resolveDepositForHandler(options, domainId);
10514
+ if (!isAddress(handler)) {
10515
+ throw createValidationFailedError$1('handler', handler, 'Expected a valid EVM address');
10516
+ }
10517
+ const depositContract = assertAddress(resolvedDepositContract, 'depositContract');
10518
+ const approvalTarget = assertAddress(deployment.approvalTarget ?? resolvedDepositContract, 'approvalTarget');
10519
+ // 1. dApp calldata: selector + ABI-encoded params (amount slots stay as placeholders).
10520
+ let dappInterface;
10521
+ try {
10522
+ dappInterface = new Interface([
10523
+ `function ${config.function}`
10524
+ ]);
10525
+ } catch {
10526
+ throw createValidationFailedError$1('function', config.function, 'Expected a valid Solidity function signature');
10527
+ }
10528
+ const rawFragment = dappInterface.fragments[0];
10529
+ /* v8 ignore start -- defensive: guards against unexpected library behavior */ if (rawFragment === undefined || rawFragment.type !== 'function') {
10530
+ throw createValidationFailedError$1('function', config.function, 'Expected a valid Solidity function signature');
10531
+ }
10532
+ /* v8 ignore stop */ const fragment = rawFragment;
10533
+ if (dappParams.length !== fragment.inputs.length) {
10534
+ throw createValidationFailedError$1('params', dappParams, `'${config.function}' expects ${String(fragment.inputs.length)} params, got ${String(dappParams.length)}`);
10535
+ }
10536
+ let depositCalldata;
10537
+ try {
10538
+ depositCalldata = dappInterface.encodeFunctionData(fragment, dappParams);
10539
+ } catch {
10540
+ throw createValidationFailedError$1('params', dappParams, 'ABI encoding failed — check that each param matches the expected Solidity type');
10541
+ }
10542
+ if (!Array.isArray(config.dynamicAmountIndices)) {
10543
+ throw createValidationFailedError$1('dynamicAmountIndices', config.dynamicAmountIndices, `Expected an array of amount indices for dApp '${dappId}'`);
10544
+ }
10545
+ // Array.isArray narrows to `any[]`; re-assert the concrete type.
10546
+ const dynamicAmountIndices = config.dynamicAmountIndices;
10547
+ // Amount byte offsets in depositCalldata: 4 (selector) + paramIndex * 32.
10548
+ const amountIndices = dynamicAmountIndices.map((paramIndex)=>{
10549
+ if (!Number.isInteger(paramIndex) || paramIndex < 0 || paramIndex >= fragment.inputs.length) {
10550
+ throw createValidationFailedError$1('dynamicAmountIndices', paramIndex, `Index out of range for '${config.function}' (${String(fragment.inputs.length)} params)`);
10551
+ }
10552
+ return BigInt(4 + paramIndex * 32);
10553
+ });
10554
+ // 2. Handler layer.
10555
+ const handlerCalldata = defaultAbiCoder.encode([
10556
+ 'address',
10557
+ 'address',
10558
+ 'bytes',
10559
+ 'uint256[]'
10560
+ ], [
10561
+ depositContract,
10562
+ approvalTarget,
10563
+ depositCalldata,
10564
+ amountIndices
10565
+ ]);
10566
+ // 3. Executor ABI tuple. No handler selector travels on the wire — the
10567
+ // executor applies a fixed selector internally.
10568
+ const executorTuple = defaultAbiCoder.encode([
10569
+ 'uint8',
10570
+ 'bytes32',
10571
+ 'address',
10572
+ 'bytes'
10573
+ ], [
10574
+ GENERIC_EXECUTOR_HOOK_DATA_VERSION,
10575
+ options.recoveryAddress,
10576
+ getAddress(handler),
10577
+ handlerCalldata
10578
+ ]);
10579
+ // 4. Prepend the circle-generic-executor magic header; this is the final
10580
+ // bare GE blob the executor consumes.
10581
+ const hookData = prependGenericExecutorHeader(executorTuple);
10582
+ return {
10583
+ hookData,
10584
+ handlerCalldata,
10585
+ depositCalldata,
10586
+ amountIndices,
10587
+ depositContract,
10588
+ approvalTarget
10589
+ };
10590
+ }
10591
+ /**
10592
+ * Config key of the built-in Circle Gateway deposit dApp. Its deposit contract
10593
+ * is the destination chain's GatewayWallet, resolved from `destinationChain`
10594
+ * (not a hardcoded address), so this is the only dApp whose deployment may omit
10595
+ * `depositContract`.
10596
+ */ const GATEWAY_DEPOSIT_DAPP_ID = 'gateway_deposit';
10597
+ /**
10598
+ * Built-in dApp registry. Adding a new `depositFor`-style dApp is a config entry
10599
+ * here (or via {@link BuildDepositForGenericExecutorPayloadParams.config}).
10600
+ *
10601
+ * @remarks
10602
+ * Only dApps with confirmed deployment addresses and active callers are included.
10603
+ * The built-in `gateway_deposit` entry omits `depositContract` — it is resolved
10604
+ * from the destination chain's GatewayWallet (`@core/chains`) rather than
10605
+ * duplicated here. Pass a custom registry via `config` for unlisted dApps.
10606
+ */ const DAPP_CONFIG = {
10607
+ // Circle Gateway: depositFor(address token, address depositor, uint256 value)
10608
+ [GATEWAY_DEPOSIT_DAPP_ID]: {
10609
+ function: 'depositFor(address,address,uint256)',
10610
+ dynamicAmountIndices: [
10611
+ 2
10612
+ ],
10613
+ deployments: [
10614
+ // Arc Testnet (CCTP domain 26). The deposit contract is the chain's
10615
+ // GatewayWallet, resolved from `destinationChain` (@core/chains) rather
10616
+ // than duplicated here.
10617
+ {
10618
+ domainId: 26
10619
+ }
10620
+ ]
10621
+ }
10622
+ };
10623
+ /**
10624
+ * Resolve the destination CCTP domain for an encode request.
10625
+ *
10626
+ * Prefers {@link BuildDepositForGenericExecutorPayloadParams.destinationChain}
10627
+ * (`chain.cctp.domain`) — the chain is the (network, domain) key. Falls back to
10628
+ * an explicit `domainId`. When both are supplied they must agree.
10629
+ *
10630
+ * @param options - The encode request.
10631
+ * @returns The destination CCTP domain.
10632
+ * @throws {KitError} If no domain is available, or `domainId` disagrees with
10633
+ * `destinationChain` (INPUT_VALIDATION_FAILED).
10634
+ * @internal
10635
+ */ function resolveDomainId(options) {
10636
+ const chain = options.destinationChain;
10637
+ if (chain !== undefined) {
10638
+ const chainDomain = chain.cctp?.domain;
10639
+ if (chainDomain !== undefined) {
10640
+ if (options.domainId !== undefined && options.domainId !== chainDomain) {
10641
+ throw createValidationFailedError$1('domainId', options.domainId, `does not match destinationChain '${chain.name}' CCTP domain ` + String(chainDomain));
10642
+ }
10643
+ return chainDomain;
10644
+ }
10645
+ }
10646
+ if (options.domainId !== undefined) {
10647
+ return options.domainId;
10648
+ }
10649
+ throw createValidationFailedError$1('domainId', options.domainId, "Provide 'domainId', or a 'destinationChain' with a CCTP domain");
10650
+ }
10651
+ /**
10652
+ * Resolve the deposit contract the handler calls.
10653
+ *
10654
+ * Uses the deployment's own `depositContract` when present. Only the built-in
10655
+ * {@link GATEWAY_DEPOSIT_DAPP_ID} may omit it: its deposit contract is the
10656
+ * destination chain's Gateway v1 wallet, resolved from
10657
+ * {@link BuildDepositForGenericExecutorPayloadParams.destinationChain} so the
10658
+ * address is owned once in `@core/chains`. Any other dApp that omits
10659
+ * `depositContract` is a config error and fails here rather than silently
10660
+ * targeting the GatewayWallet.
10661
+ *
10662
+ * @param deployment - The resolved dApp deployment.
10663
+ * @param options - The encode request.
10664
+ * @param dappId - The dApp key, checked against {@link GATEWAY_DEPOSIT_DAPP_ID}.
10665
+ * @returns The deposit contract address (unvalidated; the caller checks it).
10666
+ * @throws {KitError} If a non-`gateway_deposit` deployment omits
10667
+ * `depositContract`, or if `gateway_deposit` has no Gateway v1
10668
+ * `destinationChain` to resolve one (INPUT_VALIDATION_FAILED).
10669
+ * @internal
10670
+ */ function resolveDepositContract(deployment, options, dappId) {
10671
+ if (deployment.depositContract !== undefined) {
10672
+ return deployment.depositContract;
10673
+ }
10674
+ // Only gateway_deposit may omit its address (it targets the chain's
10675
+ // GatewayWallet). Any other addressless deployment is a config mistake and
10676
+ // must fail rather than silently resolve to the GatewayWallet.
10677
+ if (dappId !== GATEWAY_DEPOSIT_DAPP_ID) {
10678
+ 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");
10679
+ }
10680
+ const chain = options.destinationChain;
10681
+ if (chain === undefined || !isGatewayV1Supported(chain)) {
10682
+ 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');
10683
+ }
10684
+ return chain.gateway.contracts.v1.wallet;
10685
+ }
10686
+ /**
10687
+ * Resolve the `DepositForHandler` address for an encode request.
10688
+ *
10689
+ * An explicit `options.handler` always wins. Otherwise the handler is resolved
10690
+ * from {@link BuildDepositForGenericExecutorPayloadParams.destinationChain}
10691
+ * (`chain.gateway.contracts.v1.depositForHandler`). The chain is the (network,
10692
+ * domain) key, so a CCTP domain shared across a chain's testnet and mainnet
10693
+ * cannot resolve the wrong network's handler; a missing chain or an unregistered
10694
+ * handler throws rather than guessing.
10695
+ *
10696
+ * @param options - The encode request.
10697
+ * @param domainId - The resolved destination domain, reported in the error.
10698
+ * @returns The resolved handler address (unvalidated; the caller checks it).
10699
+ * @throws {KitError} If `handler` is omitted and cannot be resolved
10700
+ * (INPUT_VALIDATION_FAILED).
10701
+ * @internal
10702
+ */ function resolveDepositForHandler(options, domainId) {
10703
+ if (options.handler !== undefined) {
10704
+ return options.handler;
10705
+ }
10706
+ const chain = options.destinationChain;
10707
+ if (chain === undefined) {
10708
+ throw createValidationFailedError$1('handler', domainId, "No 'handler' supplied; pass 'handler' explicitly, or a " + "'destinationChain' whose Gateway config registers a DepositForHandler");
10709
+ }
10710
+ const registered = isGatewayV1Supported(chain) ? chain.gateway.contracts.v1.depositForHandler : undefined;
10711
+ if (registered === undefined) {
10712
+ throw createValidationFailedError$1('handler', domainId, `No DepositForHandler registered for domain ${String(domainId)} on ` + `chain '${chain.name}'; pass 'handler' explicitly`);
10713
+ }
10714
+ return registered;
10715
+ }
10716
+ /**
10717
+ * Validate and checksum an EVM address, throwing a consistent validation error.
10718
+ */ function assertAddress(address, field) {
10719
+ if (!isAddress(address)) {
10720
+ throw createValidationFailedError$1(field, address, 'Expected a valid EVM address');
10721
+ }
10722
+ return getAddress(address);
10723
+ }
9292
10724
 
9293
10725
  /**
9294
10726
  * Configuration for {@link retryAsync}.
@@ -9378,7 +10810,7 @@ function resolveOptions(options) {
9378
10810
  * allowlisted {@link ClientLogPayload} fields (and the allowlisted
9379
10811
  * sub-fields of `errorDetails` / `clientContext`) are copied across.
9380
10812
  * A regressing upstream mapper — or a plain-JS caller that bypasses the
9381
- * type — therefore cannot exfiltrate stray properties (secrets, PII,
10813
+ * type — therefore cannot exfiltrate stray properties (secrets,
9382
10814
  * raw error stacks) through the analytics channel. Optional fields are
9383
10815
  * only included when present so the serialised shape matches the
9384
10816
  * server's strict schema.
@@ -9401,6 +10833,9 @@ function resolveOptions(options) {
9401
10833
  if (payload.destinationChain !== undefined) safe['destinationChain'] = payload.destinationChain;
9402
10834
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
9403
10835
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
10836
+ if (payload.amountIn !== undefined) safe['amountIn'] = payload.amountIn;
10837
+ if (payload.durationMs !== undefined) safe['durationMs'] = payload.durationMs;
10838
+ if (payload.sourceAddress !== undefined) safe['sourceAddress'] = payload.sourceAddress;
9404
10839
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
9405
10840
  if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
9406
10841
  if (payload.errorDetails !== undefined) {
@@ -9597,22 +11032,36 @@ function resolveOptions(options) {
9597
11032
  }
9598
11033
 
9599
11034
  /**
9600
- * Soft signal for the case where building or emitting a telemetry payload
9601
- * threw — for example, a buggy `TelemetryContextResolver`, a regression in
11035
+ * Emit a stable console warning when building or emitting a telemetry payload
11036
+ * throws — for example, a buggy `TelemetryContextResolver`, a regression in
9602
11037
  * `extractErrorDetails`, or a synchronous failure inside `emitAnalyticsLog`
9603
- * before it could swallow the error itself. Logged with a stable prefix so
9604
- * consumers can grep for it. We deliberately do not re-throw: the caller's
9605
- * original operation error must always win.
11038
+ * before it could swallow the error itself. Uses a stable prefix so the
11039
+ * drop is discoverable via grep. Never re-throws: the caller's original
11040
+ * operation error must always win.
9606
11041
  *
9607
11042
  * @internal
9608
- */ function warnTelemetryDrop(eventType, cause) {
9609
- try {
9610
- // Pass `cause` as the second console.warn argument rather than
9611
- // string-coercing it. `String(err)` (and `err.message` alone)
9612
- // discards the stack trace, nested `cause`, and any custom Error
9613
- // properties — exactly the context an on-call needs when a
9614
- // resolver-closure regression triggers this path.
9615
- console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
11043
+ *
11044
+ * @param eventType - The telemetry event type that was being emitted.
11045
+ * @param cause - The error or value that caused the drop.
11046
+ *
11047
+ * @example
11048
+ * ```typescript
11049
+ * import { warnTelemetryDrop } from '@core/utils'
11050
+ *
11051
+ * try {
11052
+ * void emitAnalyticsLog(payload)
11053
+ * } catch (err) {
11054
+ * warnTelemetryDrop('my_event', err)
11055
+ * }
11056
+ * ```
11057
+ */ function warnTelemetryDrop(eventType, cause) {
11058
+ try {
11059
+ // Pass `cause` as the second console.warn argument rather than
11060
+ // string-coercing it. `String(err)` (and `err.message` alone)
11061
+ // discards the stack trace, nested `cause`, and any custom Error
11062
+ // properties — exactly the context an on-call needs when a
11063
+ // resolver-closure regression triggers this path.
11064
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
9616
11065
  } catch {
9617
11066
  // console.warn itself throwing is the user's environment; nothing more we
9618
11067
  // can do without risking the original operation error.
@@ -9644,6 +11093,9 @@ function resolveOptions(options) {
9644
11093
  ...context?.tokenOut != null && {
9645
11094
  tokenOut: context.tokenOut
9646
11095
  },
11096
+ ...context?.amountIn != null && {
11097
+ amountIn: context.amountIn
11098
+ },
9647
11099
  ...context?.txHash != null && {
9648
11100
  txHash: context.txHash
9649
11101
  },
@@ -9745,7 +11197,7 @@ function resolveOptions(options) {
9745
11197
  const stepEntry = stepEventMap.find(([name])=>name === failedStep?.name);
9746
11198
  // `failedStep.errorMessage` is intentionally **not** copied into the payload.
9747
11199
  // Provider messages are unbounded and frequently contain operator data
9748
- // (addresses, signatures, partial intent payloads, raw RPC responses). The
11200
+ // (signatures, partial intent payloads, raw RPC responses). The
9749
11201
  // step name plus the surrounding context fields already identify *which*
9750
11202
  // phase failed; the *why* is left to the corresponding step-level logs that
9751
11203
  // the provider emits separately. The thrown-error path (`extractErrorDetails`
@@ -9762,7 +11214,7 @@ function resolveOptions(options) {
9762
11214
  }
9763
11215
 
9764
11216
  var name$2 = "@circle-fin/bridge-kit";
9765
- var version$3 = "1.13.0";
11217
+ var version$3 = "1.14.1";
9766
11218
  var pkg$3 = {
9767
11219
  name: name$2,
9768
11220
  version: version$3};
@@ -10143,7 +11595,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
10143
11595
  * const result = evmAddressSchema.safeParse(validAddress)
10144
11596
  * console.log(result.success) // true
10145
11597
  * ```
10146
- */ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
11598
+ */ const evmAddressSchema$1 = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
10147
11599
  /**
10148
11600
  * Schema for validating transaction hashes.
10149
11601
  *
@@ -10832,6 +12284,10 @@ var TransferSpeed;
10832
12284
  token: z.literal('USDC').optional(),
10833
12285
  config: z.object({
10834
12286
  transferSpeed: z.nativeEnum(TransferSpeed).optional(),
12287
+ feePayment: z.enum([
12288
+ 'source',
12289
+ 'destination'
12290
+ ]).optional(),
10835
12291
  maxFee: z.string().min(1, 'Required').pipe(createDecimalStringValidator({
10836
12292
  allowZero: true,
10837
12293
  regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
@@ -10839,7 +12295,8 @@ var TransferSpeed;
10839
12295
  maxDecimals: 6
10840
12296
  })(z.string())).optional(),
10841
12297
  customFee: customFeeSchema.optional()
10842
- }).optional()
12298
+ }).optional(),
12299
+ quote: z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex').optional()
10843
12300
  });
10844
12301
 
10845
12302
  /**
@@ -11451,7 +12908,7 @@ var TransferSpeed;
11451
12908
  * })
11452
12909
  * ```
11453
12910
  */ function createLogger(options, stream) {
11454
- const { redact, ...pinoOptions } = {};
12911
+ const { redact, ...pinoOptions } = options ?? {};
11455
12912
  // Build redaction config
11456
12913
  const redactConfig = buildRedactConfig(redact);
11457
12914
  // Build final pino options, only include redact if defined
@@ -12014,16 +13471,73 @@ var TransferSpeed;
12014
13471
  };
12015
13472
  }
12016
13473
 
13474
+ /**
13475
+ * Dispatch a bridge step event through the provider's action dispatcher.
13476
+ *
13477
+ * Constructs the appropriate action payload and dispatches it to any registered
13478
+ * event listeners. Handles type-safe dispatching for different step types.
13479
+ * When provided, traceId from the invocation context is included for end-to-end correlation.
13480
+ *
13481
+ * @param name - The step name (approve, burn, fetchAttestation, or mint).
13482
+ * @param step - The completed bridge step containing transaction details and explorerUrl.
13483
+ * @param provider - The CCTP v2 provider with action dispatcher.
13484
+ * @param invocation - Optional invocation context containing traceId for correlation.
13485
+ *
13486
+ * @example
13487
+ * ```typescript
13488
+ * const step: BridgeStep = {
13489
+ * name: 'burn',
13490
+ * state: 'success',
13491
+ * txHash: '0xabc...',
13492
+ * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
13493
+ * data: { ... }
13494
+ * }
13495
+ * dispatchStepEvent('burn', step, provider, invocationContext)
13496
+ * ```
13497
+ */ function dispatchStepEvent(name, step, provider, invocation) {
13498
+ if (!provider.actionDispatcher) {
13499
+ return;
13500
+ }
13501
+ // Extract traceId from invocation context if provided
13502
+ const traceId = invocation?.traceId;
13503
+ const actionValues = {
13504
+ protocol: 'cctp',
13505
+ version: 'v2',
13506
+ ...traceId !== undefined && {
13507
+ traceId
13508
+ },
13509
+ values: step
13510
+ };
13511
+ switch(name){
13512
+ case 'approve':
13513
+ case 'burn':
13514
+ case 'mint':
13515
+ provider.actionDispatcher.dispatch(name, {
13516
+ ...actionValues,
13517
+ method: name
13518
+ });
13519
+ break;
13520
+ case 'fetchAttestation':
13521
+ case 'reAttest':
13522
+ provider.actionDispatcher.dispatch(name, {
13523
+ ...actionValues,
13524
+ method: name,
13525
+ values: step
13526
+ });
13527
+ break;
13528
+ }
13529
+ }
13530
+
12017
13531
  /**
12018
13532
  * Base URL for Circle's IRIS API (mainnet/production).
12019
13533
  *
12020
13534
  * The IRIS API provides attestation services for CCTP cross-chain transfers.
12021
- */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
13535
+ */ const IRIS_API_BASE_URL$1 = 'https://iris-api.circle.com';
12022
13536
  /**
12023
13537
  * Base URL for Circle's IRIS API (testnet/sandbox).
12024
13538
  *
12025
13539
  * Used for development and testing on testnet chains.
12026
- */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
13540
+ */ const IRIS_API_SANDBOX_BASE_URL$1 = 'https://iris-api-sandbox.circle.com';
12027
13541
 
12028
13542
  /**
12029
13543
  * Type guard to validate the API response structure.
@@ -12066,7 +13580,7 @@ const isFastBurnFeeResponse = (data)=>{
12066
13580
  * @param isTestnet - Whether the request is for a testnet chain
12067
13581
  * @returns The complete API URL
12068
13582
  */ function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
12069
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
13583
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
12070
13584
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
12071
13585
  }
12072
13586
  const FAST_TIER_FINALITY_THRESHOLD = 1000;
@@ -12277,6 +13791,77 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12277
13791
  }
12278
13792
  };
12279
13793
 
13794
+ /**
13795
+ * Build the forwarding `hookData` for a forwarded (Orbit-relayed) CCTP v2 burn,
13796
+ * tailored to the destination chain.
13797
+ *
13798
+ * For EVM destinations the recipient already holds ERC-20 USDC directly, so the
13799
+ * empty version-0 `cctp-forward` frame is sufficient. For Solana destinations
13800
+ * USDC is held in an Associated Token Account (ATA) that may not exist for a
13801
+ * fresh wallet, so this emits a frame carrying `createAta` + `ataOwner` that
13802
+ * instructs the relayer to create the recipient ATA (idempotently, at the
13803
+ * relayer's expense) before minting.
13804
+ *
13805
+ * `@solana/web3.js` is imported lazily so EVM-only consumers never load Solana
13806
+ * code, mirroring {@link getMintRecipientAccount}.
13807
+ *
13808
+ * @param chainType - The destination blockchain type ('evm' or 'solana').
13809
+ * @param ownerAddress - The recipient's wallet address on the destination chain
13810
+ * (base58 for Solana). Must be the same owner used to derive `mintRecipient`.
13811
+ * @returns A 0x-prefixed hookData hex string for the forwarded burn.
13812
+ * @throws {KitError} If `chainType` is neither 'evm' nor 'solana', if
13813
+ * `@solana/web3.js` cannot be loaded, or if `ownerAddress` is not a valid
13814
+ * Solana public key (all FATAL).
13815
+ *
13816
+ * @example
13817
+ * ```typescript
13818
+ * import { getForwarderHookData } from './getForwarderHookData'
13819
+ *
13820
+ * // EVM: empty forwarding frame
13821
+ * const evmHook = await getForwarderHookData('evm', '0x742d35Cc...')
13822
+ *
13823
+ * // Solana: frame instructing the relayer to create the recipient ATA
13824
+ * const solanaHook = await getForwarderHookData(
13825
+ * 'solana',
13826
+ * '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
13827
+ * )
13828
+ * ```
13829
+ */ const getForwarderHookData = async (/** The destination blockchain type - determines the hookData shape */ chainType, /** The recipient's wallet address (hex for EVM, base58 for Solana) */ ownerAddress)=>{
13830
+ if (chainType === 'evm') {
13831
+ // EVM: the recipient holds USDC directly; no ATA setup is needed.
13832
+ return buildForwardingHookData();
13833
+ }
13834
+ // Fail closed: only EVM and Solana forwarding destinations are supported.
13835
+ // Without this guard any future non-EVM chain type would silently fall
13836
+ // through to the Solana path and mis-encode hookData on a money-movement path.
13837
+ if (chainType !== 'solana') {
13838
+ throw new KitError({
13839
+ ...InputError.VALIDATION_FAILED,
13840
+ recoverability: 'FATAL',
13841
+ message: `Forwarded burns are not supported for destination chain type "${chainType}"`
13842
+ });
13843
+ }
13844
+ // Solana: encode the owner so the relayer creates the recipient ATA.
13845
+ // Resolve @solana/web3.js lazily so EVM-only consumers never load Solana code.
13846
+ const { PublicKey } = await import('@solana/web3.js').catch(()=>{
13847
+ throw new KitError({
13848
+ ...InputError.VALIDATION_FAILED,
13849
+ recoverability: 'FATAL',
13850
+ message: 'Failed to load @solana/web3.js. Please ensure it is installed: npm install @solana/web3.js'
13851
+ });
13852
+ });
13853
+ try {
13854
+ const owner = new PublicKey(ownerAddress);
13855
+ return buildSolanaAtaForwardingHookData(owner.toBytes());
13856
+ } catch (error) {
13857
+ throw new KitError({
13858
+ ...InputError.INVALID_ADDRESS,
13859
+ recoverability: 'FATAL',
13860
+ message: `Failed to build Solana forwarder hookData for recipient "${ownerAddress}": ${error instanceof Error ? error.message : String(error)}`
13861
+ });
13862
+ }
13863
+ };
13864
+
12280
13865
  /**
12281
13866
  * Validates and converts a fee value to bigint.
12282
13867
  *
@@ -12379,7 +13964,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12379
13964
 
12380
13965
  /**
12381
13966
  * The zero address, denoting a native-currency fee in a signed quote.
12382
- */ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
13967
+ */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
12383
13968
  /**
12384
13969
  * Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
12385
13970
  *
@@ -12420,7 +14005,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12420
14005
  if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
12421
14006
  throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
12422
14007
  }
12423
- const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS;
14008
+ const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
12424
14009
  const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
12425
14010
  if (isNativeFee) {
12426
14011
  return {
@@ -12558,7 +14143,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
12558
14143
  * @param isTestnet - Whether the request is for a testnet chain
12559
14144
  * @returns The complete API URL with forward=true query parameter
12560
14145
  */ function buildForwardingFeeUrl(sourceDomain, destinationDomain, isTestnet) {
12561
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
14146
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
12562
14147
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}?forward=true`;
12563
14148
  }
12564
14149
  /**
@@ -12677,6 +14262,8 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
12677
14262
  ;
12678
14263
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
12679
14264
  ;
14265
+ /** 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
14266
+ ;
12680
14267
  // Gas FLOORS, not ceilings — kept separate from the fee-estimate averages
12681
14268
  // above. `executePreparedChainRequest` submits
12682
14269
  // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
@@ -13619,7 +15206,7 @@ function hasPendingState(analysis, result) {
13619
15206
  * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
13620
15207
  * ```
13621
15208
  */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
13622
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15209
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
13623
15210
  const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
13624
15211
  url.searchParams.set('transactionHash', transactionHash);
13625
15212
  return url.toString();
@@ -13787,7 +15374,7 @@ function hasPendingState(analysis, result) {
13787
15374
  * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
13788
15375
  * ```
13789
15376
  */ const buildReAttestUrl = (nonce, isTestnet)=>{
13790
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15377
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
13791
15378
  const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
13792
15379
  return url.toString();
13793
15380
  };
@@ -14242,7 +15829,8 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14242
15829
  * - `destinationChain` — present and supports CCTP v2
14243
15830
  * - source and destination chains must both be testnet or both mainnet
14244
15831
  * - source and destination chains must differ
14245
- * - `executor` non-empty string
15832
+ * - destination — either `executor`, or both `mintRecipient` and
15833
+ * `destinationCaller`; not both
14246
15834
  * - `amount` — bigint or non-empty string coercible to bigint
14247
15835
  * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
14248
15836
  * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
@@ -14284,10 +15872,17 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14284
15872
  if (source.chain.name === dest.name) {
14285
15873
  throw createUnsupportedRouteError(source.chain.name, dest.name);
14286
15874
  }
14287
- // executor
15875
+ // Destination: GenericExecutor shorthand or explicit recipient + caller.
14288
15876
  const executor = p['executor'];
14289
- if (typeof executor !== 'string' || executor === '') {
14290
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
15877
+ const mintRecipient = p['mintRecipient'];
15878
+ const destinationCaller = p['destinationCaller'];
15879
+ const hasExecutor = typeof executor === 'string' && executor !== '';
15880
+ const hasDirectDestination = typeof mintRecipient === 'string' && mintRecipient !== '' && typeof destinationCaller === 'string' && destinationCaller !== '';
15881
+ if (!hasExecutor && !hasDirectDestination) {
15882
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor, or both mintRecipient and destinationCaller');
15883
+ }
15884
+ if (hasExecutor && hasDirectDestination) {
15885
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor or direct destination fields, not both');
14291
15886
  }
14292
15887
  // amount
14293
15888
  const rawAmount = p['amount'];
@@ -14310,7 +15905,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14310
15905
  throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
14311
15906
  }
14312
15907
  // feeToken
14313
- if (!evmAddressSchema.safeParse(p['feeToken']).success) {
15908
+ if (!evmAddressSchema$1.safeParse(p['feeToken']).success) {
14314
15909
  throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
14315
15910
  }
14316
15911
  // claim
@@ -14322,7 +15917,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
14322
15917
  if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
14323
15918
  throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
14324
15919
  }
14325
- if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
15920
+ if (!evmAddressSchema$1.safeParse(claim['refundAddress']).success) {
14326
15921
  throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
14327
15922
  }
14328
15923
  // hookData (optional)
@@ -14704,63 +16299,6 @@ const mockAttestationMessage = {
14704
16299
  });
14705
16300
  }
14706
16301
 
14707
- /**
14708
- * Dispatch a bridge step event through the provider's action dispatcher.
14709
- *
14710
- * Constructs the appropriate action payload and dispatches it to any registered
14711
- * event listeners. Handles type-safe dispatching for different step types.
14712
- * When provided, traceId from the invocation context is included for end-to-end correlation.
14713
- *
14714
- * @param name - The step name (approve, burn, fetchAttestation, or mint).
14715
- * @param step - The completed bridge step containing transaction details and explorerUrl.
14716
- * @param provider - The CCTP v2 provider with action dispatcher.
14717
- * @param invocation - Optional invocation context containing traceId for correlation.
14718
- *
14719
- * @example
14720
- * ```typescript
14721
- * const step: BridgeStep = {
14722
- * name: 'burn',
14723
- * state: 'success',
14724
- * txHash: '0xabc...',
14725
- * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
14726
- * data: { ... }
14727
- * }
14728
- * dispatchStepEvent('burn', step, provider, invocationContext)
14729
- * ```
14730
- */ function dispatchStepEvent(name, step, provider, invocation) {
14731
- if (!provider.actionDispatcher) {
14732
- return;
14733
- }
14734
- // Extract traceId from invocation context if provided
14735
- const traceId = invocation?.traceId;
14736
- const actionValues = {
14737
- protocol: 'cctp',
14738
- version: 'v2',
14739
- ...traceId !== undefined && {
14740
- traceId
14741
- },
14742
- values: step
14743
- };
14744
- switch(name){
14745
- case 'approve':
14746
- case 'burn':
14747
- case 'mint':
14748
- provider.actionDispatcher.dispatch(name, {
14749
- ...actionValues,
14750
- method: name
14751
- });
14752
- break;
14753
- case 'fetchAttestation':
14754
- case 'reAttest':
14755
- provider.actionDispatcher.dispatch(name, {
14756
- ...actionValues,
14757
- method: name,
14758
- values: step
14759
- });
14760
- break;
14761
- }
14762
- }
14763
-
14764
16302
  /**
14765
16303
  * Check whether the source adapter supports EIP-5792 atomic batching and
14766
16304
  * the consumer has not explicitly opted out via `config.batchTransactions`.
@@ -15007,7 +16545,7 @@ const mockAttestationMessage = {
15007
16545
  return step;
15008
16546
  }
15009
16547
 
15010
- var version$2 = "1.11.0";
16548
+ var version$2 = "1.13.0";
15011
16549
  var pkg$2 = {
15012
16550
  version: version$2};
15013
16551
 
@@ -15713,6 +17251,9 @@ var pkg$2 = {
15713
17251
  }
15714
17252
  }
15715
17253
 
17254
+ const logger = createLogger({
17255
+ name: 'provider-cctp-v2'
17256
+ });
15716
17257
  function isPlainObject(value) {
15717
17258
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
15718
17259
  return false;
@@ -15818,6 +17359,34 @@ function assertCCTPV2Config(config) {
15818
17359
  this.config = config;
15819
17360
  }
15820
17361
  /**
17362
+ * Emit a bridge step event through the provider's registered action
17363
+ * dispatcher.
17364
+ *
17365
+ * Kit-level orchestration that drives the burn primitives directly instead
17366
+ * of {@link CCTPV2BridgingProvider.bridge} (for example the receive-exact
17367
+ * source-fee flow) uses this to surface the same `approve`/`burn`/`mint`
17368
+ * events as the standard bridge path. It is a no-op when no dispatcher is
17369
+ * registered.
17370
+ *
17371
+ * @param name - The step name (`approve`, `burn`, `mint`, ...).
17372
+ * @param step - The completed bridge step to broadcast.
17373
+ * @param invocation - Optional invocation context carrying a `traceId` for
17374
+ * end-to-end correlation.
17375
+ * @returns Nothing.
17376
+ *
17377
+ * @example
17378
+ * ```typescript
17379
+ * const provider = new CCTPV2BridgingProvider()
17380
+ * provider.emitBridgeStep('burn', {
17381
+ * name: 'burn',
17382
+ * state: 'success',
17383
+ * txHash: '0xabc...',
17384
+ * })
17385
+ * ```
17386
+ */ emitBridgeStep(name, step, invocation) {
17387
+ dispatchStepEvent(name, step, this, invocation);
17388
+ }
17389
+ /**
15821
17390
  * Resolves the effective polling configuration for an attestation request.
15822
17391
  *
15823
17392
  * Precedence (lowest to highest): provider `config.attestation`, then the
@@ -16047,6 +17616,116 @@ function assertCCTPV2Config(config) {
16047
17616
  return estimateResult;
16048
17617
  }
16049
17618
  /**
17619
+ * Estimate source-chain gas for a prepaid-FORWARD deposit burn via
17620
+ * `TokenMessengerWithFees.depositForBurnWithHookAndFees`.
17621
+ *
17622
+ * Builds a size-correct GenericExecutor hookData placeholder — using the
17623
+ * pre-validated `contracts.executor` and `contracts.depositForHandler` — so
17624
+ * the EVM calldata length matches production. Attempts a live
17625
+ * `eth_estimateGas` via the adapter's `cctp.v2.depositForBurnWithFees`
17626
+ * action and falls back to the static
17627
+ * {@link DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM} constant when the live
17628
+ * RPC call fails.
17629
+ *
17630
+ * Gateway eligibility is the caller's responsibility: validate the
17631
+ * destination chain with `resolveGatewayExecutorContracts` (UBK) before
17632
+ * calling this method.
17633
+ *
17634
+ * @param params - Estimation parameters including adapter, chains, amount,
17635
+ * the signed fee quote returned by the Quote API, and the pre-validated
17636
+ * Gateway executor contracts resolved by the caller.
17637
+ * @returns Promise resolving to the estimated (or fallback) gas cost.
17638
+ * @throws KitError `SERVICE_INTERNAL_ERROR` (FATAL) if `dstChain`'s
17639
+ * `usdcAddress` is `null`.
17640
+ *
17641
+ * @example
17642
+ * ```typescript
17643
+ * const contracts = resolveGatewayExecutorContracts(srcChain, dstChain)
17644
+ * const gasEstimate = await CCTPV2BridgingProvider.estimateDepositBurn({
17645
+ * adapter: evmAdapter,
17646
+ * srcChain: Ethereum,
17647
+ * dstChain: ArcTestnet,
17648
+ * amountMinorUnits: 100_000_000n,
17649
+ * refundAddress: '0xUserWallet',
17650
+ * signedQuote: quote.signedQuote,
17651
+ * feeToken: quote.feeToken,
17652
+ * feeTotalAmount: BigInt(quote.feeTotalAmount),
17653
+ * resolvedContext,
17654
+ * contracts,
17655
+ * })
17656
+ * ```
17657
+ */ static async estimateDepositBurn(params) {
17658
+ const { adapter, srcChain, dstChain, amountMinorUnits, refundAddress, signedQuote, feeToken, feeTotalAmount, resolvedContext, contracts } = params;
17659
+ const { executor, depositForHandler } = contracts;
17660
+ if (dstChain.usdcAddress === null) {
17661
+ throw new KitError({
17662
+ ...ServiceError.INTERNAL_ERROR,
17663
+ recoverability: 'FATAL',
17664
+ message: `Destination chain ${dstChain.name} has no USDC address configured`
17665
+ });
17666
+ }
17667
+ // Build a size-correct hookData for eth_estimateGas. The EVM uses calldata
17668
+ // length to compute gas, so the byte layout must match production even
17669
+ // though field values are not final.
17670
+ const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
17671
+ dappId: 'gateway_deposit',
17672
+ destinationChain: dstChain,
17673
+ handler: depositForHandler,
17674
+ params: [
17675
+ dstChain.usdcAddress,
17676
+ refundAddress,
17677
+ 0n
17678
+ ],
17679
+ recoveryAddress: padAddressToBytes32(refundAddress),
17680
+ // Override deployments only — spread the canonical function signature
17681
+ // and amount indices from DAPP_CONFIG so they stay in sync.
17682
+ config: {
17683
+ gateway_deposit: {
17684
+ ...DAPP_CONFIG.gateway_deposit,
17685
+ deployments: [
17686
+ {
17687
+ domainId: dstChain.cctp.domain
17688
+ }
17689
+ ]
17690
+ }
17691
+ }
17692
+ });
17693
+ const hookData = buildForwardingHookDataWithPayload(GENERIC_EXECUTOR_HOOK_DATA_VERSION, geBlob);
17694
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee
17695
+ // item, so the hookData must carry a `cctp-forward` frame — matches the
17696
+ // assertion in `prepareDepositForBurn`.
17697
+ assertForwardHookData(hookData);
17698
+ try {
17699
+ const prepared = await adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
17700
+ fromChain: srcChain,
17701
+ toChain: dstChain,
17702
+ amount: amountMinorUnits,
17703
+ mintRecipient: executor,
17704
+ destinationCaller: executor,
17705
+ claim: {
17706
+ signedQuote,
17707
+ refundAddress
17708
+ },
17709
+ feeToken,
17710
+ feeTotalAmount,
17711
+ hookData
17712
+ }, resolvedContext);
17713
+ return await prepared.estimate(undefined);
17714
+ } catch (err) {
17715
+ logger.debug('estimateDepositBurn: live eth_estimateGas failed, using static fallback', {
17716
+ err,
17717
+ chain: srcChain.name
17718
+ });
17719
+ try {
17720
+ return await adapter.calculateTransactionFee(DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM, undefined, srcChain);
17721
+ } catch (feeErr) {
17722
+ throw createRpcEndpointError(srcChain.name, {
17723
+ rawError: feeErr
17724
+ });
17725
+ }
17726
+ }
17727
+ }
17728
+ /**
16050
17729
  * Extracts OperationContext from bridge parameters for a given wallet context.
16051
17730
  *
16052
17731
  * This method extracts the chain and address information from the wallet context
@@ -16643,8 +18322,11 @@ function assertCCTPV2Config(config) {
16643
18322
  // 2. Forwarder: Does the user want Circle's relayer to handle attestation/mint?
16644
18323
  const useCustomBurn = hasCustomContractSupport(source.chain, 'bridge');
16645
18324
  const useForwarder = destination.useForwarder === true;
16646
- // Build hookData once if forwarder is enabled (memoized internally)
16647
- const hookData = useForwarder ? buildForwardingHookData() : undefined;
18325
+ // Build hookData once if forwarder is enabled. EVM destinations get the
18326
+ // empty forwarding frame; Solana destinations get a frame instructing the
18327
+ // relayer to create the recipient's ATA (using the same owner that derived
18328
+ // `mintRecipient`) so the mint succeeds even for a fresh wallet.
18329
+ const hookData = useForwarder ? await getForwarderHookData(destination.chain.type, destinationAddressForMint) : undefined;
16648
18330
  if (useCustomBurn) {
16649
18331
  // Custom burn path: use bridge contract (with or without hook)
16650
18332
  const customBurnParams = {
@@ -16670,15 +18352,109 @@ function assertCCTPV2Config(config) {
16670
18352
  return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
16671
18353
  }
16672
18354
  /**
18355
+ * Prepare the source-chain `depositForBurnWithHookAndFees` call for the
18356
+ * GenericExecutor FORWARD path.
18357
+ *
18358
+ * Exposed as a public static method so the UBK fast-deposit flow can invoke
18359
+ * it directly without holding a provider instance. The byte layout mirrors
18360
+ * {@link CCTPV2BridgingProvider.estimateDepositBurn} so gas estimates and the
18361
+ * executed call agree.
18362
+ *
18363
+ * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18364
+ * @param params - Burn parameters including adapter, chains, deposit action,
18365
+ * amount, signer address, signed fee quote, and pre-resolved adapter context.
18366
+ * @returns The prepared `depositForBurnWithHookAndFees` burn transaction.
18367
+ * @throws {KitError} `UNSUPPORTED_ROUTE` when `dstChain` lacks a
18368
+ * GenericExecutor or DepositForHandler, or the `deposit.dappId` is unknown.
18369
+ *
18370
+ * @example
18371
+ * ```typescript
18372
+ * import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
18373
+ * import { Ethereum, ArcTestnet } from '@core/chains'
18374
+ *
18375
+ * const prepared = await CCTPV2BridgingProvider.prepareDepositForBurn({
18376
+ * adapter,
18377
+ * srcChain: Ethereum,
18378
+ * dstChain: ArcTestnet,
18379
+ * deposit: { dappId: 'gateway_deposit', params: [usdcAddress, recipient, 0n] },
18380
+ * amountMinorUnits: 100_000_000n,
18381
+ * refundAddress: '0xSender...',
18382
+ * signedQuote: quote.signedQuote,
18383
+ * feeToken: quote.feeToken,
18384
+ * feeTotalAmount: BigInt(quote.feeTotalAmount),
18385
+ * resolvedContext,
18386
+ * })
18387
+ * const txHash = await prepared.execute()
18388
+ * ```
18389
+ */ static async prepareDepositForBurn(params) {
18390
+ const { adapter, srcChain, dstChain, deposit, amountMinorUnits, refundAddress, signedQuote, feeToken, feeTotalAmount, resolvedContext } = params;
18391
+ // Resolve executor and depositForHandler from the destination chain's
18392
+ // gateway config. Throw an unsupported-route error if either is absent.
18393
+ const executor = dstChain.gateway?.contracts?.v1?.genericExecutor;
18394
+ const depositForHandler = dstChain.gateway?.contracts?.v1?.depositForHandler;
18395
+ if (!executor || !depositForHandler) {
18396
+ throw createUnsupportedRouteError(srcChain.name, dstChain.name);
18397
+ }
18398
+ // Resolve the canonical dApp config (function signature + amount indices)
18399
+ // for THIS deposit's `dappId`. Reject an unknown `dappId` rather than
18400
+ // encoding the wrong ABI selector, which would burn on the source but
18401
+ // revert in the executor call on the destination.
18402
+ const dappConfig = DAPP_CONFIG[deposit.dappId];
18403
+ if (dappConfig === undefined) {
18404
+ throw createUnsupportedRouteError(srcChain.name, dstChain.name);
18405
+ }
18406
+ // Build the bare GenericExecutor payload, then wrap it in the `cctp-forward`
18407
+ // frame required by the prepaid-FORWARD wrapper.
18408
+ const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
18409
+ dappId: deposit.dappId,
18410
+ destinationChain: dstChain,
18411
+ handler: depositForHandler,
18412
+ params: deposit.params,
18413
+ recoveryAddress: padAddressToBytes32(refundAddress),
18414
+ config: {
18415
+ [deposit.dappId]: {
18416
+ ...dappConfig,
18417
+ deployments: [
18418
+ {
18419
+ domainId: dstChain.cctp.domain
18420
+ }
18421
+ ]
18422
+ }
18423
+ }
18424
+ });
18425
+ const hookData = buildForwardingHookDataWithPayload(GENERIC_EXECUTOR_HOOK_DATA_VERSION, geBlob);
18426
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee
18427
+ // item, so the hookData must carry a `cctp-forward` frame; otherwise the
18428
+ // wrapper reverts `ForwardFeeWithoutHook`.
18429
+ assertForwardHookData(hookData);
18430
+ // Source-chain burn: `mintRecipient` AND `destinationCaller` are both the
18431
+ // executor; fees are prepaid against the signed quote.
18432
+ return adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18433
+ fromChain: srcChain,
18434
+ toChain: dstChain,
18435
+ amount: amountMinorUnits,
18436
+ mintRecipient: executor,
18437
+ destinationCaller: executor,
18438
+ hookData,
18439
+ claim: {
18440
+ signedQuote,
18441
+ refundAddress
18442
+ },
18443
+ feeToken,
18444
+ feeTotalAmount
18445
+ }, resolvedContext);
18446
+ }
18447
+ /**
16673
18448
  * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
16674
18449
  *
16675
- * Builds the source-chain `depositForBurnWithHookAndFees` call for the
16676
- * GenericExecutor FORWARD path: fees are collected up front on the source chain
16677
- * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
16678
- * the GenericExecutor, and the GE `hookData` is passed through unchanged.
18450
+ * Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
18451
+ * collected up front on the source chain against a signed quote. The
18452
+ * destination may use the GenericExecutor shorthand, or an explicit mint
18453
+ * recipient and destination caller for direct forwarding.
16679
18454
  *
16680
- * This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
16681
- * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
18455
+ * This is the low-level on-chain primitive behind the Unified Balance Kit
18456
+ * `fastCrossChainDeposit` and the Bridge Kit source-fee
18457
+ * (`feePayment: 'source'`) flow. The `hookData` and signed-quote
16682
18458
  * `claim` are produced elsewhere and passed in here:
16683
18459
  * - `hookData`: `buildForwardingHookDataWithPayload(version,
16684
18460
  * buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
@@ -16695,32 +18471,48 @@ function assertCCTPV2Config(config) {
16695
18471
  * approval covers both; the redundant second approval is skipped.
16696
18472
  *
16697
18473
  * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
16698
- * @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
18474
+ * @param params - The burn amount, destination, hook data, signed quote, and fee.
16699
18475
  * @returns The prepared approvals, the prepared burn, and the resolved fee plan.
16700
18476
  * @throws {KitError} If the wallet context is invalid, `destinationChain` does not
16701
- * support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
16702
- * a bigint or a numeric string coercible to bigint, the hookData lacks a
16703
- * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
16704
- * context cannot be resolved.
18477
+ * support CCTP v2, the destination fields are missing, `amount` or
18478
+ * `feeTotalAmount` is not a bigint or a numeric string coercible to bigint,
18479
+ * the hook data lacks a `cctp-forward` frame (guaranteed
18480
+ * `ForwardFeeWithoutHook`), or the operation context cannot be resolved.
16705
18481
  *
16706
18482
  * @example
16707
18483
  * ```typescript
18484
+ * import {
18485
+ * CCTPV2BridgingProvider,
18486
+ * type BurnWithFeesParams,
18487
+ * } from '@circle-fin/provider-cctp-v2'
18488
+ *
18489
+ * declare const source: BurnWithFeesParams['source']
18490
+ * declare const destinationChain: BurnWithFeesParams['destinationChain']
18491
+ * declare const recipient: string
18492
+ * declare const hookData: string
18493
+ * declare const claim: BurnWithFeesParams['claim']
18494
+ *
18495
+ * const provider = new CCTPV2BridgingProvider()
16708
18496
  * const { approvals, burn } = await provider.burnWithFees({
16709
18497
  * source,
16710
- * destinationChain: Arc,
18498
+ * destinationChain,
16711
18499
  * amount: 1_000_000n,
16712
- * executor: genericExecutorAddress,
16713
- * hookData: geForwardHookData,
16714
- * claim: { signedQuote: '0x01...', refundAddress: userAddress },
16715
- * feeToken: '0x0000000000000000000000000000000000000000', // native
16716
- * feeTotalAmount: 3_500_000n,
18500
+ * mintRecipient: recipient,
18501
+ * destinationCaller: '0x0000000000000000000000000000000000000000',
18502
+ * hookData,
18503
+ * claim,
18504
+ * feeToken: source.chain.usdcAddress,
18505
+ * feeTotalAmount: 10_000n,
16717
18506
  * })
16718
18507
  * for (const approval of approvals) await approval.execute()
16719
18508
  * const txHash = await burn.execute()
16720
18509
  * ```
16721
18510
  */ async burnWithFees(params) {
16722
18511
  assertBurnWithFeesParams(params);
16723
- const { source, destinationChain, executor, hookData, claim, feeToken } = params;
18512
+ const { source, destinationChain, hookData, claim, feeToken } = params;
18513
+ const hasExecutor = 'executor' in params && params.executor !== undefined;
18514
+ const mintRecipient = hasExecutor ? params.executor : params.mintRecipient;
18515
+ const destinationCaller = hasExecutor ? params.executor : params.destinationCaller;
16724
18516
  const amount = BigInt(params.amount);
16725
18517
  const feeTotalAmount = BigInt(params.feeTotalAmount);
16726
18518
  // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
@@ -16751,168 +18543,1171 @@ function assertCCTPV2Config(config) {
16751
18543
  delegate: wrapperAddress,
16752
18544
  amount: approval.amount
16753
18545
  }, context)));
16754
- // Build the burn: mintRecipient AND destinationCaller are both the executor.
18546
+ // Build the burn with either the GenericExecutor shorthand or the explicit
18547
+ // direct-forwarding recipient and caller.
16755
18548
  const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
16756
18549
  fromChain: source.chain,
16757
18550
  toChain: destinationChain,
16758
18551
  amount,
16759
- mintRecipient: executor,
16760
- destinationCaller: executor,
18552
+ mintRecipient,
18553
+ destinationCaller,
16761
18554
  hookData,
16762
18555
  claim,
16763
18556
  feeToken,
16764
18557
  feeTotalAmount
16765
18558
  }, context);
16766
18559
  return {
16767
- approvals,
16768
- burn,
16769
- feePayment
18560
+ approvals,
18561
+ burn,
18562
+ feePayment
18563
+ };
18564
+ }
18565
+ /**
18566
+ * Waits for a transaction to be mined and confirmed on the blockchain.
18567
+ *
18568
+ * This method should block until the transaction is confirmed on the blockchain.
18569
+ *
18570
+ * @param adapter - The adapter to use for transaction waiting
18571
+ * @param txHash - The hash of the transaction to wait for
18572
+ * @param chain - The chain definition where the transaction was executed
18573
+ * @param config - Optional configuration for transaction waiting (confirmations, timeout)
18574
+ * @returns The hash of the confirmed transaction
18575
+ * @example
18576
+ * ```typescript
18577
+ * const provider = new CCTPV2BridgingProvider()
18578
+ * const txHash = await provider.waitForTransaction(
18579
+ * adapter,
18580
+ * '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
18581
+ * Ethereum,
18582
+ * )
18583
+ * console.log('Transaction confirmed:', txHash)
18584
+ * ```
18585
+ */ async waitForTransaction(adapter, txHash, chain, config) {
18586
+ return adapter.waitForTransaction(txHash, config, chain);
18587
+ }
18588
+ }
18589
+
18590
+ /**
18591
+ * The default providers that will be used in addition to the providers provided
18592
+ * to the BridgeKit constructor.
18593
+ *
18594
+ * @param config - Optional configuration forwarded to the default providers
18595
+ * @returns The default bridging providers
18596
+ */ const getDefaultProviders = (config = {})=>[
18597
+ new CCTPV2BridgingProvider(config.headers ? {
18598
+ headers: config.headers
18599
+ } : {})
18600
+ ];
18601
+
18602
+ /**
18603
+ * A helper function to get a function that transforms an amount into a human-readable string or a bigint string.
18604
+ * @param formatDirection - The direction to format the amount in.
18605
+ * @returns A function that transforms an amount into a human-readable string or a bigint string.
18606
+ */ const getAmountTransformer = (formatDirection)=>formatDirection === 'to-human-readable' ? (params)=>formatAmount(params) : (params)=>parseAmount(params).toString();
18607
+ /**
18608
+ * Format the bridge result into human-readable string values for the user or bigint string values for internal use.
18609
+ *
18610
+ * @typeParam T - The specific result type (must extend BridgeResult or EstimateResult). Preserves the exact type passed in.
18611
+ * @param result - The bridge result to format.
18612
+ * @param formatDirection - The direction to format the result in.
18613
+ * - If 'to-human-readable', the result will be converted to human-readable string values.
18614
+ * - If 'to-internal', the result will be converted to bigint string values (usually for internal use).
18615
+ * @returns The formatted bridge result.
18616
+ *
18617
+ * @example
18618
+ * ```typescript
18619
+ * const result = await kit.bridge({
18620
+ * amount: '1000000',
18621
+ * token: 'USDC',
18622
+ * from: { adapter: adapter, chain: 'Ethereum' },
18623
+ * to: { adapter: adapter, chain: 'Base' },
18624
+ * })
18625
+ *
18626
+ * // Format the bridge result into human-readable string values for the user
18627
+ * const formattedResultHumanReadable = formatBridgeResult(result, 'to-human-readable')
18628
+ * console.log(formattedResultHumanReadable)
18629
+ *
18630
+ * // Format the bridge result into bigint string values for internal use
18631
+ * const formattedResultInternal = formatBridgeResult(result, 'to-internal')
18632
+ * console.log(formattedResultInternal)
18633
+ * ```
18634
+ */ const formatBridgeResult = (result, formatDirection)=>{
18635
+ const transform = getAmountTransformer(formatDirection);
18636
+ return {
18637
+ ...result,
18638
+ amount: transform({
18639
+ value: result.amount,
18640
+ token: result.token
18641
+ }),
18642
+ ...'config' in result && result.config && Object.keys(result.config).length > 0 && {
18643
+ config: {
18644
+ ...result.config,
18645
+ ...result.config.maxFee && {
18646
+ maxFee: transform({
18647
+ value: result.config.maxFee,
18648
+ token: result.token
18649
+ })
18650
+ },
18651
+ ...result.config.customFee && {
18652
+ customFee: {
18653
+ ...result.config.customFee,
18654
+ ...result.config.customFee.value && {
18655
+ value: transform({
18656
+ value: result.config.customFee.value,
18657
+ token: result.token
18658
+ })
18659
+ }
18660
+ }
18661
+ }
18662
+ }
18663
+ }
18664
+ };
18665
+ };
18666
+
18667
+ /**
18668
+ * Register all bridge-kit event type strings with the shared registry so
18669
+ * callers of `withErrorTelemetry` / `emitResultStepErrorTelemetry` are
18670
+ * compile-time checked.
18671
+ *
18672
+ * @internal
18673
+ */ /**
18674
+ * Telemetry event type identifiers for bridge-kit operations.
18675
+ *
18676
+ * @internal
18677
+ */ const BRIDGE_EVENT_TYPES = {
18678
+ BRIDGE: 'bridge_bridge',
18679
+ RETRY: 'bridge_retry',
18680
+ ESTIMATE: 'bridge_estimate'
18681
+ };
18682
+ /**
18683
+ * Ordered mapping from provider step event names to telemetry event types.
18684
+ *
18685
+ * @remarks
18686
+ * The order matches the CCTP v2 bridge execution sequence. During
18687
+ * `bridge()`, completed step events are counted so the failing step
18688
+ * can be identified by its index.
18689
+ *
18690
+ * @internal
18691
+ */ const BRIDGE_STEP_EVENT_MAP = [
18692
+ [
18693
+ 'approve',
18694
+ 'bridge_approve'
18695
+ ],
18696
+ [
18697
+ 'burn',
18698
+ 'bridge_burn'
18699
+ ],
18700
+ [
18701
+ 'fetchAttestation',
18702
+ 'bridge_fetch_attestation'
18703
+ ],
18704
+ [
18705
+ 'mint',
18706
+ 'bridge_mint'
18707
+ ]
18708
+ ];
18709
+
18710
+ /**
18711
+ * Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
18712
+ *
18713
+ * @internal
18714
+ */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
18715
+ /**
18716
+ * Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
18717
+ *
18718
+ * @internal
18719
+ */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
18720
+ /**
18721
+ * Native fee-token sentinel (the zero address).
18722
+ *
18723
+ * When `feeToken` is the zero address the quote prices fees in the source
18724
+ * chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
18725
+ * address instead to denominate fees in USDC.
18726
+ *
18727
+ * @internal
18728
+ */ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
18729
+ /**
18730
+ * API path prefix for the CCTP v2 USDC burn quote endpoint.
18731
+ *
18732
+ * The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
18733
+ * `usdc` is a fixed literal, not a token parameter.
18734
+ *
18735
+ * @internal
18736
+ */ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
18737
+ /**
18738
+ * API path prefix for the CCTP v2 USDC quote validate endpoint.
18739
+ *
18740
+ * The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
18741
+ * `POST { abiSignature, args }` body and returns `claimable`, `failedChecks`,
18742
+ * and the decoded `expiry`, `feeToken`, and `feeTotalAmount`.
18743
+ *
18744
+ * @internal
18745
+ */ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
18746
+ /**
18747
+ * Default polling configuration for Quote API calls.
18748
+ *
18749
+ * A signed quote is short-lived (typically ~2 minutes, varying per chain) and
18750
+ * a feature-flag-disabled source chain returns a
18751
+ * permanent `503 SERVICE_NOT_ENABLED`, so retrying buys little and risks
18752
+ * outliving the quote. The client therefore makes a single attempt
18753
+ * (`maxRetries: 1`) with a 15s timeout, mirroring the reference
18754
+ * implementation; callers refresh by requesting a new quote rather than
18755
+ * relying on transport retries.
18756
+ *
18757
+ * No `headers` are set here: `pollApiWithValidation` always injects
18758
+ * `Content-Type: application/json` and adds `User-Agent` in Node. Browser
18759
+ * requests omit a user-agent header to avoid a CORS preflight, so duplicating
18760
+ * either header here would be dead configuration.
18761
+ *
18762
+ * @internal
18763
+ */ const FEE_QUOTE_DEFAULT_CONFIG = {
18764
+ timeout: 15_000,
18765
+ maxRetries: 1,
18766
+ retryDelay: 200
18767
+ };
18768
+
18769
+ /**
18770
+ * Decimal string in token minor units, constrained to be strictly positive.
18771
+ *
18772
+ * @internal
18773
+ */ const positiveAmountSchema = z.string().regex(/^\d+$/, 'must be a non-negative integer string')// Re-check the digit shape here: zod still runs this refinement when the
18774
+ // regex check above fails ("dirty"), so guard BigInt() against throwing on a
18775
+ // non-numeric value before comparing.
18776
+ .refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
18777
+ /**
18778
+ * A 20-byte EVM address in `0x` hex.
18779
+ *
18780
+ * The MVP prepaid-`FORWARD` `burn/usdc` path targets EVM contracts
18781
+ * (`TokenMessengerWithFees` / `GenericExecutor`), so `feeToken` and
18782
+ * `destinationCaller` are constrained to EVM addresses by design. This is an
18783
+ * intentional scope limit, not a permanent one: it can be widened to other
18784
+ * address formats as the fee service expands to more chains.
18785
+ */ const evmAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'must be a 20-byte 0x address');
18786
+ /** Even-length `0x` hex (the empty `0x` is allowed). */ const hexSchema = z.string().regex(/^0x([a-fA-F0-9]{2})*$/, 'must be even-length 0x hex');
18787
+ /** Non-empty, even-length `0x` hex. */ const nonEmptyHexSchema = z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex');
18788
+ /** A 32-byte `0x` hex hash. */ const bytes32Schema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'must be a 32-byte 0x hash');
18789
+ /** Decimal string in minor units, allowing zero. */ const numericStringSchema = z.string().regex(/^\d+$/, 'must be a non-negative integer string');
18790
+ /** An `https:` URL, used for the optional base-URL override. */ const httpsUrlSchema = z.string().refine((value)=>{
18791
+ try {
18792
+ return new URL(value).protocol === 'https:';
18793
+ } catch {
18794
+ return false;
18795
+ }
18796
+ }, 'must be an https URL');
18797
+ const forwardParamsSchema = z.object({
18798
+ hookData: hexSchema.optional(),
18799
+ destinationCaller: evmAddressSchema.optional()
18800
+ }).strict();
18801
+ const forwardRequestSchema = z.object({
18802
+ type: z.literal('FORWARD'),
18803
+ params: forwardParamsSchema.optional()
18804
+ }).strict();
18805
+ const preFinalityRequestSchema = z.object({
18806
+ type: z.literal('PRE_FINALITY')
18807
+ }).strict();
18808
+ /**
18809
+ * A single quote request item (`FORWARD` or `PRE_FINALITY`).
18810
+ *
18811
+ * @internal
18812
+ */ const feeQuoteRequestSchema = z.discriminatedUnion('type', [
18813
+ forwardRequestSchema,
18814
+ preFinalityRequestSchema
18815
+ ]);
18816
+ /**
18817
+ * A non-empty list of quote request items with unique types.
18818
+ *
18819
+ * @internal
18820
+ */ const feeQuoteRequestsSchema = z.array(feeQuoteRequestSchema).min(1, 'at least one request item is required').refine((items)=>new Set(items.map((item)=>item.type)).size === items.length, 'request item types must be unique');
18821
+ /**
18822
+ * A structured `Partial<ApiPollingConfig>` polling override.
18823
+ *
18824
+ * Validates the field types callers actually set, so a plain-JS caller passing
18825
+ * `{ timeout: 'soon' }` is rejected at the boundary rather than failing opaquely
18826
+ * inside the transport. Unknown keys pass through so a future `ApiPollingConfig`
18827
+ * field is forwarded rather than silently dropped.
18828
+ */ const apiPollingConfigSchema = z.object({
18829
+ timeout: z.number().int().positive().optional(),
18830
+ maxRetries: z.number().int().nonnegative().optional(),
18831
+ retryDelay: z.number().int().nonnegative().optional(),
18832
+ backoff: z.enum([
18833
+ 'fixed',
18834
+ 'exponential'
18835
+ ]).optional(),
18836
+ maxRetryDelayMs: z.number().int().positive().optional(),
18837
+ headers: z.record(z.string()).optional()
18838
+ }).passthrough();
18839
+ /**
18840
+ * The validatable input for {@link fetchFeeQuote}.
18841
+ *
18842
+ * This is the single source of truth for input validation, including the CCTP
18843
+ * domains and the `isTestnet` environment flag. Validating `isTestnet` at
18844
+ * runtime matters because a plain-JS caller who omits it would otherwise leave
18845
+ * it `undefined`, which is falsy and silently selects the production base URL.
18846
+ * (`buildFeeQuoteUrl` independently re-validates the domains for standalone
18847
+ * callers.)
18848
+ *
18849
+ * @internal
18850
+ */ const fetchFeeQuoteInputSchema = z.object({
18851
+ sourceDomain: z.number().int().nonnegative(),
18852
+ destinationDomain: z.number().int().nonnegative(),
18853
+ amount: positiveAmountSchema,
18854
+ feeToken: evmAddressSchema.optional(),
18855
+ requests: feeQuoteRequestsSchema,
18856
+ isTestnet: z.boolean(),
18857
+ baseUrl: httpsUrlSchema.optional(),
18858
+ config: apiPollingConfigSchema.optional()
18859
+ }).strict();
18860
+ const feeQuoteItemSchema = z.object({
18861
+ type: z.string().min(1),
18862
+ amount: numericStringSchema,
18863
+ args: z.array(z.string()),
18864
+ argsHash: bytes32Schema
18865
+ }).passthrough();
18866
+ const exchangeRatesSchema = z.object({
18867
+ feeTokenUsd: z.string(),
18868
+ destinationTokenUsd: z.string()
18869
+ }).passthrough();
18870
+ const metadataSchema = z.object({
18871
+ destinationGasPrice: z.string().optional(),
18872
+ exchangeRates: exchangeRatesSchema.optional()
18873
+ }).passthrough();
18874
+ /** The `expiry` object the Quote API nests the quote TTL under. */ const expirySchema = z.discriminatedUnion('mode', [
18875
+ z.object({
18876
+ mode: z.literal('TIMESTAMP'),
18877
+ expiresAt: z.number().int().nonnegative()
18878
+ }).passthrough(),
18879
+ z.object({
18880
+ mode: z.literal('BLOCK_NUMBER'),
18881
+ expiresAtBlock: z.number().int().nonnegative(),
18882
+ blockEstimatedAt: z.number().int().nonnegative().optional()
18883
+ }).passthrough()
18884
+ ]);
18885
+ /**
18886
+ * Schema for a signed fee quote returned by the Quote API.
18887
+ *
18888
+ * @internal
18889
+ */ const signedFeeQuoteSchema = z.object({
18890
+ // The runtime YAML spec maps signedQuote to a looser `hex` (which allows
18891
+ // an empty `0x`); we keep the stricter non-empty form. Do not relax
18892
+ // without a reason.
18893
+ signedQuote: nonEmptyHexSchema,
18894
+ issuedAt: z.number().int().nonnegative(),
18895
+ // The API returns a mode-specific timestamp or source-block deadline.
18896
+ expiry: expirySchema,
18897
+ feeTotalAmount: numericStringSchema,
18898
+ feeToken: evmAddressSchema,
18899
+ nonce: numericStringSchema,
18900
+ items: z.array(feeQuoteItemSchema),
18901
+ metadata: metadataSchema.optional()
18902
+ }).passthrough();
18903
+ /**
18904
+ * Validate that an unknown value is a signed fee quote.
18905
+ *
18906
+ * @param value - The unknown value to validate.
18907
+ * @returns `true` when the value matches the signed-quote response shape.
18908
+ *
18909
+ * @example
18910
+ * ```typescript
18911
+ * import { isSignedFeeQuote } from '@circle-fin/provider-fee-v1'
18912
+ *
18913
+ * declare const payload: unknown
18914
+ * if (isSignedFeeQuote(payload)) {
18915
+ * console.log(payload.feeTotalAmount)
18916
+ * }
18917
+ * ```
18918
+ *
18919
+ * @internal
18920
+ */ function isSignedFeeQuote(value) {
18921
+ return signedFeeQuoteSchema.safeParse(value).success;
18922
+ }
18923
+ /**
18924
+ * Validates input to {@link validateQuote}.
18925
+ *
18926
+ * @internal
18927
+ */ const validateQuoteInputSchema = z.object({
18928
+ sourceDomain: z.number().int().nonnegative(),
18929
+ abiSignature: z.string().min(1),
18930
+ args: z.array(z.union([
18931
+ z.string(),
18932
+ z.array(z.string())
18933
+ ])),
18934
+ isTestnet: z.boolean(),
18935
+ baseUrl: httpsUrlSchema.optional(),
18936
+ config: apiPollingConfigSchema.optional()
18937
+ }).strict();
18938
+ const quoteExpiryStatusSchema = z.discriminatedUnion('mode', [
18939
+ z.object({
18940
+ mode: z.literal('TIMESTAMP'),
18941
+ expired: z.boolean(),
18942
+ secondsRemaining: z.number().int().nonnegative(),
18943
+ expiresAt: z.number().int().nonnegative()
18944
+ }).passthrough(),
18945
+ z.object({
18946
+ mode: z.literal('BLOCK_NUMBER'),
18947
+ expired: z.boolean(),
18948
+ secondsRemaining: z.number().int().nonnegative(),
18949
+ expiresAtBlock: z.number().int().nonnegative(),
18950
+ blockEstimatedAt: z.number().int().nonnegative().optional()
18951
+ }).passthrough()
18952
+ ]);
18953
+ const validateQuoteItemSchema = z.object({
18954
+ type: z.string().min(1),
18955
+ argsMatch: z.boolean(),
18956
+ amount: numericStringSchema.optional(),
18957
+ args: z.array(z.string()).optional(),
18958
+ argsHash: bytes32Schema.optional(),
18959
+ computedArgsHash: bytes32Schema.optional()
18960
+ }).passthrough();
18961
+ /**
18962
+ * Schema for a validate-quote result returned by the Iris `/validate/usdc/:sourceDomain` endpoint.
18963
+ *
18964
+ * The endpoint takes the source domain as a URL path parameter and does not
18965
+ * return it in the response body, so `sourceDomain` is intentionally not part
18966
+ * of this schema.
18967
+ *
18968
+ * @internal
18969
+ */ const validateQuoteResultSchema = z.object({
18970
+ signedQuote: nonEmptyHexSchema,
18971
+ expiry: quoteExpiryStatusSchema,
18972
+ feeTotalAmount: numericStringSchema,
18973
+ feeToken: evmAddressSchema,
18974
+ nonce: numericStringSchema,
18975
+ claimable: z.boolean(),
18976
+ // Preserve newly introduced server-side reasons as opaque strings rather
18977
+ // than rejecting the entire safety response before the SDK is updated.
18978
+ failedChecks: z.array(z.string().min(1)),
18979
+ items: z.array(validateQuoteItemSchema)
18980
+ }).passthrough();
18981
+ /**
18982
+ * Validate that an unknown value is a validate-quote result.
18983
+ *
18984
+ * @param value - The unknown value to validate.
18985
+ * @returns `true` when the value matches the validate-quote response shape.
18986
+ *
18987
+ * @example
18988
+ * ```typescript
18989
+ * import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
18990
+ *
18991
+ * declare const payload: unknown
18992
+ * if (isValidateQuoteResult(payload)) {
18993
+ * console.log(payload.claimable, payload.failedChecks)
18994
+ * }
18995
+ * ```
18996
+ *
18997
+ * @internal
18998
+ */ function isValidateQuoteResult(value) {
18999
+ return validateQuoteResultSchema.safeParse(value).success;
19000
+ }
19001
+
19002
+ /**
19003
+ * Validate that a CCTP domain id is a non-negative integer.
19004
+ *
19005
+ * @param value - The domain id to validate.
19006
+ * @param label - The parameter name, used in the error message.
19007
+ * @returns Nothing.
19008
+ * @throws {@link KitError} When the value is not a non-negative integer.
19009
+ * @internal
19010
+ */ function assertDomain(value, label) {
19011
+ if (!Number.isInteger(value) || value < 0) {
19012
+ throw new KitError({
19013
+ ...InputError.VALIDATION_FAILED,
19014
+ recoverability: 'FATAL',
19015
+ message: `Quote API getFeeQuote failed: ${label} must be a ` + `non-negative integer, received ${String(value)}`,
19016
+ cause: {
19017
+ trace: {
19018
+ [label]: value
19019
+ }
19020
+ }
19021
+ });
19022
+ }
19023
+ }
19024
+ /**
19025
+ * Build the Quote API URL for a CCTP v2 USDC burn quote.
19026
+ *
19027
+ * Resolves the environment base URL (or an explicit `baseUrl` override) and
19028
+ * appends the burn/usdc path with the source and destination CCTP domains.
19029
+ * `usdc` is a fixed path literal, not a token parameter.
19030
+ *
19031
+ * @param params - The domains and environment selector.
19032
+ * @returns The fully-qualified Quote API URL.
19033
+ * @throws {@link KitError} When either domain is not a non-negative integer.
19034
+ *
19035
+ * @example
19036
+ * ```typescript
19037
+ * import { buildFeeQuoteUrl } from '@circle-fin/provider-fee-v1'
19038
+ *
19039
+ * const url = buildFeeQuoteUrl({
19040
+ * sourceDomain: 3,
19041
+ * destinationDomain: 26,
19042
+ * isTestnet: false,
19043
+ * })
19044
+ * // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
19045
+ * ```
19046
+ *
19047
+ * @internal
19048
+ */ function buildFeeQuoteUrl(params) {
19049
+ const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
19050
+ assertDomain(sourceDomain, 'sourceDomain');
19051
+ assertDomain(destinationDomain, 'destinationDomain');
19052
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
19053
+ return new URL(`${QUOTE_BURN_USDC_PATH}/${String(sourceDomain)}/${String(destinationDomain)}`, resolvedBaseUrl).toString();
19054
+ }
19055
+
19056
+ /**
19057
+ * Assert that a quote's per-item fee amounts sum to its `feeTotalAmount`.
19058
+ *
19059
+ * A defensive integrity check on the Quote API response, enforced internally
19060
+ * by `fetchFeeQuote`. It does not compare against the destination-side
19061
+ * `feeExecuted`, which is expected to be zero for prepaid forward-only burns.
19062
+ *
19063
+ * @param quote - The signed fee quote to check.
19064
+ * @returns Nothing.
19065
+ * @throws {@link KitError} When the item amounts do not sum to `feeTotalAmount`.
19066
+ * @internal
19067
+ */ function assertFeeItemsSumToTotal(quote) {
19068
+ const itemsTotal = quote.items.reduce((sum, item)=>sum + BigInt(item.amount), 0n);
19069
+ const declaredTotal = BigInt(quote.feeTotalAmount);
19070
+ if (itemsTotal !== declaredTotal) {
19071
+ throw new KitError({
19072
+ ...InputError.VALIDATION_FAILED,
19073
+ recoverability: 'FATAL',
19074
+ message: `Quote API getFeeQuote failed: fee items sum ` + `(${itemsTotal.toString()}) does not equal feeTotalAmount ` + `(${declaredTotal.toString()})`,
19075
+ cause: {
19076
+ trace: {
19077
+ itemsTotal: itemsTotal.toString(),
19078
+ feeTotalAmount: quote.feeTotalAmount
19079
+ }
19080
+ }
19081
+ });
19082
+ }
19083
+ }
19084
+
19085
+ /**
19086
+ * Determine whether a Quote API error represents a disabled source chain.
19087
+ *
19088
+ * @param error - The error thrown by the HTTP layer.
19089
+ * @returns `true` when the error contains the `SERVICE_NOT_ENABLED` marker.
19090
+ * @internal
19091
+ */ function isServiceNotEnabled(error) {
19092
+ const body = typeof error === 'object' && error !== null && 'responseBody' in error ? error.responseBody : undefined;
19093
+ if (typeof body === 'object' && body !== null) {
19094
+ const fields = body;
19095
+ const candidates = [
19096
+ fields['errorCode'],
19097
+ fields['code'],
19098
+ fields['message'],
19099
+ fields['externalMessage'],
19100
+ fields['error']
19101
+ ];
19102
+ if (candidates.some((value)=>typeof value === 'string' && value.toUpperCase().includes('SERVICE_NOT_ENABLED'))) {
19103
+ return true;
19104
+ }
19105
+ }
19106
+ return getErrorMessage(error).toUpperCase().includes('SERVICE_NOT_ENABLED');
19107
+ }
19108
+
19109
+ const SERVICE$1 = 'Quote API';
19110
+ const OPERATION$1 = 'getFeeQuote';
19111
+ /**
19112
+ * Serialize request items for the wire body.
19113
+ *
19114
+ * `PRE_FINALITY` is emitted with no `params` key, and a `FORWARD` item only
19115
+ * carries the binding fields that are present.
19116
+ *
19117
+ * @param requests - The request items to serialize.
19118
+ * @returns The serialized request items.
19119
+ * @internal
19120
+ */ function serializeRequests(requests) {
19121
+ return requests.map((request)=>{
19122
+ if (request.type === 'PRE_FINALITY') {
19123
+ return {
19124
+ type: 'PRE_FINALITY'
19125
+ };
19126
+ }
19127
+ const params = request.params;
19128
+ if (params === undefined) {
19129
+ return {
19130
+ type: 'FORWARD'
19131
+ };
19132
+ }
19133
+ const forwardParams = {};
19134
+ if (params.hookData !== undefined) {
19135
+ forwardParams.hookData = params.hookData;
19136
+ }
19137
+ if (params.destinationCaller !== undefined) {
19138
+ forwardParams.destinationCaller = params.destinationCaller;
19139
+ }
19140
+ return {
19141
+ type: 'FORWARD',
19142
+ params: forwardParams
19143
+ };
19144
+ });
19145
+ }
19146
+ /**
19147
+ * Fetch a signed fee quote from Circle's Quote API for a CCTP v2 USDC burn.
19148
+ *
19149
+ * Validates inputs, POSTs to
19150
+ * `/v2/quote/burn/usdc/{sourceDomain}/{destinationDomain}` with a single
19151
+ * attempt (the signed quote is short-lived), and returns the typed quote. A
19152
+ * disabled source chain (`503 SERVICE_NOT_ENABLED`) surfaces as a fatal,
19153
+ * non-retryable error; other failures are mapped to a {@link KitError} via the
19154
+ * shared API error parser.
19155
+ *
19156
+ * @param params - The domains, amount, request items, and environment.
19157
+ * @returns The signed fee quote.
19158
+ * @throws {@link KitError} On invalid input, a disabled source chain, an HTTP
19159
+ * error, or an invalid response shape.
19160
+ *
19161
+ * @example
19162
+ * ```typescript
19163
+ * import { fetchFeeQuote } from '@circle-fin/provider-fee-v1'
19164
+ *
19165
+ * const quote = await fetchFeeQuote({
19166
+ * sourceDomain: 3,
19167
+ * destinationDomain: 26,
19168
+ * amount: '1000000',
19169
+ * requests: [{ type: 'FORWARD' }, { type: 'PRE_FINALITY' }],
19170
+ * isTestnet: false,
19171
+ * })
19172
+ * console.log(quote.feeTotalAmount, quote.expiry)
19173
+ * ```
19174
+ *
19175
+ * @internal
19176
+ */ async function fetchFeeQuote(params) {
19177
+ const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
19178
+ const parsed = fetchFeeQuoteInputSchema.safeParse({
19179
+ sourceDomain,
19180
+ destinationDomain,
19181
+ amount,
19182
+ feeToken,
19183
+ requests,
19184
+ isTestnet,
19185
+ baseUrl,
19186
+ config
19187
+ });
19188
+ if (!parsed.success) {
19189
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
19190
+ throw new KitError({
19191
+ ...InputError.VALIDATION_FAILED,
19192
+ recoverability: 'FATAL',
19193
+ message: `${SERVICE$1} ${OPERATION$1} failed: ${detail}`,
19194
+ cause: {
19195
+ trace: parsed.error.issues
19196
+ }
19197
+ });
19198
+ }
19199
+ const url = baseUrl === undefined ? buildFeeQuoteUrl({
19200
+ sourceDomain,
19201
+ destinationDomain,
19202
+ isTestnet
19203
+ }) : buildFeeQuoteUrl({
19204
+ sourceDomain,
19205
+ destinationDomain,
19206
+ isTestnet,
19207
+ baseUrl
19208
+ });
19209
+ const body = {
19210
+ amount,
19211
+ feeToken: feeToken ?? NATIVE_FEE_TOKEN,
19212
+ requests: serializeRequests(requests)
19213
+ };
19214
+ const pollingConfig = {
19215
+ ...FEE_QUOTE_DEFAULT_CONFIG,
19216
+ ...config
19217
+ };
19218
+ let quote;
19219
+ try {
19220
+ quote = await pollApiPost(url, body, isSignedFeeQuote, pollingConfig);
19221
+ } catch (error) {
19222
+ // Only one service-specific code (SERVICE_NOT_ENABLED) needs bespoke
19223
+ // mapping, so it is detected inline rather than via a dedicated
19224
+ // `parseFeeQuoteApiError` parser; everything else flows through the shared
19225
+ // `parseApiError`. Promote to a parser if more coded errors appear.
19226
+ if (isServiceNotEnabled(error)) {
19227
+ throw new KitError({
19228
+ ...InputError.UNSUPPORTED_ROUTE,
19229
+ recoverability: 'FATAL',
19230
+ message: `${SERVICE$1} ${OPERATION$1} failed: source chain not enabled for fee ` + `quotes (SERVICE_NOT_ENABLED)`,
19231
+ cause: {
19232
+ trace: error
19233
+ }
19234
+ });
19235
+ }
19236
+ throw parseApiError(error, {
19237
+ service: SERVICE$1,
19238
+ operation: OPERATION$1
19239
+ });
19240
+ }
19241
+ // Defense-in-depth: a self-consistent quote's per-item fees sum to the
19242
+ // declared total. Enforced here so callers cannot forget the check.
19243
+ assertFeeItemsSumToTotal(quote);
19244
+ return quote;
19245
+ }
19246
+
19247
+ const SERVICE = 'Quote API';
19248
+ const OPERATION = 'validateQuote';
19249
+ /**
19250
+ * Validate a signed fee quote against a full on-chain call via Circle's Iris
19251
+ * `/v2/quote/validate/usdc/:sourceDomain` endpoint.
19252
+ *
19253
+ * POSTs the ABI function signature and the encoded call arguments to Iris,
19254
+ * which verifies the signature, checks expiry, confirms the argsHash committed
19255
+ * in the quote matches the submitted args, and returns `claimable` plus the
19256
+ * decoded `expiry`, `feeToken`, and `feeTotalAmount`.
19257
+ *
19258
+ * @param params - The source domain, ABI signature, call arguments, and environment.
19259
+ * @returns The claimability, binding checks, and authoritative expiry status.
19260
+ * @throws {@link KitError} When input, transport, or response validation fails.
19261
+ *
19262
+ * @example
19263
+ * ```typescript
19264
+ * import { validateQuote } from '@circle-fin/provider-fee-v1'
19265
+ *
19266
+ * const result = await validateQuote({
19267
+ * sourceDomain: 3,
19268
+ * // The exact function + arguments, in ABI order, that will be burned on-chain.
19269
+ * abiSignature:
19270
+ * 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))',
19271
+ * args: [
19272
+ * '1000000',
19273
+ * '26',
19274
+ * '0x0000000000000000000000001111111111111111111111111111111111111111',
19275
+ * '0x2222222222222222222222222222222222222222',
19276
+ * '0x0000000000000000000000000000000000000000000000000000000000000000',
19277
+ * '0x636374702d666f72776172640000000000000000000000000000000000000000',
19278
+ * ['0x01abcd', '0x3333333333333333333333333333333333333333'],
19279
+ * ],
19280
+ * isTestnet: false,
19281
+ * })
19282
+ * console.log(result.claimable, result.expiry.secondsRemaining)
19283
+ * if (!result.claimable) {
19284
+ * console.error('Quote rejected:', result.failedChecks)
19285
+ * }
19286
+ * ```
19287
+ *
19288
+ * @internal
19289
+ */ async function validateQuote(params) {
19290
+ const parsed = validateQuoteInputSchema.safeParse(params);
19291
+ if (!parsed.success) {
19292
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
19293
+ throw new KitError({
19294
+ ...InputError.VALIDATION_FAILED,
19295
+ recoverability: 'FATAL',
19296
+ message: `${SERVICE} ${OPERATION} failed: ${detail}`,
19297
+ cause: {
19298
+ trace: parsed.error.issues
19299
+ }
19300
+ });
19301
+ }
19302
+ const { sourceDomain, abiSignature, args, isTestnet, baseUrl } = parsed.data;
19303
+ // `config` was validated by the schema above; spread the caller's original,
19304
+ // precisely typed `Partial<ApiPollingConfig>` so the merged polling config
19305
+ // stays assignable under `exactOptionalPropertyTypes`.
19306
+ const pollingConfig = {
19307
+ ...FEE_QUOTE_DEFAULT_CONFIG,
19308
+ ...params.config
19309
+ };
19310
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
19311
+ const url = new URL(`${QUOTE_VALIDATE_USDC_PATH}/${String(sourceDomain)}`, resolvedBaseUrl).toString();
19312
+ try {
19313
+ return await pollApiPost(url, {
19314
+ abiSignature,
19315
+ args
19316
+ }, isValidateQuoteResult, pollingConfig);
19317
+ } catch (error) {
19318
+ if (isServiceNotEnabled(error)) {
19319
+ throw new KitError({
19320
+ ...InputError.UNSUPPORTED_ROUTE,
19321
+ recoverability: 'FATAL',
19322
+ message: `${SERVICE} ${OPERATION} failed: source chain not enabled for ` + `quote validation (SERVICE_NOT_ENABLED)`,
19323
+ cause: {
19324
+ trace: error
19325
+ }
19326
+ });
19327
+ }
19328
+ throw parseApiError(error, {
19329
+ service: SERVICE,
19330
+ operation: OPERATION
19331
+ });
19332
+ }
19333
+ }
19334
+
19335
+ /** Refresh timestamp quotes this many seconds before submission. */ const QUOTE_EXPIRY_SAFETY_SECONDS = 30;
19336
+ /** 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))';
19337
+ /** Unrestricted CCTP destination caller used by the forwarding relayer. */ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
19338
+ function isQuoteNearEstimatedExpiry(quote) {
19339
+ const currentSeconds = Math.floor(Date.now() / 1_000);
19340
+ const expiresAt = quote.expiry.mode === 'TIMESTAMP' ? quote.expiry.expiresAt : quote.expiry.blockEstimatedAt;
19341
+ // A BLOCK_NUMBER quote may omit the advisory `blockEstimatedAt` estimate; when
19342
+ // it is absent, skip this wall-clock pre-check and defer to the authoritative
19343
+ // source-chain-tip validation performed downstream.
19344
+ if (expiresAt === undefined) {
19345
+ return false;
19346
+ }
19347
+ return expiresAt <= currentSeconds + QUOTE_EXPIRY_SAFETY_SECONDS;
19348
+ }
19349
+ function assertSourceFeeRoute(params) {
19350
+ const { source, destination, config } = params;
19351
+ // Keep these checks explicit so a successful assertion guarantees the CCTP
19352
+ // v2 narrowing; hasSourceFeeSupport returns a plain boolean.
19353
+ if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
19354
+ throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
19355
+ }
19356
+ if (!hasSourceFeeSupport(source.chain)) {
19357
+ 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');
19358
+ }
19359
+ const useForwarder = destination.useForwarder;
19360
+ if (useForwarder !== true) {
19361
+ throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
19362
+ }
19363
+ if (config.customFee !== undefined) {
19364
+ 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.');
19365
+ }
19366
+ }
19367
+ function buildQuoteBinding(params) {
19368
+ assertSourceFeeRoute(params);
19369
+ const mintRecipient = params.destination.recipientAddress ?? params.destination.address;
19370
+ const hookData = buildForwardingHookData();
19371
+ const requests = [
19372
+ {
19373
+ type: 'FORWARD',
19374
+ params: {
19375
+ hookData,
19376
+ destinationCaller: ZERO_ADDRESS
19377
+ }
19378
+ }
19379
+ ];
19380
+ if ((params.config.transferSpeed ?? TransferSpeed.FAST) === TransferSpeed.FAST) {
19381
+ requests.push({
19382
+ type: 'PRE_FINALITY'
19383
+ });
19384
+ }
19385
+ return {
19386
+ sourceDomain: params.source.chain.cctp.domain,
19387
+ destinationDomain: params.destination.chain.cctp.domain,
19388
+ isTestnet: params.source.chain.isTestnet,
19389
+ amount: params.amount,
19390
+ mintRecipient,
19391
+ hookData,
19392
+ destinationCaller: ZERO_ADDRESS,
19393
+ feeToken: params.source.chain.usdcAddress,
19394
+ requests
19395
+ };
19396
+ }
19397
+ async function fetchBoundQuote(binding) {
19398
+ try {
19399
+ const quote = await fetchFeeQuote({
19400
+ sourceDomain: binding.sourceDomain,
19401
+ destinationDomain: binding.destinationDomain,
19402
+ amount: binding.amount,
19403
+ feeToken: binding.feeToken,
19404
+ requests: binding.requests,
19405
+ isTestnet: binding.isTestnet
19406
+ });
19407
+ if (quote.feeToken.toLowerCase() !== binding.feeToken.toLowerCase()) {
19408
+ throw createValidationFailedError$1('feeToken', quote.feeToken, 'Fee Service must return source-chain USDC for source-fee bridging');
19409
+ }
19410
+ return quote;
19411
+ } catch (error) {
19412
+ if (isRateLimitError(error)) {
19413
+ throw new KitError({
19414
+ ...RateLimitError.RATE_LIMIT_EXCEEDED,
19415
+ recoverability: 'RETRYABLE',
19416
+ message: 'Fee Service rate limit exceeded. Retry with caller-managed exponential backoff; Bridge Kit does not retry signed quote requests automatically.',
19417
+ cause: {
19418
+ trace: error
19419
+ }
19420
+ });
19421
+ }
19422
+ throw error;
19423
+ }
19424
+ }
19425
+ async function fetchSubmissionQuote(binding) {
19426
+ let quote = await fetchBoundQuote(binding);
19427
+ if (isQuoteNearEstimatedExpiry(quote)) {
19428
+ quote = await fetchBoundQuote(binding);
19429
+ }
19430
+ if (isQuoteNearEstimatedExpiry(quote)) {
19431
+ throw createValidationFailedError$1('quote', undefined, 'Fee Service returned a quote too close to expiry for safe submission');
19432
+ }
19433
+ return quote;
19434
+ }
19435
+ async function validateBoundQuote(binding, signedQuote, refundAddress) {
19436
+ return validateQuote({
19437
+ sourceDomain: binding.sourceDomain,
19438
+ abiSignature: BURN_WITH_FEES_ABI_SIGNATURE,
19439
+ args: [
19440
+ binding.amount,
19441
+ String(binding.destinationDomain),
19442
+ padAddressToBytes32(binding.mintRecipient),
19443
+ binding.feeToken,
19444
+ padAddressToBytes32(binding.destinationCaller),
19445
+ binding.hookData,
19446
+ [
19447
+ signedQuote,
19448
+ refundAddress
19449
+ ]
19450
+ ],
19451
+ isTestnet: binding.isTestnet
19452
+ });
19453
+ }
19454
+ function isValidationSafe(binding, signedQuote, validation, expectedQuote) {
19455
+ 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);
19456
+ }
19457
+ function unsafeQuoteError(validation) {
19458
+ const detail = validation.failedChecks.length > 0 ? ` (${validation.failedChecks.join(', ')})` : '';
19459
+ return createValidationFailedError$1('quote', undefined, `The fee quote is not safe for submission${detail}; ` + 'call estimate again to obtain a valid quote');
19460
+ }
19461
+ function toExecutionFeeQuote(quote) {
19462
+ return {
19463
+ signedQuote: quote.signedQuote,
19464
+ feeToken: quote.feeToken,
19465
+ feeTotalAmount: quote.feeTotalAmount
19466
+ };
19467
+ }
19468
+ function toFeeItems(quote) {
19469
+ return quote.items.map((item)=>({
19470
+ type: item.type,
19471
+ amount: formatUnits(item.amount, 6),
19472
+ args: item.args,
19473
+ argsHash: item.argsHash
19474
+ }));
19475
+ }
19476
+ /**
19477
+ * Estimate a source-fee bridge using a source-denominated signed fee quote.
19478
+ *
19479
+ * @internal
19480
+ */ async function estimateSourceFeeBridge(params) {
19481
+ const binding = buildQuoteBinding(params);
19482
+ const quote = await fetchSubmissionQuote(binding);
19483
+ const feeTotal = formatUnits(quote.feeTotalAmount, 6);
19484
+ return {
19485
+ token: 'USDC',
19486
+ amount: formatUnits(params.amount, 6),
19487
+ source: {
19488
+ address: params.source.address,
19489
+ chain: params.source.chain.chain
19490
+ },
19491
+ destination: {
19492
+ address: params.destination.address,
19493
+ chain: params.destination.chain.chain,
19494
+ ...params.destination.recipientAddress !== undefined && {
19495
+ recipientAddress: params.destination.recipientAddress
19496
+ }
19497
+ },
19498
+ gasFees: [],
19499
+ fees: quote.items.map((item)=>({
19500
+ type: item.type === 'FORWARD' ? 'forwarder' : 'provider',
19501
+ token: 'USDC',
19502
+ amount: formatUnits(item.amount, 6)
19503
+ })),
19504
+ amountReceived: formatUnits(params.amount, 6),
19505
+ feeTotal,
19506
+ feeItems: toFeeItems(quote),
19507
+ totalDebit: formatUnits((BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString(), 6),
19508
+ quoteExpiry: quote.expiry,
19509
+ quote: quote.signedQuote
19510
+ };
19511
+ }
19512
+ async function readAllowance(params, delegate) {
19513
+ const operationContext = {
19514
+ chain: params.source.chain,
19515
+ address: params.source.address
19516
+ };
19517
+ const prepared = await params.source.adapter.prepareAction('usdc.allowance', {
19518
+ walletAddress: params.source.address,
19519
+ delegate
19520
+ }, operationContext);
19521
+ return BigInt(String(await prepared.execute()));
19522
+ }
19523
+ async function executeAndConfirm(request, params, provider) {
19524
+ const txHash = await request.execute();
19525
+ const data = await provider.waitForTransaction(params.source.adapter, txHash, params.source.chain);
19526
+ return {
19527
+ txHash,
19528
+ data
19529
+ };
19530
+ }
19531
+ async function prepareAndPreflight(params, binding, quote, provider) {
19532
+ assertSourceFeeRoute(params);
19533
+ const totalDebit = (BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString();
19534
+ const operationContext = {
19535
+ chain: params.source.chain,
19536
+ address: params.source.address
19537
+ };
19538
+ await validateBalanceForTransaction({
19539
+ adapter: params.source.adapter,
19540
+ amount: totalDebit,
19541
+ token: 'USDC',
19542
+ tokenAddress: params.source.chain.usdcAddress,
19543
+ operationContext
19544
+ });
19545
+ const prepared = await provider.burnWithFees({
19546
+ source: params.source,
19547
+ destinationChain: params.destination.chain,
19548
+ amount: params.amount,
19549
+ mintRecipient: binding.mintRecipient,
19550
+ destinationCaller: binding.destinationCaller,
19551
+ hookData: binding.hookData,
19552
+ claim: {
19553
+ signedQuote: quote.signedQuote,
19554
+ refundAddress: params.source.address
19555
+ },
19556
+ feeToken: quote.feeToken,
19557
+ feeTotalAmount: quote.feeTotalAmount
19558
+ });
19559
+ const wrapper = resolveCCTPV2ContractAddress(params.source.chain, 'tokenMessengerWithFees');
19560
+ const allowance = await readAllowance(params, wrapper);
19561
+ if (allowance >= BigInt(totalDebit)) {
19562
+ return {
19563
+ prepared,
19564
+ approveStep: {
19565
+ name: 'approve',
19566
+ state: 'noop'
19567
+ }
16770
19568
  };
16771
19569
  }
16772
- /**
16773
- * Waits for a transaction to be mined and confirmed on the blockchain.
16774
- *
16775
- * This method should block until the transaction is confirmed on the blockchain.
16776
- *
16777
- * @param adapter - The adapter to use for transaction waiting
16778
- * @param txHash - The hash of the transaction to wait for
16779
- * @param chain - The chain definition where the transaction was executed
16780
- * @param config - Optional configuration for transaction waiting (confirmations, timeout)
16781
- * @returns The hash of the confirmed transaction
16782
- * @example
16783
- * ```typescript
16784
- * const provider = new CCTPV2BridgingProvider()
16785
- * const txHash = await provider.waitForTransaction(
16786
- * adapter,
16787
- * '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
16788
- * Ethereum,
16789
- * )
16790
- * console.log('Transaction confirmed:', txHash)
16791
- * ```
16792
- */ async waitForTransaction(adapter, txHash, chain, config) {
16793
- return adapter.waitForTransaction(txHash, config, chain);
19570
+ let lastApproval;
19571
+ for (const approval of prepared.approvals){
19572
+ lastApproval = await executeAndConfirm(approval, params, provider);
16794
19573
  }
16795
- }
16796
-
16797
- /**
16798
- * The default providers that will be used in addition to the providers provided
16799
- * to the BridgeKit constructor.
16800
- *
16801
- * @param config - Optional configuration forwarded to the default providers
16802
- * @returns The default bridging providers
16803
- */ const getDefaultProviders = (config = {})=>[
16804
- new CCTPV2BridgingProvider(config.headers ? {
16805
- headers: config.headers
16806
- } : {})
16807
- ];
16808
-
16809
- /**
16810
- * A helper function to get a function that transforms an amount into a human-readable string or a bigint string.
16811
- * @param formatDirection - The direction to format the amount in.
16812
- * @returns A function that transforms an amount into a human-readable string or a bigint string.
16813
- */ const getAmountTransformer = (formatDirection)=>formatDirection === 'to-human-readable' ? (params)=>formatAmount(params) : (params)=>parseAmount(params).toString();
16814
- /**
16815
- * Format the bridge result into human-readable string values for the user or bigint string values for internal use.
16816
- *
16817
- * @typeParam T - The specific result type (must extend BridgeResult or EstimateResult). Preserves the exact type passed in.
16818
- * @param result - The bridge result to format.
16819
- * @param formatDirection - The direction to format the result in.
16820
- * - If 'to-human-readable', the result will be converted to human-readable string values.
16821
- * - If 'to-internal', the result will be converted to bigint string values (usually for internal use).
16822
- * @returns The formatted bridge result.
16823
- *
16824
- * @example
16825
- * ```typescript
16826
- * const result = await kit.bridge({
16827
- * amount: '1000000',
16828
- * token: 'USDC',
16829
- * from: { adapter: adapter, chain: 'Ethereum' },
16830
- * to: { adapter: adapter, chain: 'Base' },
16831
- * })
16832
- *
16833
- * // Format the bridge result into human-readable string values for the user
16834
- * const formattedResultHumanReadable = formatBridgeResult(result, 'to-human-readable')
16835
- * console.log(formattedResultHumanReadable)
16836
- *
16837
- * // Format the bridge result into bigint string values for internal use
16838
- * const formattedResultInternal = formatBridgeResult(result, 'to-internal')
16839
- * console.log(formattedResultInternal)
16840
- * ```
16841
- */ const formatBridgeResult = (result, formatDirection)=>{
16842
- const transform = getAmountTransformer(formatDirection);
16843
19574
  return {
16844
- ...result,
16845
- amount: transform({
16846
- value: result.amount,
16847
- token: result.token
16848
- }),
16849
- ...'config' in result && result.config && Object.keys(result.config).length > 0 && {
16850
- config: {
16851
- ...result.config,
16852
- ...result.config.maxFee && {
16853
- maxFee: transform({
16854
- value: result.config.maxFee,
16855
- token: result.token
16856
- })
16857
- },
16858
- ...result.config.customFee && {
16859
- customFee: {
16860
- ...result.config.customFee,
16861
- ...result.config.customFee.value && {
16862
- value: transform({
16863
- value: result.config.customFee.value,
16864
- token: result.token
16865
- })
16866
- }
16867
- }
16868
- }
19575
+ prepared,
19576
+ approveStep: {
19577
+ name: 'approve',
19578
+ state: 'success',
19579
+ data: lastApproval?.data,
19580
+ ...lastApproval?.txHash !== undefined && {
19581
+ txHash: lastApproval.txHash,
19582
+ explorerUrl: buildExplorerUrl(params.source.chain, lastApproval.txHash)
16869
19583
  }
16870
19584
  }
16871
19585
  };
16872
- };
16873
-
16874
- /**
16875
- * Register all bridge-kit event type strings with the shared registry so
16876
- * callers of `withErrorTelemetry` / `emitResultStepErrorTelemetry` are
16877
- * compile-time checked.
16878
- *
16879
- * @internal
16880
- */ /**
16881
- * Telemetry event type identifiers for bridge-kit operations.
16882
- *
16883
- * @internal
16884
- */ const BRIDGE_EVENT_TYPES = {
16885
- BRIDGE: 'bridge_bridge',
16886
- RETRY: 'bridge_retry',
16887
- ESTIMATE: 'bridge_estimate'
16888
- };
19586
+ }
19587
+ async function resolveExecutionQuote(binding, suppliedQuote, refundAddress) {
19588
+ if (suppliedQuote === undefined) {
19589
+ const fetchedQuote = await fetchSubmissionQuote(binding);
19590
+ return {
19591
+ quote: toExecutionFeeQuote(fetchedQuote),
19592
+ fetchedQuote
19593
+ };
19594
+ }
19595
+ const validation = await validateBoundQuote(binding, suppliedQuote, refundAddress);
19596
+ if (!isValidationSafe(binding, suppliedQuote, validation)) {
19597
+ throw unsafeQuoteError(validation);
19598
+ }
19599
+ return {
19600
+ quote: toExecutionFeeQuote(validation),
19601
+ fetchedQuote: undefined
19602
+ };
19603
+ }
16889
19604
  /**
16890
- * Ordered mapping from provider step event names to telemetry event types.
16891
- *
16892
- * @remarks
16893
- * The order matches the CCTP v2 bridge execution sequence. During
16894
- * `bridge()`, completed step events are counted so the failing step
16895
- * can be identified by its index.
19605
+ * Execute a source-fee bridge while preserving receive-exact semantics.
16896
19606
  *
16897
19607
  * @internal
16898
- */ const BRIDGE_STEP_EVENT_MAP = [
16899
- [
16900
- 'approve',
16901
- 'bridge_approve'
16902
- ],
16903
- [
16904
- 'burn',
16905
- 'bridge_burn'
16906
- ],
16907
- [
16908
- 'fetchAttestation',
16909
- 'bridge_fetch_attestation'
16910
- ],
16911
- [
16912
- 'mint',
16913
- 'bridge_mint'
16914
- ]
16915
- ];
19608
+ */ async function executeSourceFeeBridge(rawParams, params, provider) {
19609
+ // Narrows `params` to the CCTP v2 route type for the rest of this function.
19610
+ // buildQuoteBinding asserts too, but that narrows its own scope, not this one.
19611
+ assertSourceFeeRoute(params);
19612
+ const binding = buildQuoteBinding(params);
19613
+ const suppliedQuote = rawParams.quote;
19614
+ let { quote, fetchedQuote } = await resolveExecutionQuote(binding, suppliedQuote, params.source.address);
19615
+ let { prepared, approveStep } = await prepareAndPreflight(params, binding, quote, provider);
19616
+ // Validate against the current source-chain tip after approval confirmation.
19617
+ // BLOCK_NUMBER expiries cannot be checked safely with wall-clock time alone.
19618
+ let validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
19619
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
19620
+ if (suppliedQuote === undefined) {
19621
+ fetchedQuote = await fetchSubmissionQuote(binding);
19622
+ quote = toExecutionFeeQuote(fetchedQuote);
19623
+ const refreshedPreparation = await prepareAndPreflight(params, binding, quote, provider);
19624
+ prepared = refreshedPreparation.prepared;
19625
+ if (refreshedPreparation.approveStep.state !== 'noop') {
19626
+ approveStep = refreshedPreparation.approveStep;
19627
+ }
19628
+ validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
19629
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
19630
+ throw unsafeQuoteError(validation);
19631
+ }
19632
+ } else {
19633
+ throw unsafeQuoteError(validation);
19634
+ }
19635
+ }
19636
+ // Surface the same step events as the standard bridge() path so
19637
+ // kit.on('approve'|'burn'|'mint', ...) handlers fire for source-fee bridges.
19638
+ if (approveStep.state !== 'noop') {
19639
+ provider.emitBridgeStep('approve', approveStep);
19640
+ }
19641
+ const burn = await executeAndConfirm(prepared.burn, params, provider);
19642
+ const burnStep = {
19643
+ name: 'burn',
19644
+ state: 'success',
19645
+ txHash: burn.txHash,
19646
+ data: burn.data,
19647
+ explorerUrl: buildExplorerUrl(params.source.chain, burn.txHash)
19648
+ };
19649
+ provider.emitBridgeStep('burn', burnStep);
19650
+ const resultBase = {
19651
+ amount: params.amount,
19652
+ token: 'USDC',
19653
+ config: params.config,
19654
+ provider: provider.name,
19655
+ source: {
19656
+ address: params.source.address,
19657
+ chain: params.source.chain
19658
+ },
19659
+ destination: {
19660
+ address: params.destination.address,
19661
+ chain: params.destination.chain,
19662
+ ...params.destination.recipientAddress !== undefined && {
19663
+ recipientAddress: params.destination.recipientAddress
19664
+ },
19665
+ useForwarder: true
19666
+ }
19667
+ };
19668
+ const attestation = await provider.fetchRelayerMint(params.source, burn.txHash);
19669
+ const forwardTxHash = attestation.forwardTxHash;
19670
+ // The burn already moved funds. If the relayer confirms without a
19671
+ // destination hash, surface an error-state result that preserves the burn
19672
+ // step (so `retry()` can resume the mint) instead of throwing and discarding
19673
+ // the completed burn.
19674
+ if (typeof forwardTxHash !== 'string' || forwardTxHash.trim() === '') {
19675
+ const mintStep = {
19676
+ name: 'mint',
19677
+ state: 'error',
19678
+ forwarded: true,
19679
+ errorCategory: 'failed_offchain',
19680
+ errorMessage: 'Relayer confirmation did not include a destination transaction hash'
19681
+ };
19682
+ provider.emitBridgeStep('mint', mintStep);
19683
+ return {
19684
+ ...resultBase,
19685
+ state: 'error',
19686
+ steps: [
19687
+ approveStep,
19688
+ burnStep,
19689
+ mintStep
19690
+ ]
19691
+ };
19692
+ }
19693
+ const mintStep = {
19694
+ name: 'mint',
19695
+ state: 'success',
19696
+ forwarded: true,
19697
+ txHash: forwardTxHash,
19698
+ explorerUrl: buildExplorerUrl(params.destination.chain, forwardTxHash)
19699
+ };
19700
+ provider.emitBridgeStep('mint', mintStep);
19701
+ return {
19702
+ ...resultBase,
19703
+ state: 'success',
19704
+ steps: [
19705
+ approveStep,
19706
+ burnStep,
19707
+ mintStep
19708
+ ]
19709
+ };
19710
+ }
16916
19711
 
16917
19712
  /** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg$3.name);
16918
19713
  /**
@@ -17141,11 +19936,18 @@ function assertCCTPV2Config(config) {
17141
19936
  this.validateNetworkCompatibility(resolvedParams);
17142
19937
  // Merge the custom fee config into the resolved params
17143
19938
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
17144
- // Find a provider that supports this route
17145
- const provider = this.findProviderForRoute(finalResolvedParams);
17146
- // Execute the transfer using the provider
17147
- // Format the bridge result into human-readable string values for the user
17148
- const result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
19939
+ let result;
19940
+ // Execute the explicit source-fee path without changing legacy
19941
+ // useForwarder behavior for callers that did not opt in.
19942
+ if (params.config?.feePayment === 'source') {
19943
+ const sourceFeeProvider = this.findSourceFeeProvider(finalResolvedParams);
19944
+ result = formatBridgeResult(await executeSourceFeeBridge(params, finalResolvedParams, sourceFeeProvider), 'to-human-readable');
19945
+ } else {
19946
+ // Find a provider that supports this route
19947
+ const provider = this.findProviderForRoute(finalResolvedParams);
19948
+ // Execute the transfer using the provider and format the result.
19949
+ result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
19950
+ }
17149
19951
  // Emit error telemetry when the provider returns an error state
17150
19952
  // (provider records step failures in the result instead of throwing).
17151
19953
  if (result.state === 'error') {
@@ -17268,42 +20070,7 @@ function assertCCTPV2Config(config) {
17268
20070
  tokenIn: result.token
17269
20071
  });
17270
20072
  }
17271
- /**
17272
- * Estimate the cost and fees for a cross-chain USDC bridge operation.
17273
- *
17274
- * This method calculates the expected gas fees and protocol costs for bridging
17275
- * without actually executing the transaction. It performs the same validation
17276
- * as the bridge method but stops before execution.
17277
- *
17278
- * @param params - The bridge parameters for cost estimation, including optional invocation metadata
17279
- * @returns Promise resolving to detailed cost breakdown including gas estimates
17280
- * @throws {KitError} When the parameters are invalid.
17281
- * @throws {UnsupportedRouteError} When the route is not supported.
17282
- *
17283
- * @example
17284
- * ```typescript
17285
- * // Basic usage
17286
- * const estimate = await kit.estimate({
17287
- * from: { adapter: adapter, chain: 'Ethereum' },
17288
- * to: { adapter: adapter, chain: 'Base' },
17289
- * amount: '10.50',
17290
- * token: 'USDC'
17291
- * })
17292
- * console.log('Estimated cost:', estimate.totalCost)
17293
- *
17294
- * // With custom invocation metadata
17295
- * const estimate = await kit.estimate({
17296
- * from: { adapter: adapter, chain: 'Ethereum' },
17297
- * to: { adapter: adapter, chain: 'Base' },
17298
- * amount: '10.50',
17299
- * token: 'USDC',
17300
- * invocationMeta: {
17301
- * traceId: 'custom-trace-id',
17302
- * callers: [{ type: 'app', name: 'MyDApp', version: '1.0.0' }],
17303
- * },
17304
- * })
17305
- * ```
17306
- */ async estimate(params) {
20073
+ async estimate(params) {
17307
20074
  return withErrorTelemetry(async ()=>{
17308
20075
  // First validate the parameters
17309
20076
  assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
@@ -17313,6 +20080,10 @@ function assertCCTPV2Config(config) {
17313
20080
  this.validateNetworkCompatibility(resolvedParams);
17314
20081
  // Merge the custom fee config into the resolved params
17315
20082
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
20083
+ if (params.config?.feePayment === 'source') {
20084
+ this.findSourceFeeProvider(finalResolvedParams);
20085
+ return estimateSourceFeeBridge(finalResolvedParams);
20086
+ }
17316
20087
  // Find a provider that supports this route
17317
20088
  const provider = this.findProviderForRoute(finalResolvedParams);
17318
20089
  // Estimate the transfer using the provider and format amounts to human-readable strings
@@ -17361,6 +20132,9 @@ function assertCCTPV2Config(config) {
17361
20132
  * // Get only chains that support forwarding
17362
20133
  * const forwarderChains = kit.getSupportedChains({ forwarderSupported: true })
17363
20134
  *
20135
+ * // Get only chains that can pay fees on the source chain (receive-exact)
20136
+ * const sourceFeeChains = kit.getSupportedChains({ sourceFeeSupported: true })
20137
+ *
17364
20138
  * console.log('Supported chains:')
17365
20139
  * allChains.forEach(chain => {
17366
20140
  * console.log(`- ${chain.name} (${chain.type})`)
@@ -17409,6 +20183,10 @@ function assertCCTPV2Config(config) {
17409
20183
  return options.forwarderSupported ? fs.source || fs.destination : !fs.source && !fs.destination;
17410
20184
  });
17411
20185
  }
20186
+ // Apply source-paid ("receive-exact") fee support filter if provided
20187
+ if (options?.sourceFeeSupported !== undefined) {
20188
+ chains = chains.filter((chain)=>hasSourceFeeSupport(chain) === options.sourceFeeSupported);
20189
+ }
17412
20190
  return chains;
17413
20191
  }
17414
20192
  /**
@@ -17443,6 +20221,20 @@ function assertCCTPV2Config(config) {
17443
20221
  return provider;
17444
20222
  }
17445
20223
  /**
20224
+ * Find the default CCTP v2 provider for a source-fee forwarding route.
20225
+ *
20226
+ * @param params - The resolved provider parameters.
20227
+ * @returns The CCTP v2 provider that supports the forwarded route.
20228
+ * @throws {UnsupportedRouteError} When no source-fee provider supports the route.
20229
+ * @internal
20230
+ */ findSourceFeeProvider(params) {
20231
+ const provider = this.providers.find((candidate)=>candidate instanceof CCTPV2BridgingProvider && candidate.supportsRoute(params.source.chain, params.destination.chain, params.token, true));
20232
+ if (!(provider instanceof CCTPV2BridgingProvider)) {
20233
+ throw createUnsupportedRouteError(params.source.chain.name, params.destination.chain.name);
20234
+ }
20235
+ return provider;
20236
+ }
20237
+ /**
17446
20238
  * Merge custom fee configuration into provider parameters.
17447
20239
  *
17448
20240
  * Prioritizes any custom fee configuration already present on the
@@ -17651,7 +20443,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
17651
20443
  };
17652
20444
 
17653
20445
  var name$1 = "@circle-fin/swap-kit";
17654
- var version$1 = "1.5.2";
20446
+ var version$1 = "1.6.1";
17655
20447
  var pkg$1 = {
17656
20448
  name: name$1,
17657
20449
  version: version$1};
@@ -17665,7 +20457,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17665
20457
  * Catches obviously malformed addresses at parse time; chain-specific validation
17666
20458
  * is performed in buildServiceParams.
17667
20459
  */ const destinationAddressSchema = z.union([
17668
- evmAddressSchema,
20460
+ evmAddressSchema$1,
17669
20461
  solanaAddressSchema
17670
20462
  ]);
17671
20463
  /**
@@ -17711,9 +20503,16 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17711
20503
  message: 'stopLimit must be greater than 0'
17712
20504
  }).optional(),
17713
20505
  customFee: serviceSwapCustomFeeSchema.optional(),
20506
+ apiKey: z.string({
20507
+ invalid_type_error: 'apiKey must be a string'
20508
+ })// Tolerate '' so the `process.env.CIRCLE_API_KEY ?? ''` idiom falls back to
20509
+ // kitKey via resolveApiKey instead of being rejected here.
20510
+ .optional(),
17714
20511
  kitKey: z.string({
17715
20512
  invalid_type_error: 'kitKey must be a string'
17716
- }).min(1, 'kitKey must be a non-empty string').optional(),
20513
+ })// Tolerate '' so an unset kit-key env var yields the permissionless path,
20514
+ // matching buildServiceParams (which already omits an empty credential).
20515
+ .optional(),
17717
20516
  provider: z.string({
17718
20517
  invalid_type_error: 'provider must be a string'
17719
20518
  }).min(1, 'provider must be a non-empty string').optional(),
@@ -17886,6 +20685,41 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17886
20685
  *
17887
20686
  * @internal
17888
20687
  */ const MAX_RATE_ADDRESSES_PER_REQUEST = 100;
20688
+ /**
20689
+ * Environment prefixes carried by Circle platform API keys.
20690
+ *
20691
+ * A Circle API key is `<ENV>_API_KEY:<keyId>:<keySecret>`, where `<ENV>` is one
20692
+ * of these prefixes.
20693
+ *
20694
+ * @internal
20695
+ */ const API_KEY_ENV_PREFIXES = [
20696
+ 'TEST',
20697
+ 'LIVE',
20698
+ 'SAND',
20699
+ 'SANDBOX',
20700
+ 'SMOK',
20701
+ 'PROD',
20702
+ 'STAG',
20703
+ 'DEV'
20704
+ ];
20705
+ /**
20706
+ * Accepted credential formats for Stablecoin Service authentication.
20707
+ *
20708
+ * Matches a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
20709
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`). API keys are the
20710
+ * recommended credential; kit keys remain accepted as the legacy path.
20711
+ *
20712
+ * @remarks
20713
+ * This is a local pre-flight check, not the authority — the Stablecoin Service
20714
+ * validates the credential and answers 401 when it rejects one. The prefix list
20715
+ * is therefore deliberately permissive: a valid key carrying a prefix this SDK
20716
+ * has not been taught about should reach the service and be judged there rather
20717
+ * than be refused locally, since refusing locally is indistinguishable from an
20718
+ * outage to the caller. Kept as the single source of truth so the pattern is
20719
+ * not restated per call site.
20720
+ *
20721
+ * @internal
20722
+ */ const API_KEY_PATTERN = new RegExp(`^(?:KIT_KEY|(?:${API_KEY_ENV_PREFIXES.join('|')})_API_KEY)` + ':[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$');
17889
20723
 
17890
20724
  /**
17891
20725
  * Zod schema for validating stop limits.
@@ -17954,13 +20788,14 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17954
20788
  /**
17955
20789
  * Zod schema for validating API keys.
17956
20790
  *
17957
- * Validates that the API key is a valid API key format.
20791
+ * Accepts a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
20792
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`).
17958
20793
  *
17959
20794
  * @example
17960
20795
  * ```typescript
17961
20796
  * import { apiKeySchema } from '@core/service-client'
17962
20797
  *
17963
- * const result = apiKeySchema.safeParse('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
20798
+ * const result = apiKeySchema.safeParse('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
17964
20799
  * if (!result.success) {
17965
20800
  * console.error('Invalid API key format:', result.error.issues)
17966
20801
  * }
@@ -17968,7 +20803,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
17968
20803
  */ const apiKeySchema = z.string({
17969
20804
  required_error: 'API key is required',
17970
20805
  invalid_type_error: 'Invalid API key format'
17971
- }).regex(/^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/, 'Invalid API key format');
20806
+ }).regex(API_KEY_PATTERN, 'Invalid API key format');
17972
20807
  /**
17973
20808
  * Zod schema for platform fees configuration.
17974
20809
  *
@@ -18407,6 +21242,40 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
18407
21242
  transaction: createSwapTransactionSchema
18408
21243
  });
18409
21244
 
21245
+ /**
21246
+ * Resolve the credential to authenticate a Stablecoin Service request with.
21247
+ *
21248
+ * `apiKey` is the supported field; `kitKey` is the deprecated alias kept for
21249
+ * existing integrations. When both are supplied `apiKey` wins, so a caller
21250
+ * migrating field-by-field cannot be silently pinned to a stale credential.
21251
+ *
21252
+ * An empty-string value is treated as absent on either field. Without this, the
21253
+ * common `process.env.CIRCLE_API_KEY ?? ''` idiom (which yields `''` when the
21254
+ * variable is unset) would either shadow a working `kitKey` or, on a bare
21255
+ * `kitKey: ''`, reach downstream validation as an invalid credential instead of
21256
+ * falling through to the permissionless path.
21257
+ *
21258
+ * @param source - Object carrying either credential field, or neither.
21259
+ * @returns The credential to use, or `undefined` for the permissionless
21260
+ * (keyless) path.
21261
+ *
21262
+ * @example
21263
+ * ```typescript
21264
+ * import { resolveApiKey } from '@core/service-client'
21265
+ *
21266
+ * resolveApiKey({ apiKey: 'TEST_API_KEY:id:secret' }) // 'TEST_API_KEY:id:secret'
21267
+ * resolveApiKey({ kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
21268
+ * resolveApiKey({ apiKey: '', kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
21269
+ * resolveApiKey({ kitKey: '' }) // undefined
21270
+ * resolveApiKey({}) // undefined
21271
+ * ```
21272
+ */ const resolveApiKey = (source)=>{
21273
+ // Treat an empty-string value as absent on either field so the
21274
+ // `env ?? ''` idiom falls through to the next credential (or permissionless).
21275
+ const normalize = (value)=>value !== undefined && value !== '' ? value : undefined;
21276
+ return normalize(source.apiKey) ?? normalize(source.kitKey);
21277
+ };
21278
+
18410
21279
  /**
18411
21280
  * Zod schema for validating EVM adapter capabilities.
18412
21281
  *
@@ -18836,7 +21705,7 @@ const abiParameterSchema = z.object({
18836
21705
  */ z.object({
18837
21706
  type: z.literal('evm'),
18838
21707
  abi: abiSchema,
18839
- address: evmAddressSchema,
21708
+ address: evmAddressSchema$1,
18840
21709
  functionName: z.string({
18841
21710
  required_error: 'Function name is required',
18842
21711
  invalid_type_error: 'Function name must be a string'
@@ -18873,7 +21742,7 @@ const abiParameterSchema = z.object({
18873
21742
  * }
18874
21743
  * ```
18875
21744
  */ z.object({
18876
- address: evmAddressSchema,
21745
+ address: evmAddressSchema$1,
18877
21746
  value: z.bigint({
18878
21747
  required_error: 'Value is required for native transfers',
18879
21748
  invalid_type_error: 'Value must be a bigint'
@@ -18959,7 +21828,7 @@ z.object({
18959
21828
  signature: evmSignatureSchema,
18960
21829
  tokenInputs: z.array(z.object({
18961
21830
  permitType: z.nativeEnum(PermitType),
18962
- token: evmAddressSchema,
21831
+ token: evmAddressSchema$1,
18963
21832
  amount: z.bigint().refine((value)=>value >= 0n, {
18964
21833
  message: 'amount must be a non-negative bigint'
18965
21834
  }),
@@ -19379,7 +22248,7 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
19379
22248
  /**
19380
22249
  * Fee recipient address (required).
19381
22250
  * Must be a valid EVM address or Solana address.
19382
- */ recipientAddress: z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
22251
+ */ recipientAddress: z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
19383
22252
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
19384
22253
  })
19385
22254
  }).strict();
@@ -19391,7 +22260,8 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
19391
22260
  * - slippageBps: Optional positive number for slippage tolerance
19392
22261
  * - stopLimit: Optional decimal string for minimum output
19393
22262
  * - customFee: Optional fee configuration
19394
- * - kitKey: Optional string identifier
22263
+ * - apiKey: Optional credential string
22264
+ * - kitKey: Optional credential string (deprecated alias for apiKey)
19395
22265
  */ const swapConfigSchema = z.object({
19396
22266
  allowanceStrategy: allowanceStrategySchema.optional(),
19397
22267
  slippageBps: z.number().int().min(0).optional(),
@@ -19401,11 +22271,12 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
19401
22271
  attributeName: 'stopLimit'
19402
22272
  })(z.string())).optional(),
19403
22273
  customFee: swapCustomFeeSchema.optional(),
22274
+ apiKey: z.string().optional(),
19404
22275
  kitKey: z.string().optional()
19405
22276
  });
19406
22277
  const swapDestinationSchema = z.object({
19407
22278
  chain: optionalSwapChainIdentifierField,
19408
- recipientAddress: z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
22279
+ recipientAddress: z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
19409
22280
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
19410
22281
  }).optional()
19411
22282
  }).strict();
@@ -19594,7 +22465,7 @@ new Set(Object.values(Blockchain));
19594
22465
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
19595
22466
 
19596
22467
  var name = "@circle-fin/earn-kit";
19597
- var version = "1.5.1";
22468
+ var version = "1.6.1";
19598
22469
  var pkg = {
19599
22470
  name: name,
19600
22471
  version: version};
@@ -19666,7 +22537,7 @@ function isNonNegativeBigIntLike(value) {
19666
22537
  }
19667
22538
  }
19668
22539
  const hexSignatureSchema = evmSignatureSchema;
19669
- const hexAddressSchema = evmAddressSchema;
22540
+ const hexAddressSchema = evmAddressSchema$1;
19670
22541
  // '0x' prefix + 32 bytes * 2 hex chars.
19671
22542
  const BYTES32_HEX_LENGTH = 66;
19672
22543
  const bridgeFeeTokenSchema = hexAddressSchema;
@@ -20571,7 +23442,7 @@ createTokenRegistry();
20571
23442
  * fast instead of round-tripping to the service.
20572
23443
  *
20573
23444
  * @internal
20574
- */ const recipientEvmAddressSchema = evmAddressSchema.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
23445
+ */ const recipientEvmAddressSchema = evmAddressSchema$1.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
20575
23446
  /**
20576
23447
  * Schema for the adapter context within earn operations.
20577
23448
  *
@@ -20603,19 +23474,39 @@ const sourceAdapterContextSchema = z.object({
20603
23474
  /**
20604
23475
  * Schema for the EarnConfig options.
20605
23476
  *
20606
- * Validate the optional Kit Key field using the standard `apiKeySchema`
20607
- * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
20608
- * operates in permissionless mode. `baseUrl` overrides the Earn Service
20609
- * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
20610
- * batched execution. Both are forwarded to the provider, so this `.strict()`
20611
- * schema must accept them or a valid config object is rejected.
23477
+ * Validate the *resolved* credential using the standard `apiKeySchema` format
23478
+ * (`<ENV>_API_KEY:<keyId>:<keySecret>`, or a legacy
23479
+ * `KIT_KEY:<keyId>:<keySecret>`). `apiKey` takes precedence over the deprecated
23480
+ * `kitKey`, so a malformed `kitKey` that is being ignored must not fail a config
23481
+ * that supplies a valid `apiKey` (and vice versa) only the credential that
23482
+ * would actually be sent is format-checked. When neither is supplied the SDK
23483
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service endpoint
23484
+ * (e.g. staging); `batchTransactions: false` opts out of atomic batched
23485
+ * execution. All are forwarded to the provider, so this `.strict()` schema must
23486
+ * accept them or a valid config object is rejected.
20612
23487
  *
20613
23488
  * @internal
20614
23489
  */ const earnConfigSchema = z.object({
20615
- kitKey: apiKeySchema.optional(),
23490
+ apiKey: z.string().optional(),
23491
+ kitKey: z.string().optional(),
20616
23492
  baseUrl: z.string().optional(),
20617
23493
  batchTransactions: z.boolean().optional()
20618
- }).strict();
23494
+ }).strict().superRefine((config, ctx)=>{
23495
+ const credential = resolveApiKey(config);
23496
+ if (credential === undefined) {
23497
+ return;
23498
+ }
23499
+ const result = apiKeySchema.safeParse(credential);
23500
+ if (!result.success) {
23501
+ ctx.addIssue({
23502
+ code: z.ZodIssueCode.custom,
23503
+ path: [
23504
+ credential === config.apiKey ? 'apiKey' : 'kitKey'
23505
+ ],
23506
+ message: result.error.issues[0]?.message ?? 'Invalid API key format'
23507
+ });
23508
+ }
23509
+ });
20619
23510
  /**
20620
23511
  * Canonical decimal form: a leading digit with no leading zeros (a single
20621
23512
  * '0' is only allowed immediately before the decimal point). Rejects the
@@ -20687,7 +23578,7 @@ const sourceAdapterContextSchema = z.object({
20687
23578
  * currently supports EVM vault addresses on Arc Testnet.
20688
23579
  *
20689
23580
  * @internal
20690
- */ const vaultAddressSchema = evmAddressSchema.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
23581
+ */ const vaultAddressSchema = evmAddressSchema$1.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
20691
23582
  /**
20692
23583
  * Validation schema for VaultQuery.
20693
23584
  *
@@ -20962,7 +23853,7 @@ const sameChainGetDepositQuoteParamsSchema = z.object({
20962
23853
  const crossChainGetDepositQuoteParamsSchema = z.object({
20963
23854
  from: sourceAdapterContextSchema,
20964
23855
  chain: earnBridgeDestinationChainIdentifierSchema,
20965
- address: evmAddressSchema,
23856
+ address: evmAddressSchema$1,
20966
23857
  vaultAddress: vaultAddressSchema,
20967
23858
  amount: amountSchema,
20968
23859
  transferSpeed: z.enum([