@circle-fin/app-kit 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/swap.mjs CHANGED
@@ -16,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';
@@ -44,6 +55,51 @@ import { keccak256 } from '@ethersproject/keccak256';
44
55
  * }
45
56
  * ```
46
57
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
58
+ /**
59
+ * Check whether the current runtime exposes a browser DOM.
60
+ *
61
+ * @remarks
62
+ * This intentionally does not treat every non-Node runtime as a browser.
63
+ * Server-side edge runtimes such as Cloudflare Workers, Deno, and Supabase
64
+ * Edge Functions do not expose Node globals but can safely use server
65
+ * credentials. A Node.js runtime remains server-side even when a test or SSR
66
+ * environment provides a DOM shim.
67
+ *
68
+ * @returns `true` when running in a browser window, `false` otherwise.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { isBrowserEnvironment } from '@core/utils'
73
+ *
74
+ * if (isBrowserEnvironment()) {
75
+ * throw new Error('Server-only secrets must not be used in the browser')
76
+ * }
77
+ * ```
78
+ */ const isBrowserEnvironment = ()=>{
79
+ const browserWindow = globalThis.window;
80
+ return !isNodeEnvironment() && browserWindow?.document !== undefined;
81
+ };
82
+ /**
83
+ * Return the SDK User-Agent request header only when running in Node.js.
84
+ *
85
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
86
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
87
+ * attribution header because they cannot set it reliably.
88
+ *
89
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { getNodeUserAgentHeader } from '@core/utils'
94
+ *
95
+ * const headers = {
96
+ * 'Content-Type': 'application/json',
97
+ * ...getNodeUserAgentHeader(),
98
+ * }
99
+ * ```
100
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
101
+ 'User-Agent': getUserAgent()
102
+ } : {};
47
103
  /**
48
104
  * Detect the runtime environment and return a shortened identifier.
49
105
  *
@@ -8021,13 +8077,12 @@ const swapTokenEnumSchema = z.enum([
8021
8077
  headers: {
8022
8078
  ...DEFAULT_CONFIG$1.headers,
8023
8079
  ...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
- }
8080
+ // Browsers forbid setting a user-agent request header, and the custom
8081
+ // fallback header the SDK used instead trips CORS preflight against the
8082
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
8083
+ // blocking the request. So send the SDK user agent only in Node;
8084
+ // browsers omit it entirely.
8085
+ ...getNodeUserAgentHeader()
8031
8086
  }
8032
8087
  };
8033
8088
  let lastError;
@@ -9528,6 +9583,7 @@ const swapTokenEnumSchema = z.enum([
9528
9583
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
9529
9584
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
9530
9585
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
9586
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
9531
9587
  if (payload.errorDetails !== undefined) {
9532
9588
  const errorDetails = {
9533
9589
  ...payload.errorDetails.errorCode !== undefined && {
@@ -9598,18 +9654,15 @@ const swapTokenEnumSchema = z.enum([
9598
9654
  timeoutHandle.unref();
9599
9655
  }
9600
9656
  try {
9601
- const isNode = isNodeEnvironment();
9602
- const userAgent = getUserAgent();
9603
9657
  await fetch(getLogsUrl(), {
9604
9658
  method: 'POST',
9605
9659
  headers: {
9606
9660
  '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
- }
9661
+ // Browsers forbid setting a user-agent request header, and the custom
9662
+ // fallback header the SDK used instead trips CORS preflight (it isn't
9663
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
9664
+ // it only in Node; browsers omit it entirely.
9665
+ ...getNodeUserAgentHeader()
9613
9666
  },
9614
9667
  body: JSON.stringify(toSafePayload(payload)),
9615
9668
  signal: controller.signal
@@ -9777,7 +9830,7 @@ const swapTokenEnumSchema = z.enum([
9777
9830
  // discards the stack trace, nested `cause`, and any custom Error
9778
9831
  // properties — exactly the context an on-call needs when a
9779
9832
  // resolver-closure regression triggers this path.
9780
- console.warn(`[stablecoin-kits telemetry] dropped error event '${eventType}':`, cause);
9833
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
9781
9834
  } catch {
9782
9835
  // console.warn itself throwing is the user's environment; nothing more we
9783
9836
  // can do without risking the original operation error.
@@ -9793,7 +9846,9 @@ const swapTokenEnumSchema = z.enum([
9793
9846
  sdkVersion: config.sdkVersion,
9794
9847
  eventType,
9795
9848
  timestamp: new Date().toISOString(),
9796
- errorDetails,
9849
+ ...errorDetails !== undefined && {
9850
+ errorDetails
9851
+ },
9797
9852
  clientContext: buildClientContext(),
9798
9853
  ...context?.sourceChain != null && {
9799
9854
  sourceChain: context.sourceChain
@@ -9809,9 +9864,45 @@ const swapTokenEnumSchema = z.enum([
9809
9864
  },
9810
9865
  ...context?.txHash != null && {
9811
9866
  txHash: context.txHash
9867
+ },
9868
+ ...context?.correlationId != null && {
9869
+ correlationId: context.correlationId
9812
9870
  }
9813
9871
  };
9814
9872
  }
9873
+ /**
9874
+ * Emit telemetry for a completed operation without affecting its caller.
9875
+ *
9876
+ * No-ops when `config.disabled` is `true`. Like {@link withErrorTelemetry},
9877
+ * failures while constructing or submitting the telemetry payload are reported
9878
+ * as a soft warning and never change a completed operation's result.
9879
+ *
9880
+ * @param eventType - The telemetry event type for the completed operation.
9881
+ * @param config - Per-kit SDK identity and disabled flag.
9882
+ * @param context - Optional chain, token, and transaction context.
9883
+ * @returns Nothing.
9884
+ * @throws Never — telemetry failures are reported as warnings.
9885
+ *
9886
+ * @example
9887
+ * ```typescript
9888
+ * import { emitSuccessTelemetry } from '@core/utils'
9889
+ *
9890
+ * emitSuccessTelemetry(
9891
+ * 'bridge_bridge',
9892
+ * { sdkName: 'bridge-kit', sdkVersion: '1.0.0', disabled: false },
9893
+ * { sourceChain: 'Ethereum', destinationChain: 'Base', tokenIn: 'USDC' },
9894
+ * )
9895
+ * ```
9896
+ */ function emitSuccessTelemetry(eventType, config, context) {
9897
+ if (config.disabled) {
9898
+ return;
9899
+ }
9900
+ try {
9901
+ void emitAnalyticsLog(buildPayload(config, eventType, undefined, context));
9902
+ } catch (telemetryError) {
9903
+ warnTelemetryDrop(eventType, telemetryError);
9904
+ }
9905
+ }
9815
9906
  /**
9816
9907
  * Wrap an async operation with error telemetry.
9817
9908
  *
@@ -9870,7 +9961,7 @@ const swapTokenEnumSchema = z.enum([
9870
9961
  }
9871
9962
 
9872
9963
  var name$2 = "@circle-fin/bridge-kit";
9873
- var version$2 = "1.12.1";
9964
+ var version$2 = "1.12.2";
9874
9965
  var pkg$2 = {
9875
9966
  name: name$2,
9876
9967
  version: version$2};
@@ -10731,7 +10822,13 @@ var TransferSpeed;
10731
10822
  /**
10732
10823
  * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
10733
10824
  * hookData must start with.
10734
- */ Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
10825
+ *
10826
+ * Encoded with `TextEncoder` (a browser-safe Web API) rather than `Buffer.from`
10827
+ * so this module-level constant does not reference the Node `Buffer` global at
10828
+ * import time. App Kit inlines this provider into its bundle without a Buffer polyfill, and a
10829
+ * bare `Buffer` here crashes browser bundles (e.g. Vite) on load — even for apps
10830
+ * that never touch the prepaid FORWARD path. Mirrors `buildForwardingHookData`.
10831
+ */ Array.from(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX)).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
10735
10832
 
