@circle-fin/app-kit 1.9.0 → 1.11.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/swap.mjs CHANGED
@@ -16,15 +16,27 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ // Buffer polyfill setup - executes before any other code
20
+ // Ensures globalThis.Buffer is available for Solana libraries
21
+ import { Buffer } from 'buffer';
22
+ if (typeof globalThis !== 'undefined' && typeof globalThis.Buffer === 'undefined') {
23
+ globalThis.Buffer = Buffer;
24
+ }
25
+ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
26
+ window.Buffer = Buffer;
27
+ }
28
+
29
+
19
30
  import { z } from 'zod';
20
31
  import 'pino';
32
+ import { hexlify, hexZeroPad } from '@ethersproject/bytes';
33
+ import '@ethersproject/abi';
34
+ import { getAddress } from '@ethersproject/address';
21
35
  import { PublicKey } from '@solana/web3.js';
22
36
  import 'bn.js';
23
37
  import '@coral-xyz/anchor';
24
38
  import bs58 from 'bs58';
25
39
  import '@noble/curves/ed25519';
26
- import { hexlify, hexZeroPad } from '@ethersproject/bytes';
27
- import { getAddress } from '@ethersproject/address';
28
40
  import { formatUnits as formatUnits$1 } from '@ethersproject/units';
29
41
  import { keccak256 } from '@ethersproject/keccak256';
30
42
 
@@ -43,6 +55,51 @@ import { keccak256 } from '@ethersproject/keccak256';
43
55
  * }
44
56
  * ```
45
57
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
58
+ /**
59
+ * Check whether the current runtime exposes a browser DOM.
60
+ *
61
+ * @remarks
62
+ * This intentionally does not treat every non-Node runtime as a browser.
63
+ * Server-side edge runtimes such as Cloudflare Workers, Deno, and Supabase
64
+ * Edge Functions do not expose Node globals but can safely use server
65
+ * credentials. A Node.js runtime remains server-side even when a test or SSR
66
+ * environment provides a DOM shim.
67
+ *
68
+ * @returns `true` when running in a browser window, `false` otherwise.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { isBrowserEnvironment } from '@core/utils'
73
+ *
74
+ * if (isBrowserEnvironment()) {
75
+ * throw new Error('Server-only secrets must not be used in the browser')
76
+ * }
77
+ * ```
78
+ */ const isBrowserEnvironment = ()=>{
79
+ const browserWindow = globalThis.window;
80
+ return !isNodeEnvironment() && browserWindow?.document !== undefined;
81
+ };
82
+ /**
83
+ * Return the SDK User-Agent request header only when running in Node.js.
84
+ *
85
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
86
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
87
+ * attribution header because they cannot set it reliably.
88
+ *
89
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { getNodeUserAgentHeader } from '@core/utils'
94
+ *
95
+ * const headers = {
96
+ * 'Content-Type': 'application/json',
97
+ * ...getNodeUserAgentHeader(),
98
+ * }
99
+ * ```
100
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
101
+ 'User-Agent': getUserAgent()
102
+ } : {};
46
103
  /**
47
104
  * Detect the runtime environment and return a shortened identifier.
48
105
  *
@@ -3527,7 +3584,10 @@ var EarnChain;
3527
3584
  contracts: {
3528
3585
  v1: {
3529
3586
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3530
- minter: GATEWAY_MINTER_EVM_TESTNET
3587
+ minter: GATEWAY_MINTER_EVM_TESTNET,
3588
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3589
+ // deposit into the GatewayWallet above.
3590
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3531
3591
  }
3532
3592
  },
3533
3593
  forwarderSupported: {
@@ -6595,7 +6655,10 @@ var Chains = /*#__PURE__*/Object.freeze({
6595
6655
  minter: z.string({
6596
6656
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
6597
6657
  invalid_type_error: 'Gateway minter address must be a string.'
6598
- }).min(1, 'Gateway minter address cannot be empty.')
6658
+ }).min(1, 'Gateway minter address cannot be empty.'),
6659
+ depositForHandler: z.string({
6660
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
6661
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
6599
6662
  }).strict() // Reject any additional properties not defined in the schema
6600
6663
  ;
6601
6664
  /**
@@ -8014,13 +8077,12 @@ const swapTokenEnumSchema = z.enum([
8014
8077
  headers: {
8015
8078
  ...DEFAULT_CONFIG$1.headers,
8016
8079
  ...config.headers ?? {},
8017
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
8018
- // This is why we use the 'X-User-Agent' header instead.
8019
- ...typeof window === 'undefined' ? {
8020
- 'User-Agent': getUserAgent()
8021
- } : {
8022
- 'X-User-Agent': getUserAgent()
8023
- }
8080
+ // Browsers forbid setting a user-agent request header, and the custom
8081
+ // fallback header the SDK used instead trips CORS preflight against the
8082
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
8083
+ // blocking the request. So send the SDK user agent only in Node;
8084
+ // browsers omit it entirely.
8085
+ ...getNodeUserAgentHeader()
8024
8086
  }
8025
8087
  };
8026
8088
  let lastError;
@@ -9480,6 +9542,13 @@ const swapTokenEnumSchema = z.enum([
9480
9542
  return explorerUrl;
9481
9543
  }
9482
9544
 
9545
+ /**
9546
+ * CCTP forwarding magic bytes prefix.
9547
+ *
9548
+ * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9549
+ * This prefix is right-padded to 24 bytes in the final hookData.
9550
+ */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
9551
+
9483
9552
  /**
9484
9553
  * Project an arbitrary payload onto the exact set of fields the telemetry
9485
9554
  * endpoint accepts.
@@ -9514,6 +9583,7 @@ const swapTokenEnumSchema = z.enum([
9514
9583
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
9515
9584
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
9516
9585
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
9586
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
9517
9587
  if (payload.errorDetails !== undefined) {
9518
9588
  const errorDetails = {
9519
9589
  ...payload.errorDetails.errorCode !== undefined && {
@@ -9584,18 +9654,15 @@ const swapTokenEnumSchema = z.enum([
9584
9654
  timeoutHandle.unref();
9585
9655
  }
9586
9656
  try {
9587
- const isNode = isNodeEnvironment();
9588
- const userAgent = getUserAgent();
9589
9657
  await fetch(getLogsUrl(), {
9590
9658
  method: 'POST',
9591
9659
  headers: {
9592
9660
  'Content-Type': 'application/json',
9593
- // Browser restricts setting User-Agent; use X-User-Agent instead.
9594
- ...isNode ? {
9595
- 'User-Agent': userAgent
9596
- } : {
9597
- 'X-User-Agent': userAgent
9598
- }
9661
+ // Browsers forbid setting a user-agent request header, and the custom
9662
+ // fallback header the SDK used instead trips CORS preflight (it isn't
9663
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
9664
+ // it only in Node; browsers omit it entirely.
9665
+ ...getNodeUserAgentHeader()
9599
9666
  },
9600
9667
  body: JSON.stringify(toSafePayload(payload)),
9601
9668
  signal: controller.signal
@@ -9763,7 +9830,7 @@ const swapTokenEnumSchema = z.enum([
9763
9830
  // discards the stack trace, nested `cause`, and any custom Error
9764
9831
  // properties — exactly the context an on-call needs when a
9765
9832
  // resolver-closure regression triggers this path.
9766
- console.warn(`[stablecoin-kits telemetry] dropped error event '${eventType}':`, cause);
9833
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
9767
9834
  } catch {
9768
9835
  // console.warn itself throwing is the user's environment; nothing more we
9769
9836
  // can do without risking the original operation error.
@@ -9779,7 +9846,9 @@ const swapTokenEnumSchema = z.enum([
9779
9846
  sdkVersion: config.sdkVersion,
9780
9847
  eventType,
9781
9848
  timestamp: new Date().toISOString(),
9782
- errorDetails,
9849
+ ...errorDetails !== undefined && {
9850
+ errorDetails
9851
+ },
9783
9852
  clientContext: buildClientContext(),
9784
9853
  ...context?.sourceChain != null && {
9785
9854
  sourceChain: context.sourceChain
@@ -9795,9 +9864,45 @@ const swapTokenEnumSchema = z.enum([
9795
9864
  },
9796
9865
  ...context?.txHash != null && {
9797
9866
  txHash: context.txHash
9867
+ },
9868
+ ...context?.correlationId != null && {
9869
+ correlationId: context.correlationId
9798
9870
  }
9799
9871
  };
9800
9872
  }
9873
+ /**
9874
+ * Emit telemetry for a completed operation without affecting its caller.
9875
+ *
9876
+ * No-ops when `config.disabled` is `true`. Like {@link withErrorTelemetry},
9877
+ * failures while constructing or submitting the telemetry payload are reported
9878
+ * as a soft warning and never change a completed operation's result.
9879
+ *
9880
+ * @param eventType - The telemetry event type for the completed operation.
9881
+ * @param config - Per-kit SDK identity and disabled flag.
9882
+ * @param context - Optional chain, token, and transaction context.
9883
+ * @returns Nothing.
9884
+ * @throws Never — telemetry failures are reported as warnings.
9885
+ *
9886
+ * @example
9887
+ * ```typescript
9888
+ * import { emitSuccessTelemetry } from '@core/utils'
9889
+ *
9890
+ * emitSuccessTelemetry(
9891
+ * 'bridge_bridge',
9892
+ * { sdkName: 'bridge-kit', sdkVersion: '1.0.0', disabled: false },
9893
+ * { sourceChain: 'Ethereum', destinationChain: 'Base', tokenIn: 'USDC' },
9894
+ * )
9895
+ * ```
9896
+ */ function emitSuccessTelemetry(eventType, config, context) {
9897
+ if (config.disabled) {
9898
+ return;
9899
+ }
9900
+ try {
9901
+ void emitAnalyticsLog(buildPayload(config, eventType, undefined, context));
9902
+ } catch (telemetryError) {
9903
+ warnTelemetryDrop(eventType, telemetryError);
9904
+ }
9905
+ }
9801
9906
  /**
9802
9907
  * Wrap an async operation with error telemetry.
9803
9908
  *
@@ -9856,7 +9961,7 @@ const swapTokenEnumSchema = z.enum([
9856
9961
  }
9857
9962
 
9858
9963
  var name$2 = "@circle-fin/bridge-kit";
9859
- var version$2 = "1.12.0";
9964
+ var version$2 = "1.12.2";
9860
9965
  var pkg$2 = {
9861
9966
  name: name$2,
9862
9967
  version: version$2};
@@ -10714,6 +10819,17 @@ var TransferSpeed;
10714
10819
  clock: z.any().optional()
10715
10820
  }).passthrough();
10716
10821
 
10822
+ /**
10823
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
10824
+ * hookData must start with.
10825
+ *
10826
+ * Encoded with `TextEncoder` (a browser-safe Web API) rather than `Buffer.from`
10827
+ * so this module-level constant does not reference the Node `Buffer` global at
10828
+ * import time. App Kit inlines this provider into its bundle without a Buffer polyfill, and a
10829
+ * bare `Buffer` here crashes browser bundles (e.g. Vite) on load — even for apps
10830
+ * that never touch the prepaid FORWARD path. Mirrors `buildForwardingHookData`.
10831
+ */ Array.from(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX)).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
10832
+
10717
10833
  /**
10718
10834
  * The minimum finality threshold for CCTPv2 transfers.
10719
10835
  *
@@ -10746,7 +10862,7 @@ var TransferSpeed;
10746
10862
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
10747
10863
 
10748
10864
  var name$1 = "@circle-fin/swap-kit";
10749
- var version$1 = "1.3.2";
10865
+ var version$1 = "1.5.0";
10750
10866
  var pkg$1 = {
10751
10867
  name: name$1,
10752
10868
  version: version$1};
@@ -10811,7 +10927,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
10811
10927
  }).min(1, 'kitKey must be a non-empty string').optional(),
10812
10928
  provider: z.string({
10813
10929
  invalid_type_error: 'provider must be a string'
10814
- }).min(1, 'provider must be a non-empty string').optional()
10930
+ }).min(1, 'provider must be a non-empty string').optional(),
10931
+ batchTransactions: z.boolean({
10932
+ invalid_type_error: 'batchTransactions must be a boolean'
10933
+ }).optional()
10815
10934
  });
10816
10935
  /**
10817
10936
  * Zod schema for adapter context.
@@ -11342,7 +11461,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11342
11461
  /**
11343
11462
  * Circle Stablecoin Service API Key.
11344
11463
  * Must be a valid API key format.
11345
- */ apiKey: apiKeySchema
11464
+ */ apiKey: apiKeySchema.optional()
11346
11465
  }).superRefine(requireCrossChainQuoteToAddress);
