@circle-fin/app-kit 1.10.0 → 1.12.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/estimateSwap.mjs CHANGED
@@ -16,6 +16,17 @@
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';
21
32
  import { hexlify, hexZeroPad } from '@ethersproject/bytes';
@@ -27,6 +38,7 @@ import '@coral-xyz/anchor';
27
38
  import bs58 from 'bs58';
28
39
  import '@noble/curves/ed25519';
29
40
  import { formatUnits as formatUnits$1 } from '@ethersproject/units';
41
+ import 'viem';
30
42
  import { keccak256 } from '@ethersproject/keccak256';
31
43
 
32
44
  // Import global type declarations
@@ -44,6 +56,51 @@ import { keccak256 } from '@ethersproject/keccak256';
44
56
  * }
45
57
  * ```
46
58
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
59
+ /**
60
+ * Check whether the current runtime exposes a browser DOM.
61
+ *
62
+ * @remarks
63
+ * This intentionally does not treat every non-Node runtime as a browser.
64
+ * Server-side edge runtimes such as Cloudflare Workers, Deno, and Supabase
65
+ * Edge Functions do not expose Node globals but can safely use server
66
+ * credentials. A Node.js runtime remains server-side even when a test or SSR
67
+ * environment provides a DOM shim.
68
+ *
69
+ * @returns `true` when running in a browser window, `false` otherwise.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * import { isBrowserEnvironment } from '@core/utils'
74
+ *
75
+ * if (isBrowserEnvironment()) {
76
+ * throw new Error('Server-only secrets must not be used in the browser')
77
+ * }
78
+ * ```
79
+ */ const isBrowserEnvironment = ()=>{
80
+ const browserWindow = globalThis.window;
81
+ return !isNodeEnvironment() && browserWindow?.document !== undefined;
82
+ };
83
+ /**
84
+ * Return the SDK User-Agent request header only when running in Node.js.
85
+ *
86
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
87
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
88
+ * attribution header because they cannot set it reliably.
89
+ *
90
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * import { getNodeUserAgentHeader } from '@core/utils'
95
+ *
96
+ * const headers = {
97
+ * 'Content-Type': 'application/json',
98
+ * ...getNodeUserAgentHeader(),
99
+ * }
100
+ * ```
101
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
102
+ 'User-Agent': getUserAgent()
103
+ } : {};
47
104
  /**
48
105
  * Detect the runtime environment and return a shortened identifier.
49
106
  *
@@ -2946,6 +3003,8 @@ class KitError extends Error {
2946
3003
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2947
3004
  Blockchain["XDC"] = "XDC";
2948
3005
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
3006
+ Blockchain["X_Layer"] = "X_Layer";
3007
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2949
3008
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2950
3009
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2951
3010
  })(Blockchain || (Blockchain = {}));
@@ -2999,6 +3058,7 @@ var BridgeChain;
2999
3058
  BridgeChain["Unichain"] = "Unichain";
3000
3059
  BridgeChain["World_Chain"] = "World_Chain";
3001
3060
  BridgeChain["XDC"] = "XDC";
3061
+ BridgeChain["X_Layer"] = "X_Layer";
3002
3062
  // Testnet chains with CCTPv2 support
3003
3063
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
3004
3064
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -3024,6 +3084,7 @@ var BridgeChain;
3024
3084
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
3025
3085
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
3026
3086
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
3087
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
3027
3088
  })(BridgeChain || (BridgeChain = {}));
3028
3089
  var UnifiedBalanceChain;
3029
3090
  (function(UnifiedBalanceChain) {
@@ -5572,7 +5633,8 @@ var EarnChain;
5572
5633
  isTestnet: true,
5573
5634
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
5574
5635
  rpcEndpoints: [
5575
- 'https://rpc-amoy.polygon.technology'
5636
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
5637
+ 'https://polygon-amoy.drpc.org'
5576
5638
  ],
5577
5639
  eurcAddress: null,
5578
5640
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -6437,6 +6499,104 @@ var EarnChain;
6437
6499
  }
6438
6500
  });
6439
6501
 
6502
+ /**
6503
+ * X Layer Mainnet chain definition
6504
+ * @remarks
6505
+ * This represents the official production network for the X Layer blockchain.
6506
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
6507
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
6508
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
6509
+ */ const XLayer = defineChain({
6510
+ type: 'evm',
6511
+ chain: Blockchain.X_Layer,
6512
+ name: 'X Layer',
6513
+ title: 'X Layer Mainnet',
6514
+ nativeCurrency: {
6515
+ name: 'OKB',
6516
+ symbol: 'OKB',
6517
+ decimals: 18
6518
+ },
6519
+ chainId: 196,
6520
+ isTestnet: false,
6521
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
6522
+ rpcEndpoints: [
6523
+ 'https://xlayerrpc.okx.com'
6524
+ ],
6525
+ eurcAddress: null,
6526
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
6527
+ usdtAddress: null,
6528
+ cctp: {
6529
+ domain: 37,
6530
+ contracts: {
6531
+ v2: {
6532
+ type: 'split',
6533
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6534
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6535
+ confirmations: 65,
6536
+ fastConfirmations: 1
6537
+ }
6538
+ },
6539
+ forwarderSupported: {
6540
+ source: false,
6541
+ destination: false
6542
+ }
6543
+ },
6544
+ kitContracts: {
6545
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
6546
+ }
6547
+ });
6548
+
6549
+ /**
6550
+ * X Layer Testnet chain definition
6551
+ * @remarks
6552
+ * This represents the official test network for the X Layer blockchain.
6553
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
6554
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
6555
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
6556
+ */ const XLayerTestnet = defineChain({
6557
+ type: 'evm',
6558
+ chain: Blockchain.X_Layer_Testnet,
6559
+ name: 'X Layer Testnet',
6560
+ title: 'X Layer Testnet',
6561
+ nativeCurrency: {
6562
+ name: 'OKB',
6563
+ symbol: 'OKB',
6564
+ decimals: 18
6565
+ },
6566
+ chainId: 1952,
6567
+ isTestnet: true,
6568
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
6569
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
6570
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
6571
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
6572
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
6573
+ rpcEndpoints: [
6574
+ 'https://testrpc.xlayer.tech'
6575
+ ],
6576
+ eurcAddress: null,
6577
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
6578
+ usdtAddress: null,
6579
+ cctp: {
6580
+ domain: 37,
6581
+ contracts: {
6582
+ v2: {
6583
+ type: 'split',
6584
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6585
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6586
+ confirmations: 65,
6587
+ fastConfirmations: 1
6588
+ }
6589
+ },
6590
+ forwarderSupported: {
6591
+ source: false,
6592
+ destination: false
6593
+ }
6594
+ },
6595
+ kitContracts: {
6596
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
6597
+ }
6598
+ });
6599
+
6440
6600
  /**
6441
6601
  * ZKSync Era Mainnet chain definition
6442
6602
  * @remarks
@@ -6556,6 +6716,8 @@ var Chains = /*#__PURE__*/Object.freeze({
6556
6716
  WorldChainSepolia: WorldChainSepolia,
6557
6717
  XDC: XDC,
6558
6718
  XDCApothem: XDCApothem,
6719
+ XLayer: XLayer,
6720
+ XLayerTestnet: XLayerTestnet,
6559
6721
  ZKSyncEra: ZKSyncEra,
6560
6722
  ZKSyncEraSepolia: ZKSyncEraSepolia
6561
6723
  });
