@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.cjs CHANGED
@@ -18,15 +18,27 @@
18
18
 
19
19
  'use strict';
20
20
 
21
+ // Buffer polyfill setup - executes before any other code
22
+ // Ensures globalThis.Buffer is available for Solana libraries
23
+ const { Buffer } = require('buffer');
24
+ if (typeof globalThis !== 'undefined' && typeof globalThis.Buffer === 'undefined') {
25
+ globalThis.Buffer = Buffer;
26
+ }
27
+ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
28
+ window.Buffer = Buffer;
29
+ }
30
+
31
+
21
32
  var zod = require('zod');
22
33
  require('pino');
34
+ var bytes = require('@ethersproject/bytes');
35
+ require('@ethersproject/abi');
36
+ var address = require('@ethersproject/address');
23
37
  var web3_js = require('@solana/web3.js');
24
38
  require('bn.js');
25
39
  require('@coral-xyz/anchor');
26
40
  var bs58 = require('bs58');
27
41
  require('@noble/curves/ed25519');
28
- var bytes = require('@ethersproject/bytes');
29
- var address = require('@ethersproject/address');
30
42
  var units = require('@ethersproject/units');
31
43
  var keccak256 = require('@ethersproject/keccak256');
32
44
 
@@ -49,6 +61,51 @@ var bs58__default = /*#__PURE__*/_interopDefault(bs58);
49
61
  * }
50
62
  * ```
51
63
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
64
+ /**
65
+ * Check whether the current runtime exposes a browser DOM.
66
+ *
67
+ * @remarks
68
+ * This intentionally does not treat every non-Node runtime as a browser.
69
+ * Server-side edge runtimes such as Cloudflare Workers, Deno, and Supabase
70
+ * Edge Functions do not expose Node globals but can safely use server
71
+ * credentials. A Node.js runtime remains server-side even when a test or SSR
72
+ * environment provides a DOM shim.
73
+ *
74
+ * @returns `true` when running in a browser window, `false` otherwise.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { isBrowserEnvironment } from '@core/utils'
79
+ *
80
+ * if (isBrowserEnvironment()) {
81
+ * throw new Error('Server-only secrets must not be used in the browser')
82
+ * }
83
+ * ```
84
+ */ const isBrowserEnvironment = ()=>{
85
+ const browserWindow = globalThis.window;
86
+ return !isNodeEnvironment() && browserWindow?.document !== undefined;
87
+ };
88
+ /**
89
+ * Return the SDK User-Agent request header only when running in Node.js.
90
+ *
91
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
92
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
93
+ * attribution header because they cannot set it reliably.
94
+ *
95
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * import { getNodeUserAgentHeader } from '@core/utils'
100
+ *
101
+ * const headers = {
102
+ * 'Content-Type': 'application/json',
103
+ * ...getNodeUserAgentHeader(),
104
+ * }
105
+ * ```
106
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
107
+ 'User-Agent': getUserAgent()
108
+ } : {};
52
109
  /**
53
110
  * Detect the runtime environment and return a shortened identifier.
54
111
  *
@@ -3533,7 +3590,10 @@ var EarnChain;
3533
3590
  contracts: {
3534
3591
  v1: {
3535
3592
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3536
- minter: GATEWAY_MINTER_EVM_TESTNET
3593
+ minter: GATEWAY_MINTER_EVM_TESTNET,
3594
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3595
+ // deposit into the GatewayWallet above.
3596
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3537
3597
  }
3538
3598
  },
3539
3599
  forwarderSupported: {
@@ -6601,7 +6661,10 @@ var Chains = {
6601
6661
  minter: zod.z.string({
6602
6662
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
6603
6663
  invalid_type_error: 'Gateway minter address must be a string.'
6604
- }).min(1, 'Gateway minter address cannot be empty.')
6664
+ }).min(1, 'Gateway minter address cannot be empty.'),
6665
+ depositForHandler: zod.z.string({
6666
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
6667
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
6605
6668
  }).strict() // Reject any additional properties not defined in the schema
6606
6669
  ;
6607
6670
  /**
@@ -8020,13 +8083,12 @@ const swapTokenEnumSchema = zod.z.enum([
8020
8083
  headers: {
8021
8084
  ...DEFAULT_CONFIG$1.headers,
8022
8085
  ...config.headers ?? {},
8023
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
8024
- // This is why we use the 'X-User-Agent' header instead.
8025
- ...typeof window === 'undefined' ? {
8026
- 'User-Agent': getUserAgent()
8027
- } : {
8028
- 'X-User-Agent': getUserAgent()
8029
- }
8086
+ // Browsers forbid setting a user-agent request header, and the custom
8087
+ // fallback header the SDK used instead trips CORS preflight against the
8088
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
8089
+ // blocking the request. So send the SDK user agent only in Node;
8090
+ // browsers omit it entirely.
8091
+ ...getNodeUserAgentHeader()
8030
8092
  }
8031
8093
  };
8032
8094
  let lastError;
@@ -9486,6 +9548,13 @@ const swapTokenEnumSchema = zod.z.enum([
9486
9548
  return explorerUrl;
9487
9549
  }
9488
9550
 
9551
+ /**
9552
+ * CCTP forwarding magic bytes prefix.
9553
+ *
9554
+ * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9555
+ * This prefix is right-padded to 24 bytes in the final hookData.
9556
+ */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
9557
+
9489
9558
  /**
9490
9559
  * Project an arbitrary payload onto the exact set of fields the telemetry
9491
9560
  * endpoint accepts.
@@ -9520,6 +9589,7 @@ const swapTokenEnumSchema = zod.z.enum([
9520
9589
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
9521
9590
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
9522
9591
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
9592
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
9523
9593
  if (payload.errorDetails !== undefined) {
9524
9594
  const errorDetails = {
9525
9595
  ...payload.errorDetails.errorCode !== undefined && {
@@ -9590,18 +9660,15 @@ const swapTokenEnumSchema = zod.z.enum([
9590
9660
  timeoutHandle.unref();
9591
9661
  }
9592
9662
  try {
9593
- const isNode = isNodeEnvironment();
9594
- const userAgent = getUserAgent();
9595
9663
  await fetch(getLogsUrl(), {
9596
9664
  method: 'POST',
9597
9665
  headers: {
9598
9666
  'Content-Type': 'application/json',
9599
- // Browser restricts setting User-Agent; use X-User-Agent instead.
9600
- ...isNode ? {
9601
- 'User-Agent': userAgent
9602
- } : {
9603
- 'X-User-Agent': userAgent
9604
- }
9667
+ // Browsers forbid setting a user-agent request header, and the custom
9668
+ // fallback header the SDK used instead trips CORS preflight (it isn't
9669
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
9670
+ // it only in Node; browsers omit it entirely.
9671
+ ...getNodeUserAgentHeader()
9605
9672
  },
9606
9673
  body: JSON.stringify(toSafePayload(payload)),
9607
9674
  signal: controller.signal
@@ -9769,7 +9836,7 @@ const swapTokenEnumSchema = zod.z.enum([
9769
9836
  // discards the stack trace, nested `cause`, and any custom Error
9770
9837
  // properties — exactly the context an on-call needs when a
9771
9838
  // resolver-closure regression triggers this path.
9772
- console.warn(`[stablecoin-kits telemetry] dropped error event '${eventType}':`, cause);
9839
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
9773
9840
  } catch {
9774
9841
  // console.warn itself throwing is the user's environment; nothing more we
9775
9842
  // can do without risking the original operation error.
@@ -9785,7 +9852,9 @@ const swapTokenEnumSchema = zod.z.enum([
9785
9852
  sdkVersion: config.sdkVersion,
9786
9853
  eventType,
9787
9854
  timestamp: new Date().toISOString(),
9788
- errorDetails,
9855
+ ...errorDetails !== undefined && {
9856
+ errorDetails
9857
+ },
9789
9858
  clientContext: buildClientContext(),
9790
9859
  ...context?.sourceChain != null && {
9791
9860
  sourceChain: context.sourceChain
@@ -9801,9 +9870,45 @@ const swapTokenEnumSchema = zod.z.enum([
9801
9870
  },
9802
9871
  ...context?.txHash != null && {
9803
9872
  txHash: context.txHash
9873
+ },
9874
+ ...context?.correlationId != null && {
9875
+ correlationId: context.correlationId
9804
9876
  }
9805
9877
  };
9806
9878
  }
9879
+ /**
9880
+ * Emit telemetry for a completed operation without affecting its caller.
9881
+ *
9882
+ * No-ops when `config.disabled` is `true`. Like {@link withErrorTelemetry},
9883
+ * failures while constructing or submitting the telemetry payload are reported
9884
+ * as a soft warning and never change a completed operation's result.
9885
+ *
9886
+ * @param eventType - The telemetry event type for the completed operation.
9887
+ * @param config - Per-kit SDK identity and disabled flag.
9888
+ * @param context - Optional chain, token, and transaction context.
9889
+ * @returns Nothing.
9890
+ * @throws Never — telemetry failures are reported as warnings.
9891
+ *
9892
+ * @example
9893
+ * ```typescript
9894
+ * import { emitSuccessTelemetry } from '@core/utils'
9895
+ *
9896
+ * emitSuccessTelemetry(
9897
+ * 'bridge_bridge',
9898
+ * { sdkName: 'bridge-kit', sdkVersion: '1.0.0', disabled: false },
9899
+ * { sourceChain: 'Ethereum', destinationChain: 'Base', tokenIn: 'USDC' },
9900
+ * )
9901
+ * ```
9902
+ */ function emitSuccessTelemetry(eventType, config, context) {
9903
+ if (config.disabled) {
9904
+ return;
9905
+ }
9906
+ try {
9907
+ void emitAnalyticsLog(buildPayload(config, eventType, undefined, context));
9908
+ } catch (telemetryError) {
9909
+ warnTelemetryDrop(eventType, telemetryError);
9910
+ }
9911
+ }
9807
9912
  /**
9808
9913
  * Wrap an async operation with error telemetry.
9809
9914
  *
@@ -9862,7 +9967,7 @@ const swapTokenEnumSchema = zod.z.enum([
9862
9967
  }
9863
9968
 
9864
9969
  var name$2 = "@circle-fin/bridge-kit";
9865
- var version$2 = "1.12.0";
9970
+ var version$2 = "1.12.2";
9866
9971
  var pkg$2 = {
9867
9972
  name: name$2,
9868
9973
  version: version$2};
@@ -10720,6 +10825,17 @@ var TransferSpeed;
10720
10825
  clock: zod.z.any().optional()
10721
10826
  }).passthrough();
10722
10827
 
10828
+ /**
10829
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
10830
+ * hookData must start with.
10831
+ *
10832
+ * Encoded with `TextEncoder` (a browser-safe Web API) rather than `Buffer.from`
10833
+ * so this module-level constant does not reference the Node `Buffer` global at
10834
+ * import time. App Kit inlines this provider into its bundle without a Buffer polyfill, and a
10835
+ * bare `Buffer` here crashes browser bundles (e.g. Vite) on load — even for apps
10836
+ * that never touch the prepaid FORWARD path. Mirrors `buildForwardingHookData`.
10837
+ */ Array.from(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX)).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
10838
+
10723
10839
  /**
10724
10840
  * The minimum finality threshold for CCTPv2 transfers.
10725
10841
  *
@@ -10752,7 +10868,7 @@ var TransferSpeed;
10752
10868
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
10753
10869
 
10754
10870
  var name$1 = "@circle-fin/swap-kit";
10755
- var version$1 = "1.3.2";
10871
+ var version$1 = "1.5.0";
10756
10872
  var pkg$1 = {
10757
10873
  name: name$1,
10758
10874
  version: version$1};
@@ -10817,7 +10933,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
10817
10933
  }).min(1, 'kitKey must be a non-empty string').optional(),
10818
10934
  provider: zod.z.string({
10819
10935
  invalid_type_error: 'provider must be a string'
10820
- }).min(1, 'provider must be a non-empty string').optional()
10936
+ }).min(1, 'provider must be a non-empty string').optional(),
10937
+ batchTransactions: zod.z.boolean({
10938
+ invalid_type_error: 'batchTransactions must be a boolean'
10939
+ }).optional()
10821
10940
  });
10822
10941
  /**
10823
10942
  * Zod schema for adapter context.
@@ -11348,7 +11467,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11348
11467
  /**
11349
11468
  * Circle Stablecoin Service API Key.
11350
11469
  * Must be a valid API key format.
11351
- */ apiKey: apiKeySchema
11470
+ */ apiKey: apiKeySchema.optional()
11352
11471
  }).superRefine(requireCrossChainQuoteToAddress);