11347
11466
  /**
11348
11467
  * Zod schema for validating CreateSwapRequest parameters.
@@ -11400,7 +11519,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11400
11519
  /**
11401
11520
  * Circle Stablecoin Service API Key.
11402
11521
  * Must be a valid API key format.
11403
- */ apiKey: apiKeySchema
11522
+ */ apiKey: apiKeySchema.optional()
11404
11523
  });
11405
11524
  /**
11406
11525
  * Zod schema for validating GetSwapStatusResponse data.
@@ -11436,7 +11555,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11436
11555
  toChain: z.string({
11437
11556
  invalid_type_error: 'toChain must be a string'
11438
11557
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
11439
- apiKey: apiKeySchema
11558
+ apiKey: apiKeySchema.optional()
11440
11559
  });
11441
11560
  /**
11442
11561
  * Zod schema for validating CreateSwapResponse payloads.
@@ -11445,13 +11564,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11445
11564
  required_error: 'fee token is required',
11446
11565
  invalid_type_error: 'fee token must be a string'
11447
11566
  }).min(1, 'fee token must be a non-empty string'),
11448
- amount: feeAmountSchema
11567
+ amount: feeAmountSchema,
11568
+ decimals: z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
11569
+ symbol: z.string({
11570
+ invalid_type_error: 'fee token symbol must be a string'
11571
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
11449
11572
  });
11450
11573
  /**
11451
11574
  * Developer fee item schema with basis field.
11452
- */ const createSwapDeveloperFeeItemSchema = z.object({
11453
- token: z.string().min(1, 'fee token must be a non-empty string'),
11454
- amount: feeAmountSchema,
11575
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
11455
11576
  basis: z.enum([
11456
11577
  'inputAmount',
11457
11578
  'estimatedAmount'
@@ -11543,7 +11664,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11543
11664
  addresses: z.array(z.string({
11544
11665
  invalid_type_error: 'addresses entries must be strings'
11545
11666
  }).min(1, 'addresses entries must be non-empty strings')).min(1, 'addresses must contain at least one entry when provided').max(MAX_RATE_ADDRESSES_PER_REQUEST, `addresses supports at most ${String(MAX_RATE_ADDRESSES_PER_REQUEST)} values per request`).optional(),
11546
- apiKey: apiKeySchema
11667
+ apiKey: apiKeySchema.optional()
11547
11668
  });
11548
11669
  /**
11549
11670
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -11574,6 +11695,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11574
11695
  required_error: 'estimatedAmount is required',
11575
11696
  invalid_type_error: 'estimatedAmount must be a string'
11576
11697
  }).min(1, 'estimatedAmount must be a non-empty string'),
11698
+ // Per-swap join key echoed back verbatim on success telemetry. Optional so a
11699
+ // not-yet-upgraded service (no field) still validates during rollout. A
11700
+ // malformed/non-UUID value is coerced to `undefined` (no telemetry id) rather
11701
+ // than throwing: this is a telemetry-only field (stripped from the developer
11702
+ // result, never used for control flow), so it must not be able to abort the
11703
+ // swap via `parseCreateSwapResponse().parse()`. Mirrors the best-effort,
11704
+ // never-throw contract of the rest of the telemetry stack. Implemented with
11705
+ // `preprocess` rather than Zod's `.catch()` because static analysis misreads
11706
+ // `.catch` on the schema chain as an unhandled Promise (S7785).
11707
+ correlationId: z.preprocess((value)=>z.string().uuid().safeParse(value).success ? value : undefined, z.string().optional()),
11577
11708
  config: createSwapRequestBaseSchema.shape.config.optional(),
11578
11709
  fees: createSwapFeesSchema.optional(),
11579
11710
  transaction: createSwapTransactionSchema
@@ -11660,6 +11791,45 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11660
11791
  * }
11661
11792
  * ```
11662
11793
  */ const isGetTokenRatesResponse = (obj)=>getTokenRatesResponseSchema.safeParse(obj).success;
11794
+ /**
11795
+ * Assert that a Stablecoin Service kit key is not being supplied from a browser.
11796
+ *
11797
+ * The kit key (`KIT_KEY:<id>:<secret>`) is a server-only secret. Every
11798
+ * Stablecoin Service request that attaches an `Authorization: Bearer` header
11799
+ * funnels through this package, so calling this guard before that header is
11800
+ * built prevents the secret from being sent from — and thus bundled into — a
11801
+ * client application. In Node.js the check is a no-op, preserving the
11802
+ * legitimate "hold the kit key on the server, forward the prepared transaction
11803
+ * to the client" flow. When no kit key is supplied the permissionless (keyless)
11804
+ * client path remains fully allowed.
11805
+ *
11806
+ * @param apiKey - The inline kit key for the request, or `undefined` when none
11807
+ * was supplied (permissionless mode).
11808
+ * @returns Nothing.
11809
+ * @throws KitError with VALIDATION_FAILED when a kit key is supplied while
11810
+ * running in a browser environment. The secret value is never echoed.
11811
+ *
11812
+ * @example
11813
+ * ```typescript
11814
+ * import { assertBrowserSafeApiKey } from '@core/service-client'
11815
+ *
11816
+ * // Server (Node.js): no-op, request proceeds with the Authorization header.
11817
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11818
+ *
11819
+ * // Browser: throws to stop the secret from leaking into the client bundle.
11820
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11821
+ *
11822
+ * // Browser, permissionless: allowed.
11823
+ * assertBrowserSafeApiKey(undefined)
11824
+ * ```
11825
+ */ const assertBrowserSafeApiKey = (apiKey)=>{
11826
+ if (apiKey === undefined) {
11827
+ return;
11828
+ }
11829
+ if (isBrowserEnvironment()) {
11830
+ throw createValidationFailedError$1('kitKey', '[redacted]', 'kitKey must not be provided in a browser environment — it is a server-only secret. Run kit operations that use a kit key on your server, or omit kitKey to use the permissionless (keyless) client path');
11831
+ }
11832
+ };
11663
11833
 
11664
11834
  /**
11665
11835
  * Create a cross-chain bridge and swap transaction through the Stablecoin Service.
@@ -11717,11 +11887,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11717
11887
  const url = new URL('/v1/stablecoinKits/swap', STABLECOIN_SERVICE_BASE_URL).toString();
11718
11888
  // Remove the API key from the request body
11719
11889
  const { apiKey, ...requestBody } = validatedParams;
11890
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
11891
+ assertBrowserSafeApiKey(apiKey);
11720
11892
  const effectiveConfig = {
11721
11893
  ...DEFAULT_CONFIG,
11722
11894
  headers: {
11723
11895
  ...DEFAULT_CONFIG.headers,
11724
- Authorization: `Bearer ${apiKey}`
11896
+ // Permissionless mode: no Authorization header when the kit key is absent.
11897
+ ...apiKey !== undefined && {
11898
+ Authorization: `Bearer ${apiKey}`
11899
+ }
11725
11900
  }
11726
11901
  };
11727
11902
  try {
@@ -11868,6 +12043,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11868
12043
  }
11869
12044
  // Use validated data
11870
12045
  const validatedParams = result.data;
12046
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12047
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11871
12048
  // Build the API URL
11872
12049
  const url = buildQuoteUrl(validatedParams);
11873
12050
  // Merge default config with Authorization header
@@ -11875,7 +12052,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11875
12052
  ...DEFAULT_CONFIG,
11876
12053
  headers: {
11877
12054
  ...DEFAULT_CONFIG.headers,
11878
- Authorization: `Bearer ${validatedParams.apiKey}`
12055
+ // Permissionless mode: no Authorization header when the kit key is absent.
12056
+ ...validatedParams.apiKey !== undefined && {
12057
+ Authorization: `Bearer ${validatedParams.apiKey}`
12058
+ }
11879
12059
  }
11880
12060
  };
11881
12061
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
@@ -11930,17 +12110,24 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11930
12110
  const validatedParams = {
11931
12111
  txHash: result.data.txHash,
11932
12112
  chain: result.data.chain,
11933
- apiKey: result.data.apiKey,
12113
+ ...result.data.apiKey !== undefined && {
12114
+ apiKey: result.data.apiKey
12115
+ },
11934
12116
  ...result.data.toChain !== undefined && {
11935
12117
  toChain: result.data.toChain
11936
12118
  }
11937
12119
  };
12120
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12121
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11938
12122
  const url = buildSwapStatusUrl(validatedParams);
11939
12123
  const effectiveConfig = {
11940
12124
  ...DEFAULT_CONFIG,
11941
12125
  headers: {
11942
12126
  ...DEFAULT_CONFIG.headers,
11943
- Authorization: `Bearer ${validatedParams.apiKey}`
12127
+ // Permissionless mode: no Authorization header when the kit key is absent.
12128
+ ...validatedParams.apiKey !== undefined && {
12129
+ Authorization: `Bearer ${validatedParams.apiKey}`
12130
+ }
11944
12131
  }
11945
12132
  };
11946
12133
  return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
@@ -12029,17 +12216,24 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12029
12216
  }
12030
12217
  const validatedParams = {
12031
12218
  chain: result.data.chain,
12032
- apiKey: result.data.apiKey,
12219
+ ...result.data.apiKey !== undefined && {
12220
+ apiKey: result.data.apiKey
12221
+ },
12033
12222
  ...result.data.addresses !== undefined && {
12034
12223
  addresses: result.data.addresses
12035
12224
  }
12036
12225
  };
12226
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12227
+ assertBrowserSafeApiKey(validatedParams.apiKey);
12037
12228
  const url = buildTokenRatesUrl(validatedParams);
12038
12229
  const effectiveConfig = {
12039
12230
  ...DEFAULT_CONFIG,
12040
12231
  headers: {
12041
12232
  ...DEFAULT_CONFIG.headers,
12042
- Authorization: `Bearer ${validatedParams.apiKey}`
12233
+ // Permissionless mode: no Authorization header when the kit key is absent.
12234
+ ...validatedParams.apiKey !== undefined && {
12235
+ Authorization: `Bearer ${validatedParams.apiKey}`
12236
+ }
12043
12237
  }
12044
12238
  };
12045
12239
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
@@ -12849,6 +13043,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
12849
13043
  */ function hasSignTypedData(adapter) {
12850
13044
  return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
12851
13045
  }
13046
+ /**
13047
+ * Type guard to check if an adapter can actually produce an EIP-712
13048
+ * typed-data signature.
13049
+ *
13050
+ * @remarks
13051
+ * Strengthens {@link hasSignTypedData}: having a `signTypedData` method
13052
+ * does not guarantee it can succeed. Adapters whose signer is delegated
13053
+ * (e.g. through a signing strategy backed by a smart contract account)
13054
+ * expose the method but reject typed-data payloads at runtime. Such
13055
+ * adapters report their real capability through an optional
13056
+ * `supportsSignTypedData()` method, which this guard consults when
13057
+ * present. Adapters without the capability method are assumed able to
13058
+ * sign, preserving the previous duck-typing behavior.
13059
+ *
13060
+ * @param adapter - The adapter to check
13061
+ * @returns True if calling `signTypedData` can be expected to succeed
13062
+ *
13063
+ * @example
13064
+ * ```typescript
13065
+ * import { canSignTypedData } from '@core/adapter-evm'
13066
+ *
13067
+ * if (canSignTypedData(adapter)) {
13068
+ * const signature = await adapter.signTypedData(typedData, context)
13069
+ * } else {
13070
+ * // take an on-chain approval path instead of a permit signature
13071
+ * }
13072
+ * ```
13073
+ */ function canSignTypedData(adapter) {
13074
+ if (!hasSignTypedData(adapter)) {
13075
+ return false;
13076
+ }
13077
+ if (typeof adapter.supportsSignTypedData === 'function') {
13078
+ // The value is `boolean` per the interface, but a plain-JS adapter may
13079
+ // return anything; treat it as untrusted and coerce to a strict
13080
+ // boolean. Comparing an `unknown` (not a `boolean`) also keeps the
13081
+ // lint autofix from stripping this as a redundant `=== true`.
13082
+ const supported = adapter.supportsSignTypedData();
13083
+ return supported === true;
13084
+ }
13085
+ return true;
13086
+ }
12852
13087
 
12853
13088
  /**
12854
13089
  * Build EIP-2612 typed data for permit signing.
@@ -13271,10 +13506,13 @@ enc.encode('used_transfer_spec_hash');
13271
13506
  * at usage time rather than construction time.
13272
13507
  *
13273
13508
  * Validates:
13274
- * - Kit key is present and matches required format (KIT_KEY:id:secret)
13509
+ * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
13510
+ * An absent or empty kit key is permitted (permissionless mode) — the swap
13511
+ * service now treats the key as optional.
13275
13512
  *
13276
- * @param kitKey - The inline kit key from the swap operation config
13277
- * @throws KitError with VALIDATION_FAILED if kit key is invalid or missing
13513
+ * @param kitKey - The inline kit key from the swap operation config (optional)
13514
+ * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
13515
+ * match the KIT_KEY:<keyId>:<keySecret> format
13278
13516
  *
13279
13517
  * @example
13280
13518
  * ```typescript
@@ -13286,9 +13524,11 @@ enc.encode('used_transfer_spec_hash');
13286
13524
  * assertKitKey(kitKey)
13287
13525
  * ```
13288
13526
  */ function assertKitKey(kitKey) {
13289
- // Validate API key format using existing schema from service-client
13527
+ // Permissionless mode: the swap service treats the kit key as optional, so an
13528
+ // absent (or empty) key is valid. Only validate the format when a key is
13529
+ // actually provided.
13290
13530
  if (!kitKey) {
13291
- throw createValidationFailedError$1('kitKey', kitKey, 'Kit key is required. Expected format: KIT_KEY:<keyId>:<keySecret>. Provide it inline via config.kitKey parameter. Get your free Kit Key at: https://developers.circle.com/w3s/keys#kit-keys');
13531
+ return;
13292
13532
  }
13293
13533
  const apiKeyResult = apiKeySchema.safeParse(kitKey);
13294
13534
  if (!apiKeyResult.success) {
@@ -13585,8 +13825,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13585
13825
  validateResolvedAddress(resolvedTokenInAddress, chain);
13586
13826
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
13587
13827
  validateResolvedAddress(to, destinationChain);
13588
- const kitKey = config?.kitKey ?? '';
13589
- // Validates the kit key
13828
+ const kitKey = config?.kitKey;
13829
+ // Validate the kit key format when one is provided (permissionless otherwise).
13590
13830
  assertKitKey(kitKey);
13591
13831
  // Validate custom fee configuration if present
13592
13832
  const customFee = config?.customFee;
@@ -13631,7 +13871,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13631
13871
  }
13632
13872
  }
13633
13873
  },
13634
- apiKey: kitKey
13874
+ // Map kitKey → apiKey for the service client; omitted in permissionless mode.
13875
+ ...kitKey ? {
13876
+ apiKey: kitKey
13877
+ } : {}
13635
13878
  };