10736
10833
  /**
10737
10834
  * The minimum finality threshold for CCTPv2 transfers.
@@ -10765,7 +10862,7 @@ var TransferSpeed;
10765
10862
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
10766
10863
 
10767
10864
  var name$1 = "@circle-fin/swap-kit";
10768
- var version$1 = "1.4.0";
10865
+ var version$1 = "1.5.0";
10769
10866
  var pkg$1 = {
10770
10867
  name: name$1,
10771
10868
  version: version$1};
@@ -11598,6 +11695,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11598
11695
  required_error: 'estimatedAmount is required',
11599
11696
  invalid_type_error: 'estimatedAmount must be a string'
11600
11697
  }).min(1, 'estimatedAmount must be a non-empty string'),
11698
+ // Per-swap join key echoed back verbatim on success telemetry. Optional so a
11699
+ // not-yet-upgraded service (no field) still validates during rollout. A
11700
+ // malformed/non-UUID value is coerced to `undefined` (no telemetry id) rather
11701
+ // than throwing: this is a telemetry-only field (stripped from the developer
11702
+ // result, never used for control flow), so it must not be able to abort the
11703
+ // swap via `parseCreateSwapResponse().parse()`. Mirrors the best-effort,
11704
+ // never-throw contract of the rest of the telemetry stack. Implemented with
11705
+ // `preprocess` rather than Zod's `.catch()` because static analysis misreads
11706
+ // `.catch` on the schema chain as an unhandled Promise (S7785).
11707
+ correlationId: z.preprocess((value)=>z.string().uuid().safeParse(value).success ? value : undefined, z.string().optional()),
11601
11708
  config: createSwapRequestBaseSchema.shape.config.optional(),
11602
11709
  fees: createSwapFeesSchema.optional(),
11603
11710
  transaction: createSwapTransactionSchema
@@ -11684,6 +11791,45 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11684
11791
  * }
11685
11792
  * ```
11686
11793
  */ const isGetTokenRatesResponse = (obj)=>getTokenRatesResponseSchema.safeParse(obj).success;