@@ -8021,13 +8183,12 @@ const swapTokenEnumSchema = z.enum([
8021
8183
  headers: {
8022
8184
  ...DEFAULT_CONFIG$1.headers,
8023
8185
  ...config.headers ?? {},
8024
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
8025
- // This is why we use the 'X-User-Agent' header instead.
8026
- ...typeof window === 'undefined' ? {
8027
- 'User-Agent': getUserAgent()
8028
- } : {
8029
- 'X-User-Agent': getUserAgent()
8030
- }
8186
+ // Browsers forbid setting a user-agent request header, and the custom
8187
+ // fallback header the SDK used instead trips CORS preflight against the
8188
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
8189
+ // blocking the request. So send the SDK user agent only in Node;
8190
+ // browsers omit it entirely.
8191
+ ...getNodeUserAgentHeader()
8031
8192
  }
8032
8193
  };
8033
8194
  let lastError;
@@ -8585,6 +8746,7 @@ const swapTokenEnumSchema = z.enum([
8585
8746
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
8586
8747
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
8587
8748
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
8749
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
8588
8750
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
8589
8751
  // =========================================================================
8590
8752
  // Testnets (alphabetically sorted)
@@ -8619,6 +8781,7 @@ const swapTokenEnumSchema = z.enum([
8619
8781
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
8620
8782
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
8621
8783
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8784
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
8622
8785
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
8623
8786
  }
8624
8787
  };
@@ -9528,6 +9691,7 @@ const swapTokenEnumSchema = z.enum([
9528
9691
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
9529
9692
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
9530
9693
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
9694
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
9531
9695
  if (payload.errorDetails !== undefined) {
9532
9696
  const errorDetails = {
9533
9697
  ...payload.errorDetails.errorCode !== undefined && {
@@ -9598,18 +9762,15 @@ const swapTokenEnumSchema = z.enum([
9598
9762
  timeoutHandle.unref();
9599
9763
  }
9600
9764
  try {
9601
- const isNode = isNodeEnvironment();
9602
- const userAgent = getUserAgent();
9603
9765
  await fetch(getLogsUrl(), {
9604
9766
  method: 'POST',
9605
9767
  headers: {
9606
9768
  'Content-Type': 'application/json',
9607
- // Browser restricts setting User-Agent; use X-User-Agent instead.
9608
- ...isNode ? {
9609
- 'User-Agent': userAgent
9610
- } : {
9611
- 'X-User-Agent': userAgent
9612
- }
9769
+ // Browsers forbid setting a user-agent request header, and the custom
9770
+ // fallback header the SDK used instead trips CORS preflight (it isn't
9771
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
9772
+ // it only in Node; browsers omit it entirely.
9773
+ ...getNodeUserAgentHeader()
9613
9774
  },
9614
9775
  body: JSON.stringify(toSafePayload(payload)),
9615
9776
  signal: controller.signal
@@ -9777,7 +9938,7 @@ const swapTokenEnumSchema = z.enum([
9777
9938
  // discards the stack trace, nested `cause`, and any custom Error
9778
9939
  // properties — exactly the context an on-call needs when a
9779
9940
  // resolver-closure regression triggers this path.
9780
- console.warn(`[stablecoin-kits telemetry] dropped error event '${eventType}':`, cause);
9941
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
9781
9942
  } catch {
9782
9943
  // console.warn itself throwing is the user's environment; nothing more we
9783
9944
  // can do without risking the original operation error.
@@ -9793,7 +9954,9 @@ const swapTokenEnumSchema = z.enum([
9793
9954
  sdkVersion: config.sdkVersion,
9794
9955
  eventType,
9795
9956
  timestamp: new Date().toISOString(),
9796
- errorDetails,
9957
+ ...errorDetails !== undefined && {
9958
+ errorDetails
9959
+ },
9797
9960
  clientContext: buildClientContext(),
9798
9961
  ...context?.sourceChain != null && {
9799
9962
  sourceChain: context.sourceChain
@@ -9809,9 +9972,45 @@ const swapTokenEnumSchema = z.enum([
9809
9972
  },
9810
9973
  ...context?.txHash != null && {
9811
9974
  txHash: context.txHash
9975
+ },
9976
+ ...context?.correlationId != null && {
9977
+ correlationId: context.correlationId
9812
9978
  }
9813
9979
  };
9814
9980
  }
9981
+ /**
9982
+ * Emit telemetry for a completed operation without affecting its caller.
9983
+ *
9984
+ * No-ops when `config.disabled` is `true`. Like {@link withErrorTelemetry},
9985
+ * failures while constructing or submitting the telemetry payload are reported
9986
+ * as a soft warning and never change a completed operation's result.
9987
+ *
9988
+ * @param eventType - The telemetry event type for the completed operation.
9989
+ * @param config - Per-kit SDK identity and disabled flag.
9990
+ * @param context - Optional chain, token, and transaction context.
9991
+ * @returns Nothing.
9992
+ * @throws Never — telemetry failures are reported as warnings.
9993
+ *
9994
+ * @example
9995
+ * ```typescript
9996
+ * import { emitSuccessTelemetry } from '@core/utils'
9997
+ *
9998
+ * emitSuccessTelemetry(
9999
+ * 'bridge_bridge',
10000
+ * { sdkName: 'bridge-kit', sdkVersion: '1.0.0', disabled: false },
10001
+ * { sourceChain: 'Ethereum', destinationChain: 'Base', tokenIn: 'USDC' },
10002
+ * )
10003
+ * ```
10004
+ */ function emitSuccessTelemetry(eventType, config, context) {
10005
+ if (config.disabled) {
10006
+ return;
10007
+ }
10008
+ try {
10009
+ void emitAnalyticsLog(buildPayload(config, eventType, undefined, context));
10010
+ } catch (telemetryError) {
10011
+ warnTelemetryDrop(eventType, telemetryError);
10012
+ }
10013
+ }
9815
10014
  /**
9816
10015
  * Wrap an async operation with error telemetry.
9817
10016
  *
@@ -9870,7 +10069,7 @@ const swapTokenEnumSchema = z.enum([
9870
10069
  }
9871
10070
 
9872
10071
  var name$2 = "@circle-fin/bridge-kit";
9873
- var version$2 = "1.12.1";
10072
+ var version$2 = "1.13.0";
9874
10073
  var pkg$2 = {
9875
10074
  name: name$2,
9876
10075
  version: version$2};
@@ -9906,13 +10105,21 @@ var pkg$2 = {
9906
10105
  computeFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
9907
10106
  calculateFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
9908
10107
  resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
9909
- }).strict().refine((data)=>{
10108
+ }).strict().superRefine((data, ctx)=>{
9910
10109
  const hasComputeFee = data.computeFee !== undefined;
9911
10110
  const hasCalculateFee = data.calculateFee !== undefined;
9912
- // XOR: exactly one must be provided
9913
- return hasComputeFee !== hasCalculateFee;
9914
- }, {
9915
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
10111
+ if (hasComputeFee && hasCalculateFee) {
10112
+ ctx.addIssue({
10113
+ code: z.ZodIssueCode.custom,
10114
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
10115
+ });
10116
+ }
10117
+ if (!hasComputeFee && !hasCalculateFee) {
10118
+ ctx.addIssue({
10119
+ code: z.ZodIssueCode.custom,
10120
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
10121
+ });
10122
+ }
9916
10123
  });
9917
10124
 
9918
10125
  /**
@@ -10731,7 +10938,13 @@ var TransferSpeed;
10731
10938
  /**
10732
10939
  * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
10733
10940
  * hookData must start with.
10734
- */ Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
10941
+ *
10942
+ * Encoded with `TextEncoder` (a browser-safe Web API) rather than `Buffer.from`
10943
+ * so this module-level constant does not reference the Node `Buffer` global at
10944
+ * import time. App Kit inlines this provider into its bundle without a Buffer polyfill, and a
10945
+ * bare `Buffer` here crashes browser bundles (e.g. Vite) on load — even for apps
10946
+ * that never touch the prepaid FORWARD path. Mirrors `buildForwardingHookData`.
10947
+ */ Array.from(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX)).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
10735
10948
 
10736
10949
  /**
10737
10950
  * The minimum finality threshold for CCTPv2 transfers.
@@ -10765,7 +10978,7 @@ var TransferSpeed;
10765
10978
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
10766
10979
 
10767
10980
  var name$1 = "@circle-fin/swap-kit";
10768
- var version$1 = "1.4.0";
10981
+ var version$1 = "1.5.1";
10769
10982
  var pkg$1 = {
10770
10983
  name: name$1,
10771
10984
  version: version$1};
@@ -11598,6 +11811,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11598
11811
  required_error: 'estimatedAmount is required',
11599
11812
  invalid_type_error: 'estimatedAmount must be a string'
11600
11813
  }).min(1, 'estimatedAmount must be a non-empty string'),
11814
+ // Per-swap join key echoed back verbatim on success telemetry. Optional so a
11815
+ // not-yet-upgraded service (no field) still validates during rollout. A
11816
+ // malformed/non-UUID value is coerced to `undefined` (no telemetry id) rather
11817
+ // than throwing: this is a telemetry-only field (stripped from the developer
11818
+ // result, never used for control flow), so it must not be able to abort the
11819
+ // swap via `parseCreateSwapResponse().parse()`. Mirrors the best-effort,
11820
+ // never-throw contract of the rest of the telemetry stack. Implemented with
11821
+ // `preprocess` rather than Zod's `.catch()` because static analysis misreads
11822
+ // `.catch` on the schema chain as an unhandled Promise (S7785).
11823
+ correlationId: z.preprocess((value)=>z.string().uuid().safeParse(value).success ? value : undefined, z.string().optional()),
11601
11824
  config: createSwapRequestBaseSchema.shape.config.optional(),
11602
11825
  fees: createSwapFeesSchema.optional(),
11603
11826
  transaction: createSwapTransactionSchema
@@ -11684,6 +11907,45 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11684
11907
  * }
11685
11908
  * ```
11686
11909
  */ const isGetTokenRatesResponse = (obj)=>getTokenRatesResponseSchema.safeParse(obj).success;
11910
+ /**
11911
+ * Assert that a Stablecoin Service kit key is not being supplied from a browser.
11912
+ *
11913
+ * The kit key (`KIT_KEY:<id>:<secret>`) is a server-only secret. Every
11914
+ * Stablecoin Service request that attaches an `Authorization: Bearer` header
11915
+ * funnels through this package, so calling this guard before that header is
11916
+ * built prevents the secret from being sent from — and thus bundled into — a
11917
+ * client application. In Node.js the check is a no-op, preserving the
11918
+ * legitimate "hold the kit key on the server, forward the prepared transaction
11919
+ * to the client" flow. When no kit key is supplied the permissionless (keyless)
11920
+ * client path remains fully allowed.
11921
+ *
11922
+ * @param apiKey - The inline kit key for the request, or `undefined` when none
11923
+ * was supplied (permissionless mode).
11924
+ * @returns Nothing.
11925
+ * @throws KitError with VALIDATION_FAILED when a kit key is supplied while
11926
+ * running in a browser environment. The secret value is never echoed.
11927
+ *
11928
+ * @example
11929
+ * ```typescript
11930
+ * import { assertBrowserSafeApiKey } from '@core/service-client'
11931
+ *
11932
+ * // Server (Node.js): no-op, request proceeds with the Authorization header.
11933
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11934
+ *
11935
+ * // Browser: throws to stop the secret from leaking into the client bundle.
11936
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11937
+ *
11938
+ * // Browser, permissionless: allowed.
11939
+ * assertBrowserSafeApiKey(undefined)
11940
+ * ```
11941
+ */ const assertBrowserSafeApiKey = (apiKey)=>{
11942
+ if (apiKey === undefined) {
11943
+ return;
11944
+ }
11945
+ if (isBrowserEnvironment()) {
11946
+ 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');
11947
+ }
11948
+ };
11687
11949
 
11688
11950
  /**
11689
11951
  * Create a cross-chain bridge and swap transaction through the Stablecoin Service.
@@ -11741,6 +12003,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11741
12003
  const url = new URL('/v1/stablecoinKits/swap', STABLECOIN_SERVICE_BASE_URL).toString();
11742
12004
  // Remove the API key from the request body
11743
12005
  const { apiKey, ...requestBody } = validatedParams;
12006
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12007
+ assertBrowserSafeApiKey(apiKey);
11744
12008
  const effectiveConfig = {
11745
12009
  ...DEFAULT_CONFIG,
11746
12010
  headers: {
@@ -11895,6 +12159,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11895
12159
  }
11896
12160
  // Use validated data
11897
12161
  const validatedParams = result.data;
12162
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12163
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11898
12164
  // Build the API URL
11899
12165
  const url = buildQuoteUrl(validatedParams);
11900
12166
  // Merge default config with Authorization header
@@ -11967,6 +12233,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11967
12233
  toChain: result.data.toChain
11968
12234
  }
11969
12235
  };
12236
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12237
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11970
12238
  const url = buildSwapStatusUrl(validatedParams);
11971
12239
  const effectiveConfig = {
11972
12240
  ...DEFAULT_CONFIG,
@@ -12071,6 +12339,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12071
12339
  addresses: result.data.addresses
12072
12340
  }
12073
12341
  };
12342
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12343
+ assertBrowserSafeApiKey(validatedParams.apiKey);
12074
12344
  const url = buildTokenRatesUrl(validatedParams);
12075
12345
  const effectiveConfig = {
12076
12346
  ...DEFAULT_CONFIG,
@@ -16497,6 +16767,11 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16497
16767
  apiKey: serviceParams.apiKey
16498
16768
  }
16499
16769
  });
16770
+ // Per-swap correlation id returned by the service as a top-level response
16771
+ // field for every chain (EVM + Solana). Attached to success telemetry so a
16772
+ // swap can be correlated across records; never used for control flow.
16773
+ // Undefined only against a not-yet-upgraded service that omits it.
16774
+ const correlationId = serviceResponse.correlationId;
16500
16775
  // Build and return SwapResult
16501
16776
  return {
16502
16777
  tokenIn,
@@ -16506,6 +16781,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16506
16781
  fromAddress: serviceParams.fromAddress,
16507
16782
  toAddress: serviceParams.toAddress,
16508
16783
  txHash,
16784
+ ...correlationId !== undefined && {
16785
+ correlationId
16786
+ },
16509
16787
  executedTransactions,
16510
16788
  ...config !== undefined && {
16511
16789
  config
@@ -19405,7 +19683,13 @@ async function resolveTokenDecimals(context, resolvedParams) {
19405
19683
  * amountIn: '50.00'
19406
19684
  * })
19407
19685
  * ```
19408
- */ async function swap(context, params, /** @internal */ onBroadcast) {
19686
+ */ async function swap(context, params, /**
19687
+ * @internal
19688
+ * Invoked after a successful broadcast with the on-chain `txHash` and the
19689
+ * service-issued `correlationId` (join key for success telemetry). The
19690
+ * service returns `correlationId` for every chain (EVM + Solana); it is
19691
+ * undefined only against a not-yet-upgraded service that omits the field.
19692
+ */ onBroadcast) {
19409
19693
  // Step 1: Validate parameters using schema
19410
19694
  assertSwapParams(params, swapParamsSchema);
19411
19695
  // Step 2: Resolve parameters (chain definitions, wallet addresses, token aliases preserved)
@@ -19425,13 +19709,16 @@ async function resolveTokenDecimals(context, resolvedParams) {
19425
19709
  // Step 5: Execute swap via provider
19426
19710
  const swapParams = buildServiceSwapParams(resolvedParams, paramsWithFees);
19427
19711
  const providerResult = await provider.swap(swapParams);
19712
+ // `correlationId` is an internal telemetry join key, not part of the public
19713
+ // SwapResult — strip it here so it never leaks into the formatted result.
19714
+ const { correlationId, ...providerResultPublic } = providerResult;
19428
19715
  // A throwing `onBroadcast` must never strand the caller after a
19429
19716
  // successful swap broadcast — the chain has moved. `safeInvokeCallback`
19430
19717
  // swallows the error and surfaces a `console.warn` prefixed with
19431
19718
  // `[stablecoin-kits swap-kit] callback threw and was swallowed:` so a
19432
19719
  // kit-side closure bug stays debuggable rather than vanishing.
19433
19720
  safeInvokeCallback('swap-kit', ()=>{
19434
- onBroadcast?.(providerResult.txHash);
19721
+ onBroadcast?.(providerResultPublic.txHash, correlationId);
19435
19722
  });
19436
19723
  const { tokenInDecimals, tokenOutDecimals } = await resolveTokenDecimals(context, resolvedParams);
19437
19724
  // Step 6: Compose chain identity (owned by the kit, derived from the
@@ -19440,10 +19727,10 @@ async function resolveTokenDecimals(context, resolvedParams) {
19440
19727
  // resolves `progress` — the provider's snapshot, or a terminal `'DONE'` for
19441
19728
  // a provider that omits it (a synchronous same-chain completion).
19442
19729
  const composedResult = {
19443
- ...providerResult,
19730
+ ...providerResultPublic,
19444
19731
  chainIn: resolvedParams.from.chain,
19445
19732
  chainOut: resolvedParams.toChain ?? resolvedParams.from.chain,
19446
- progress: providerResult.progress ?? {
19733
+ progress: providerResultPublic.progress ?? {
19447
19734
  status: 'DONE'
19448
19735
  }
19449
19736
  };
@@ -20296,7 +20583,13 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20296
20583
  */ class SwapKit {
20297
20584
  context;
20298
20585
  /** Whether error telemetry is disabled. */ disableErrorReporting;
20299
- /** Per-kit telemetry identity for shared helpers. */ telemetryConfig;
20586
+ /** Per-kit telemetry identity for error reporting. */ telemetryConfig;
20587
+ /**
20588
+ * Per-kit telemetry identity for success/analytics events. Gated by
20589
+ * `disableAnalytics` (independent of `disableErrorReporting`) so a developer
20590
+ * can opt out of volume analytics without also silencing error reports —
20591
+ * mirrors EarnKit.
20592
+ */ analyticsTelemetryConfig;
20300
20593
  /**
20301
20594
  * Create a new SwapKit instance.
20302
20595
  *
@@ -20350,6 +20643,11 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20350
20643
  sdkVersion: pkg$1.version,
20351
20644
  disabled: this.disableErrorReporting
20352
20645
  };
20646
+ this.analyticsTelemetryConfig = {
20647
+ sdkName: SDK_NAME,
20648
+ sdkVersion: pkg$1.version,
20649
+ disabled: config.disableAnalytics === true
20650
+ };
20353
20651
  }
20354
20652
  /**
20355
20653
  * Estimate the output amount and fees for a swap operation.
@@ -20390,8 +20688,12 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20390
20688
  * console.log(`Fees:`, quote.fees)
20391
20689
  * ```
20392
20690
  */ async estimate(params) {
20691
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
20393
20692
  return withErrorTelemetry(async ()=>estimate(this.context, params), SWAP_EVENT_TYPES.ESTIMATE, this.telemetryConfig, {
20394
20693
  sourceChain: resolveChainName(params.from.chain),
20694
+ ...destinationChain != null && {
20695
+ destinationChain
20696
+ },
20395
20697
  tokenIn: params.tokenIn,
20396
20698
  tokenOut: params.tokenOut
20397
20699
  });
@@ -20450,16 +20752,54 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20450
20752
  * ```
20451
20753
  */ async swap(params) {
20452
20754
  let txHash;
20453
- return withErrorTelemetry(async ()=>swap(this.context, params, (h)=>{
20454
- txHash = h;
20455
- }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, ()=>({
20755
+ let correlationId;
20756
+ // Shared context builder so the error resolver and the success emit stay in
20757
+ // lockstep — a field added here reaches both call sites. Reads the per-call
20758
+ // locals lazily, so txHash/correlationId (set during the swap) are captured
20759
+ // whenever it is invoked.
20760
+ // Destination chain is the primary attribution dimension for cross-chain
20761
+ // swaps; resolved once from the (static) params. Omitted for same-chain
20762
+ // swaps that leave `to.chain` unset (destination == source).
20763
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
20764
+ const buildTelemetryContext = ()=>({
20456
20765
  sourceChain: resolveChainName(params.from.chain),
20766
+ ...destinationChain != null && {
20767
+ destinationChain
20768
+ },
20457
20769
  tokenIn: params.tokenIn,
20458
20770
  tokenOut: params.tokenOut,
20459
20771
  ...txHash != null && {
20460
20772
  txHash
20773
+ },
20774
+ ...correlationId != null && {
20775
+ correlationId
20461
20776
  }
20462
- }));
20777
+ });
20778
+ const result = await withErrorTelemetry(async ()=>swap(this.context, params, (h, cId)=>{
20779
+ txHash = h;
20780
+ correlationId = cId;
20781
+ }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, buildTelemetryContext);
20782
+ // withErrorTelemetry only emits on failure. Record the successful swap here
20783
+ // so the backend can attribute swap volume to a developer: the client event
20784
+ // carries the (burn) txHash + correlationId, which joins to the
20785
+ // server-emitted event carrying entity_id. Best-effort; never throws.
20786
+ //
20787
+ // Emit whenever a broadcast happened, i.e. we have the source/burn txHash.
20788
+ // For a cross-chain swap that is the source-chain burn (progress is still
20789
+ // PENDING while the destination mint settles) — we intentionally attribute
20790
+ // at broadcast using the burn txHash rather than tracking the destination
20791
+ // leg, which keeps the capture simple and self-contained in swap().
20792
+ //
20793
+ // Guard against a terminal-failure result: the EVM provider throws on
20794
+ // revert today, but the kit is provider-agnostic, so a provider that
20795
+ // returns a FAILED/NOT_FOUND result without throwing must not be recorded
20796
+ // as a successful swap. Routed through analyticsTelemetryConfig so it is
20797
+ // gated by disableAnalytics, independent of error reporting.
20798
+ const status = result.progress?.status;
20799
+ if (txHash != null && status !== 'FAILED' && status !== 'NOT_FOUND') {
20800
+ emitSuccessTelemetry(SWAP_EVENT_TYPES.SWAP, this.analyticsTelemetryConfig, buildTelemetryContext());
20801
+ }
20802
+ return result;
20463
20803
  }
20464
20804
  /**
20465
20805
  * Fetch the current status of a swap from the Stablecoin Service.
@@ -20838,9 +21178,14 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20838
21178
  const kit = new SwapKit({
20839
21179
  ...context.disableErrorReporting != null && {
20840
21180
  disableErrorReporting: context.disableErrorReporting
21181
+ },
21182
+ ...context.disableAnalytics != null && {
21183
+ disableAnalytics: context.disableAnalytics
20841
21184
  }
20842
21185
  });
20843
- if (hasBoth) {
21186
+ if (context.customFeePolicy?.swap != null) {
21187
+ kit.setCustomFeePolicy(context.customFeePolicy.swap);
21188
+ } else if (hasBoth) {
20844
21189
  kit.setCustomFeePolicy({
20845
21190
  computeFee: async (params)=>{
20846
21191
  // Adapt provider-level params (with tokenIn/tokenOut)
@@ -20899,7 +21244,7 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20899
21244
  };
20900
21245
 
20901
21246
  var name = "@circle-fin/earn-kit";
20902
- var version = "1.3.0";
21247
+ var version = "1.5.0";
20903
21248
  var pkg = {
20904
21249
  name: name,
20905
21250
  version: version};
@@ -21910,11 +22255,16 @@ const sourceAdapterContextSchema = z.object({
21910
22255
  *
21911
22256
  * Validate the optional Kit Key field using the standard `apiKeySchema`
21912
22257
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
21913
- * operates in permissionless mode.
22258
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
22259
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
22260
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
22261
+ * schema must accept them or a valid config object is rejected.
21914
22262
  *
21915
22263
  * @internal
21916
22264
  */ const earnConfigSchema = z.object({
21917
- kitKey: apiKeySchema.optional()
22265
+ kitKey: apiKeySchema.optional(),
22266
+ baseUrl: z.string().optional(),
22267
+ batchTransactions: z.boolean().optional()
21918
22268
  }).strict();
21919
22269
  /**
21920
22270
  * Canonical decimal form: a leading digit with no leading zeros (a single
@@ -22363,6 +22713,8 @@ function hasCrossChainDepositQuoteShape(params) {
22363
22713
  config: earnConfigSchema.optional()
22364
22714
  });
22365
22715
 
22716
+ /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg.name);
22717
+
22366
22718
  // Auto-register this kit for user agent tracking
22367
22719
  registerKit(`${pkg.name}/${pkg.version}`);
22368
22720