13636
13879
  }
13637
13880
 
@@ -14285,6 +14528,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14285
14528
  }
14286
14529
  }
14287
14530
 
14531
+ /**
14532
+ * Determine whether an adapter can produce an EIP-2612 permit signature.
14533
+ *
14534
+ * @remarks
14535
+ * A gasless permit needs two adapter capabilities: fetching the token's
14536
+ * EIP-2612 nonce and producing an EIP-712 typed-data signature. The
14537
+ * typed-data check uses {@link canSignTypedData} rather than a bare
14538
+ * `hasSignTypedData` guard so that an adapter routed through a signing
14539
+ * strategy that cannot produce typed-data signatures — one whose manifest
14540
+ * omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
14541
+ * — is correctly excluded. Such an adapter falls back to an on-chain approval
14542
+ * (batched into a single submission when it supports atomic execution) instead
14543
+ * of attempting a permit its strategy would reject.
14544
+ *
14545
+ * @param adapter - The source adapter to inspect.
14546
+ * @returns `true` when the adapter can both fetch a nonce and sign typed data.
14547
+ *
14548
+ * @example
14549
+ * ```typescript
14550
+ * import { adapterSupportsPermit } from './utils'
14551
+ *
14552
+ * if (adapterSupportsPermit(adapter)) {
14553
+ * // gasless permit path — fold the approval into the swap transaction
14554
+ * } else {
14555
+ * // on-chain approval path (batched when supportsAtomicBatch is true)
14556
+ * }
14557
+ * ```
14558
+ */ function adapterSupportsPermit(adapter) {
14559
+ return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
14560
+ }
14561
+
14288
14562
  /**
14289
14563
  * Generate EIP-2612 permit signature for token approval.
14290
14564
  *
@@ -14420,8 +14694,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14420
14694
  }
14421
14695
  // Skip permit generation if the adapter lacks the required capabilities.
14422
14696
  // handleEvmTokenApproval will have already sent an on-chain approval in this case.
14423
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
14424
- if (!adapterSupportsPermit) {
14697
+ if (!adapterSupportsPermit(adapter)) {
14425
14698
  return [
14426
14699
  createFallbackTokenInput(tokenInAddress, inputAmount)
14427
14700
  ];
@@ -14980,6 +15253,65 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
14980
15253
  return `Insufficient ${displaySymbol} balance for swap operation.\n\n` + `Wallet: ${walletAddress}\n` + `Current balance: ${currentDisplay}\n` + `Required: ${requiredDisplay}\n` + `Shortfall: ${shortfallDisplay}\n\n` + `This swap requires ${requiredSummary} to complete the transaction.\n\n` + `Action: Add at least ${actionAmount} to your wallet to complete this swap.`;
14981
15254
  }
14982
15255
 
15256
+ /**
15257
+ * Determine which chain a fee token should be resolved and formatted against.
15258
+ *
15259
+ * @remarks
15260
+ * Fees returned by the service may be denominated in either the input token
15261
+ * (on the source chain) or the output token (on the destination chain). A
15262
+ * contract address only resolves on the chain it belongs to, so formatting a
15263
+ * destination-denominated fee against the source chain causes
15264
+ * {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
15265
+ * units (e.g. a cross-chain swap charging a fee in the destination output
15266
+ * token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
15267
+ * resolved against the source chain). This is the fallback for fee items that
15268
+ * are not self-described with their own `decimals`/`chain`.
15269
+ *
15270
+ * Prefer the source chain (covers same-chain swaps and input-denominated
15271
+ * fees), then fall back to the destination chain when the token only resolves
15272
+ * there. When neither chain recognises the token, default to the source chain
15273
+ * so existing on-chain decimal lookups via the source adapter still apply.
15274
+ *
15275
+ * Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
15276
+ * source-first preference keeps them on the source chain. That is correct for
15277
+ * registry stablecoins, and for `'NATIVE'` only when both chains share native
15278
+ * decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
15279
+ * `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
15280
+ * mis-scaled. This is latent — providers emit the address form, and
15281
+ * self-describing fee items carry their own `decimals` and never reach this
15282
+ * helper — so the gap only opens for a future `'NATIVE'` fee that arrives
15283
+ * without `decimals` on a cross-native-decimal route.
15284
+ *
15285
+ * @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
15286
+ * @param sourceChain - The chain the swap originates from.
15287
+ * @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
15288
+ * @returns The chain definition the fee token should be resolved against.
15289
+ *
15290
+ * @example
15291
+ * ```typescript
15292
+ * import { resolveFeeChain } from './resolveFeeChain'
15293
+ * import { Ethereum, Base } from '@core/chains'
15294
+ *
15295
+ * // Cross-chain swap fee charged in the destination (output) token
15296
+ * resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
15297
+ * // => Base (EURC resolves on Base, not Ethereum)
15298
+ *
15299
+ * // Symbol or source-token fees stay on the source chain
15300
+ * resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
15301
+ * ```
15302
+ */ function resolveFeeChain(token, sourceChain, destinationChain) {
15303
+ if (sourceChain.chain === destinationChain.chain) {
15304
+ return sourceChain;
15305
+ }
15306
+ if (resolveTokenSymbol(token, sourceChain) !== null) {
15307
+ return sourceChain;
15308
+ }
15309
+ if (resolveTokenSymbol(token, destinationChain) !== null) {
15310
+ return destinationChain;
15311
+ }
15312
+ return sourceChain;
15313
+ }
15314
+
14983
15315
  const TOKEN_REGISTRY$1 = createTokenRegistry();