11794
+ /**
11795
+ * Assert that a Stablecoin Service kit key is not being supplied from a browser.
11796
+ *
11797
+ * The kit key (`KIT_KEY:<id>:<secret>`) is a server-only secret. Every
11798
+ * Stablecoin Service request that attaches an `Authorization: Bearer` header
11799
+ * funnels through this package, so calling this guard before that header is
11800
+ * built prevents the secret from being sent from — and thus bundled into — a
11801
+ * client application. In Node.js the check is a no-op, preserving the
11802
+ * legitimate "hold the kit key on the server, forward the prepared transaction
11803
+ * to the client" flow. When no kit key is supplied the permissionless (keyless)
11804
+ * client path remains fully allowed.
11805
+ *
11806
+ * @param apiKey - The inline kit key for the request, or `undefined` when none
11807
+ * was supplied (permissionless mode).
11808
+ * @returns Nothing.
11809
+ * @throws KitError with VALIDATION_FAILED when a kit key is supplied while
11810
+ * running in a browser environment. The secret value is never echoed.
11811
+ *
11812
+ * @example
11813
+ * ```typescript
11814
+ * import { assertBrowserSafeApiKey } from '@core/service-client'
11815
+ *
11816
+ * // Server (Node.js): no-op, request proceeds with the Authorization header.
11817
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11818
+ *
11819
+ * // Browser: throws to stop the secret from leaking into the client bundle.
11820
+ * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
11821
+ *
11822
+ * // Browser, permissionless: allowed.
11823
+ * assertBrowserSafeApiKey(undefined)
11824
+ * ```
11825
+ */ const assertBrowserSafeApiKey = (apiKey)=>{
11826
+ if (apiKey === undefined) {
11827
+ return;
11828
+ }
11829
+ if (isBrowserEnvironment()) {
11830
+ throw createValidationFailedError$1('kitKey', '[redacted]', 'kitKey must not be provided in a browser environment — it is a server-only secret. Run kit operations that use a kit key on your server, or omit kitKey to use the permissionless (keyless) client path');
11831
+ }
11832
+ };
11687
11833
 