11353
11472
  /**
11354
11473
  * Zod schema for validating CreateSwapRequest parameters.
@@ -11406,7 +11525,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11406
11525
  /**
11407
11526
  * Circle Stablecoin Service API Key.
11408
11527
  * Must be a valid API key format.
11409
- */ apiKey: apiKeySchema
11528
+ */ apiKey: apiKeySchema.optional()
11410
11529
  });
11411
11530
  /**
11412
11531
  * Zod schema for validating GetSwapStatusResponse data.
@@ -11442,7 +11561,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11442
11561
  toChain: zod.z.string({
11443
11562
  invalid_type_error: 'toChain must be a string'
11444
11563
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
11445
- apiKey: apiKeySchema
11564
+ apiKey: apiKeySchema.optional()
11446
11565
  });
11447
11566
  /**
11448
11567
  * Zod schema for validating CreateSwapResponse payloads.
@@ -11451,13 +11570,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11451
11570
  required_error: 'fee token is required',
11452
11571
  invalid_type_error: 'fee token must be a string'
11453
11572
  }).min(1, 'fee token must be a non-empty string'),
11454
- amount: feeAmountSchema
11573
+ amount: feeAmountSchema,
11574
+ decimals: zod.z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
11575
+ symbol: zod.z.string({
11576
+ invalid_type_error: 'fee token symbol must be a string'
11577
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
11455
11578
  });
11456
11579
  /**
11457
11580
  * Developer fee item schema with basis field.
11458
- */ const createSwapDeveloperFeeItemSchema = zod.z.object({
11459
- token: zod.z.string().min(1, 'fee token must be a non-empty string'),
11460
- amount: feeAmountSchema,
11581
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
11461
11582
  basis: zod.z.enum([
11462
11583
  'inputAmount',
11463
11584
  'estimatedAmount'
@@ -11549,7 +11670,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11549
11670
  addresses: zod.z.array(zod.z.string({
11550
11671
  invalid_type_error: 'addresses entries must be strings'
11551
11672
  }).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(),
11552
- apiKey: apiKeySchema
11673
+ apiKey: apiKeySchema.optional()
11553
11674
  });
11554
11675
  /**
11555
11676
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -11580,6 +11701,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11580
11701
  required_error: 'estimatedAmount is required',
11581
11702
  invalid_type_error: 'estimatedAmount must be a string'
11582
11703
  }).min(1, 'estimatedAmount must be a non-empty string'),
11704
+ // Per-swap join key echoed back verbatim on success telemetry. Optional so a
11705
+ // not-yet-upgraded service (no field) still validates during rollout. A
11706
+ // malformed/non-UUID value is coerced to `undefined` (no telemetry id) rather
11707
+ // than throwing: this is a telemetry-only field (stripped from the developer
11708
+ // result, never used for control flow), so it must not be able to abort the
11709
+ // swap via `parseCreateSwapResponse().parse()`. Mirrors the best-effort,
11710
+ // never-throw contract of the rest of the telemetry stack. Implemented with
11711
+ // `preprocess` rather than Zod's `.catch()` because static analysis misreads
11712
+ // `.catch` on the schema chain as an unhandled Promise (S7785).
11713
+ correlationId: zod.z.preprocess((value)=>zod.z.string().uuid().safeParse(value).success ? value : undefined, zod.z.string().optional()),
11583
11714
  config: createSwapRequestBaseSchema.shape.config.optional(),
11584
11715
  fees: createSwapFeesSchema.optional(),
11585
11716
  transaction: createSwapTransactionSchema
@@ -11666,6 +11797,45 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11666
11797
  * }
11667
11798
  * ```
11668
11799
  */ const isGetTokenRatesResponse = (obj)=>getTokenRatesResponseSchema.safeParse(obj).success;
11800
+ /**
11801
+ * Assert that a Stablecoin Service kit key is not being supplied from a browser.
11802
+ *
11803
+ * The kit key (`KIT_KEY:<id>:<secret>`) is a server-only secret. Every
11804
+ * Stablecoin Service request that attaches an `Authorization: Bearer` header
11805
+ * funnels through this package, so calling this guard before that header is
11806
+ * built prevents the secret from being sent from — and thus bundled into — a
11807
+ * client application. In Node.js the check is a no-op, preserving the
11808
+ * legitimate "hold the kit key on the server, forward the prepared transaction
11809
+ * to the client" flow. When no kit key is supplied the permissionless (keyless)
11810
+ * client path remains fully allowed.
11811
+ *
11812
+ * @param apiKey - The inline kit key for the request, or `undefined` when none
11813
+ * was supplied (permissionless mode).
11814
+ * @returns Nothing.
11815
+ * @throws KitError with VALIDATION_FAILED when a kit key is supplied while
11816
+ * running in a browser environment. The secret value is never echoed.
11817
+ *
11818
+ * @example
11819
+ * ```typescript
11820
+ * import { assertBrowserSafeApiKey } from '@core/service-client'
11821
+ *
11822
+ * // Server (Node.js): no-op, request proceeds with the Authorization header.
11823
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11824
+ *
11825
+ * // Browser: throws to stop the secret from leaking into the client bundle.
11826
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11827
+ *
11828
+ * // Browser, permissionless: allowed.
11829
+ * assertBrowserSafeApiKey(undefined)
11830
+ * ```
11831
+ */ const assertBrowserSafeApiKey = (apiKey)=>{
11832
+ if (apiKey === undefined) {
11833
+ return;
11834
+ }
11835
+ if (isBrowserEnvironment()) {
11836
+ 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');
11837
+ }
11838
+ };
11669
11839
 
11670
11840
  /**
11671
11841
  * Create a cross-chain bridge and swap transaction through the Stablecoin Service.
@@ -11723,11 +11893,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11723
11893
  const url = new URL('/v1/stablecoinKits/swap', STABLECOIN_SERVICE_BASE_URL).toString();
11724
11894
  // Remove the API key from the request body
11725
11895
  const { apiKey, ...requestBody } = validatedParams;
11896
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
11897
+ assertBrowserSafeApiKey(apiKey);
11726
11898
  const effectiveConfig = {
11727
11899
  ...DEFAULT_CONFIG,
11728
11900
  headers: {
11729
11901
  ...DEFAULT_CONFIG.headers,
11730
- Authorization: `Bearer ${apiKey}`
11902
+ // Permissionless mode: no Authorization header when the kit key is absent.
11903
+ ...apiKey !== undefined && {
11904
+ Authorization: `Bearer ${apiKey}`
11905
+ }
11731
11906
  }
11732
11907
  };
11733
11908
  try {
@@ -11874,6 +12049,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11874
12049
  }
11875
12050
  // Use validated data
11876
12051
  const validatedParams = result.data;
12052
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12053
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11877
12054
  // Build the API URL
11878
12055
  const url = buildQuoteUrl(validatedParams);
11879
12056
  // Merge default config with Authorization header
@@ -11881,7 +12058,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11881
12058
  ...DEFAULT_CONFIG,
11882
12059
  headers: {
11883
12060
  ...DEFAULT_CONFIG.headers,
11884
- Authorization: `Bearer ${validatedParams.apiKey}`
12061
+ // Permissionless mode: no Authorization header when the kit key is absent.
12062
+ ...validatedParams.apiKey !== undefined && {
12063
+ Authorization: `Bearer ${validatedParams.apiKey}`
12064
+ }
11885
12065
  }
11886
12066
  };
11887
12067
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
@@ -11936,17 +12116,24 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11936
12116
  const validatedParams = {
11937
12117
  txHash: result.data.txHash,
11938
12118
  chain: result.data.chain,
11939
- apiKey: result.data.apiKey,
12119
+ ...result.data.apiKey !== undefined && {
12120
+ apiKey: result.data.apiKey
12121
+ },
11940
12122
  ...result.data.toChain !== undefined && {
11941
12123
  toChain: result.data.toChain
11942
12124
  }
11943
12125
  };
12126
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12127
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11944
12128
  const url = buildSwapStatusUrl(validatedParams);
11945
12129
  const effectiveConfig = {
11946
12130
  ...DEFAULT_CONFIG,
11947
12131
  headers: {
11948
12132
  ...DEFAULT_CONFIG.headers,
11949
- Authorization: `Bearer ${validatedParams.apiKey}`
12133
+ // Permissionless mode: no Authorization header when the kit key is absent.
12134
+ ...validatedParams.apiKey !== undefined && {
12135
+ Authorization: `Bearer ${validatedParams.apiKey}`
12136
+ }
11950
12137
  }
11951
12138
  };
11952
12139
  return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
@@ -12035,17 +12222,24 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12035
12222
  }
12036
12223
  const validatedParams = {
12037
12224
  chain: result.data.chain,
12038
- apiKey: result.data.apiKey,
12225
+ ...result.data.apiKey !== undefined && {
12226
+ apiKey: result.data.apiKey
12227
+ },
12039
12228
  ...result.data.addresses !== undefined && {
12040
12229
  addresses: result.data.addresses
12041
12230
  }
12042
12231
  };
12232
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12233
+ assertBrowserSafeApiKey(validatedParams.apiKey);
12043
12234
  const url = buildTokenRatesUrl(validatedParams);
12044
12235
  const effectiveConfig = {
12045
12236
  ...DEFAULT_CONFIG,
12046
12237
  headers: {
12047
12238
  ...DEFAULT_CONFIG.headers,
12048
- Authorization: `Bearer ${validatedParams.apiKey}`
12239
+ // Permissionless mode: no Authorization header when the kit key is absent.
12240
+ ...validatedParams.apiKey !== undefined && {
12241
+ Authorization: `Bearer ${validatedParams.apiKey}`
12242
+ }
12049
12243
  }
12050
12244
  };
12051
12245
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
@@ -12855,6 +13049,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
12855
13049
  */ function hasSignTypedData(adapter) {
12856
13050
  return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
12857
13051
  }
13052
+ /**
13053
+ * Type guard to check if an adapter can actually produce an EIP-712
13054
+ * typed-data signature.
13055
+ *
13056
+ * @remarks
13057
+ * Strengthens {@link hasSignTypedData}: having a `signTypedData` method
13058
+ * does not guarantee it can succeed. Adapters whose signer is delegated
13059
+ * (e.g. through a signing strategy backed by a smart contract account)
13060
+ * expose the method but reject typed-data payloads at runtime. Such
13061
+ * adapters report their real capability through an optional
13062
+ * `supportsSignTypedData()` method, which this guard consults when
13063
+ * present. Adapters without the capability method are assumed able to
13064
+ * sign, preserving the previous duck-typing behavior.
13065
+ *
13066
+ * @param adapter - The adapter to check
13067
+ * @returns True if calling `signTypedData` can be expected to succeed
13068
+ *
13069
+ * @example
13070
+ * ```typescript
13071
+ * import { canSignTypedData } from '@core/adapter-evm'
13072
+ *
13073
+ * if (canSignTypedData(adapter)) {
13074
+ * const signature = await adapter.signTypedData(typedData, context)
13075
+ * } else {
13076
+ * // take an on-chain approval path instead of a permit signature
13077
+ * }
13078
+ * ```
13079
+ */ function canSignTypedData(adapter) {
13080
+ if (!hasSignTypedData(adapter)) {
13081
+ return false;
13082
+ }
13083
+ if (typeof adapter.supportsSignTypedData === 'function') {
13084
+ // The value is `boolean` per the interface, but a plain-JS adapter may
13085
+ // return anything; treat it as untrusted and coerce to a strict
13086
+ // boolean. Comparing an `unknown` (not a `boolean`) also keeps the
13087
+ // lint autofix from stripping this as a redundant `=== true`.
13088
+ const supported = adapter.supportsSignTypedData();
13089
+ return supported === true;
13090
+ }
13091
+ return true;
13092
+ }
12858
13093
 
12859
13094
  /**
12860
13095
  * Build EIP-2612 typed data for permit signing.
@@ -13277,10 +13512,13 @@ enc.encode('used_transfer_spec_hash');
13277
13512
  * at usage time rather than construction time.
13278
13513
  *
13279
13514
  * Validates:
13280
- * - Kit key is present and matches required format (KIT_KEY:id:secret)
13515
+ * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
13516
+ * An absent or empty kit key is permitted (permissionless mode) — the swap
13517
+ * service now treats the key as optional.
13281
13518
  *
13282
- * @param kitKey - The inline kit key from the swap operation config
13283
- * @throws KitError with VALIDATION_FAILED if kit key is invalid or missing
13519
+ * @param kitKey - The inline kit key from the swap operation config (optional)
13520
+ * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
13521
+ * match the KIT_KEY:<keyId>:<keySecret> format
13284
13522
  *
13285
13523
  * @example
13286
13524
  * ```typescript
@@ -13292,9 +13530,11 @@ enc.encode('used_transfer_spec_hash');
13292
13530
  * assertKitKey(kitKey)
13293
13531
  * ```
13294
13532
  */ function assertKitKey(kitKey) {
13295
- // Validate API key format using existing schema from service-client
13533
+ // Permissionless mode: the swap service treats the kit key as optional, so an
13534
+ // absent (or empty) key is valid. Only validate the format when a key is
13535
+ // actually provided.
13296
13536
  if (!kitKey) {
13297
- 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');
13537
+ return;
13298
13538
  }
13299
13539
  const apiKeyResult = apiKeySchema.safeParse(kitKey);
13300
13540
  if (!apiKeyResult.success) {
@@ -13591,8 +13831,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13591
13831
  validateResolvedAddress(resolvedTokenInAddress, chain);
13592
13832
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
13593
13833
  validateResolvedAddress(to, destinationChain);
13594
- const kitKey = config?.kitKey ?? '';
13595
- // Validates the kit key
13834
+ const kitKey = config?.kitKey;
13835
+ // Validate the kit key format when one is provided (permissionless otherwise).
13596
13836
  assertKitKey(kitKey);
13597
13837
  // Validate custom fee configuration if present
13598
13838
  const customFee = config?.customFee;
@@ -13637,7 +13877,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13637
13877
  }
13638
13878
  }
13639
13879
  },
13640
- apiKey: kitKey
13880
+ // Map kitKey → apiKey for the service client; omitted in permissionless mode.
13881
+ ...kitKey ? {
13882
+ apiKey: kitKey
13883
+ } : {}
13641
13884
  };
13642
13885
  }