14984
15316
  /**
14985
15317
  * Format a raw base-unit amount into a human-readable decimal string.
@@ -15070,6 +15402,186 @@ const TOKEN_REGISTRY$1 = createTokenRegistry();
15070
15402
  }
15071
15403
  }
15072
15404
 
15405
+ /**
15406
+ * Runtime guard for {@link BatchCapableSwapAdapter}.
15407
+ *
15408
+ * @param adapter - The adapter to inspect.
15409
+ * @returns `true` when the adapter exposes both batch methods.
15410
+ *
15411
+ * @example
15412
+ * ```typescript
15413
+ * if (isBatchCapableSwapAdapter(adapter)) {
15414
+ * // adapter.supportsAtomicBatch / adapter.batchExecute are available
15415
+ * }
15416
+ * ```
15417
+ */ function isBatchCapableSwapAdapter(adapter) {
15418
+ return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
15419
+ }
15420
+ /**
15421
+ * Decide whether the EVM swap should take the batched approve-and-swap path.
15422
+ *
15423
+ * @remarks
15424
+ * Batching only helps when an on-chain approval would otherwise be required, so
15425
+ * it is skipped for native tokens (no approval) and for the gasless permit path
15426
+ * (already a single transaction). USDT is skipped because its reset-to-zero
15427
+ * allowance flow cannot be expressed as a fixed approve+swap pair. When those
15428
+ * gates pass, the adapter's actual atomic-batch capability is queried; any
15429
+ * failure resolves to `false` so the swap falls back to the sequential path.
15430
+ *
15431
+ * @param args - The decision inputs.
15432
+ * @param args.adapter - The source adapter.
15433
+ * @param args.chain - The source chain definition.
15434
+ * @param args.tokenInAddress - The resolved input-token address.
15435
+ * @param args.allowanceStrategy - Optional allowance strategy override.
15436
+ * @param args.batchTransactions - Optional explicit opt-out (`false` disables).
15437
+ * @returns `true` when the batched approve-and-swap path should be used.
15438
+ *
15439
+ * @example
15440
+ * ```typescript
15441
+ * const useBatched = await shouldUseBatchedSwap({
15442
+ * adapter,
15443
+ * chain,
15444
+ * tokenInAddress: '0xA0b8...',
15445
+ * allowanceStrategy: config?.allowanceStrategy,
15446
+ * batchTransactions: config?.batchTransactions,
15447
+ * })
15448
+ * ```
15449
+ */ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
15450
+ // Explicit opt-out.
15451
+ if (batchTransactions === false) {
15452
+ return false;
15453
+ }
15454
+ // Batching is an EVM capability (EIP-5792 or a signing strategy).
15455
+ if (chain.type !== 'evm') {
15456
+ return false;
15457
+ }
15458
+ // Native tokens need no approval — the swap is already a single transaction.
15459
+ if (isNativeEvmAddress(tokenInAddress)) {
15460
+ return false;
15461
+ }
15462
+ // A gasless permit folds the approval into the swap transaction, so there is
15463
+ // nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
15464
+ const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
15465
+ if (canUsePermit) {
15466
+ return false;
15467
+ }
15468
+ // USDT's reset-to-zero allowance dance cannot be expressed as a fixed
15469
+ // approve+swap pair; leave it on the sequential path.
15470
+ const usdt = chain.usdtAddress?.toLowerCase();
15471
+ if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
15472
+ return false;
15473
+ }
15474
+ if (!isBatchCapableSwapAdapter(adapter)) {
15475
+ return false;
15476
+ }
15477
+ try {
15478
+ return await adapter.supportsAtomicBatch(chain);
15479
+ } catch {
15480
+ return false;
15481
+ }
15482
+ }
15483
+ /**
15484
+ * Execute the approval and swap as a single atomic batch.
15485
+ *
15486
+ * @remarks
15487
+ * Extracts the raw call data from both prepared requests, submits them as one
15488
+ * batch via `adapter.batchExecute`, and maps the swap receipt back to a
15489
+ * transaction hash. The `fromAddress` is threaded for adapters routed through a
15490
+ * signing strategy (which have no wallet account to read the sender from); the
15491
+ * wallet-client path ignores it.
15492
+ *
15493
+ * Following the batch contract, `batchExecute` never throws once the batch is
15494
+ * submitted — a missing or failed swap receipt is surfaced here as a thrown
15495
+ * {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
15496
+ * batch and double-swap.
15497
+ *
15498
+ * @param args - The execution inputs.
15499
+ * @param args.adapter - The batch-capable source adapter.
15500
+ * @param args.chain - The EVM chain to execute on.
15501
+ * @param args.approveRequest - The prepared ERC-20 approval request.
15502
+ * @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
15503
+ * @param args.fromAddress - The address authorizing the batch.
15504
+ * @returns The swap transaction hash and the executed approval + swap records.
15505
+ * @throws {@link KitError} when the prepared requests cannot yield call data.
15506
+ * @throws {@link KitError} when the batch does not confirm or the swap reverts.
15507
+ *
15508
+ * @example
15509
+ * ```typescript
15510
+ * const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
15511
+ * adapter,
15512
+ * chain,
15513
+ * approveRequest,
15514
+ * swapRequest,
15515
+ * fromAddress: '0x742d...',
15516
+ * })
15517
+ * ```
15518
+ */ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
15519
+ if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
15520
+ throw new KitError({
15521
+ ...InputError.UNSUPPORTED_ACTION,
15522
+ recoverability: 'FATAL',
15523
+ message: 'Batched swap requires EVM prepared requests with getCallData() support.'
15524
+ });
15525
+ }
15526
+ const approveCallData = approveRequest.getCallData();
15527
+ const swapCallData = swapRequest.getCallData();
15528
+ const batchResult = await adapter.batchExecute([
15529
+ approveCallData,
15530
+ swapCallData
15531
+ ], chain, {
15532
+ fromAddress
15533
+ });
15534
+ const swapReceipt = batchResult.receipts[1];
15535
+ // A missing swap receipt means the batch never confirmed (polling timed out
15536
+ // or the wallet returned fewer receipts than calls). Re-throw the underlying
15537
+ // error when present (already FATAL); otherwise surface a FATAL timeout so the
15538
+ // caller checks the batch status rather than resubmitting.
15539
+ if (swapReceipt === undefined || swapReceipt.txHash === '') {
15540
+ if (isKitError(batchResult.error)) {
15541
+ throw batchResult.error;
15542
+ }
15543
+ throw new KitError({
15544
+ ...NetworkError.TIMEOUT,
15545
+ recoverability: 'FATAL',
15546
+ message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
15547
+ // Preserve the underlying confirmation failure when it isn't a KitError —
15548
+ // the signing-strategy path returns a raw viem error (e.g. a dropped or
15549
+ // replaced tx) — so the root cause survives behind the generic timeout.
15550
+ cause: {
15551
+ trace: {
15552
+ batchId: batchResult.batchId,
15553
+ ...batchResult.error != null && {
15554
+ error: batchResult.error
15555
+ }
15556
+ }
15557
+ }
15558
+ });
15559
+ }
15560
+ if (swapReceipt.status !== 'success') {
15561
+ throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
15562
+ }
15563
+ const executedTransactions = [];
15564
+ const approveReceipt = batchResult.receipts[0];
15565
+ // An atomic batch is a single on-chain transaction, so the approve and swap
15566
+ // receipts share one hash. Only surface a distinct approval record when it is
15567
+ // genuinely a separate transaction; otherwise the lone swap record represents
15568
+ // the batch, avoiding a phantom duplicate tx in executedTransactions.
15569
+ if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
15570
+ executedTransactions.push({
15571
+ type: 'approval',
15572
+ txHash: approveReceipt.txHash
15573
+ });
15574
+ }
15575
+ executedTransactions.push({
15576
+ type: 'swap',
15577
+ txHash: swapReceipt.txHash
15578
+ });
15579
+ return {
15580
+ swapTxHash: swapReceipt.txHash,
15581
+ executedTransactions
15582
+ };
15583
+ }
15584
+
15073
15585
  /**
15074
15586
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
15075
15587
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -15196,7 +15708,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15196
15708
  const statusResult = await getSwapStatus$1({
15197
15709
  txHash,
15198
15710
  chain: chain.chain,
15199
- apiKey
15711
+ ...apiKey !== undefined && {
15712
+ apiKey
15713
+ }
15200
15714
  });
15201
15715
  if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
15202
15716
  return {
@@ -15787,8 +16301,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15787
16301
  // Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
15788
16302
  const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
15789
16303
  const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
15790
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
15791
- const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
16304
+ const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
15792
16305
  const needsApproval = !isNativeToken && !canUsePermitFlow;
15793
16306
  if (!needsApproval) {
15794
16307
  return;
@@ -16067,34 +16580,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16067
16580
  const serviceResponse = await createSwap(serviceParams);
16068
16581
  // Track executed transactions
16069
16582
  const executedTransactions = [];
16070
- // Prepare swap action based on chain type
16071
- let preparedAction;
16072
- if (chain.type === 'solana') {
16073
- // Solana: No approval needed, directly prepare swap action
16074
- preparedAction = await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext);
16075
- } else {
16076
- // EVM chains: Handle token approval if needed
16077
- await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16078
- // EVM chains: prepareEvmSwapAction handles EIP-2612 permit generation
16079
- // Adapter contract address is read from chain.kitContracts.adapter
16080
- // Use the already-resolved context from above
16081
- preparedAction = await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy);
16082
- }
16583
+ // Prepare swap action(s) based on chain type and batch capability.
16584
+ // Returns either a single prepared action (Solana / sequential EVM) or a
16585
+ // batched approve+swap plan (EVM atomic-batch path).
16586
+ const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
16587
+ adapter,
16588
+ chain,
16589
+ serviceResponse,
16590
+ resolvedContext,
16591
+ executionCtx,
16592
+ config,
16593
+ executedTransactions
16594
+ });
16083
16595
  // Execute swap transaction via adapter
16084
16596
  // For EVM chains, use gas limit from proxy service API
16085
16597
  let txHash;
16086
16598
  const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
16087
16599
  try {
16088
- txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16089
- executedTransactions.push({
16090
- type: 'swap',
16091
- txHash
16092
- });
16093
- // Wait for transaction confirmation and verify success
16094
- const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16095
- if (txReceipt.status === 'reverted') {
16096
- const explorerUrl = buildExplorerUrl(chain, txHash);
16097
- throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16600
+ if (batchedSwapPlan) {
16601
+ // Approve + swap submitted as one atomic batch. batchExecute confirms
16602
+ // the swap internally, so no separate waitForTransaction is needed.
16603
+ const batched = await executeBatchedApproveAndSwap({
16604
+ adapter: adapter,
16605
+ chain: chain,
16606
+ approveRequest: batchedSwapPlan.approveRequest,
16607
+ swapRequest: batchedSwapPlan.swapRequest,
16608
+ fromAddress: executionCtx.fromAddress
16609
+ });
16610
+ txHash = batched.swapTxHash;
16611
+ executedTransactions.push(...batched.executedTransactions);
16612
+ } else if (preparedAction) {
16613
+ txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16614
+ executedTransactions.push({
16615
+ type: 'swap',
16616
+ txHash
16617
+ });
16618
+ // Wait for transaction confirmation and verify success
16619
+ const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16620
+ if (txReceipt.status === 'reverted') {
16621
+ const explorerUrl = buildExplorerUrl(chain, txHash);
16622
+ throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16623
+ }
16624
+ } else {
16625
+ // Unreachable: the preparation step always yields either a batched plan
16626
+ // or a prepared action.
16627
+ throw new KitError({
16628
+ ...InputError.UNSUPPORTED_ACTION,
16629
+ recoverability: 'FATAL',
16630
+ message: 'No swap execution path was prepared.'
16631
+ });
16098
16632
  }
16099
16633
  } catch (err) {
16100
16634
  handleSwapExecutionError(err, txHash, chain);
@@ -16113,8 +16647,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16113
16647
  isCrossChainSwap,
16114
16648
  txHash,
16115
16649
  chain,
16116
- apiKey: serviceParams.apiKey
16650
+ ...serviceParams.apiKey !== undefined && {
16651
+ apiKey: serviceParams.apiKey
16652
+ }
16117
16653
  });
16654
+ // Per-swap correlation id returned by the service as a top-level response
16655
+ // field for every chain (EVM + Solana). Attached to success telemetry so a
16656
+ // swap can be correlated across records; never used for control flow.
16657
+ // Undefined only against a not-yet-upgraded service that omits it.
16658
+ const correlationId = serviceResponse.correlationId;
16118
16659
  // Build and return SwapResult
16119
16660
  return {
16120
16661
  tokenIn,
@@ -16124,6 +16665,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16124
16665
  fromAddress: serviceParams.fromAddress,
16125
16666
  toAddress: serviceParams.toAddress,
16126
16667
  txHash,
16668
+ ...correlationId !== undefined && {
16669
+ correlationId
16670
+ },
16127
16671
  executedTransactions,
16128
16672
  ...config !== undefined && {
16129
16673
  config
@@ -16138,6 +16682,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16138
16682
  };
16139
16683
  }
16140
16684
  /**
16685
+ * Prepare the swap execution request(s) for the source wallet's chain.
16686
+ *
16687
+ * Produces either a single {@link PreparedChainRequest} (Solana, or the
16688
+ * sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
16689
+ * atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
16690
+ * batching). The caller executes whichever field is populated. Any on-chain
16691
+ * approval sent on the sequential path is appended to `executedTransactions`.
16692
+ *
16693
+ * @typeParam TFromAdapterCapabilities - Source-adapter capability set.
16694
+ * @param args - Inputs derived from the validated swap request.
16695
+ * @param args.adapter - Source-chain wallet adapter.
16696
+ * @param args.chain - Source chain definition.
16697
+ * @param args.serviceResponse - Validated createSwap response.
16698
+ * @param args.resolvedContext - Resolved operation context.
16699
+ * @param args.executionCtx - Minimal on-chain execution context.
16700
+ * @param args.config - Optional swap configuration (allowance/batch flags).
16701
+ * @param args.executedTransactions - Array appended with any sent approval.
16702
+ * @returns The prepared action or the batched approve+swap plan.
16703
+ * @throws KitError when the EVM atomic-batch path is selected but the chain
16704
+ * has no configured adapter contract.
16705
+ */ async prepareSwapRequests(args) {
16706
+ const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
16707
+ if (chain.type === 'solana') {
16708
+ // Solana: No approval needed, directly prepare swap action
16709
+ return {
16710
+ preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
16711
+ };
16712
+ }
16713
+ const useBatch = await shouldUseBatchedSwap({
16714
+ adapter,
16715
+ chain,
16716
+ tokenInAddress: executionCtx.tokenInAddress,
16717
+ allowanceStrategy: config?.allowanceStrategy,
16718
+ batchTransactions: config?.batchTransactions
16719
+ });
16720
+ if (useBatch) {
16721
+ // EVM chains: fuse the ERC-20 approval and the swap into a single atomic
16722
+ // batch (one signing challenge for smart-contract wallets). Force the
16723
+ // swap onto the pre-approval (PermitType.NONE) path since the approval
16724
+ // rides in the same batch.
16725
+ const adapterContractAddress = chain.kitContracts?.adapter;
16726
+ if (!adapterContractAddress) {
16727
+ throw new KitError({
16728
+ ...InputError.VALIDATION_FAILED,
16729
+ recoverability: 'FATAL',
16730
+ message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
16731
+ cause: {
16732
+ trace: {
16733
+ chain: chain.name
16734
+ }
16735
+ }
16736
+ });
16737
+ }
16738
+ const [approveRequest, swapRequest] = await Promise.all([
16739
+ this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
16740
+ prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
16741
+ ]);
16742
+ return {
16743
+ batchedSwapPlan: {
16744
+ approveRequest,
16745
+ swapRequest
16746
+ }
16747
+ };
16748
+ }
16749
+ // EVM chains: Handle token approval if needed, then prepare the swap.
16750
+ // prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
16751
+ // contract address is read from chain.kitContracts.adapter.
16752
+ await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16753
+ return {
16754
+ preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
16755
+ };
16756
+ }
16757
+ /**
16141
16758
  * Executes a swap transaction with the appropriate gas limit for the chain type.
16142
16759
  *
16143
16760
  * For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
@@ -16186,8 +16803,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16186
16803
  */ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