11688
11834
  /**
11689
11835
  * Create a cross-chain bridge and swap transaction through the Stablecoin Service.
@@ -11741,6 +11887,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11741
11887
  const url = new URL('/v1/stablecoinKits/swap', STABLECOIN_SERVICE_BASE_URL).toString();
11742
11888
  // Remove the API key from the request body
11743
11889
  const { apiKey, ...requestBody } = validatedParams;
11890
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
11891
+ assertBrowserSafeApiKey(apiKey);
11744
11892
  const effectiveConfig = {
11745
11893
  ...DEFAULT_CONFIG,
11746
11894
  headers: {
@@ -11895,6 +12043,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11895
12043
  }
11896
12044
  // Use validated data
11897
12045
  const validatedParams = result.data;
12046
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12047
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11898
12048
  // Build the API URL
11899
12049
  const url = buildQuoteUrl(validatedParams);
11900
12050
  // Merge default config with Authorization header
@@ -11967,6 +12117,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11967
12117
  toChain: result.data.toChain
11968
12118
  }
11969
12119
  };
12120
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12121
+ assertBrowserSafeApiKey(validatedParams.apiKey);
11970
12122
  const url = buildSwapStatusUrl(validatedParams);
11971
12123
  const effectiveConfig = {
11972
12124
  ...DEFAULT_CONFIG,
@@ -12071,6 +12223,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12071
12223
  addresses: result.data.addresses
12072
12224
  }
12073
12225
  };
12226
+ // Never let a server-only kit key leave a browser (no-op in Node.js).
12227
+ assertBrowserSafeApiKey(validatedParams.apiKey);
12074
12228
  const url = buildTokenRatesUrl(validatedParams);
12075
12229
  const effectiveConfig = {
12076
12230
  ...DEFAULT_CONFIG,
@@ -16497,6 +16651,11 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16497
16651
  apiKey: serviceParams.apiKey
16498
16652
  }
16499
16653
  });
16654
+ // Per-swap correlation id returned by the service as a top-level response
16655
+ // field for every chain (EVM + Solana). Attached to success telemetry so a
16656
+ // swap can be correlated across records; never used for control flow.
16657
+ // Undefined only against a not-yet-upgraded service that omits it.
16658
+ const correlationId = serviceResponse.correlationId;
16500
16659
  // Build and return SwapResult
16501
16660
  return {
16502
16661
  tokenIn,
@@ -16506,6 +16665,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16506
16665
  fromAddress: serviceParams.fromAddress,
16507
16666
  toAddress: serviceParams.toAddress,
16508
16667
  txHash,
16668
+ ...correlationId !== undefined && {
16669
+ correlationId
16670
+ },
16509
16671
  executedTransactions,
16510
16672
  ...config !== undefined && {
16511
16673
  config
@@ -19405,7 +19567,13 @@ async function resolveTokenDecimals(context, resolvedParams) {
19405
19567
  * amountIn: '50.00'
19406
19568
  * })
19407
19569
  * ```
19408
- */ async function swap$1(context, params, /** @internal */ onBroadcast) {
19570
+ */ async function swap$1(context, params, /**
19571
+ * @internal
19572
+ * Invoked after a successful broadcast with the on-chain `txHash` and the
19573
+ * service-issued `correlationId` (join key for success telemetry). The
19574
+ * service returns `correlationId` for every chain (EVM + Solana); it is
19575
+ * undefined only against a not-yet-upgraded service that omits the field.
19576
+ */ onBroadcast) {
19409
19577
  // Step 1: Validate parameters using schema
19410
19578
  assertSwapParams(params, swapParamsSchema);
19411
19579
  // Step 2: Resolve parameters (chain definitions, wallet addresses, token aliases preserved)
@@ -19425,13 +19593,16 @@ async function resolveTokenDecimals(context, resolvedParams) {
19425
19593
  // Step 5: Execute swap via provider
19426
19594
  const swapParams = buildServiceSwapParams(resolvedParams, paramsWithFees);
19427
19595
  const providerResult = await provider.swap(swapParams);
19596
+ // `correlationId` is an internal telemetry join key, not part of the public
19597
+ // SwapResult — strip it here so it never leaks into the formatted result.
19598
+ const { correlationId, ...providerResultPublic } = providerResult;
19428
19599
  // A throwing `onBroadcast` must never strand the caller after a
19429
19600
  // successful swap broadcast — the chain has moved. `safeInvokeCallback`
19430
19601
  // swallows the error and surfaces a `console.warn` prefixed with
19431
19602
  // `[stablecoin-kits swap-kit] callback threw and was swallowed:` so a
19432
19603
  // kit-side closure bug stays debuggable rather than vanishing.
19433
19604
  safeInvokeCallback('swap-kit', ()=>{
19434
- onBroadcast?.(providerResult.txHash);
19605
+ onBroadcast?.(providerResultPublic.txHash, correlationId);
19435
19606
  });
19436
19607
  const { tokenInDecimals, tokenOutDecimals } = await resolveTokenDecimals(context, resolvedParams);
19437
19608
  // Step 6: Compose chain identity (owned by the kit, derived from the
@@ -19440,10 +19611,10 @@ async function resolveTokenDecimals(context, resolvedParams) {
19440
19611
  // resolves `progress` — the provider's snapshot, or a terminal `'DONE'` for
19441
19612
  // a provider that omits it (a synchronous same-chain completion).
19442
19613
  const composedResult = {
19443
- ...providerResult,
19614
+ ...providerResultPublic,
19444
19615
  chainIn: resolvedParams.from.chain,
19445
19616
  chainOut: resolvedParams.toChain ?? resolvedParams.from.chain,
19446
- progress: providerResult.progress ?? {
19617
+ progress: providerResultPublic.progress ?? {
19447
19618
  status: 'DONE'
19448
19619
  }
19449
19620
  };
@@ -20296,7 +20467,13 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20296
20467
  */ class SwapKit {
20297
20468
  context;
20298
20469
  /** Whether error telemetry is disabled. */ disableErrorReporting;
20299
- /** Per-kit telemetry identity for shared helpers. */ telemetryConfig;
20470
+ /** Per-kit telemetry identity for error reporting. */ telemetryConfig;
20471
+ /**
20472
+ * Per-kit telemetry identity for success/analytics events. Gated by
20473
+ * `disableAnalytics` (independent of `disableErrorReporting`) so a developer
20474
+ * can opt out of volume analytics without also silencing error reports —
20475
+ * mirrors EarnKit.
20476
+ */ analyticsTelemetryConfig;
20300
20477
  /**
20301
20478
  * Create a new SwapKit instance.
20302
20479
  *
@@ -20350,6 +20527,11 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20350
20527
  sdkVersion: pkg$1.version,
20351
20528
  disabled: this.disableErrorReporting
20352
20529
  };
20530
+ this.analyticsTelemetryConfig = {
20531
+ sdkName: SDK_NAME,
20532
+ sdkVersion: pkg$1.version,
20533
+ disabled: config.disableAnalytics === true
20534
+ };
20353
20535
  }
20354
20536
  /**
20355
20537
  * Estimate the output amount and fees for a swap operation.
@@ -20390,8 +20572,12 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20390
20572
  * console.log(`Fees:`, quote.fees)
20391
20573
  * ```
20392
20574
  */ async estimate(params) {
20575
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
20393
20576
  return withErrorTelemetry(async ()=>estimate(this.context, params), SWAP_EVENT_TYPES.ESTIMATE, this.telemetryConfig, {
20394
20577
  sourceChain: resolveChainName(params.from.chain),
20578
+ ...destinationChain != null && {
20579
+ destinationChain
20580
+ },
20395
20581
  tokenIn: params.tokenIn,
20396
20582
  tokenOut: params.tokenOut
20397
20583
  });
@@ -20450,16 +20636,54 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
20450
20636
  * ```
20451
20637
  */ async swap(params) {
20452
20638
  let txHash;
20453
- return withErrorTelemetry(async ()=>swap$1(this.context, params, (h)=>{
20454
- txHash = h;
20455
- }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, ()=>({
20639
+ let correlationId;
20640
+ // Shared context builder so the error resolver and the success emit stay in
20641
+ // lockstep — a field added here reaches both call sites. Reads the per-call
20642
+ // locals lazily, so txHash/correlationId (set during the swap) are captured
20643
+ // whenever it is invoked.
20644
+ // Destination chain is the primary attribution dimension for cross-chain
20645
+ // swaps; resolved once from the (static) params. Omitted for same-chain
20646
+ // swaps that leave `to.chain` unset (destination == source).
20647
+ const destinationChain = params.to?.chain ? resolveChainName(params.to.chain) : undefined;
20648
+ const buildTelemetryContext = ()=>({
20456
20649
  sourceChain: resolveChainName(params.from.chain),
20650
+ ...destinationChain != null && {
20651
+ destinationChain
20652
+ },
20457
20653
  tokenIn: params.tokenIn,
20458
20654
  tokenOut: params.tokenOut,
20459
20655
  ...txHash != null && {
20460
20656
  txHash
20657
+ },
20658
+ ...correlationId != null && {
20659
+ correlationId
20461
20660
  }
20462
- }));
20661
+ });
20662
+ const result = await withErrorTelemetry(async ()=>swap$1(this.context, params, (h, cId)=>{
20663
+ txHash = h;
20664
+ correlationId = cId;
20665
+ }), SWAP_EVENT_TYPES.SWAP, this.telemetryConfig, buildTelemetryContext);
20666
+ // withErrorTelemetry only emits on failure. Record the successful swap here
20667
+ // so the backend can attribute swap volume to a developer: the client event
20668
+ // carries the (burn) txHash + correlationId, which joins to the
20669
+ // server-emitted event carrying entity_id. Best-effort; never throws.
20670
+ //
20671
+ // Emit whenever a broadcast happened, i.e. we have the source/burn txHash.
20672
+ // For a cross-chain swap that is the source-chain burn (progress is still
20673
+ // PENDING while the destination mint settles) — we intentionally attribute
20674
+ // at broadcast using the burn txHash rather than tracking the destination
20675
+ // leg, which keeps the capture simple and self-contained in swap().
20676
+ //
20677
+ // Guard against a terminal-failure result: the EVM provider throws on
20678
+ // revert today, but the kit is provider-agnostic, so a provider that
20679
+ // returns a FAILED/NOT_FOUND result without throwing must not be recorded
20680
+ // as a successful swap. Routed through analyticsTelemetryConfig so it is
20681
+ // gated by disableAnalytics, independent of error reporting.
20682
+ const status = result.progress?.status;
20683
+ if (txHash != null && status !== 'FAILED' && status !== 'NOT_FOUND') {
20684
+ emitSuccessTelemetry(SWAP_EVENT_TYPES.SWAP, this.analyticsTelemetryConfig, buildTelemetryContext());
20685
+ }
20686
+ return result;
20463
20687
  }
20464
20688
  /**
20465
20689
  * Fetch the current status of a swap from the Stablecoin Service.
@@ -20838,6 +21062,9 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20838
21062
  const kit = new SwapKit({
20839
21063
  ...context.disableErrorReporting != null && {
20840
21064
  disableErrorReporting: context.disableErrorReporting
21065
+ },
21066
+ ...context.disableAnalytics != null && {
21067
+ disableAnalytics: context.disableAnalytics
20841
21068
  }
20842
21069
  });
20843
21070
  if (hasBoth) {
@@ -20899,7 +21126,7 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20899
21126
  };
20900
21127
 
20901
21128
  var name = "@circle-fin/earn-kit";
20902
- var version = "1.3.0";
21129
+ var version = "1.4.0";
20903
21130
  var pkg = {
20904
21131
  name: name,
20905
21132
  version: version};
@@ -22363,6 +22590,8 @@ function hasCrossChainDepositQuoteShape(params) {
22363
22590
  config: earnConfigSchema.optional()
22364
22591
  });
22365
22592
 
22593
+ /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg.name);
22594
+
22366
22595
  // Auto-register this kit for user agent tracking
22367
22596
  registerKit(`${pkg.name}/${pkg.version}`);
22368
22597