13643
13886
 
@@ -14291,6 +14534,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14291
14534
  }
14292
14535
  }
14293
14536
 
14537
+ /**
14538
+ * Determine whether an adapter can produce an EIP-2612 permit signature.
14539
+ *
14540
+ * @remarks
14541
+ * A gasless permit needs two adapter capabilities: fetching the token's
14542
+ * EIP-2612 nonce and producing an EIP-712 typed-data signature. The
14543
+ * typed-data check uses {@link canSignTypedData} rather than a bare
14544
+ * `hasSignTypedData` guard so that an adapter routed through a signing
14545
+ * strategy that cannot produce typed-data signatures — one whose manifest
14546
+ * omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
14547
+ * — is correctly excluded. Such an adapter falls back to an on-chain approval
14548
+ * (batched into a single submission when it supports atomic execution) instead
14549
+ * of attempting a permit its strategy would reject.
14550
+ *
14551
+ * @param adapter - The source adapter to inspect.
14552
+ * @returns `true` when the adapter can both fetch a nonce and sign typed data.
14553
+ *
14554
+ * @example
14555
+ * ```typescript
14556
+ * import { adapterSupportsPermit } from './utils'
14557
+ *
14558
+ * if (adapterSupportsPermit(adapter)) {
14559
+ * // gasless permit path — fold the approval into the swap transaction
14560
+ * } else {
14561
+ * // on-chain approval path (batched when supportsAtomicBatch is true)
14562
+ * }
14563
+ * ```
14564
+ */ function adapterSupportsPermit(adapter) {
14565
+ return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
14566
+ }
14567
+
14294
14568
  /**
14295
14569
  * Generate EIP-2612 permit signature for token approval.
14296
14570
  *
@@ -14426,8 +14700,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14426
14700
  }
14427
14701
  // Skip permit generation if the adapter lacks the required capabilities.
14428
14702
  // handleEvmTokenApproval will have already sent an on-chain approval in this case.
14429
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
14430
- if (!adapterSupportsPermit) {
14703
+ if (!adapterSupportsPermit(adapter)) {
14431
14704
  return [
14432
14705
  createFallbackTokenInput(tokenInAddress, inputAmount)
14433
14706
  ];
@@ -14986,6 +15259,65 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
14986
15259
  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.`;
14987
15260
  }
14988
15261
 
15262
+ /**
15263
+ * Determine which chain a fee token should be resolved and formatted against.
15264
+ *
15265
+ * @remarks
15266
+ * Fees returned by the service may be denominated in either the input token
15267
+ * (on the source chain) or the output token (on the destination chain). A
15268
+ * contract address only resolves on the chain it belongs to, so formatting a
15269
+ * destination-denominated fee against the source chain causes
15270
+ * {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
15271
+ * units (e.g. a cross-chain swap charging a fee in the destination output
15272
+ * token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
15273
+ * resolved against the source chain). This is the fallback for fee items that
15274
+ * are not self-described with their own `decimals`/`chain`.
15275
+ *
15276
+ * Prefer the source chain (covers same-chain swaps and input-denominated
15277
+ * fees), then fall back to the destination chain when the token only resolves
15278
+ * there. When neither chain recognises the token, default to the source chain
15279
+ * so existing on-chain decimal lookups via the source adapter still apply.
15280
+ *
15281
+ * Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
15282
+ * source-first preference keeps them on the source chain. That is correct for
15283
+ * registry stablecoins, and for `'NATIVE'` only when both chains share native
15284
+ * decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
15285
+ * `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
15286
+ * mis-scaled. This is latent — providers emit the address form, and
15287
+ * self-describing fee items carry their own `decimals` and never reach this
15288
+ * helper — so the gap only opens for a future `'NATIVE'` fee that arrives
15289
+ * without `decimals` on a cross-native-decimal route.
15290
+ *
15291
+ * @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
15292
+ * @param sourceChain - The chain the swap originates from.
15293
+ * @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
15294
+ * @returns The chain definition the fee token should be resolved against.
15295
+ *
15296
+ * @example
15297
+ * ```typescript
15298
+ * import { resolveFeeChain } from './resolveFeeChain'
15299
+ * import { Ethereum, Base } from '@core/chains'
15300
+ *
15301
+ * // Cross-chain swap fee charged in the destination (output) token
15302
+ * resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
15303
+ * // => Base (EURC resolves on Base, not Ethereum)
15304
+ *
15305
+ * // Symbol or source-token fees stay on the source chain
15306
+ * resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
15307
+ * ```
15308
+ */ function resolveFeeChain(token, sourceChain, destinationChain) {
15309
+ if (sourceChain.chain === destinationChain.chain) {
15310
+ return sourceChain;
15311
+ }
15312
+ if (resolveTokenSymbol(token, sourceChain) !== null) {
15313
+ return sourceChain;
15314
+ }
15315
+ if (resolveTokenSymbol(token, destinationChain) !== null) {
15316
+ return destinationChain;
15317
+ }
15318
+ return sourceChain;
15319
+ }
15320
+
14989
15321
  const TOKEN_REGISTRY$1 = createTokenRegistry();