16187
16804
  if (!fees) return [];
16188
16805
  const [providerFees, swapFees, developerFees] = await Promise.all([
16189
- this.formatServiceFees(fees.provider, chain, 'provider', adapter),
16190
- this.formatServiceFees(fees.swap, chain, 'swap', adapter),
16806
+ this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
16807
+ this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
16191
16808
  recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
16192
16809
  ]);
16193
16810
  return [
@@ -16197,6 +16814,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16197
16814
  ];
16198
16815
  }
16199
16816
  /**
16817
+ * Resolve a single fee item to its display token and human-readable amount.
16818
+ *
16819
+ * @remarks
16820
+ * Prefer the self-describing metadata the service attaches to each fee:
16821
+ * `decimals` (and `symbol`) come straight from the provider quote, so they
16822
+ * are authoritative even for a token absent from the SDK registry on both
16823
+ * chains. That is the case {@link resolveFeeChain} cannot recover — a
16824
+ * destination-denominated fee token resolves on neither the source registry
16825
+ * nor the source-bound adapter, leaving the amount as raw base units. When
16826
+ * the service omits `decimals` (optional during rollout), fall back to
16827
+ * inferring the fee token's chain and resolving via the registry/adapter.
16828
+ *
16829
+ * Like {@link formatTokenValue}, this never throws: fee display is cosmetic
16830
+ * and must not fail an estimate/swap. A malformed self-describing `decimals`
16831
+ * (e.g. a non-numeric `amount` or invalid decimal count that makes
16832
+ * {@link formatUnits} throw) falls through to chain-based resolution rather
16833
+ * than propagating out of {@link buildFormattedFees}.
16834
+ *
16835
+ * @param fee - The fee item from the service response.
16836
+ * @param chain - The source chain definition.
16837
+ * @param destinationChain - The destination chain definition.
16838
+ * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16839
+ * @returns Promise resolving to the formatted amount and display token.
16840
+ */ async formatFeeValue(fee, chain, destinationChain, adapter) {
16841
+ if (fee.decimals != null) {
16842
+ try {
16843
+ return {
16844
+ amount: formatUnits(fee.amount, fee.decimals),
16845
+ token: fee.symbol ?? fee.token
16846
+ };
16847
+ } catch {
16848
+ // Malformed service metadata — fall through to chain-based resolution,
16849
+ // which never throws (worst case: raw passthrough).
16850
+ }
16851
+ }
16852
+ const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
16853
+ return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16854
+ }
16855
+ /**
16200
16856
  * Format service fee items into the SDK's ServiceSwapFee structure.
16201
16857
  *
16202
16858
  * @remarks
@@ -16207,14 +16863,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16207
16863
  * - Raw passthrough only when both registry and adapter fail
16208
16864
  *
16209
16865
  * @param feeItems - Array of fee items from the service response.
16210
- * @param chain - The chain definition for token resolution and formatting.
16866
+ * @param chain - The source chain definition for token resolution and formatting.
16867
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16211
16868
  * @param type - The fee type to assign ('provider' or 'swap').
16212
16869
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16213
16870
  * @returns Promise resolving to formatted ServiceSwapFee array.
16214
- */ async formatServiceFees(feeItems, chain, type, adapter) {
16871
+ */ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
16215
16872
  if (!feeItems) return [];
16216
16873
  return Promise.all(feeItems.map(async (fee)=>{
16217
- const formatted = await formatTokenValue(fee.amount, fee.token, chain, adapter);
16874
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16218
16875
  return {
16219
16876
  token: formatted.token,
16220
16877
  amount: formatted.amount,
@@ -16226,16 +16883,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16226
16883
  * Format developer fee items into the SDK's ServiceSwapFee structure.
16227
16884
  *
16228
16885
  * @param feeItems - Array of developer fee items from the service response.
16229
- * @param chain - The chain definition for token resolution and formatting.
16886
+ * @param chain - The source chain definition for token resolution and formatting.
16887
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16230
16888
  * @param recipientAddress - The developer's fee recipient address from config.
16231
16889
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16232
16890
  * @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
16233
16891
  */ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
16234
16892
  if (!feeItems) return [];
16235
- const isCrossChainSwap = destinationChain.chain !== chain.chain;
16236
16893
  return Promise.all(feeItems.map(async (fee)=>{
16237
- const feeChain = !isCrossChainSwap && fee.basis === 'estimatedAmount' ? destinationChain : chain;
16238
- const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16894
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16239
16895
  return {
16240
16896
  token: formatted.token,
16241
16897
  amount: formatted.amount,
@@ -18630,22 +19286,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18630
19286
  try {
18631
19287
  // Step 1: Build quote params directly (no need for buildServiceParams)
18632
19288
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
19289
+ // The kit key is optional (permissionless mode); when absent the quote is
19290
+ // fetched without an Authorization header.
18633
19291
  const kitKey = params.config?.kitKey;
18634
- if (!kitKey) {
18635
- throw new KitError({
18636
- code: 1098,
18637
- name: 'INPUT_VALIDATION_FAILED',
18638
- type: 'INPUT',
18639
- recoverability: 'FATAL',
18640
- message: 'kitKey is required in config for callback-based fees',
18641
- cause: {
18642
- trace: {
18643
- operation: 'handleOutputFeeCallback',
18644
- params
18645
- }
18646
- }
18647
- });
18648
- }
18649
19292
  // Resolve token aliases to addresses for the quote API
18650
19293
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
18651
19294
  const chain = params.from.chain;
@@ -18670,7 +19313,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18670
19313
  ...params.config?.slippageBps !== undefined && {
18671
19314
  slippageBps: params.config.slippageBps
18672
19315
  },
18673
- apiKey: kitKey
19316
+ ...kitKey ? {
19317
+ apiKey: kitKey
19318
+ } : {}
18674
19319
  };
18675
19320
  // Step 2: Get quote from service
18676
19321
  const quoteResponse = await getQuote(quoteParams);
@@ -18922,7 +19567,13 @@ async function resolveTokenDecimals(context, resolvedParams) {
18922
19567
  * amountIn: '50.00'
18923
19568
  * })
18924
19569
  * ```
18925
- */ async function swap$1(context, params, /** @internal */ onBroadcast) {
19570
+ */ async function swap$1(context, params, /**
19571
+ * @internal
19572
+ * Invoked after a successful broadcast with the on-chain `txHash` and the
19573
+ * service-issued `correlationId` (join key for success telemetry). The
19574
+ * service returns `correlationId` for every chain (EVM + Solana); it is
19575
+ * undefined only against a not-yet-upgraded service that omits the field.
19576
+ */ onBroadcast) {
18926
19577
  // Step 1: Validate parameters using schema
18927
19578
  assertSwapParams(params, swapParamsSchema);
18928
19579
  // Step 2: Resolve parameters (chain definitions, wallet addresses, token aliases preserved)
@@ -18942,13 +19593,16 @@ async function resolveTokenDecimals(context, resolvedParams) {
18942
19593
  // Step 5: Execute swap via provider
18943
19594
  const swapParams = buildServiceSwapParams(resolvedParams, paramsWithFees);
18944
19595
  const providerResult = await provider.swap(swapParams);
19596
+ // `correlationId` is an internal telemetry join key, not part of the public
19597
+ // SwapResult — strip it here so it never leaks into the formatted result.
19598
+ const { correlationId, ...providerResultPublic } = providerResult;
18945
19599
  // A throwing `onBroadcast` must never strand the caller after a
18946
19600
  // successful swap broadcast — the chain has moved. `safeInvokeCallback`
18947
19601
  // swallows the error and surfaces a `console.warn` prefixed with
18948
19602
  // `[stablecoin-kits swap-kit] callback threw and was swallowed:` so a
18949
19603
  // kit-side closure bug stays debuggable rather than vanishing.
18950
19604
  safeInvokeCallback('swap-kit', ()=>{
18951
- onBroadcast?.(providerResult.txHash);
19605
+ onBroadcast?.(providerResultPublic.txHash, correlationId);
18952
19606
  });
18953
19607
  const { tokenInDecimals, tokenOutDecimals } = await resolveTokenDecimals(context, resolvedParams);
18954
19608
  // Step 6: Compose chain identity (owned by the kit, derived from the
@@ -18957,10 +19611,10 @@ async function resolveTokenDecimals(context, resolvedParams) {
18957
19611
  // resolves `progress` — the provider's snapshot, or a terminal `'DONE'` for
18958
19612
  // a provider that omits it (a synchronous same-chain completion).
18959
19613
  const composedResult = {
18960
- ...providerResult,
19614
+ ...providerResultPublic,
18961
19615
  chainIn: resolvedParams.from.chain,
18962
19616
  chainOut: resolvedParams.toChain ?? resolvedParams.from.chain,
18963
- progress: providerResult.progress ?? {
19617
+ progress: providerResultPublic.progress ?? {
18964
19618
  status: 'DONE'
18965
19619
  }
18966
19620
  };
@@ -19112,7 +19766,9 @@ const sleep$1 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
19112
19766
  ...isCrossChain && {
19113
19767
  toChain: chainOut
19114
19768
  },
19115
- apiKey: params.kitKey
19769
+ ...params.kitKey ? {
19770
+ apiKey: params.kitKey
19771
+ } : {}
19116
19772
  };
19117
19773
  let raw = await getSwapStatus$1(request);
19118
19774
  // When the service hasn't finished indexing a just-submitted swap it
@@ -19252,7 +19908,9 @@ const isResultShape = (params)=>'result' in params;
19252
19908
  ...chainOut !== undefined && {
19253
19909
  chainOut
19254
19910
  },
19255
- kitKey: params.kitKey
19911
+ ...params.kitKey ? {
19912
+ kitKey: params.kitKey
19913
+ } : {}
19256
19914
  };
19257
19915
  const deadline = Date.now() + timeoutMs;
19258
19916
  let pollIndex = 0;
@@ -19413,7 +20071,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19413
20071
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
19414
20072
  return getTokenRates$1({
19415
20073
  chain,
19416
- apiKey: params.kitKey,
20074
+ ...params.kitKey ? {
20075
+ apiKey: params.kitKey
20076
+ } : {},
19417
20077
  ...resolvedAddresses !== undefined && {
19418
20078
  addresses: resolvedAddresses
19419
20079
  }
@@ -19807,7 +20467,13 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19807
20467
  */ class SwapKit {
19808
20468
  context;
19809
20469
  /** Whether error telemetry is disabled. */ disableErrorReporting;
19810
- /** Per-kit telemetry identity for shared helpers. */ telemetryConfig;
20470
+ /** Per-kit telemetry identity for error reporting. */ telemetryConfig;
20471
+ /**
20472
+ * Per-kit telemetry identity for success/analytics events. Gated by
20473
+ * `disableAnalytics` (independent of `disableErrorReporting`) so a developer
20474
+ * can opt out of volume analytics without also silencing error reports —
20475
+ * mirrors EarnKit.
20476
+ */ analyticsTelemetryConfig;
19811
20477
  /**
19812
20478
  * Create a new SwapKit instance.
19813
20479
  *
@@ -19861,6 +20527,11 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19861
20527
  sdkVersion: pkg$1.version,
19862
20528
  disabled: this.disableErrorReporting
19863
20529
  };
20530
+ this.analyticsTelemetryConfig = {
20531
+ sdkName: SDK_NAME,
20532
+ sdkVersion: pkg$1.version,
20533
+ disabled: config.disableAnalytics === true
20534
+ };
19864
20535
  }
19865
20536
  /**
19866
20537
  * Estimate the output amount and fees for a swap operation.
@@ -19901,8 +20572,12 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19901
20572
  * console.log(`Fees:`, quote.fees)
19902
20573
  * ```
19903
20574
  */ async estimate(params) {
20575
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
19904
20576
  return withErrorTelemetry(async ()=>estimate(this.context, params), SWAP_EVENT_TYPES.ESTIMATE, this.telemetryConfig, {
19905
20577
  sourceChain: resolveChainName(params.from.chain),
20578
+ ...destinationChain != null && {
20579
+ destinationChain
20580
+ },
19906
20581
  tokenIn: params.tokenIn,
19907
20582
  tokenOut: params.tokenOut
19908
20583
  });
@@ -19961,16 +20636,54 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19961
20636
  * ```
19962
20637
  */ async swap(params) {
19963
20638
  let txHash;
19964
- return withErrorTelemetry(async ()=>swap$1(this.context, params, (h)=>{
19965
- txHash = h;
19966
- }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, ()=>({
20639
+ let correlationId;
20640
+ // Shared context builder so the error resolver and the success emit stay in
20641
+ // lockstep — a field added here reaches both call sites. Reads the per-call
20642
+ // locals lazily, so txHash/correlationId (set during the swap) are captured
20643
+ // whenever it is invoked.
20644
+ // Destination chain is the primary attribution dimension for cross-chain
20645
+ // swaps; resolved once from the (static) params. Omitted for same-chain
20646
+ // swaps that leave `to.chain` unset (destination == source).
20647
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
20648
+ const buildTelemetryContext = ()=>({
19967
20649
  sourceChain: resolveChainName(params.from.chain),
20650
+ ...destinationChain != null && {
20651
+ destinationChain
20652
+ },
19968
20653
  tokenIn: params.tokenIn,
19969
20654
  tokenOut: params.tokenOut,
19970
20655
  ...txHash != null && {
19971
20656
  txHash
20657
+ },
20658
+ ...correlationId != null && {
20659
+ correlationId
19972
20660
  }
19973
- }));
20661
+ });
20662
+ const result = await withErrorTelemetry(async ()=>swap$1(this.context, params, (h, cId)=>{
20663
+ txHash = h;
20664
+ correlationId = cId;
20665
+ }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, buildTelemetryContext);
20666
+ // withErrorTelemetry only emits on failure. Record the successful swap here
20667
+ // so the backend can attribute swap volume to a developer: the client event
20668
+ // carries the (burn) txHash + correlationId, which joins to the
20669
+ // server-emitted event carrying entity_id. Best-effort; never throws.
20670
+ //
20671
+ // Emit whenever a broadcast happened, i.e. we have the source/burn txHash.
20672
+ // For a cross-chain swap that is the source-chain burn (progress is still
20673
+ // PENDING while the destination mint settles) — we intentionally attribute
20674
+ // at broadcast using the burn txHash rather than tracking the destination
20675
+ // leg, which keeps the capture simple and self-contained in swap().
20676
+ //
20677
+ // Guard against a terminal-failure result: the EVM provider throws on
20678
+ // revert today, but the kit is provider-agnostic, so a provider that
20679
+ // returns a FAILED/NOT_FOUND result without throwing must not be recorded
20680
+ // as a successful swap. Routed through analyticsTelemetryConfig so it is
20681
+ // gated by disableAnalytics, independent of error reporting.
20682
+ const status = result.progress?.status;
20683
+ if (txHash != null && status !== 'FAILED' && status !== 'NOT_FOUND') {
20684
+ emitSuccessTelemetry(SWAP_EVENT_TYPES.SWAP, this.analyticsTelemetryConfig, buildTelemetryContext());
20685
+ }
20686
+ return result;
19974
20687
  }
19975
20688
  /**
19976
20689
  * Fetch the current status of a swap from the Stablecoin Service.
@@ -20349,6 +21062,9 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20349
21062
  const kit = new SwapKit({
20350
21063
  ...context.disableErrorReporting != null && {
20351
21064
  disableErrorReporting: context.disableErrorReporting
21065
+ },
21066
+ ...context.disableAnalytics != null && {
21067
+ disableAnalytics: context.disableAnalytics
20352
21068
  }
20353
21069
  });
20354
21070
  if (hasBoth) {
@@ -20410,7 +21126,7 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20410
21126
  };
20411
21127
 
20412
21128
  var name = "@circle-fin/earn-kit";
20413
- var version = "1.2.2";
21129
+ var version = "1.4.0";
20414
21130
  var pkg = {
20415
21131
  name: name,
20416
21132
  version: version};
@@ -20540,7 +21256,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20540
21256
  asset: z.string(),
20541
21257
  assetAddress: z.string(),
20542
21258
  lltv: z.number(),
20543
- supplyUsd: z.number()
21259
+ supplyUsd: z.number(),
21260
+ // Optional during the expand/contract window (a backend that predates the
21261
+ // field omits the key), mirroring the `.optional()` facets on the base
21262
+ // schema; `null` when the product exposes no per-market allocation (V2).
21263
+ allocationPct: z.number().nullable().optional()
20544
21264
  });
20545
21265
  /**
20546
21266
  * Zod schema for a Morpho vault warning in the API response.
@@ -20554,7 +21274,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20554
21274
  ])
20555
21275
  });
20556
21276
  /**
20557
- * Zod schema for a single vault info object in the API response.
21277
+ * Zod schema for the manager (curator) facet in the API response.
21278
+ *
21279
+ * @internal
21280
+ */ const managerSchema = z.object({
21281
+ name: z.string(),
21282
+ address: z.string().optional(),
21283
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
21284
+ // are added here as the providers that emit them land, rather than shipped
21285
+ // speculatively.
21286
+ type: z.enum([
21287
+ 'curator'
21288
+ ])
21289
+ });
21290
+ /**
21291
+ * Zod schema for the APY profile facet in the API response.
21292
+ *
21293
+ * @internal
21294
+ */ const apyProfileSchema = z.object({
21295
+ current: z.number(),
21296
+ native: z.number().nullable(),
21297
+ d7: z.number().nullable(),
21298
+ d30: z.number().nullable(),
21299
+ d90: z.number().nullable(),
21300
+ rewardShare: z.number().nullable(),
21301
+ source: z.string().optional(),
21302
+ asOf: z.string().optional()
21303
+ });
21304
+ /**
21305
+ * Zod schema for the fee split facet in the API response.
21306
+ *
21307
+ * @internal
21308
+ */ const feeInfoSchema = z.object({
21309
+ performance: z.number().nullable(),
21310
+ management: z.number().nullable()
21311
+ });
21312
+ /**
21313
+ * Zod schema for the liquidity profile facet in the API response.
21314
+ *
21315
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
21316
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
21317
+ *
21318
+ * @internal
21319
+ */ const liquidityProfileSchema = z.object({
21320
+ totalDeposits: amountJsonSchema,
21321
+ available: amountJsonSchema,
21322
+ totalSupply: amountJsonSchema,
21323
+ status: z.enum([
21324
+ 'active',
21325
+ 'low_liquidity'
21326
+ ])
21327
+ });
21328
+ /**
21329
+ * Zod schema for the risk signals facet in the API response.
21330
+ *
21331
+ * @internal
21332
+ */ const riskSignalsSchema = z.object({
21333
+ circleSentinel: z.boolean(),
21334
+ warnings: z.array(vaultWarningSchema).optional(),
21335
+ earnKitWarnings: z.array(z.string()).optional()
21336
+ });
21337
+ /**
21338
+ * Zod schema for the universal earn-opportunity base in the API response.
21339
+ *
21340
+ * Retains every existing deprecated flat field (kept validated through the
21341
+ * expand/contract window so default-strip does not drop them) and adds the
21342
+ * new nested facets. The nested facets are `.optional()` during the
21343
+ * transition so the SDK still validates against a not-yet-fully-deployed
21344
+ * backend; they become required after Expand ships.
20558
21345
  *
20559
21346
  * @internal
20560
21347
  */ const vaultInfoResponseSchema = z.object({
@@ -20579,6 +21366,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20579
21366
  warnings: z.array(vaultWarningSchema).optional(),
20580
21367
  earnKitWarnings: z.array(z.string()).optional()
20581
21368
  });
21369
+ /**
21370
+ * Shared base schema: existing flat fields (kept) plus the new nested
21371
+ * facets and neutral identity. Facets are `.optional()` during the
21372
+ * transition; flip to required once the backend is confirmed emitting.
21373
+ *
21374
+ * @internal
21375
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
21376
+ address: z.string().optional(),
21377
+ asOf: z.string().optional(),
21378
+ manager: managerSchema.nullable().optional(),
21379
+ apyProfile: apyProfileSchema.optional(),
21380
+ fee: feeInfoSchema.optional(),
21381
+ liquidityProfile: liquidityProfileSchema.optional(),
21382
+ riskSignals: riskSignalsSchema.optional()
21383
+ });
21384
+ /**
21385
+ * Zod schema for the `vault` opportunity variant.
21386
+ *
21387
+ * @internal
21388
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
21389
+ productType: z.literal('vault'),
21390
+ collateral: z.array(collateralSchema)
21391
+ });
21392
+ /**
21393
+ * Discriminated union over `productType`. Add union members here as new
21394
+ * product types (e.g. `lending_market`, `rwa_token`) land.
21395
+ *
21396
+ * @internal
21397
+ */ const earnOpportunityVariants = [
21398
+ vaultOpportunitySchema
21399
+ ];
21400
+ /** @internal */ const earnOpportunitySchema = z.discriminatedUnion('productType', earnOpportunityVariants);
21401
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
21402
+ /**
21403
+ * Tolerant list parser for earn opportunities.
21404
+ *
21405
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
21406
+ * `z.array` fails the whole array if any element fails. Two migration-window
21407
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
21408
+ *
21409
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
21410
+ * only opportunity type then, so default a missing discriminant to `'vault'`
21411
+ * rather than dropping every vault the backend returns.
21412
+ * - A future backend adds a *second* `productType` this SDK version does not
21413
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
21414
+ * of rejecting the whole list.
21415
+ *
21416
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
21417
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
21418
+ * primitives, or an object whose `productType` is malformed — is passed through
21419
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
21420
+ * validation failure. It is deliberately not silently dropped (which would hide
21421
+ * malformed backend data) and never throws here (an unguarded property read on
21422
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
21423
+ * `ZodError`).
21424
+ *
21425
+ * @internal
21426
+ */ const earnOpportunityListSchema = z.preprocess((raw)=>{
21427
+ if (!Array.isArray(raw)) {
21428
+ return raw;
21429
+ }
21430
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
21431
+ // map/filter chain stays type-safe and no `any` leaks into the return.
21432
+ const entries = raw;
21433
+ return entries.map((entry)=>{
21434
+ // Only touch plain objects; non-objects fall through to fail validation.
21435
+ if (typeof entry !== 'object' || entry === null) {
21436
+ return entry;
21437
+ }
21438
+ const record = entry;
21439
+ // Older backend predating productType: default to the only type then.
21440
+ return record.productType === undefined ? {
21441
+ ...record,
21442
+ productType: 'vault'
21443
+ } : record;
21444
+ }).filter((entry)=>{
21445
+ // Drop ONLY a present-but-unknown string discriminant (a future
21446
+ // productType this SDK version doesn't know). Everything else —
21447
+ // non-objects, a non-string productType — flows through to
21448
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
21449
+ if (typeof entry !== 'object' || entry === null) {
21450
+ return true;
21451
+ }
21452
+ const productType = entry.productType;
21453
+ if (typeof productType !== 'string') {
21454
+ return true;
21455
+ }
21456
+ return knownProductTypes.has(productType);
21457
+ });
21458
+ }, z.array(earnOpportunitySchema));
20582
21459
  // ---------------------------------------------------------------------------
20583
21460
  // Position response schema
20584
21461
  // ---------------------------------------------------------------------------
@@ -20708,6 +21585,7 @@ const positionPnlSchema = z.discriminatedUnion('status', [
20708
21585
  *
20709
21586
  * @internal
20710
21587
  */ const depositPayloadSchema = z.object({
21588
+ execId: bridgeDepositExecIdSchema,
20711
21589
  executionParams: depositExecutionParamsSchema,
20712
21590
  signature: hexSignatureSchema
20713
21591
  });
@@ -20799,6 +21677,21 @@ const bridgeDepositPrepareReviewSchema = z.object({
20799
21677
  amount: amountJsonSchema,
20800
21678
  vaultAddress: hexAddressSchema
20801
21679
  }).passthrough();
21680
+ /** @internal */ const bridgeQuoteExpirySchema = z.discriminatedUnion('mode', [
21681
+ z.object({
21682
+ mode: z.literal('TIMESTAMP'),
21683
+ expiresAt: z.string().datetime({
21684
+ offset: true
21685
+ })
21686
+ }),
21687
+ z.object({
21688
+ mode: z.literal('BLOCK_NUMBER'),
21689
+ expiresAtBlock: z.number().int(),
21690
+ blockEstimatedAt: z.string().datetime({
21691
+ offset: true
21692
+ }).optional()
21693
+ })
21694
+ ]).optional().catch(undefined);
20802
21695
  /**
20803
21696
  * Zod schema for the bridge deposit prepare payload.
20804
21697
  *
@@ -20810,6 +21703,10 @@ const bridgeDepositPrepareReviewSchema = z.object({
20810
21703
  execId: bridgeDepositExecIdSchema,
20811
21704
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
20812
21705
  expiresAt: z.string().datetime(),
21706
+ quoteIssuedAt: z.string().datetime({
21707
+ offset: true
21708
+ }).optional().catch(undefined),
21709
+ quoteExpiry: bridgeQuoteExpirySchema,
20813
21710
  review: bridgeDepositPrepareReviewSchema
20814
21711
  });
20815
21712
  /**
@@ -20875,6 +21772,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
20875
21772
  *
20876
21773
  * @internal
20877
21774
  */ const withdrawPayloadSchema = z.object({
21775
+ execId: bridgeDepositExecIdSchema,
20878
21776
  executionParams: withdrawExecutionParamsSchema,
20879
21777
  signature: hexSignatureSchema
20880
21778
  });
@@ -20888,6 +21786,27 @@ const bridgeDepositPrepareReviewSchema = z.object({
20888
21786
  data: withdrawPayloadSchema
20889
21787
  });
20890
21788
  // ---------------------------------------------------------------------------
21789
+ // Transaction report response schema
21790
+ // ---------------------------------------------------------------------------
21791
+ /**
21792
+ * Zod schema for the transaction report payload inside the API `data` envelope.
21793
+ *
21794
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
21795
+ * schema accepts any object shape and does not require specific fields.
21796
+ *
21797
+ * @internal
21798
+ */ const transactionReportPayloadSchema = z.object({}).passthrough();
21799
+ /**
21800
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
21801
+ *
21802
+ * The Earn Service API wraps the transaction report payload in a `data`
21803
+ * envelope.
21804
+ *
21805
+ * @internal
21806
+ */ z.object({
21807
+ data: transactionReportPayloadSchema
21808
+ });
21809
+ // ---------------------------------------------------------------------------
20891
21810
  // Claim rewards response schema
20892
21811
  // ---------------------------------------------------------------------------
20893
21812
  /**
@@ -20948,6 +21867,30 @@ const bridgeDepositPrepareReviewSchema = z.object({
20948
21867
  token: z.string(),
20949
21868
  amount: amountJsonSchema
20950
21869
  });
21870
+ /**
21871
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
21872
+ *
21873
+ * The Earn Service backend estimates gas server-side and returns one entry per
21874
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
21875
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
21876
+ * integer string in the chain's native base units. When the backend cannot
21877
+ * estimate an action it returns `fees: null` with an `error` message instead.
21878
+ *
21879
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
21880
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
21881
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
21882
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
21883
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
21884
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
21885
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
21886
+ * `fee`) must never fail Zod validation and reject the entire quote.
21887
+ *
21888
+ * @internal
21889
+ */ const quoteGasFeeSchema = z.object({
21890
+ name: z.string().optional(),
21891
+ fees: z.unknown(),
21892
+ error: z.string().optional()
21893
+ }).passthrough();
20951
21894
  /**
20952
21895
  * Zod schema for the inner deposit quote payload.
20953
21896
  *
@@ -20963,7 +21906,8 @@ const bridgeDepositPrepareReviewSchema = z.object({
20963
21906
  expectedShares: amountJsonSchema,
20964
21907
  sharePrice: z.string(),
20965
21908
  currentApy: z.number(),
20966
- fees: z.array(feeSchema).optional()
21909
+ fees: z.array(feeSchema).optional(),
21910
+ gasFees: z.array(quoteGasFeeSchema).optional()
20967
21911
  });
20968
21912
  /**
20969
21913
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -20990,6 +21934,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
20990
21934
  sharePrice: z.string(),
20991
21935
  maxWithdrawable: amountJsonSchema,
20992
21936
  fees: z.array(feeSchema),
21937
+ gasFees: z.array(quoteGasFeeSchema).optional(),
20993
21938
  warnings: z.array(z.string()).optional()
20994
21939
  });
20995
21940
  /**
@@ -21047,7 +21992,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
21047
21992
  *
21048
21993
  * @internal
21049
21994
  */ const getVaultsPayloadSchema = z.object({
21050
- vaults: z.array(vaultInfoResponseSchema),
21995
+ vaults: earnOpportunityListSchema,
21051
21996
  errors: z.array(vaultErrorSchema)
21052
21997
  });
21053
21998
  /**
@@ -21077,7 +22022,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
21077
22022
  *
21078
22023
  * @internal
21079
22024
  */ const exploreVaultsPayloadSchema = z.object({
21080
- vaults: z.array(vaultInfoResponseSchema),
22025
+ vaults: earnOpportunityListSchema,
21081
22026
  pagination: explorePaginationSchema
21082
22027
  });
21083
22028
  /**
@@ -21645,6 +22590,8 @@ function hasCrossChainDepositQuoteShape(params) {
21645
22590
  config: earnConfigSchema.optional()
21646
22591
  });
21647
22592
 
22593
+ /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg.name);
22594
+
21648
22595
  // Auto-register this kit for user agent tracking
21649
22596
  registerKit(`${pkg.name}/${pkg.version}`);
21650
22597