14990
15322
  /**
14991
15323
  * Format a raw base-unit amount into a human-readable decimal string.
@@ -15076,6 +15408,186 @@ const TOKEN_REGISTRY$1 = createTokenRegistry();
15076
15408
  }
15077
15409
  }
15078
15410
 
15411
+ /**
15412
+ * Runtime guard for {@link BatchCapableSwapAdapter}.
15413
+ *
15414
+ * @param adapter - The adapter to inspect.
15415
+ * @returns `true` when the adapter exposes both batch methods.
15416
+ *
15417
+ * @example
15418
+ * ```typescript
15419
+ * if (isBatchCapableSwapAdapter(adapter)) {
15420
+ * // adapter.supportsAtomicBatch / adapter.batchExecute are available
15421
+ * }
15422
+ * ```
15423
+ */ function isBatchCapableSwapAdapter(adapter) {
15424
+ return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
15425
+ }
15426
+ /**
15427
+ * Decide whether the EVM swap should take the batched approve-and-swap path.
15428
+ *
15429
+ * @remarks
15430
+ * Batching only helps when an on-chain approval would otherwise be required, so
15431
+ * it is skipped for native tokens (no approval) and for the gasless permit path
15432
+ * (already a single transaction). USDT is skipped because its reset-to-zero
15433
+ * allowance flow cannot be expressed as a fixed approve+swap pair. When those
15434
+ * gates pass, the adapter's actual atomic-batch capability is queried; any
15435
+ * failure resolves to `false` so the swap falls back to the sequential path.
15436
+ *
15437
+ * @param args - The decision inputs.
15438
+ * @param args.adapter - The source adapter.
15439
+ * @param args.chain - The source chain definition.
15440
+ * @param args.tokenInAddress - The resolved input-token address.
15441
+ * @param args.allowanceStrategy - Optional allowance strategy override.
15442
+ * @param args.batchTransactions - Optional explicit opt-out (`false` disables).
15443
+ * @returns `true` when the batched approve-and-swap path should be used.
15444
+ *
15445
+ * @example
15446
+ * ```typescript
15447
+ * const useBatched = await shouldUseBatchedSwap({
15448
+ * adapter,
15449
+ * chain,
15450
+ * tokenInAddress: '0xA0b8...',
15451
+ * allowanceStrategy: config?.allowanceStrategy,
15452
+ * batchTransactions: config?.batchTransactions,
15453
+ * })
15454
+ * ```
15455
+ */ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
15456
+ // Explicit opt-out.
15457
+ if (batchTransactions === false) {
15458
+ return false;
15459
+ }
15460
+ // Batching is an EVM capability (EIP-5792 or a signing strategy).
15461
+ if (chain.type !== 'evm') {
15462
+ return false;
15463
+ }
15464
+ // Native tokens need no approval — the swap is already a single transaction.
15465
+ if (isNativeEvmAddress(tokenInAddress)) {
15466
+ return false;
15467
+ }
15468
+ // A gasless permit folds the approval into the swap transaction, so there is
15469
+ // nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
15470
+ const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
15471
+ if (canUsePermit) {
15472
+ return false;
15473
+ }
15474
+ // USDT's reset-to-zero allowance dance cannot be expressed as a fixed
15475
+ // approve+swap pair; leave it on the sequential path.
15476
+ const usdt = chain.usdtAddress?.toLowerCase();
15477
+ if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
15478
+ return false;
15479
+ }
15480
+ if (!isBatchCapableSwapAdapter(adapter)) {
15481
+ return false;
15482
+ }
15483
+ try {
15484
+ return await adapter.supportsAtomicBatch(chain);
15485
+ } catch {
15486
+ return false;
15487
+ }
15488
+ }
15489
+ /**
15490
+ * Execute the approval and swap as a single atomic batch.
15491
+ *
15492
+ * @remarks
15493
+ * Extracts the raw call data from both prepared requests, submits them as one
15494
+ * batch via `adapter.batchExecute`, and maps the swap receipt back to a
15495
+ * transaction hash. The `fromAddress` is threaded for adapters routed through a
15496
+ * signing strategy (which have no wallet account to read the sender from); the
15497
+ * wallet-client path ignores it.
15498
+ *
15499
+ * Following the batch contract, `batchExecute` never throws once the batch is
15500
+ * submitted — a missing or failed swap receipt is surfaced here as a thrown
15501
+ * {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
15502
+ * batch and double-swap.
15503
+ *
15504
+ * @param args - The execution inputs.
15505
+ * @param args.adapter - The batch-capable source adapter.
15506
+ * @param args.chain - The EVM chain to execute on.
15507
+ * @param args.approveRequest - The prepared ERC-20 approval request.
15508
+ * @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
15509
+ * @param args.fromAddress - The address authorizing the batch.
15510
+ * @returns The swap transaction hash and the executed approval + swap records.
15511
+ * @throws {@link KitError} when the prepared requests cannot yield call data.
15512
+ * @throws {@link KitError} when the batch does not confirm or the swap reverts.
15513
+ *
15514
+ * @example
15515
+ * ```typescript
15516
+ * const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
15517
+ * adapter,
15518
+ * chain,
15519
+ * approveRequest,
15520
+ * swapRequest,
15521
+ * fromAddress: '0x742d...',
15522
+ * })
15523
+ * ```
15524
+ */ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
15525
+ if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
15526
+ throw new KitError({
15527
+ ...InputError.UNSUPPORTED_ACTION,
15528
+ recoverability: 'FATAL',
15529
+ message: 'Batched swap requires EVM prepared requests with getCallData() support.'
15530
+ });
15531
+ }
15532
+ const approveCallData = approveRequest.getCallData();
15533
+ const swapCallData = swapRequest.getCallData();
15534
+ const batchResult = await adapter.batchExecute([
15535
+ approveCallData,
15536
+ swapCallData
15537
+ ], chain, {
15538
+ fromAddress
15539
+ });
15540
+ const swapReceipt = batchResult.receipts[1];
15541
+ // A missing swap receipt means the batch never confirmed (polling timed out
15542
+ // or the wallet returned fewer receipts than calls). Re-throw the underlying
15543
+ // error when present (already FATAL); otherwise surface a FATAL timeout so the
15544
+ // caller checks the batch status rather than resubmitting.
15545
+ if (swapReceipt === undefined || swapReceipt.txHash === '') {
15546
+ if (isKitError(batchResult.error)) {
15547
+ throw batchResult.error;
15548
+ }
15549
+ throw new KitError({
15550
+ ...NetworkError.TIMEOUT,
15551
+ recoverability: 'FATAL',
15552
+ message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
15553
+ // Preserve the underlying confirmation failure when it isn't a KitError —
15554
+ // the signing-strategy path returns a raw viem error (e.g. a dropped or
15555
+ // replaced tx) — so the root cause survives behind the generic timeout.
15556
+ cause: {
15557
+ trace: {
15558
+ batchId: batchResult.batchId,
15559
+ ...batchResult.error != null && {
15560
+ error: batchResult.error
15561
+ }
15562
+ }
15563
+ }
15564
+ });
15565
+ }
15566
+ if (swapReceipt.status !== 'success') {
15567
+ throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
15568
+ }
15569
+ const executedTransactions = [];
15570
+ const approveReceipt = batchResult.receipts[0];
15571
+ // An atomic batch is a single on-chain transaction, so the approve and swap
15572
+ // receipts share one hash. Only surface a distinct approval record when it is
15573
+ // genuinely a separate transaction; otherwise the lone swap record represents
15574
+ // the batch, avoiding a phantom duplicate tx in executedTransactions.
15575
+ if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
15576
+ executedTransactions.push({
15577
+ type: 'approval',
15578
+ txHash: approveReceipt.txHash
15579
+ });
15580
+ }
15581
+ executedTransactions.push({
15582
+ type: 'swap',
15583
+ txHash: swapReceipt.txHash
15584
+ });
15585
+ return {
15586
+ swapTxHash: swapReceipt.txHash,
15587
+ executedTransactions
15588
+ };
15589
+ }
15590
+
15079
15591
  /**
15080
15592
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
15081
15593
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -15202,7 +15714,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15202
15714
  const statusResult = await getSwapStatus$1({
15203
15715
  txHash,
15204
15716
  chain: chain.chain,
15205
- apiKey
15717
+ ...apiKey !== undefined && {
15718
+ apiKey
15719
+ }
15206
15720
  });
15207
15721
  if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
15208
15722
  return {
@@ -15793,8 +16307,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15793
16307
  // Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
15794
16308
  const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
15795
16309
  const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
15796
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
15797
- const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
16310
+ const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
15798
16311
  const needsApproval = !isNativeToken && !canUsePermitFlow;
15799
16312
  if (!needsApproval) {
15800
16313
  return;
@@ -16073,34 +16586,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16073
16586
  const serviceResponse = await createSwap(serviceParams);
16074
16587
  // Track executed transactions
16075
16588
  const executedTransactions = [];
16076
- // Prepare swap action based on chain type
16077
- let preparedAction;
16078
- if (chain.type === 'solana') {
16079
- // Solana: No approval needed, directly prepare swap action
16080
- preparedAction = await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext);
16081
- } else {
16082
- // EVM chains: Handle token approval if needed
16083
- await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16084
- // EVM chains: prepareEvmSwapAction handles EIP-2612 permit generation
16085
- // Adapter contract address is read from chain.kitContracts.adapter
16086
- // Use the already-resolved context from above
16087
- preparedAction = await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy);
16088
- }
16589
+ // Prepare swap action(s) based on chain type and batch capability.
16590
+ // Returns either a single prepared action (Solana / sequential EVM) or a
16591
+ // batched approve+swap plan (EVM atomic-batch path).
16592
+ const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
16593
+ adapter,
16594
+ chain,
16595
+ serviceResponse,
16596
+ resolvedContext,
16597
+ executionCtx,
16598
+ config,
16599
+ executedTransactions
16600
+ });
16089
16601
  // Execute swap transaction via adapter
16090
16602
  // For EVM chains, use gas limit from proxy service API
16091
16603
  let txHash;
16092
16604
  const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
16093
16605
  try {
16094
- txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16095
- executedTransactions.push({
16096
- type: 'swap',
16097
- txHash
16098
- });
16099
- // Wait for transaction confirmation and verify success
16100
- const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16101
- if (txReceipt.status === 'reverted') {
16102
- const explorerUrl = buildExplorerUrl(chain, txHash);
16103
- throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16606
+ if (batchedSwapPlan) {
16607
+ // Approve + swap submitted as one atomic batch. batchExecute confirms
16608
+ // the swap internally, so no separate waitForTransaction is needed.
16609
+ const batched = await executeBatchedApproveAndSwap({
16610
+ adapter: adapter,
16611
+ chain: chain,
16612
+ approveRequest: batchedSwapPlan.approveRequest,
16613
+ swapRequest: batchedSwapPlan.swapRequest,
16614
+ fromAddress: executionCtx.fromAddress
16615
+ });
16616
+ txHash = batched.swapTxHash;
16617
+ executedTransactions.push(...batched.executedTransactions);
16618
+ } else if (preparedAction) {
16619
+ txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16620
+ executedTransactions.push({
16621
+ type: 'swap',
16622
+ txHash
16623
+ });
16624
+ // Wait for transaction confirmation and verify success
16625
+ const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16626
+ if (txReceipt.status === 'reverted') {
16627
+ const explorerUrl = buildExplorerUrl(chain, txHash);
16628
+ throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16629
+ }
16630
+ } else {
16631
+ // Unreachable: the preparation step always yields either a batched plan
16632
+ // or a prepared action.
16633
+ throw new KitError({
16634
+ ...InputError.UNSUPPORTED_ACTION,
16635
+ recoverability: 'FATAL',
16636
+ message: 'No swap execution path was prepared.'
16637
+ });
16104
16638
  }
16105
16639
  } catch (err) {
16106
16640
  handleSwapExecutionError(err, txHash, chain);
@@ -16119,8 +16653,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16119
16653
  isCrossChainSwap,
16120
16654
  txHash,
16121
16655
  chain,
16122
- apiKey: serviceParams.apiKey
16656
+ ...serviceParams.apiKey !== undefined && {
16657
+ apiKey: serviceParams.apiKey
16658
+ }
16123
16659
  });
16660
+ // Per-swap correlation id returned by the service as a top-level response
16661
+ // field for every chain (EVM + Solana). Attached to success telemetry so a
16662
+ // swap can be correlated across records; never used for control flow.
16663
+ // Undefined only against a not-yet-upgraded service that omits it.
16664
+ const correlationId = serviceResponse.correlationId;
16124
16665
  // Build and return SwapResult
16125
16666
  return {
16126
16667
  tokenIn,
@@ -16130,6 +16671,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16130
16671
  fromAddress: serviceParams.fromAddress,
16131
16672
  toAddress: serviceParams.toAddress,
16132
16673
  txHash,
16674
+ ...correlationId !== undefined && {
16675
+ correlationId
16676
+ },
16133
16677
  executedTransactions,
16134
16678
  ...config !== undefined && {
16135
16679
  config
@@ -16144,6 +16688,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16144
16688
  };
16145
16689
  }
16146
16690
  /**
16691
+ * Prepare the swap execution request(s) for the source wallet's chain.
16692
+ *
16693
+ * Produces either a single {@link PreparedChainRequest} (Solana, or the
16694
+ * sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
16695
+ * atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
16696
+ * batching). The caller executes whichever field is populated. Any on-chain
16697
+ * approval sent on the sequential path is appended to `executedTransactions`.
16698
+ *
16699
+ * @typeParam TFromAdapterCapabilities - Source-adapter capability set.
16700
+ * @param args - Inputs derived from the validated swap request.
16701
+ * @param args.adapter - Source-chain wallet adapter.
16702
+ * @param args.chain - Source chain definition.
16703
+ * @param args.serviceResponse - Validated createSwap response.
16704
+ * @param args.resolvedContext - Resolved operation context.
16705
+ * @param args.executionCtx - Minimal on-chain execution context.
16706
+ * @param args.config - Optional swap configuration (allowance/batch flags).
16707
+ * @param args.executedTransactions - Array appended with any sent approval.
16708
+ * @returns The prepared action or the batched approve+swap plan.
16709
+ * @throws KitError when the EVM atomic-batch path is selected but the chain
16710
+ * has no configured adapter contract.
16711
+ */ async prepareSwapRequests(args) {
16712
+ const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
16713
+ if (chain.type === 'solana') {
16714
+ // Solana: No approval needed, directly prepare swap action
16715
+ return {
16716
+ preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
16717
+ };
16718
+ }
16719
+ const useBatch = await shouldUseBatchedSwap({
16720
+ adapter,
16721
+ chain,
16722
+ tokenInAddress: executionCtx.tokenInAddress,
16723
+ allowanceStrategy: config?.allowanceStrategy,
16724
+ batchTransactions: config?.batchTransactions
16725
+ });
16726
+ if (useBatch) {
16727
+ // EVM chains: fuse the ERC-20 approval and the swap into a single atomic
16728
+ // batch (one signing challenge for smart-contract wallets). Force the
16729
+ // swap onto the pre-approval (PermitType.NONE) path since the approval
16730
+ // rides in the same batch.
16731
+ const adapterContractAddress = chain.kitContracts?.adapter;
16732
+ if (!adapterContractAddress) {
16733
+ throw new KitError({
16734
+ ...InputError.VALIDATION_FAILED,
16735
+ recoverability: 'FATAL',
16736
+ message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
16737
+ cause: {
16738
+ trace: {
16739
+ chain: chain.name
16740
+ }
16741
+ }
16742
+ });
16743
+ }
16744
+ const [approveRequest, swapRequest] = await Promise.all([
16745
+ this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
16746
+ prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
16747
+ ]);
16748
+ return {
16749
+ batchedSwapPlan: {
16750
+ approveRequest,
16751
+ swapRequest
16752
+ }
16753
+ };
16754
+ }
16755
+ // EVM chains: Handle token approval if needed, then prepare the swap.
16756
+ // prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
16757
+ // contract address is read from chain.kitContracts.adapter.
16758
+ await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16759
+ return {
16760
+ preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
16761
+ };
16762
+ }
16763
+ /**
16147
16764
  * Executes a swap transaction with the appropriate gas limit for the chain type.
16148
16765
  *
16149
16766
  * For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
@@ -16192,8 +16809,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16192
16809
  */ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
16193
16810
  if (!fees) return [];
16194
16811
  const [providerFees, swapFees, developerFees] = await Promise.all([
16195
- this.formatServiceFees(fees.provider, chain, 'provider', adapter),
16196
- this.formatServiceFees(fees.swap, chain, 'swap', adapter),
16812
+ this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
16813
+ this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
16197
16814
  recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
16198
16815
  ]);
16199
16816
  return [
@@ -16203,6 +16820,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16203
16820
  ];
16204
16821
  }
16205
16822
  /**
16823
+ * Resolve a single fee item to its display token and human-readable amount.
16824
+ *
16825
+ * @remarks
16826
+ * Prefer the self-describing metadata the service attaches to each fee:
16827
+ * `decimals` (and `symbol`) come straight from the provider quote, so they
16828
+ * are authoritative even for a token absent from the SDK registry on both
16829
+ * chains. That is the case {@link resolveFeeChain} cannot recover — a
16830
+ * destination-denominated fee token resolves on neither the source registry
16831
+ * nor the source-bound adapter, leaving the amount as raw base units. When
16832
+ * the service omits `decimals` (optional during rollout), fall back to
16833
+ * inferring the fee token's chain and resolving via the registry/adapter.
16834
+ *
16835
+ * Like {@link formatTokenValue}, this never throws: fee display is cosmetic
16836
+ * and must not fail an estimate/swap. A malformed self-describing `decimals`
16837
+ * (e.g. a non-numeric `amount` or invalid decimal count that makes
16838
+ * {@link formatUnits} throw) falls through to chain-based resolution rather
16839
+ * than propagating out of {@link buildFormattedFees}.
16840
+ *
16841
+ * @param fee - The fee item from the service response.
16842
+ * @param chain - The source chain definition.
16843
+ * @param destinationChain - The destination chain definition.
16844
+ * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16845
+ * @returns Promise resolving to the formatted amount and display token.
16846
+ */ async formatFeeValue(fee, chain, destinationChain, adapter) {
16847
+ if (fee.decimals != null) {
16848
+ try {
16849
+ return {
16850
+ amount: formatUnits(fee.amount, fee.decimals),
16851
+ token: fee.symbol ?? fee.token
16852
+ };
16853
+ } catch {
16854
+ // Malformed service metadata — fall through to chain-based resolution,
16855
+ // which never throws (worst case: raw passthrough).
16856
+ }
16857
+ }
16858
+ const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
16859
+ return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16860
+ }
16861
+ /**
16206
16862
  * Format service fee items into the SDK's ServiceSwapFee structure.
16207
16863
  *
16208
16864
  * @remarks
@@ -16213,14 +16869,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16213
16869
  * - Raw passthrough only when both registry and adapter fail
16214
16870
  *
16215
16871
  * @param feeItems - Array of fee items from the service response.
16216
- * @param chain - The chain definition for token resolution and formatting.
16872
+ * @param chain - The source chain definition for token resolution and formatting.
16873
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16217
16874
  * @param type - The fee type to assign ('provider' or 'swap').
16218
16875
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16219
16876
  * @returns Promise resolving to formatted ServiceSwapFee array.
16220
- */ async formatServiceFees(feeItems, chain, type, adapter) {
16877
+ */ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
16221
16878
  if (!feeItems) return [];
16222
16879
  return Promise.all(feeItems.map(async (fee)=>{
16223
- const formatted = await formatTokenValue(fee.amount, fee.token, chain, adapter);
16880
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16224
16881
  return {
16225
16882
  token: formatted.token,
16226
16883
  amount: formatted.amount,
@@ -16232,16 +16889,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16232
16889
  * Format developer fee items into the SDK's ServiceSwapFee structure.
16233
16890
  *
16234
16891
  * @param feeItems - Array of developer fee items from the service response.
16235
- * @param chain - The chain definition for token resolution and formatting.
16892
+ * @param chain - The source chain definition for token resolution and formatting.
16893
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16236
16894
  * @param recipientAddress - The developer's fee recipient address from config.
16237
16895
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16238
16896
  * @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
16239
16897
  */ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
16240
16898
  if (!feeItems) return [];
16241
- const isCrossChainSwap = destinationChain.chain !== chain.chain;
16242
16899
  return Promise.all(feeItems.map(async (fee)=>{
16243
- const feeChain = !isCrossChainSwap && fee.basis === 'estimatedAmount' ? destinationChain : chain;
16244
- const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16900
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16245
16901
  return {
16246
16902
  token: formatted.token,
16247
16903
  amount: formatted.amount,
@@ -18636,22 +19292,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18636
19292
  try {
18637
19293
  // Step 1: Build quote params directly (no need for buildServiceParams)
18638
19294
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
19295
+ // The kit key is optional (permissionless mode); when absent the quote is
19296
+ // fetched without an Authorization header.
18639
19297
  const kitKey = params.config?.kitKey;
18640
- if (!kitKey) {
18641
- throw new KitError({
18642
- code: 1098,
18643
- name: 'INPUT_VALIDATION_FAILED',
18644
- type: 'INPUT',
18645
- recoverability: 'FATAL',
18646
- message: 'kitKey is required in config for callback-based fees',
18647
- cause: {
18648
- trace: {
18649
- operation: 'handleOutputFeeCallback',
18650
- params
18651
- }
18652
- }
18653
- });
18654
- }
18655
19298
  // Resolve token aliases to addresses for the quote API
18656
19299
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
18657
19300
  const chain = params.from.chain;
@@ -18676,7 +19319,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18676
19319
  ...params.config?.slippageBps !== undefined && {
18677
19320
  slippageBps: params.config.slippageBps
18678
19321
  },
18679
- apiKey: kitKey
19322
+ ...kitKey ? {
19323
+ apiKey: kitKey
19324
+ } : {}
18680
19325
  };
18681
19326
  // Step 2: Get quote from service
18682
19327
  const quoteResponse = await getQuote(quoteParams);
@@ -18928,7 +19573,13 @@ async function resolveTokenDecimals(context, resolvedParams) {
18928
19573
  * amountIn: '50.00'
18929
19574
  * })
18930
19575
  * ```
18931
- */ async function swap$1(context, params, /** @internal */ onBroadcast) {
19576
+ */ async function swap$1(context, params, /**
19577
+ * @internal
19578
+ * Invoked after a successful broadcast with the on-chain `txHash` and the
19579
+ * service-issued `correlationId` (join key for success telemetry). The
19580
+ * service returns `correlationId` for every chain (EVM + Solana); it is
19581
+ * undefined only against a not-yet-upgraded service that omits the field.
19582
+ */ onBroadcast) {
18932
19583
  // Step 1: Validate parameters using schema
18933
19584
  assertSwapParams(params, swapParamsSchema);
18934
19585
  // Step 2: Resolve parameters (chain definitions, wallet addresses, token aliases preserved)
@@ -18948,13 +19599,16 @@ async function resolveTokenDecimals(context, resolvedParams) {
18948
19599
  // Step 5: Execute swap via provider
18949
19600
  const swapParams = buildServiceSwapParams(resolvedParams, paramsWithFees);
18950
19601
  const providerResult = await provider.swap(swapParams);
19602
+ // `correlationId` is an internal telemetry join key, not part of the public
19603
+ // SwapResult — strip it here so it never leaks into the formatted result.
19604
+ const { correlationId, ...providerResultPublic } = providerResult;
18951
19605
  // A throwing `onBroadcast` must never strand the caller after a
18952
19606
  // successful swap broadcast — the chain has moved. `safeInvokeCallback`
18953
19607
  // swallows the error and surfaces a `console.warn` prefixed with
18954
19608
  // `[stablecoin-kits swap-kit] callback threw and was swallowed:` so a
18955
19609
  // kit-side closure bug stays debuggable rather than vanishing.
18956
19610
  safeInvokeCallback('swap-kit', ()=>{
18957
- onBroadcast?.(providerResult.txHash);
19611
+ onBroadcast?.(providerResultPublic.txHash, correlationId);
18958
19612
  });
18959
19613
  const { tokenInDecimals, tokenOutDecimals } = await resolveTokenDecimals(context, resolvedParams);
18960
19614
  // Step 6: Compose chain identity (owned by the kit, derived from the
@@ -18963,10 +19617,10 @@ async function resolveTokenDecimals(context, resolvedParams) {
18963
19617
  // resolves `progress` — the provider's snapshot, or a terminal `'DONE'` for
18964
19618
  // a provider that omits it (a synchronous same-chain completion).
18965
19619
  const composedResult = {
18966
- ...providerResult,
19620
+ ...providerResultPublic,
18967
19621
  chainIn: resolvedParams.from.chain,
18968
19622
  chainOut: resolvedParams.toChain ?? resolvedParams.from.chain,
18969
- progress: providerResult.progress ?? {
19623
+ progress: providerResultPublic.progress ?? {
18970
19624
  status: 'DONE'
18971
19625
  }
18972
19626
  };
@@ -19118,7 +19772,9 @@ const sleep$1 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
19118
19772
  ...isCrossChain && {
19119
19773
  toChain: chainOut
19120
19774
  },
19121
- apiKey: params.kitKey
19775
+ ...params.kitKey ? {
19776
+ apiKey: params.kitKey
19777
+ } : {}
19122
19778
  };
19123
19779
  let raw = await getSwapStatus$1(request);
19124
19780
  // When the service hasn't finished indexing a just-submitted swap it
@@ -19258,7 +19914,9 @@ const isResultShape = (params)=>'result' in params;
19258
19914
  ...chainOut !== undefined && {
19259
19915
  chainOut
19260
19916
  },
19261
- kitKey: params.kitKey
19917
+ ...params.kitKey ? {
19918
+ kitKey: params.kitKey
19919
+ } : {}
19262
19920
  };
19263
19921
  const deadline = Date.now() + timeoutMs;
19264
19922
  let pollIndex = 0;
@@ -19419,7 +20077,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19419
20077
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
19420
20078
  return getTokenRates$1({
19421
20079
  chain,
19422
- apiKey: params.kitKey,
20080
+ ...params.kitKey ? {
20081
+ apiKey: params.kitKey
20082
+ } : {},
19423
20083
  ...resolvedAddresses !== undefined && {
19424
20084
  addresses: resolvedAddresses
19425
20085
  }
@@ -19813,7 +20473,13 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19813
20473
  */ class SwapKit {
19814
20474
  context;
19815
20475
  /** Whether error telemetry is disabled. */ disableErrorReporting;
19816
- /** Per-kit telemetry identity for shared helpers. */ telemetryConfig;
20476
+ /** Per-kit telemetry identity for error reporting. */ telemetryConfig;
20477
+ /**
20478
+ * Per-kit telemetry identity for success/analytics events. Gated by
20479
+ * `disableAnalytics` (independent of `disableErrorReporting`) so a developer
20480
+ * can opt out of volume analytics without also silencing error reports —
20481
+ * mirrors EarnKit.
20482
+ */ analyticsTelemetryConfig;
19817
20483
  /**
19818
20484
  * Create a new SwapKit instance.
19819
20485
  *
@@ -19867,6 +20533,11 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19867
20533
  sdkVersion: pkg$1.version,
19868
20534
  disabled: this.disableErrorReporting
19869
20535
  };
20536
+ this.analyticsTelemetryConfig = {
20537
+ sdkName: SDK_NAME,
20538
+ sdkVersion: pkg$1.version,
20539
+ disabled: config.disableAnalytics === true
20540
+ };
19870
20541
  }
19871
20542
  /**
19872
20543
  * Estimate the output amount and fees for a swap operation.
@@ -19907,8 +20578,12 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19907
20578
  * console.log(`Fees:`, quote.fees)
19908
20579
  * ```
19909
20580
  */ async estimate(params) {
20581
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
19910
20582
  return withErrorTelemetry(async ()=>estimate(this.context, params), SWAP_EVENT_TYPES.ESTIMATE, this.telemetryConfig, {
19911
20583
  sourceChain: resolveChainName(params.from.chain),
20584
+ ...destinationChain != null && {
20585
+ destinationChain
20586
+ },
19912
20587
  tokenIn: params.tokenIn,
19913
20588
  tokenOut: params.tokenOut
19914
20589
  });
@@ -19967,16 +20642,54 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19967
20642
  * ```
19968
20643
  */ async swap(params) {
19969
20644
  let txHash;
19970
- return withErrorTelemetry(async ()=>swap$1(this.context, params, (h)=>{
19971
- txHash = h;
19972
- }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, ()=>({
20645
+ let correlationId;
20646
+ // Shared context builder so the error resolver and the success emit stay in
20647
+ // lockstep — a field added here reaches both call sites. Reads the per-call
20648
+ // locals lazily, so txHash/correlationId (set during the swap) are captured
20649
+ // whenever it is invoked.
20650
+ // Destination chain is the primary attribution dimension for cross-chain
20651
+ // swaps; resolved once from the (static) params. Omitted for same-chain
20652
+ // swaps that leave `to.chain` unset (destination == source).
20653
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
20654
+ const buildTelemetryContext = ()=>({
19973
20655
  sourceChain: resolveChainName(params.from.chain),
20656
+ ...destinationChain != null && {
20657
+ destinationChain
20658
+ },
19974
20659
  tokenIn: params.tokenIn,
19975
20660
  tokenOut: params.tokenOut,
19976
20661
  ...txHash != null && {
19977
20662
  txHash
20663
+ },
20664
+ ...correlationId != null && {
20665
+ correlationId
19978
20666
  }
19979
- }));
20667
+ });
20668
+ const result = await withErrorTelemetry(async ()=>swap$1(this.context, params, (h, cId)=>{
20669
+ txHash = h;
20670
+ correlationId = cId;
20671
+ }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, buildTelemetryContext);
20672
+ // withErrorTelemetry only emits on failure. Record the successful swap here
20673
+ // so the backend can attribute swap volume to a developer: the client event
20674
+ // carries the (burn) txHash + correlationId, which joins to the
20675
+ // server-emitted event carrying entity_id. Best-effort; never throws.
20676
+ //
20677
+ // Emit whenever a broadcast happened, i.e. we have the source/burn txHash.
20678
+ // For a cross-chain swap that is the source-chain burn (progress is still
20679
+ // PENDING while the destination mint settles) — we intentionally attribute
20680
+ // at broadcast using the burn txHash rather than tracking the destination
20681
+ // leg, which keeps the capture simple and self-contained in swap().
20682
+ //
20683
+ // Guard against a terminal-failure result: the EVM provider throws on
20684
+ // revert today, but the kit is provider-agnostic, so a provider that
20685
+ // returns a FAILED/NOT_FOUND result without throwing must not be recorded
20686
+ // as a successful swap. Routed through analyticsTelemetryConfig so it is
20687
+ // gated by disableAnalytics, independent of error reporting.
20688
+ const status = result.progress?.status;
20689
+ if (txHash != null && status !== 'FAILED' && status !== 'NOT_FOUND') {
20690
+ emitSuccessTelemetry(SWAP_EVENT_TYPES.SWAP, this.analyticsTelemetryConfig, buildTelemetryContext());
20691
+ }
20692
+ return result;
19980
20693
  }
19981
20694
  /**
19982
20695
  * Fetch the current status of a swap from the Stablecoin Service.
@@ -20355,6 +21068,9 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20355
21068
  const kit = new SwapKit({
20356
21069
  ...context.disableErrorReporting != null && {
20357
21070
  disableErrorReporting: context.disableErrorReporting
21071
+ },
21072
+ ...context.disableAnalytics != null && {
21073
+ disableAnalytics: context.disableAnalytics
20358
21074
  }
20359
21075
  });
20360
21076
  if (hasBoth) {
@@ -20416,7 +21132,7 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20416
21132
  };
20417
21133
 
20418
21134
  var name = "@circle-fin/earn-kit";
20419
- var version = "1.2.2";
21135
+ var version = "1.4.0";
20420
21136
  var pkg = {
20421
21137
  name: name,
20422
21138
  version: version};
@@ -20546,7 +21262,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20546
21262
  asset: zod.z.string(),
20547
21263
  assetAddress: zod.z.string(),
20548
21264
  lltv: zod.z.number(),
20549
- supplyUsd: zod.z.number()
21265
+ supplyUsd: zod.z.number(),
21266
+ // Optional during the expand/contract window (a backend that predates the
21267
+ // field omits the key), mirroring the `.optional()` facets on the base
21268
+ // schema; `null` when the product exposes no per-market allocation (V2).
21269
+ allocationPct: zod.z.number().nullable().optional()
20550
21270
  });
20551
21271
  /**
20552
21272
  * Zod schema for a Morpho vault warning in the API response.
@@ -20560,7 +21280,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20560
21280
  ])
20561
21281
  });
20562
21282
  /**
20563
- * Zod schema for a single vault info object in the API response.
21283
+ * Zod schema for the manager (curator) facet in the API response.
21284
+ *
21285
+ * @internal
21286
+ */ const managerSchema = zod.z.object({
21287
+ name: zod.z.string(),
21288
+ address: zod.z.string().optional(),
21289
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
21290
+ // are added here as the providers that emit them land, rather than shipped
21291
+ // speculatively.
21292
+ type: zod.z.enum([
21293
+ 'curator'
21294
+ ])
21295
+ });
21296
+ /**
21297
+ * Zod schema for the APY profile facet in the API response.
21298
+ *
21299
+ * @internal
21300
+ */ const apyProfileSchema = zod.z.object({
21301
+ current: zod.z.number(),
21302
+ native: zod.z.number().nullable(),
21303
+ d7: zod.z.number().nullable(),
21304
+ d30: zod.z.number().nullable(),
21305
+ d90: zod.z.number().nullable(),
21306
+ rewardShare: zod.z.number().nullable(),
21307
+ source: zod.z.string().optional(),
21308
+ asOf: zod.z.string().optional()
21309
+ });
21310
+ /**
21311
+ * Zod schema for the fee split facet in the API response.
21312
+ *
21313
+ * @internal
21314
+ */ const feeInfoSchema = zod.z.object({
21315
+ performance: zod.z.number().nullable(),
21316
+ management: zod.z.number().nullable()
21317
+ });
21318
+ /**
21319
+ * Zod schema for the liquidity profile facet in the API response.
21320
+ *
21321
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
21322
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
21323
+ *
21324
+ * @internal
21325
+ */ const liquidityProfileSchema = zod.z.object({
21326
+ totalDeposits: amountJsonSchema,
21327
+ available: amountJsonSchema,
21328
+ totalSupply: amountJsonSchema,
21329
+ status: zod.z.enum([
21330
+ 'active',
21331
+ 'low_liquidity'
21332
+ ])
21333
+ });
21334
+ /**
21335
+ * Zod schema for the risk signals facet in the API response.
21336
+ *
21337
+ * @internal
21338
+ */ const riskSignalsSchema = zod.z.object({
21339
+ circleSentinel: zod.z.boolean(),
21340
+ warnings: zod.z.array(vaultWarningSchema).optional(),
21341
+ earnKitWarnings: zod.z.array(zod.z.string()).optional()
21342
+ });
21343
+ /**
21344
+ * Zod schema for the universal earn-opportunity base in the API response.
21345
+ *
21346
+ * Retains every existing deprecated flat field (kept validated through the
21347
+ * expand/contract window so default-strip does not drop them) and adds the
21348
+ * new nested facets. The nested facets are `.optional()` during the
21349
+ * transition so the SDK still validates against a not-yet-fully-deployed
21350
+ * backend; they become required after Expand ships.
20564
21351
  *
20565
21352
  * @internal
20566
21353
  */ const vaultInfoResponseSchema = zod.z.object({
@@ -20585,6 +21372,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20585
21372
  warnings: zod.z.array(vaultWarningSchema).optional(),
20586
21373
  earnKitWarnings: zod.z.array(zod.z.string()).optional()
20587
21374
  });
21375
+ /**
21376
+ * Shared base schema: existing flat fields (kept) plus the new nested
21377
+ * facets and neutral identity. Facets are `.optional()` during the
21378
+ * transition; flip to required once the backend is confirmed emitting.
21379
+ *
21380
+ * @internal
21381
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
21382
+ address: zod.z.string().optional(),
21383
+ asOf: zod.z.string().optional(),
21384
+ manager: managerSchema.nullable().optional(),
21385
+ apyProfile: apyProfileSchema.optional(),
21386
+ fee: feeInfoSchema.optional(),
21387
+ liquidityProfile: liquidityProfileSchema.optional(),
21388
+ riskSignals: riskSignalsSchema.optional()
21389
+ });
21390
+ /**
21391
+ * Zod schema for the `vault` opportunity variant.
21392
+ *
21393
+ * @internal
21394
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
21395
+ productType: zod.z.literal('vault'),
21396
+ collateral: zod.z.array(collateralSchema)
21397
+ });
21398
+ /**
21399
+ * Discriminated union over `productType`. Add union members here as new
21400
+ * product types (e.g. `lending_market`, `rwa_token`) land.
21401
+ *
21402
+ * @internal
21403
+ */ const earnOpportunityVariants = [
21404
+ vaultOpportunitySchema
21405
+ ];
21406
+ /** @internal */ const earnOpportunitySchema = zod.z.discriminatedUnion('productType', earnOpportunityVariants);
21407
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
21408
+ /**
21409
+ * Tolerant list parser for earn opportunities.
21410
+ *
21411
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
21412
+ * `z.array` fails the whole array if any element fails. Two migration-window
21413
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
21414
+ *
21415
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
21416
+ * only opportunity type then, so default a missing discriminant to `'vault'`
21417
+ * rather than dropping every vault the backend returns.
21418
+ * - A future backend adds a *second* `productType` this SDK version does not
21419
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
21420
+ * of rejecting the whole list.
21421
+ *
21422
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
21423
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
21424
+ * primitives, or an object whose `productType` is malformed — is passed through
21425
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
21426
+ * validation failure. It is deliberately not silently dropped (which would hide
21427
+ * malformed backend data) and never throws here (an unguarded property read on
21428
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
21429
+ * `ZodError`).
21430
+ *
21431
+ * @internal
21432
+ */ const earnOpportunityListSchema = zod.z.preprocess((raw)=>{
21433
+ if (!Array.isArray(raw)) {
21434
+ return raw;
21435
+ }
21436
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
21437
+ // map/filter chain stays type-safe and no `any` leaks into the return.
21438
+ const entries = raw;
21439
+ return entries.map((entry)=>{
21440
+ // Only touch plain objects; non-objects fall through to fail validation.
21441
+ if (typeof entry !== 'object' || entry === null) {
21442
+ return entry;
21443
+ }
21444
+ const record = entry;
21445
+ // Older backend predating productType: default to the only type then.
21446
+ return record.productType === undefined ? {
21447
+ ...record,
21448
+ productType: 'vault'
21449
+ } : record;
21450
+ }).filter((entry)=>{
21451
+ // Drop ONLY a present-but-unknown string discriminant (a future
21452
+ // productType this SDK version doesn't know). Everything else —
21453
+ // non-objects, a non-string productType — flows through to
21454
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
21455
+ if (typeof entry !== 'object' || entry === null) {
21456
+ return true;
21457
+ }
21458
+ const productType = entry.productType;
21459
+ if (typeof productType !== 'string') {
21460
+ return true;
21461
+ }
21462
+ return knownProductTypes.has(productType);
21463
+ });
21464
+ }, zod.z.array(earnOpportunitySchema));
20588
21465
  // ---------------------------------------------------------------------------
20589
21466
  // Position response schema
20590
21467
  // ---------------------------------------------------------------------------
@@ -20714,6 +21591,7 @@ const positionPnlSchema = zod.z.discriminatedUnion('status', [
20714
21591
  *
20715
21592
  * @internal
20716
21593
  */ const depositPayloadSchema = zod.z.object({
21594
+ execId: bridgeDepositExecIdSchema,
20717
21595
  executionParams: depositExecutionParamsSchema,
20718
21596
  signature: hexSignatureSchema
20719
21597
  });
@@ -20805,6 +21683,21 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20805
21683
  amount: amountJsonSchema,
20806
21684
  vaultAddress: hexAddressSchema
20807
21685
  }).passthrough();
21686
+ /** @internal */ const bridgeQuoteExpirySchema = zod.z.discriminatedUnion('mode', [
21687
+ zod.z.object({
21688
+ mode: zod.z.literal('TIMESTAMP'),
21689
+ expiresAt: zod.z.string().datetime({
21690
+ offset: true
21691
+ })
21692
+ }),
21693
+ zod.z.object({
21694
+ mode: zod.z.literal('BLOCK_NUMBER'),
21695
+ expiresAtBlock: zod.z.number().int(),
21696
+ blockEstimatedAt: zod.z.string().datetime({
21697
+ offset: true
21698
+ }).optional()
21699
+ })
21700
+ ]).optional().catch(undefined);
20808
21701
  /**
20809
21702
  * Zod schema for the bridge deposit prepare payload.
20810
21703
  *
@@ -20816,6 +21709,10 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20816
21709
  execId: bridgeDepositExecIdSchema,
20817
21710
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
20818
21711
  expiresAt: zod.z.string().datetime(),
21712
+ quoteIssuedAt: zod.z.string().datetime({
21713
+ offset: true
21714
+ }).optional().catch(undefined),
21715
+ quoteExpiry: bridgeQuoteExpirySchema,
20819
21716
  review: bridgeDepositPrepareReviewSchema
20820
21717
  });
20821
21718
  /**
@@ -20881,6 +21778,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20881
21778
  *
20882
21779
  * @internal
20883
21780
  */ const withdrawPayloadSchema = zod.z.object({
21781
+ execId: bridgeDepositExecIdSchema,
20884
21782
  executionParams: withdrawExecutionParamsSchema,
20885
21783
  signature: hexSignatureSchema
20886
21784
  });
@@ -20894,6 +21792,27 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20894
21792
  data: withdrawPayloadSchema
20895
21793
  });
20896
21794
  // ---------------------------------------------------------------------------
21795
+ // Transaction report response schema
21796
+ // ---------------------------------------------------------------------------
21797
+ /**
21798
+ * Zod schema for the transaction report payload inside the API `data` envelope.
21799
+ *
21800
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
21801
+ * schema accepts any object shape and does not require specific fields.
21802
+ *
21803
+ * @internal
21804
+ */ const transactionReportPayloadSchema = zod.z.object({}).passthrough();
21805
+ /**
21806
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
21807
+ *
21808
+ * The Earn Service API wraps the transaction report payload in a `data`
21809
+ * envelope.
21810
+ *
21811
+ * @internal
21812
+ */ zod.z.object({
21813
+ data: transactionReportPayloadSchema
21814
+ });
21815
+ // ---------------------------------------------------------------------------
20897
21816
  // Claim rewards response schema
20898
21817
  // ---------------------------------------------------------------------------
20899
21818
  /**
@@ -20954,6 +21873,30 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20954
21873
  token: zod.z.string(),
20955
21874
  amount: amountJsonSchema
20956
21875
  });
21876
+ /**
21877
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
21878
+ *
21879
+ * The Earn Service backend estimates gas server-side and returns one entry per
21880
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
21881
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
21882
+ * integer string in the chain's native base units. When the backend cannot
21883
+ * estimate an action it returns `fees: null` with an `error` message instead.
21884
+ *
21885
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
21886
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
21887
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
21888
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
21889
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
21890
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
21891
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
21892
+ * `fee`) must never fail Zod validation and reject the entire quote.
21893
+ *
21894
+ * @internal
21895
+ */ const quoteGasFeeSchema = zod.z.object({
21896
+ name: zod.z.string().optional(),
21897
+ fees: zod.z.unknown(),
21898
+ error: zod.z.string().optional()
21899
+ }).passthrough();
20957
21900
  /**
20958
21901
  * Zod schema for the inner deposit quote payload.
20959
21902
  *
@@ -20969,7 +21912,8 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20969
21912
  expectedShares: amountJsonSchema,
20970
21913
  sharePrice: zod.z.string(),
20971
21914
  currentApy: zod.z.number(),
20972
- fees: zod.z.array(feeSchema).optional()
21915
+ fees: zod.z.array(feeSchema).optional(),
21916
+ gasFees: zod.z.array(quoteGasFeeSchema).optional()
20973
21917
  });
20974
21918
  /**
20975
21919
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -20996,6 +21940,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20996
21940
  sharePrice: zod.z.string(),
20997
21941
  maxWithdrawable: amountJsonSchema,
20998
21942
  fees: zod.z.array(feeSchema),
21943
+ gasFees: zod.z.array(quoteGasFeeSchema).optional(),
20999
21944
  warnings: zod.z.array(zod.z.string()).optional()
21000
21945
  });
21001
21946
  /**
@@ -21053,7 +21998,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
21053
21998
  *
21054
21999
  * @internal
21055
22000
  */ const getVaultsPayloadSchema = zod.z.object({
21056
- vaults: zod.z.array(vaultInfoResponseSchema),
22001
+ vaults: earnOpportunityListSchema,
21057
22002
  errors: zod.z.array(vaultErrorSchema)
21058
22003
  });
21059
22004
  /**
@@ -21083,7 +22028,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
21083
22028
  *
21084
22029
  * @internal
21085
22030
  */ const exploreVaultsPayloadSchema = zod.z.object({
21086
- vaults: zod.z.array(vaultInfoResponseSchema),
22031
+ vaults: earnOpportunityListSchema,
21087
22032
  pagination: explorePaginationSchema
21088
22033
  });
21089
22034
  /**
@@ -21651,6 +22596,8 @@ function hasCrossChainDepositQuoteShape(params) {
21651
22596
  config: earnConfigSchema.optional()
21652
22597
  });
21653
22598
 
22599
+ /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg.name);
22600
+
21654
22601
  // Auto-register this kit for user agent tracking
21655
22602
  registerKit(`${pkg.name}/${pkg.version}`);
21656
22603