@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/earn.cjs CHANGED
@@ -18,15 +18,29 @@
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
+ require('@ethersproject/bytes');
35
+ require('@ethersproject/abi');
36
+ 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
  require('bs58');
27
41
  require('@noble/curves/ed25519');
28
- var keccak256 = require('@ethersproject/keccak256');
29
42
  var units = require('@ethersproject/units');
43
+ var keccak256 = require('@ethersproject/keccak256');
30
44
 
31
45
  // Import global type declarations
32
46
  /**
@@ -43,6 +57,51 @@ var units = require('@ethersproject/units');
43
57
  * }
44
58
  * ```
45
59
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
60
+ /**
61
+ * Check whether the current runtime exposes a browser DOM.
62
+ *
63
+ * @remarks
64
+ * This intentionally does not treat every non-Node runtime as a browser.
65
+ * Server-side edge runtimes such as Cloudflare Workers, Deno, and Supabase
66
+ * Edge Functions do not expose Node globals but can safely use server
67
+ * credentials. A Node.js runtime remains server-side even when a test or SSR
68
+ * environment provides a DOM shim.
69
+ *
70
+ * @returns `true` when running in a browser window, `false` otherwise.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * import { isBrowserEnvironment } from '@core/utils'
75
+ *
76
+ * if (isBrowserEnvironment()) {
77
+ * throw new Error('Server-only secrets must not be used in the browser')
78
+ * }
79
+ * ```
80
+ */ const isBrowserEnvironment = ()=>{
81
+ const browserWindow = globalThis.window;
82
+ return !isNodeEnvironment() && browserWindow?.document !== undefined;
83
+ };
84
+ /**
85
+ * Return the SDK User-Agent request header only when running in Node.js.
86
+ *
87
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
88
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
89
+ * attribution header because they cannot set it reliably.
90
+ *
91
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * import { getNodeUserAgentHeader } from '@core/utils'
96
+ *
97
+ * const headers = {
98
+ * 'Content-Type': 'application/json',
99
+ * ...getNodeUserAgentHeader(),
100
+ * }
101
+ * ```
102
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
103
+ 'User-Agent': getUserAgent()
104
+ } : {};
46
105
  /**
47
106
  * Detect the runtime environment and return a shortened identifier.
48
107
  *
@@ -1761,14 +1820,14 @@ class KitError extends Error {
1761
1820
  }
1762
1821
 
1763
1822
  /**
1764
- * Standardized error definitions for Earn/Zenith operations.
1823
+ * Standardized error definitions for Earn operations.
1765
1824
  *
1766
1825
  * These error codes provide fine-grained categorization of failures
1767
- * from the Zenith earn service, enabling SDK consumers to distinguish
1826
+ * from the Earn service, enabling SDK consumers to distinguish
1768
1827
  * between input errors (fix your request) and service errors (retry later).
1769
1828
  *
1770
1829
  * Error code ranges:
1771
- * - 1100-1105: INPUT errors — invalid inputs, unsupported configurations
1830
+ * - 1100-1106: INPUT errors — invalid, unsupported, or stale request state
1772
1831
  * - 8100-8105: SERVICE errors — retryable backend/provider failures
1773
1832
  *
1774
1833
  * @example
@@ -1821,6 +1880,14 @@ class KitError extends Error {
1821
1880
  name: 'EARN_UNSUPPORTED_BRIDGE_ROUTE',
1822
1881
  type: 'INPUT'
1823
1882
  },
1883
+ /**
1884
+ * The bridge quote expired. This is an INPUT error because the prepared
1885
+ * request is stale and must be replaced instead of retried.
1886
+ */ BRIDGE_QUOTE_EXPIRED: {
1887
+ code: 1106,
1888
+ name: 'EARN_BRIDGE_QUOTE_EXPIRED',
1889
+ type: 'INPUT'
1890
+ },
1824
1891
  /** The proxy signing call failed — retryable. */ SIGNING_FAILED: {
1825
1892
  code: 8100,
1826
1893
  name: 'EARN_SIGNING_FAILED',
@@ -1880,6 +1947,9 @@ function getOptionalString(value) {
1880
1947
  * internal-error, vault-refresh-busy, off-chain-paused, position-PnL-pending,
1881
1948
  * bridge failures/status lookup failures
1882
1949
  *
1950
+ * Quote expiry is INPUT/FATAL because callers must start a fresh bridge prepare
1951
+ * flow rather than retry the stale prepared bundle.
1952
+ *
1883
1953
  * Unrecognized codes fall through to `parseApiError` for HTTP-status-based
1884
1954
  * handling.
1885
1955
  *
@@ -2113,6 +2183,13 @@ function getOptionalString(value) {
2113
2183
  errorDef: EarnError.PROVIDER_ERROR,
2114
2184
  recoverability: 'FATAL'
2115
2185
  }
2186
+ ],
2187
+ [
2188
+ 380506,
2189
+ {
2190
+ errorDef: EarnError.BRIDGE_QUOTE_EXPIRED,
2191
+ recoverability: 'FATAL'
2192
+ }
2116
2193
  ]
2117
2194
  ]);
2118
2195
  /**
@@ -2836,7 +2913,10 @@ var EarnChain;
2836
2913
  contracts: {
2837
2914
  v1: {
2838
2915
  wallet: GATEWAY_WALLET_EVM_TESTNET,
2839
- minter: GATEWAY_MINTER_EVM_TESTNET
2916
+ minter: GATEWAY_MINTER_EVM_TESTNET,
2917
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
2918
+ // deposit into the GatewayWallet above.
2919
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
2840
2920
  }
2841
2921
  },
2842
2922
  forwarderSupported: {
@@ -5904,7 +5984,10 @@ var Chains = {
5904
5984
  minter: zod.z.string({
5905
5985
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
5906
5986
  invalid_type_error: 'Gateway minter address must be a string.'
5907
- }).min(1, 'Gateway minter address cannot be empty.')
5987
+ }).min(1, 'Gateway minter address cannot be empty.'),
5988
+ depositForHandler: zod.z.string({
5989
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
5990
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
5908
5991
  }).strict() // Reject any additional properties not defined in the schema
5909
5992
  ;
5910
5993
  /**
@@ -6367,6 +6450,39 @@ const swapTokenEnumSchema = zod.z.enum([
6367
6450
  throw new Error(`Invalid chain identifier type: ${typeof chainIdentifier}. Expected ChainDefinition object, Blockchain enum, or string literal.`);
6368
6451
  }
6369
6452
 
6453
+ /**
6454
+ * Resolve a chain identifier to a plain chain-name string.
6455
+ *
6456
+ * Accept a string literal (`'Ethereum'`), a `ChainDefinition`-like
6457
+ * object (`{ chain: 'Ethereum' }`), or `null`/`undefined` and return
6458
+ * the chain name as a string. Return `undefined` when the value
6459
+ * cannot be resolved.
6460
+ *
6461
+ * @remarks
6462
+ * Unlike `resolveChainIdentifier` (which returns a full `ChainDefinition`
6463
+ * and throws on invalid input), this helper is intentionally lenient and
6464
+ * never throws — it is safe to call in error-handling and telemetry paths.
6465
+ *
6466
+ * @param value - A string, chain-definition object, or nullish value.
6467
+ * @returns The chain name string, or `undefined`.
6468
+ *
6469
+ * @example
6470
+ * ```typescript
6471
+ * import { resolveChainName } from '@core/chains'
6472
+ *
6473
+ * resolveChainName('Ethereum') // 'Ethereum'
6474
+ * resolveChainName({ chain: 'Ethereum' }) // 'Ethereum'
6475
+ * resolveChainName(undefined) // undefined
6476
+ * ```
6477
+ */ function resolveChainName(value) {
6478
+ if (value == null) return undefined;
6479
+ if (typeof value === 'string') return value;
6480
+ if (typeof value === 'object' && 'chain' in value && typeof value.chain === 'string') {
6481
+ return value.chain;
6482
+ }
6483
+ return undefined;
6484
+ }
6485
+
6370
6486
  /**
6371
6487
  * Extracts chain information including name, display name, and expected address format.
6372
6488
  *
@@ -6751,13 +6867,12 @@ const swapTokenEnumSchema = zod.z.enum([
6751
6867
  headers: {
6752
6868
  ...DEFAULT_CONFIG$1.headers,
6753
6869
  ...config.headers ?? {},
6754
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
6755
- // This is why we use the 'X-User-Agent' header instead.
6756
- ...typeof window === 'undefined' ? {
6757
- 'User-Agent': getUserAgent()
6758
- } : {
6759
- 'X-User-Agent': getUserAgent()
6760
- }
6870
+ // Browsers forbid setting a user-agent request header, and the custom
6871
+ // fallback header the SDK used instead trips CORS preflight against the
6872
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
6873
+ // blocking the request. So send the SDK user agent only in Node;
6874
+ // browsers omit it entirely.
6875
+ ...getNodeUserAgentHeader()
6761
6876
  }
6762
6877
  };
6763
6878
  let lastError;
@@ -8016,6 +8131,223 @@ const swapTokenEnumSchema = zod.z.enum([
8016
8131
  return explorerUrl;
8017
8132
  }
8018
8133
 
8134
+ /**
8135
+ * CCTP forwarding magic bytes prefix.
8136
+ *
8137
+ * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
8138
+ * This prefix is right-padded to 24 bytes in the final hookData.
8139
+ */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
8140
+
8141
+ /**
8142
+ * Project an arbitrary payload onto the exact set of fields the telemetry
8143
+ * endpoint accepts.
8144
+ *
8145
+ * @remarks
8146
+ * Defense-in-depth before the last network hop: rather than
8147
+ * `JSON.stringify`-ing the caller's object verbatim, only the
8148
+ * allowlisted {@link ClientLogPayload} fields (and the allowlisted
8149
+ * sub-fields of `errorDetails` / `clientContext`) are copied across.
8150
+ * A regressing upstream mapper — or a plain-JS caller that bypasses the
8151
+ * type — therefore cannot exfiltrate stray properties (secrets, PII,
8152
+ * raw error stacks) through the analytics channel. Optional fields are
8153
+ * only included when present so the serialised shape matches the
8154
+ * server's strict schema.
8155
+ *
8156
+ * @internal
8157
+ */ function toSafePayload(payload) {
8158
+ const clientContext = {
8159
+ platform: payload.clientContext.platform,
8160
+ os: payload.clientContext.os,
8161
+ runtimeName: payload.clientContext.runtimeName
8162
+ };
8163
+ const safe = {
8164
+ sdkName: payload.sdkName,
8165
+ sdkVersion: payload.sdkVersion,
8166
+ eventType: payload.eventType,
8167
+ timestamp: payload.timestamp,
8168
+ clientContext
8169
+ };
8170
+ if (payload.sourceChain !== undefined) safe['sourceChain'] = payload.sourceChain;
8171
+ if (payload.destinationChain !== undefined) safe['destinationChain'] = payload.destinationChain;
8172
+ if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
8173
+ if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
8174
+ if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
8175
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
8176
+ if (payload.errorDetails !== undefined) {
8177
+ const errorDetails = {
8178
+ ...payload.errorDetails.errorCode !== undefined && {
8179
+ errorCode: payload.errorDetails.errorCode
8180
+ },
8181
+ ...payload.errorDetails.errorType !== undefined && {
8182
+ errorType: payload.errorDetails.errorType
8183
+ }
8184
+ };
8185
+ safe['errorDetails'] = errorDetails;
8186
+ }
8187
+ return safe;
8188
+ }
8189
+ /**
8190
+ * Default telemetry endpoint.
8191
+ *
8192
+ * Override via the `STABLECOIN_KITS_TELEMETRY_URL` environment variable
8193
+ * (e.g. for staging or local development).
8194
+ *
8195
+ * @internal
8196
+ */ const DEFAULT_LOGS_URL = 'https://api.circle.com/v1/stablecoinKits/logs';
8197
+ /**
8198
+ * Resolve the telemetry endpoint URL.
8199
+ *
8200
+ * @internal
8201
+ */ function getLogsUrl() {
8202
+ if (isNodeEnvironment() && typeof process.env['STABLECOIN_KITS_TELEMETRY_URL'] === 'string' && process.env['STABLECOIN_KITS_TELEMETRY_URL'].length > 0) {
8203
+ return process.env['STABLECOIN_KITS_TELEMETRY_URL'];
8204
+ }
8205
+ return DEFAULT_LOGS_URL;
8206
+ }
8207
+ /**
8208
+ * Send a telemetry event to the proxy service.
8209
+ *
8210
+ * @remarks
8211
+ * Fire-and-forget: the returned promise is intentionally not awaited
8212
+ * by the caller. A fetch failure (network error, non-2xx, timeout)
8213
+ * is silently swallowed so telemetry never blocks or fails user
8214
+ * operations.
8215
+ *
8216
+ * @param payload - The structured log payload matching the server schema.
8217
+ *
8218
+ * @example
8219
+ * ```typescript
8220
+ * import { emitAnalyticsLog } from '@core/utils'
8221
+ *
8222
+ * // Fire-and-forget — do not await
8223
+ * void emitAnalyticsLog(payload)
8224
+ * ```
8225
+ */ async function emitAnalyticsLog(payload) {
8226
+ // Hand-rolled timeout via `AbortController` + `setTimeout` rather than
8227
+ // `AbortSignal.timeout(...)` so we can `clearTimeout` the handle in a
8228
+ // `finally`. `AbortSignal.timeout` registers a timer that stays on the
8229
+ // event loop until it fires even if the fetch already settled, which
8230
+ // manifests as spurious `TimeoutError` unhandled rejections during
8231
+ // process teardown (notably between e2e test fork lifecycles). See
8232
+ // nodejs/node#48298 for the underlying issue.
8233
+ const controller = new AbortController();
8234
+ const timeoutHandle = setTimeout(()=>{
8235
+ controller.abort(new DOMException('Telemetry request timed out', 'TimeoutError'));
8236
+ }, 5_000);
8237
+ // Don't let the timer keep the Node event loop alive in short-lived
8238
+ // CLIs / test processes; telemetry is best-effort and must never
8239
+ // block clean process exit. `unref` only exists on Node's `Timeout`
8240
+ // object, not on the `number` returned by the browser's `setTimeout`,
8241
+ // so we feature-detect rather than call unconditionally.
8242
+ if (typeof timeoutHandle.unref === 'function') {
8243
+ timeoutHandle.unref();
8244
+ }
8245
+ try {
8246
+ await fetch(getLogsUrl(), {
8247
+ method: 'POST',
8248
+ headers: {
8249
+ 'Content-Type': 'application/json',
8250
+ // Browsers forbid setting a user-agent request header, and the custom
8251
+ // fallback header the SDK used instead trips CORS preflight (it isn't
8252
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
8253
+ // it only in Node; browsers omit it entirely.
8254
+ ...getNodeUserAgentHeader()
8255
+ },
8256
+ body: JSON.stringify(toSafePayload(payload)),
8257
+ signal: controller.signal
8258
+ });
8259
+ } catch {
8260
+ // Silently swallow — telemetry must never break user operations.
8261
+ } finally{
8262
+ clearTimeout(timeoutHandle);
8263
+ }
8264
+ }
8265
+
8266
+ /**
8267
+ * Build the `clientContext` object for telemetry payloads.
8268
+ *
8269
+ * @remarks
8270
+ * Use the exported `getRuntime()` and `isNodeEnvironment()` from
8271
+ * `@core/utils` to detect the runtime environment. The returned
8272
+ * string is parsed into the structured `ClientContext` fields
8273
+ * expected by the server schema.
8274
+ *
8275
+ * @returns A {@link ClientContext} with platform, OS, and runtime name
8276
+ * populated from the current environment.
8277
+ *
8278
+ * @example
8279
+ * ```typescript
8280
+ * import { buildClientContext } from '@core/utils'
8281
+ *
8282
+ * const ctx = buildClientContext()
8283
+ * // Node: { platform: 'node', os: 'darwin', runtimeName: null }
8284
+ * // Browser: { platform: 'browser', os: null, runtimeName: 'chrome' }
8285
+ * ```
8286
+ */ function buildClientContext() {
8287
+ const runtime = getRuntime();
8288
+ if (runtime.startsWith('browser/')) {
8289
+ return {
8290
+ platform: 'browser',
8291
+ os: null,
8292
+ runtimeName: runtime.slice('browser/'.length).toLowerCase()
8293
+ };
8294
+ }
8295
+ if (runtime.startsWith('node/')) {
8296
+ return {
8297
+ platform: 'node',
8298
+ os: isNodeEnvironment() ? process.platform : null,
8299
+ runtimeName: null
8300
+ };
8301
+ }
8302
+ return {
8303
+ platform: 'node',
8304
+ os: null,
8305
+ runtimeName: null
8306
+ };
8307
+ }
8308
+
8309
+ /**
8310
+ * Extract structured error details from an unknown error value.
8311
+ *
8312
+ * @remarks
8313
+ * Handle three cases:
8314
+ * - `KitError` — extract `code` and `name`.
8315
+ * - `Error` — extract `name`.
8316
+ * - Anything else — return empty details.
8317
+ *
8318
+ * Only structured, bounded fields (`errorCode`, `errorType`) are
8319
+ * included. Free-text fields (`message`, `stack`) are intentionally
8320
+ * omitted to avoid leaking secrets or PII through vendor telemetry.
8321
+ *
8322
+ * @param error - The thrown value to extract details from.
8323
+ * @returns A {@link ErrorDetails} object suitable for telemetry payloads.
8324
+ *
8325
+ * @example
8326
+ * ```typescript
8327
+ * import { extractErrorDetails } from '@core/utils'
8328
+ *
8329
+ * try {
8330
+ * await riskyOperation()
8331
+ * } catch (error) {
8332
+ * const details = extractErrorDetails(error)
8333
+ * // { errorCode: '1001', errorType: 'INPUT_NETWORK_MISMATCH' }
8334
+ * }
8335
+ * ```
8336
+ */ function extractErrorDetails(error) {
8337
+ if (error instanceof KitError) {
8338
+ return {
8339
+ errorCode: String(error.code),
8340
+ errorType: error.name
8341
+ };
8342
+ }
8343
+ if (error instanceof Error) {
8344
+ return {
8345
+ errorType: error.name
8346
+ };
8347
+ }
8348
+ return {};
8349
+ }
8350
+
8019
8351
  /**
8020
8352
  * Strip the `@circle-fin/` scope from a kit package name to produce the
8021
8353
  * short SDK name used in telemetry payloads.
@@ -8034,8 +8366,154 @@ const swapTokenEnumSchema = zod.z.enum([
8034
8366
  return pkgName.replace('@circle-fin/', '');
8035
8367
  }
8036
8368
 
8369
+ /**
8370
+ * Soft signal for the case where building or emitting a telemetry payload
8371
+ * threw — for example, a buggy `TelemetryContextResolver`, a regression in
8372
+ * `extractErrorDetails`, or a synchronous failure inside `emitAnalyticsLog`
8373
+ * before it could swallow the error itself. Logged with a stable prefix so
8374
+ * consumers can grep for it. We deliberately do not re-throw: the caller's
8375
+ * original operation error must always win.
8376
+ *
8377
+ * @internal
8378
+ */ function warnTelemetryDrop(eventType, cause) {
8379
+ try {
8380
+ // Pass `cause` as the second console.warn argument rather than
8381
+ // string-coercing it. `String(err)` (and `err.message` alone)
8382
+ // discards the stack trace, nested `cause`, and any custom Error
8383
+ // properties — exactly the context an on-call needs when a
8384
+ // resolver-closure regression triggers this path.
8385
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
8386
+ } catch {
8387
+ // console.warn itself throwing is the user's environment; nothing more we
8388
+ // can do without risking the original operation error.
8389
+ }
8390
+ }
8391
+ /**
8392
+ * Build a telemetry payload from common fields.
8393
+ *
8394
+ * @internal
8395
+ */ function buildPayload(config, eventType, errorDetails, context) {
8396
+ return {
8397
+ sdkName: config.sdkName,
8398
+ sdkVersion: config.sdkVersion,
8399
+ eventType,
8400
+ timestamp: new Date().toISOString(),
8401
+ ...errorDetails !== undefined && {
8402
+ errorDetails
8403
+ },
8404
+ clientContext: buildClientContext(),
8405
+ ...context?.sourceChain != null && {
8406
+ sourceChain: context.sourceChain
8407
+ },
8408
+ ...context?.destinationChain != null && {
8409
+ destinationChain: context.destinationChain
8410
+ },
8411
+ ...context?.tokenIn != null && {
8412
+ tokenIn: context.tokenIn
8413
+ },
8414
+ ...context?.tokenOut != null && {
8415
+ tokenOut: context.tokenOut
8416
+ },
8417
+ ...context?.txHash != null && {
8418
+ txHash: context.txHash
8419
+ },
8420
+ ...context?.correlationId != null && {
8421
+ correlationId: context.correlationId
8422
+ }
8423
+ };
8424
+ }
8425
+ /**
8426
+ * Emit telemetry for a completed operation without affecting its caller.
8427
+ *
8428
+ * No-ops when `config.disabled` is `true`. Like {@link withErrorTelemetry},
8429
+ * failures while constructing or submitting the telemetry payload are reported
8430
+ * as a soft warning and never change a completed operation's result.
8431
+ *
8432
+ * @param eventType - The telemetry event type for the completed operation.
8433
+ * @param config - Per-kit SDK identity and disabled flag.
8434
+ * @param context - Optional chain, token, and transaction context.
8435
+ * @returns Nothing.
8436
+ * @throws Never — telemetry failures are reported as warnings.
8437
+ *
8438
+ * @example
8439
+ * ```typescript
8440
+ * import { emitSuccessTelemetry } from '@core/utils'
8441
+ *
8442
+ * emitSuccessTelemetry(
8443
+ * 'bridge_bridge',
8444
+ * { sdkName: 'bridge-kit', sdkVersion: '1.0.0', disabled: false },
8445
+ * { sourceChain: 'Ethereum', destinationChain: 'Base', tokenIn: 'USDC' },
8446
+ * )
8447
+ * ```
8448
+ */ function emitSuccessTelemetry(eventType, config, context) {
8449
+ if (config.disabled) {
8450
+ return;
8451
+ }
8452
+ try {
8453
+ void emitAnalyticsLog(buildPayload(config, eventType, undefined, context));
8454
+ } catch (telemetryError) {
8455
+ warnTelemetryDrop(eventType, telemetryError);
8456
+ }
8457
+ }
8458
+ /**
8459
+ * Wrap an async operation with error telemetry.
8460
+ *
8461
+ * Execute `fn` and, if it throws, emit an error telemetry payload
8462
+ * before re-throwing. No-ops when `config.disabled` is `true`.
8463
+ *
8464
+ * `context` may be a static {@link TelemetryContext} or a
8465
+ * {@link TelemetryContextResolver}. The resolver is invoked in the
8466
+ * catch branch, so it can read state — most importantly `txHash` —
8467
+ * that the wrapped operation set after a successful broadcast. The
8468
+ * resolver must close over per-call locals only; passing instance
8469
+ * state would break isolation between concurrent invocations.
8470
+ *
8471
+ * @param fn - The async operation to execute.
8472
+ * @param eventType - The telemetry event type for this operation.
8473
+ * @param config - Per-kit SDK identity and disabled flag.
8474
+ * @param context - Optional context, static or lazily resolved.
8475
+ * @returns The result of the operation.
8476
+ * @throws Re-throws any error after emitting telemetry.
8477
+ *
8478
+ * @example
8479
+ * ```typescript
8480
+ * import { withErrorTelemetry } from '@core/utils'
8481
+ *
8482
+ * let txHash: string | undefined
8483
+ * const result = await withErrorTelemetry(
8484
+ * () => provider.swap(params, h => { txHash = h }),
8485
+ * 'swap_swap',
8486
+ * { sdkName: 'swap-kit', sdkVersion: '1.0.0', disabled: false },
8487
+ * () => ({
8488
+ * sourceChain: 'Ethereum',
8489
+ * tokenIn: 'USDC',
8490
+ * tokenOut: 'EURC',
8491
+ * ...(txHash != null && { txHash }),
8492
+ * }),
8493
+ * )
8494
+ * ```
8495
+ */ async function withErrorTelemetry(fn, eventType, config, context) {
8496
+ try {
8497
+ return await fn();
8498
+ } catch (error) {
8499
+ if (!config.disabled) {
8500
+ try {
8501
+ const resolved = typeof context === 'function' ? context() : context;
8502
+ void emitAnalyticsLog(buildPayload(config, eventType, extractErrorDetails(error), resolved));
8503
+ } catch (telemetryError) {
8504
+ // Never let telemetry emission mask the original operation error.
8505
+ // But surface a soft signal so silent telemetry drops are
8506
+ // discoverable (e.g. a regression in a resolver closure or in
8507
+ // `extractErrorDetails`) instead of vanishing without any trace.
8508
+ warnTelemetryDrop(eventType, telemetryError);
8509
+ }
8510
+ }
8511
+ throw error;
8512
+ }
8513
+ }
8514
+
8037
8515
  var name$3 = "@circle-fin/bridge-kit";
8038
- var version$3 = "1.12.0";
8516
+ var version$3 = "1.12.2";
8039
8517
  var pkg$3 = {
8040
8518
  name: name$3,
8041
8519
  version: version$3};
@@ -8860,6 +9338,17 @@ var TransferSpeed;
8860
9338
  clock: zod.z.any().optional()
8861
9339
  }).passthrough();
8862
9340
 
9341
+ /**
9342
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
9343
+ * hookData must start with.
9344
+ *
9345
+ * Encoded with `TextEncoder` (a browser-safe Web API) rather than `Buffer.from`
9346
+ * so this module-level constant does not reference the Node `Buffer` global at
9347
+ * import time. App Kit inlines this provider into its bundle without a Buffer polyfill, and a
9348
+ * bare `Buffer` here crashes browser bundles (e.g. Vite) on load — even for apps
9349
+ * that never touch the prepaid FORWARD path. Mirrors `buildForwardingHookData`.
9350
+ */ Array.from(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX)).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
9351
+
8863
9352
  /**
8864
9353
  * The minimum finality threshold for CCTPv2 transfers.
8865
9354
  *
@@ -8892,7 +9381,7 @@ var TransferSpeed;
8892
9381
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
8893
9382
 
8894
9383
  var name$2 = "@circle-fin/swap-kit";
8895
- var version$2 = "1.3.2";
9384
+ var version$2 = "1.5.0";
8896
9385
  var pkg$2 = {
8897
9386
  name: name$2,
8898
9387
  version: version$2};
@@ -8957,7 +9446,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
8957
9446
  }).min(1, 'kitKey must be a non-empty string').optional(),
8958
9447
  provider: zod.z.string({
8959
9448
  invalid_type_error: 'provider must be a string'
8960
- }).min(1, 'provider must be a non-empty string').optional()
9449
+ }).min(1, 'provider must be a non-empty string').optional(),
9450
+ batchTransactions: zod.z.boolean({
9451
+ invalid_type_error: 'batchTransactions must be a boolean'
9452
+ }).optional()
8961
9453
  });
8962
9454
  /**
8963
9455
  * Zod schema for adapter context.
@@ -9396,7 +9888,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9396
9888
  /**
9397
9889
  * Circle Stablecoin Service API Key.
9398
9890
  * Must be a valid API key format.
9399
- */ apiKey: apiKeySchema
9891
+ */ apiKey: apiKeySchema.optional()
9400
9892
  }).superRefine(requireCrossChainQuoteToAddress);
9401
9893
  /**
9402
9894
  * Zod schema for validating CreateSwapRequest parameters.
@@ -9454,7 +9946,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9454
9946
  /**
9455
9947
  * Circle Stablecoin Service API Key.
9456
9948
  * Must be a valid API key format.
9457
- */ apiKey: apiKeySchema
9949
+ */ apiKey: apiKeySchema.optional()
9458
9950
  });
9459
9951
  /**
9460
9952
  * Zod schema for validating GetSwapStatusResponse data.
@@ -9490,7 +9982,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9490
9982
  toChain: zod.z.string({
9491
9983
  invalid_type_error: 'toChain must be a string'
9492
9984
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
9493
- apiKey: apiKeySchema
9985
+ apiKey: apiKeySchema.optional()
9494
9986
  });
9495
9987
  /**
9496
9988
  * Zod schema for validating CreateSwapResponse payloads.
@@ -9499,13 +9991,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9499
9991
  required_error: 'fee token is required',
9500
9992
  invalid_type_error: 'fee token must be a string'
9501
9993
  }).min(1, 'fee token must be a non-empty string'),
9502
- amount: feeAmountSchema
9994
+ amount: feeAmountSchema,
9995
+ decimals: zod.z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
9996
+ symbol: zod.z.string({
9997
+ invalid_type_error: 'fee token symbol must be a string'
9998
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
9503
9999
  });
9504
10000
  /**
9505
10001
  * Developer fee item schema with basis field.
9506
- */ const createSwapDeveloperFeeItemSchema = zod.z.object({
9507
- token: zod.z.string().min(1, 'fee token must be a non-empty string'),
9508
- amount: feeAmountSchema,
10002
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
9509
10003
  basis: zod.z.enum([
9510
10004
  'inputAmount',
9511
10005
  'estimatedAmount'
@@ -9597,7 +10091,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9597
10091
  addresses: zod.z.array(zod.z.string({
9598
10092
  invalid_type_error: 'addresses entries must be strings'
9599
10093
  }).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(),
9600
- apiKey: apiKeySchema
10094
+ apiKey: apiKeySchema.optional()
9601
10095
  });
9602
10096
  /**
9603
10097
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -9628,6 +10122,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9628
10122
  required_error: 'estimatedAmount is required',
9629
10123
  invalid_type_error: 'estimatedAmount must be a string'
9630
10124
  }).min(1, 'estimatedAmount must be a non-empty string'),
10125
+ // Per-swap join key echoed back verbatim on success telemetry. Optional so a
10126
+ // not-yet-upgraded service (no field) still validates during rollout. A
10127
+ // malformed/non-UUID value is coerced to `undefined` (no telemetry id) rather
10128
+ // than throwing: this is a telemetry-only field (stripped from the developer
10129
+ // result, never used for control flow), so it must not be able to abort the
10130
+ // swap via `parseCreateSwapResponse().parse()`. Mirrors the best-effort,
10131
+ // never-throw contract of the rest of the telemetry stack. Implemented with
10132
+ // `preprocess` rather than Zod's `.catch()` because static analysis misreads
10133
+ // `.catch` on the schema chain as an unhandled Promise (S7785).
10134
+ correlationId: zod.z.preprocess((value)=>zod.z.string().uuid().safeParse(value).success ? value : undefined, zod.z.string().optional()),
9631
10135
  config: createSwapRequestBaseSchema.shape.config.optional(),
9632
10136
  fees: createSwapFeesSchema.optional(),
9633
10137
  transaction: createSwapTransactionSchema
@@ -12186,7 +12690,7 @@ new Set(Object.values(Blockchain));
12186
12690
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
12187
12691
 
12188
12692
  var name$1 = "@circle-fin/earn-kit";
12189
- var version$1 = "1.2.2";
12693
+ var version$1 = "1.4.0";
12190
12694
  var pkg$1 = {
12191
12695
  name: name$1,
12192
12696
  version: version$1};
@@ -12653,7 +13157,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12653
13157
  *
12654
13158
  * @param params - Adapter, chain, token/delegate/wallet addresses, the required
12655
13159
  * allowance for the signed payload, and a revert message for on-chain failure.
12656
- * @returns The approval transaction hash when an approval was submitted, or
13160
+ * @returns The approval transaction result when an approval was submitted, or
12657
13161
  * `undefined` when the existing allowance already covers `requiredAllowance`
12658
13162
  * (or `requiredAllowance` is zero).
12659
13163
  * @throws {@link KitError} If the `token.allowance` response is malformed.
@@ -12661,7 +13165,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12661
13165
  *
12662
13166
  * @example
12663
13167
  * ```typescript
12664
- * const txHash = await approveAllowanceIfNeeded({
13168
+ * const approval = await approveAllowanceIfNeeded({
12665
13169
  * adapter,
12666
13170
  * chain,
12667
13171
  * tokenAddress: usdcAddress,
@@ -12723,7 +13227,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12723
13227
  maxAttempts: params.allowancePropagation?.maxAttempts ?? DEFAULT_PROPAGATION_ATTEMPTS,
12724
13228
  delayMs: params.allowancePropagation?.delayMs ?? DEFAULT_PROPAGATION_DELAY_MS
12725
13229
  });
12726
- return approvalTxHash;
13230
+ return {
13231
+ txHash: approvalTxHash,
13232
+ ...approvalReceipt.gasUsed !== undefined && {
13233
+ gasUsed: approvalReceipt.gasUsed
13234
+ },
13235
+ ...approvalReceipt.effectiveGasPrice !== undefined && {
13236
+ effectiveGasPrice: approvalReceipt.effectiveGasPrice
13237
+ }
13238
+ };
12727
13239
  }
12728
13240
 
12729
13241
  /** @internal */ function isSameAddress(actual, expected) {
@@ -12871,7 +13383,13 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12871
13383
  }
12872
13384
  return {
12873
13385
  txHash,
12874
- explorerUrl
13386
+ explorerUrl,
13387
+ ...receipt.gasUsed !== undefined && {
13388
+ gasUsed: receipt.gasUsed
13389
+ },
13390
+ ...receipt.effectiveGasPrice !== undefined && {
13391
+ effectiveGasPrice: receipt.effectiveGasPrice
13392
+ }
12875
13393
  };
12876
13394
  }
12877
13395
 
@@ -13183,112 +13701,6 @@ const EARN_OPERATIONS = new Set([
13183
13701
  return hasEarnServiceParamsShape(operation, candidate['params']);
13184
13702
  }
13185
13703
 
13186
- function buildGasFeeBase(name, chain) {
13187
- return {
13188
- name,
13189
- token: chain.nativeCurrency.symbol,
13190
- blockchain: chain.chain
13191
- };
13192
- }
13193
- function buildGasFeeSuccess(name, chain, fees) {
13194
- return {
13195
- ...buildGasFeeBase(name, chain),
13196
- fees
13197
- };
13198
- }
13199
- function buildGasFeeFailure(name, chain, error) {
13200
- return {
13201
- ...buildGasFeeBase(name, chain),
13202
- fees: null,
13203
- error: getErrorMessage(error)
13204
- };
13205
- }
13206
- async function estimatePreparedGasFee(name, chain, prepared) {
13207
- try {
13208
- const estimate = bufferEstimatedGas(await prepared.estimate());
13209
- if (estimate.gas <= 0n) {
13210
- throw createValidationFailedError('estimate.gas', estimate.gas.toString(), 'gas estimate must be greater than zero');
13211
- }
13212
- return buildGasFeeSuccess(name, chain, estimate);
13213
- } catch (error) {
13214
- return buildGasFeeFailure(name, chain, error);
13215
- }
13216
- }
13217
- async function estimateApprovalGasFeeIfNeeded(params) {
13218
- const { adapter, chain, address, tokenAddress, delegate, requiredAllowance } = params;
13219
- if (requiredAllowance <= 0n) {
13220
- return undefined;
13221
- }
13222
- try {
13223
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
13224
- tokenAddress,
13225
- delegate
13226
- }, {
13227
- chain,
13228
- address
13229
- });
13230
- const allowanceRaw = await allowancePrepared.execute();
13231
- const currentAllowance = parseAllowanceResponse(allowanceRaw);
13232
- if (currentAllowance >= requiredAllowance) {
13233
- return undefined;
13234
- }
13235
- // Reuse the execute path's approval builder so the estimate simulates the
13236
- // exact approval (action, amount, and WARM_SLOT_RESIDUAL) that
13237
- // approveAllowanceIfNeeded later submits.
13238
- const approvalPrepared = await prepareApprovalAction({
13239
- adapter,
13240
- chain,
13241
- address,
13242
- tokenAddress,
13243
- delegate,
13244
- currentAllowance,
13245
- requiredAllowance
13246
- });
13247
- return await estimatePreparedGasFee('Approve', chain, approvalPrepared);
13248
- } catch (error) {
13249
- return buildGasFeeFailure('Approve', chain, error);
13250
- }
13251
- }
13252
- /**
13253
- * Estimate gas fee entries for an earn quote without submitting transactions.
13254
- *
13255
- * Each entry is produced by simulating the prepared transaction against
13256
- * current chain state. When an approval is required (allowance below the
13257
- * signed payload's required amount), the subsequent action simulation runs
13258
- * without that approval in place and is expected to revert — the action entry
13259
- * then carries `fees: null` with the revert message while the approval entry
13260
- * still estimates normally. Quote consumers must treat that as "estimate
13261
- * pending approval", not a hard failure.
13262
- *
13263
- * @internal
13264
- */ async function estimateEarnQuoteGasFees(params) {
13265
- const { adapter, chain, address, actionName, actionKey, actionParams, approval } = params;
13266
- const gasFees = [];
13267
- if (approval !== undefined) {
13268
- const approvalEstimate = await estimateApprovalGasFeeIfNeeded({
13269
- adapter,
13270
- chain,
13271
- address,
13272
- tokenAddress: approval.token,
13273
- delegate: approval.delegate,
13274
- requiredAllowance: approval.requiredAllowance
13275
- });
13276
- if (approvalEstimate !== undefined) {
13277
- gasFees.push(approvalEstimate);
13278
- }
13279
- }
13280
- try {
13281
- const actionPrepared = await adapter.prepareAction(actionKey, actionParams, {
13282
- chain,
13283
- address
13284
- });
13285
- gasFees.push(await estimatePreparedGasFee(actionName, chain, actionPrepared));
13286
- } catch (error) {
13287
- gasFees.push(buildGasFeeFailure(actionName, chain, error));
13288
- }
13289
- return gasFees;
13290
- }
13291
-
13292
13704
  // ---------------------------------------------------------------------------
13293
13705
  // Shared primitives
13294
13706
  // ---------------------------------------------------------------------------
@@ -13366,7 +13778,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
13366
13778
  asset: zod.z.string(),
13367
13779
  assetAddress: zod.z.string(),
13368
13780
  lltv: zod.z.number(),
13369
- supplyUsd: zod.z.number()
13781
+ supplyUsd: zod.z.number(),
13782
+ // Optional during the expand/contract window (a backend that predates the
13783
+ // field omits the key), mirroring the `.optional()` facets on the base
13784
+ // schema; `null` when the product exposes no per-market allocation (V2).
13785
+ allocationPct: zod.z.number().nullable().optional()
13370
13786
  });
13371
13787
  /**
13372
13788
  * Zod schema for a Morpho vault warning in the API response.
@@ -13380,15 +13796,82 @@ const bridgeFeeTokenSchema = hexAddressSchema;
13380
13796
  ])
13381
13797
  });
13382
13798
  /**
13383
- * Zod schema for a single vault info object in the API response.
13799
+ * Zod schema for the manager (curator) facet in the API response.
13384
13800
  *
13385
13801
  * @internal
13386
- */ const vaultInfoResponseSchema = zod.z.object({
13387
- vaultAddress: zod.z.string(),
13388
- chain: zod.z.string(),
13802
+ */ const managerSchema = zod.z.object({
13389
13803
  name: zod.z.string(),
13390
- protocol: zod.z.string(),
13391
- asset: zod.z.string(),
13804
+ address: zod.z.string().optional(),
13805
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
13806
+ // are added here as the providers that emit them land, rather than shipped
13807
+ // speculatively.
13808
+ type: zod.z.enum([
13809
+ 'curator'
13810
+ ])
13811
+ });
13812
+ /**
13813
+ * Zod schema for the APY profile facet in the API response.
13814
+ *
13815
+ * @internal
13816
+ */ const apyProfileSchema = zod.z.object({
13817
+ current: zod.z.number(),
13818
+ native: zod.z.number().nullable(),
13819
+ d7: zod.z.number().nullable(),
13820
+ d30: zod.z.number().nullable(),
13821
+ d90: zod.z.number().nullable(),
13822
+ rewardShare: zod.z.number().nullable(),
13823
+ source: zod.z.string().optional(),
13824
+ asOf: zod.z.string().optional()
13825
+ });
13826
+ /**
13827
+ * Zod schema for the fee split facet in the API response.
13828
+ *
13829
+ * @internal
13830
+ */ const feeInfoSchema = zod.z.object({
13831
+ performance: zod.z.number().nullable(),
13832
+ management: zod.z.number().nullable()
13833
+ });
13834
+ /**
13835
+ * Zod schema for the liquidity profile facet in the API response.
13836
+ *
13837
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
13838
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
13839
+ *
13840
+ * @internal
13841
+ */ const liquidityProfileSchema = zod.z.object({
13842
+ totalDeposits: amountJsonSchema,
13843
+ available: amountJsonSchema,
13844
+ totalSupply: amountJsonSchema,
13845
+ status: zod.z.enum([
13846
+ 'active',
13847
+ 'low_liquidity'
13848
+ ])
13849
+ });
13850
+ /**
13851
+ * Zod schema for the risk signals facet in the API response.
13852
+ *
13853
+ * @internal
13854
+ */ const riskSignalsSchema = zod.z.object({
13855
+ circleSentinel: zod.z.boolean(),
13856
+ warnings: zod.z.array(vaultWarningSchema).optional(),
13857
+ earnKitWarnings: zod.z.array(zod.z.string()).optional()
13858
+ });
13859
+ /**
13860
+ * Zod schema for the universal earn-opportunity base in the API response.
13861
+ *
13862
+ * Retains every existing deprecated flat field (kept validated through the
13863
+ * expand/contract window so default-strip does not drop them) and adds the
13864
+ * new nested facets. The nested facets are `.optional()` during the
13865
+ * transition so the SDK still validates against a not-yet-fully-deployed
13866
+ * backend; they become required after Expand ships.
13867
+ *
13868
+ * @internal
13869
+ */ const vaultInfoResponseSchema = zod.z.object({
13870
+ vaultAddress: zod.z.string(),
13871
+ chain: zod.z.string(),
13872
+ name: zod.z.string(),
13873
+ protocol: zod.z.string(),
13874
+ asset: zod.z.string(),
13392
13875
  assetAddress: zod.z.string(),
13393
13876
  currentApy: zod.z.number(),
13394
13877
  nativeApy: zod.z.number(),
@@ -13405,6 +13888,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
13405
13888
  warnings: zod.z.array(vaultWarningSchema).optional(),
13406
13889
  earnKitWarnings: zod.z.array(zod.z.string()).optional()
13407
13890
  });
13891
+ /**
13892
+ * Shared base schema: existing flat fields (kept) plus the new nested
13893
+ * facets and neutral identity. Facets are `.optional()` during the
13894
+ * transition; flip to required once the backend is confirmed emitting.
13895
+ *
13896
+ * @internal
13897
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
13898
+ address: zod.z.string().optional(),
13899
+ asOf: zod.z.string().optional(),
13900
+ manager: managerSchema.nullable().optional(),
13901
+ apyProfile: apyProfileSchema.optional(),
13902
+ fee: feeInfoSchema.optional(),
13903
+ liquidityProfile: liquidityProfileSchema.optional(),
13904
+ riskSignals: riskSignalsSchema.optional()
13905
+ });
13906
+ /**
13907
+ * Zod schema for the `vault` opportunity variant.
13908
+ *
13909
+ * @internal
13910
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
13911
+ productType: zod.z.literal('vault'),
13912
+ collateral: zod.z.array(collateralSchema)
13913
+ });
13914
+ /**
13915
+ * Discriminated union over `productType`. Add union members here as new
13916
+ * product types (e.g. `lending_market`, `rwa_token`) land.
13917
+ *
13918
+ * @internal
13919
+ */ const earnOpportunityVariants = [
13920
+ vaultOpportunitySchema
13921
+ ];
13922
+ /** @internal */ const earnOpportunitySchema = zod.z.discriminatedUnion('productType', earnOpportunityVariants);
13923
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
13924
+ /**
13925
+ * Tolerant list parser for earn opportunities.
13926
+ *
13927
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
13928
+ * `z.array` fails the whole array if any element fails. Two migration-window
13929
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
13930
+ *
13931
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
13932
+ * only opportunity type then, so default a missing discriminant to `'vault'`
13933
+ * rather than dropping every vault the backend returns.
13934
+ * - A future backend adds a *second* `productType` this SDK version does not
13935
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
13936
+ * of rejecting the whole list.
13937
+ *
13938
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
13939
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
13940
+ * primitives, or an object whose `productType` is malformed — is passed through
13941
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
13942
+ * validation failure. It is deliberately not silently dropped (which would hide
13943
+ * malformed backend data) and never throws here (an unguarded property read on
13944
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
13945
+ * `ZodError`).
13946
+ *
13947
+ * @internal
13948
+ */ const earnOpportunityListSchema = zod.z.preprocess((raw)=>{
13949
+ if (!Array.isArray(raw)) {
13950
+ return raw;
13951
+ }
13952
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
13953
+ // map/filter chain stays type-safe and no `any` leaks into the return.
13954
+ const entries = raw;
13955
+ return entries.map((entry)=>{
13956
+ // Only touch plain objects; non-objects fall through to fail validation.
13957
+ if (typeof entry !== 'object' || entry === null) {
13958
+ return entry;
13959
+ }
13960
+ const record = entry;
13961
+ // Older backend predating productType: default to the only type then.
13962
+ return record.productType === undefined ? {
13963
+ ...record,
13964
+ productType: 'vault'
13965
+ } : record;
13966
+ }).filter((entry)=>{
13967
+ // Drop ONLY a present-but-unknown string discriminant (a future
13968
+ // productType this SDK version doesn't know). Everything else —
13969
+ // non-objects, a non-string productType — flows through to
13970
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
13971
+ if (typeof entry !== 'object' || entry === null) {
13972
+ return true;
13973
+ }
13974
+ const productType = entry.productType;
13975
+ if (typeof productType !== 'string') {
13976
+ return true;
13977
+ }
13978
+ return knownProductTypes.has(productType);
13979
+ });
13980
+ }, zod.z.array(earnOpportunitySchema));
13408
13981
  // ---------------------------------------------------------------------------
13409
13982
  // Position response schema
13410
13983
  // ---------------------------------------------------------------------------
@@ -13534,6 +14107,7 @@ const positionPnlSchema = zod.z.discriminatedUnion('status', [
13534
14107
  *
13535
14108
  * @internal
13536
14109
  */ const depositPayloadSchema = zod.z.object({
14110
+ execId: bridgeDepositExecIdSchema,
13537
14111
  executionParams: depositExecutionParamsSchema,
13538
14112
  signature: hexSignatureSchema
13539
14113
  });
@@ -13625,6 +14199,21 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13625
14199
  amount: amountJsonSchema,
13626
14200
  vaultAddress: hexAddressSchema
13627
14201
  }).passthrough();
14202
+ /** @internal */ const bridgeQuoteExpirySchema = zod.z.discriminatedUnion('mode', [
14203
+ zod.z.object({
14204
+ mode: zod.z.literal('TIMESTAMP'),
14205
+ expiresAt: zod.z.string().datetime({
14206
+ offset: true
14207
+ })
14208
+ }),
14209
+ zod.z.object({
14210
+ mode: zod.z.literal('BLOCK_NUMBER'),
14211
+ expiresAtBlock: zod.z.number().int(),
14212
+ blockEstimatedAt: zod.z.string().datetime({
14213
+ offset: true
14214
+ }).optional()
14215
+ })
14216
+ ]).optional().catch(undefined);
13628
14217
  /**
13629
14218
  * Zod schema for the bridge deposit prepare payload.
13630
14219
  *
@@ -13636,6 +14225,10 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13636
14225
  execId: bridgeDepositExecIdSchema,
13637
14226
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
13638
14227
  expiresAt: zod.z.string().datetime(),
14228
+ quoteIssuedAt: zod.z.string().datetime({
14229
+ offset: true
14230
+ }).optional().catch(undefined),
14231
+ quoteExpiry: bridgeQuoteExpirySchema,
13639
14232
  review: bridgeDepositPrepareReviewSchema
13640
14233
  });
13641
14234
  /**
@@ -13701,6 +14294,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13701
14294
  *
13702
14295
  * @internal
13703
14296
  */ const withdrawPayloadSchema = zod.z.object({
14297
+ execId: bridgeDepositExecIdSchema,
13704
14298
  executionParams: withdrawExecutionParamsSchema,
13705
14299
  signature: hexSignatureSchema
13706
14300
  });
@@ -13714,6 +14308,27 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13714
14308
  data: withdrawPayloadSchema
13715
14309
  });
13716
14310
  // ---------------------------------------------------------------------------
14311
+ // Transaction report response schema
14312
+ // ---------------------------------------------------------------------------
14313
+ /**
14314
+ * Zod schema for the transaction report payload inside the API `data` envelope.
14315
+ *
14316
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
14317
+ * schema accepts any object shape and does not require specific fields.
14318
+ *
14319
+ * @internal
14320
+ */ const transactionReportPayloadSchema = zod.z.object({}).passthrough();
14321
+ /**
14322
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
14323
+ *
14324
+ * The Earn Service API wraps the transaction report payload in a `data`
14325
+ * envelope.
14326
+ *
14327
+ * @internal
14328
+ */ const transactionReportResponseSchema = zod.z.object({
14329
+ data: transactionReportPayloadSchema
14330
+ });
14331
+ // ---------------------------------------------------------------------------
13717
14332
  // Claim rewards response schema
13718
14333
  // ---------------------------------------------------------------------------
13719
14334
  /**
@@ -13774,6 +14389,30 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13774
14389
  token: zod.z.string(),
13775
14390
  amount: amountJsonSchema
13776
14391
  });
14392
+ /**
14393
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
14394
+ *
14395
+ * The Earn Service backend estimates gas server-side and returns one entry per
14396
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
14397
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
14398
+ * integer string in the chain's native base units. When the backend cannot
14399
+ * estimate an action it returns `fees: null` with an `error` message instead.
14400
+ *
14401
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
14402
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
14403
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
14404
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
14405
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
14406
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
14407
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
14408
+ * `fee`) must never fail Zod validation and reject the entire quote.
14409
+ *
14410
+ * @internal
14411
+ */ const quoteGasFeeSchema = zod.z.object({
14412
+ name: zod.z.string().optional(),
14413
+ fees: zod.z.unknown(),
14414
+ error: zod.z.string().optional()
14415
+ }).passthrough();
13777
14416
  /**
13778
14417
  * Zod schema for the inner deposit quote payload.
13779
14418
  *
@@ -13789,7 +14428,8 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13789
14428
  expectedShares: amountJsonSchema,
13790
14429
  sharePrice: zod.z.string(),
13791
14430
  currentApy: zod.z.number(),
13792
- fees: zod.z.array(feeSchema).optional()
14431
+ fees: zod.z.array(feeSchema).optional(),
14432
+ gasFees: zod.z.array(quoteGasFeeSchema).optional()
13793
14433
  });
13794
14434
  /**
13795
14435
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -13816,6 +14456,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13816
14456
  sharePrice: zod.z.string(),
13817
14457
  maxWithdrawable: amountJsonSchema,
13818
14458
  fees: zod.z.array(feeSchema),
14459
+ gasFees: zod.z.array(quoteGasFeeSchema).optional(),
13819
14460
  warnings: zod.z.array(zod.z.string()).optional()
13820
14461
  });
13821
14462
  /**
@@ -13873,7 +14514,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13873
14514
  *
13874
14515
  * @internal
13875
14516
  */ const getVaultsPayloadSchema = zod.z.object({
13876
- vaults: zod.z.array(vaultInfoResponseSchema),
14517
+ vaults: earnOpportunityListSchema,
13877
14518
  errors: zod.z.array(vaultErrorSchema)
13878
14519
  });
13879
14520
  /**
@@ -13903,7 +14544,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13903
14544
  *
13904
14545
  * @internal
13905
14546
  */ const exploreVaultsPayloadSchema = zod.z.object({
13906
- vaults: zod.z.array(vaultInfoResponseSchema),
14547
+ vaults: earnOpportunityListSchema,
13907
14548
  pagination: explorePaginationSchema
13908
14549
  });
13909
14550
  /**
@@ -13996,6 +14637,16 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
13996
14637
  */ function isWithdrawResponse(value) {
13997
14638
  return withdrawResponseSchema.safeParse(value).success;
13998
14639
  }
14640
+ /**
14641
+ * Type guard for the transaction report API response.
14642
+ *
14643
+ * @param value - Unknown response value to validate
14644
+ * @returns True when the value matches the transaction report response shape
14645
+ *
14646
+ * @internal
14647
+ */ function isTransactionReportResponse(value) {
14648
+ return transactionReportResponseSchema.safeParse(value).success;
14649
+ }
13999
14650
  /**
14000
14651
  * Type guard for the claim rewards API response.
14001
14652
  *
@@ -14038,7 +14689,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
14038
14689
  }
14039
14690
 
14040
14691
  var name = "@circle-fin/provider-earn-service";
14041
- var version = "1.2.2";
14692
+ var version = "1.3.1";
14042
14693
  var pkg = {
14043
14694
  name: name,
14044
14695
  version: version};
@@ -14100,15 +14751,25 @@ var pkg = {
14100
14751
  *
14101
14752
  * @internal
14102
14753
  */ function buildConfig(serviceConfig) {
14754
+ // The kit key is a server-only secret. Reject it in the browser so it cannot
14755
+ // leak into a client bundle (no-op in Node.js). Keyless usage stays allowed.
14756
+ if (serviceConfig?.kitKey !== undefined && isBrowserEnvironment()) {
14757
+ throw createValidationFailedError('kitKey', '[redacted]', 'kitKey must not be provided in a browser environment — it is a server-only secret. Run EarnKit operations that use a kit key on your server, or omit kitKey to use the permissionless (keyless) client path');
14758
+ }
14103
14759
  const baseUrl = serviceConfig?.baseUrl ?? EARN_SERVICE_BASE_URL;
14104
- const sdkVersion = resolveSdkVersionHeader();
14760
+ // The API CORS policy does not allow this custom header. Keep the existing
14761
+ // per-request version attribution for Node callers, but omit it in browsers
14762
+ // so public EarnKit endpoints do not fail at CORS preflight.
14763
+ const sdkVersionHeader = isNodeEnvironment() ? {
14764
+ [SDK_VERSION_HEADER]: resolveSdkVersionHeader()
14765
+ } : {};
14105
14766
  if (serviceConfig?.kitKey === undefined) {
14106
14767
  return {
14107
14768
  pollingConfig: {
14108
14769
  ...DEFAULT_CONFIG,
14109
14770
  headers: {
14110
14771
  ...DEFAULT_CONFIG.headers,
14111
- [SDK_VERSION_HEADER]: sdkVersion
14772
+ ...sdkVersionHeader
14112
14773
  }
14113
14774
  },
14114
14775
  baseUrl
@@ -14126,7 +14787,7 @@ var pkg = {
14126
14787
  ...DEFAULT_CONFIG,
14127
14788
  headers: {
14128
14789
  ...DEFAULT_CONFIG.headers,
14129
- [SDK_VERSION_HEADER]: sdkVersion,
14790
+ ...sdkVersionHeader,
14130
14791
  Authorization: `Bearer ${serviceConfig.kitKey}`
14131
14792
  }
14132
14793
  },
@@ -14158,7 +14819,7 @@ var pkg = {
14158
14819
  }
14159
14820
 
14160
14821
  /**
14161
- * Convert an API vault info object into the SDK {@link VaultInfo} shape.
14822
+ * Convert an API vault info object into the SDK {@link EarnOpportunity} shape.
14162
14823
  *
14163
14824
  * Map the API chain code back to the SDK chain identifier and hydrate the
14164
14825
  * amount payloads into {@link Amount} instances.
@@ -14169,16 +14830,29 @@ var pkg = {
14169
14830
  *
14170
14831
  * @internal
14171
14832
  */ function toVaultInfo(data) {
14172
- const { totalDeposits, liquidity, ...vault } = data;
14833
+ const { totalDeposits, liquidity, liquidityProfile, ...vault } = data;
14173
14834
  const chain = toSdkChain(vault.chain);
14174
14835
  if (chain === undefined) {
14175
14836
  throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
14176
14837
  }
14838
+ // The nested facets are `.optional()` in the schema (a backend that predates
14839
+ // them omits them) and are typed optional on `EarnOpportunity` to match.
14840
+ // Convert the nested liquidity amounts when present and pass the remaining
14841
+ // facets straight through; each absent facet stays absent rather than being
14842
+ // asserted present by a cast.
14177
14843
  return {
14178
14844
  ...vault,
14179
14845
  chain,
14180
14846
  totalDeposits: Amount.fromJSON(totalDeposits),
14181
- liquidity: Amount.fromJSON(liquidity)
14847
+ liquidity: Amount.fromJSON(liquidity),
14848
+ ...liquidityProfile !== undefined && {
14849
+ liquidityProfile: {
14850
+ ...liquidityProfile,
14851
+ totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
14852
+ available: Amount.fromJSON(liquidityProfile.available),
14853
+ totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
14854
+ }
14855
+ }
14182
14856
  };
14183
14857
  }
14184
14858
 
@@ -14213,8 +14887,11 @@ function toVaultError(error) {
14213
14887
  }
14214
14888
  try {
14215
14889
  const response = await pollApiGet(url.toString(), isGetVaultsResponse, pollingConfig);
14890
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
14891
+ // does not run the schema's preprocess. Parse explicitly so unknown
14892
+ // `productType` values are dropped before `toVaultInfo`.
14216
14893
  return {
14217
- vaults: response.data.vaults.map(toVaultInfo),
14894
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
14218
14895
  errors: response.data.errors.map(toVaultError)
14219
14896
  };
14220
14897
  } catch (error) {
@@ -14263,8 +14940,11 @@ function toVaultError(error) {
14263
14940
  }
14264
14941
  try {
14265
14942
  const response = await pollApiGet(url.toString(), isExploreVaultsResponse, pollingConfig);
14943
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
14944
+ // does not run the schema's preprocess. Parse explicitly so unknown
14945
+ // `productType` values are dropped before `toVaultInfo`.
14266
14946
  return {
14267
- vaults: response.data.vaults.map(toVaultInfo),
14947
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
14268
14948
  pagination: response.data.pagination
14269
14949
  };
14270
14950
  } catch (error) {
@@ -14428,6 +15108,12 @@ function toPositionInfo(data) {
14428
15108
  execId: response.data.execId,
14429
15109
  preparedBundle,
14430
15110
  expiresAt: response.data.expiresAt,
15111
+ ...response.data.quoteIssuedAt !== undefined && {
15112
+ quoteIssuedAt: response.data.quoteIssuedAt
15113
+ },
15114
+ ...response.data.quoteExpiry !== undefined && {
15115
+ quoteExpiry: response.data.quoteExpiry
15116
+ },
14431
15117
  review: response.data.review
14432
15118
  };
14433
15119
  } catch (error) {
@@ -14769,7 +15455,110 @@ function toClaimedAmount(reward) {
14769
15455
  }
14770
15456
  }
14771
15457
 
14772
- function toDepositQuoteInfo(data) {
15458
+ /**
15459
+ * Map the Earn Service's server-side quote gas estimates into the SDK
15460
+ * {@link EarnGasFeeEstimate} shape.
15461
+ *
15462
+ * The Earn Service estimates gas for each action (`Approve`, `Deposit`,
15463
+ * `Withdraw`) and returns `{ name, fees: { gas, gasPrice, fee } }` with raw
15464
+ * integer strings.
15465
+ * The SDK type additionally carries `token` (the chain's native currency
15466
+ * symbol) and `blockchain`, which are filled in here from the chain
15467
+ * definition.
15468
+ *
15469
+ * Gas reporting is best-effort: a malformed entry (e.g. a non-integer string
15470
+ * that fails `BigInt` parsing) degrades to a `{ fees: null, error }` estimate
15471
+ * rather than throwing, so one bad entry never fails the whole quote.
15472
+ *
15473
+ * @param gasFees - Backend gas-fee entries from the quote response, if any.
15474
+ * @param chain - Chain definition, used for the native token symbol and
15475
+ * blockchain identifier.
15476
+ * @returns One {@link EarnGasFeeEstimate} per backend entry (empty when the
15477
+ * backend returned none).
15478
+ *
15479
+ * @example
15480
+ * ```typescript
15481
+ * toQuoteGasFees(
15482
+ * [{ name: 'Deposit', fees: { gas: '364142', gasPrice: '21000000000', fee: '7646982000000000' } }],
15483
+ * arcTestnet,
15484
+ * )
15485
+ * // [{ name: 'Deposit', token: 'USDC', blockchain: 'Arc_Testnet',
15486
+ * // fees: { gas: 364142n, gasPrice: 21000000000n, fee: '7646982000000000' } }]
15487
+ * ```
15488
+ *
15489
+ * @internal
15490
+ */ function toQuoteGasFees(gasFees, chain) {
15491
+ if (gasFees === undefined) {
15492
+ return [];
15493
+ }
15494
+ return gasFees.map((entry)=>{
15495
+ const base = {
15496
+ // `name` is optional on the wire; label an unnamed entry rather than
15497
+ // emitting `name: undefined`.
15498
+ name: entry.name ?? 'Unknown',
15499
+ token: chain.nativeCurrency.symbol,
15500
+ blockchain: chain.chain
15501
+ };
15502
+ // The Earn Service itself reports a failed estimate as `fees: null` with
15503
+ // an error; propagate that soft failure verbatim.
15504
+ if (entry.fees === null || entry.fees === undefined) {
15505
+ return {
15506
+ ...base,
15507
+ fees: null,
15508
+ error: entry.error ?? 'gas estimate unavailable'
15509
+ };
15510
+ }
15511
+ // `fees` is `unknown` at the schema layer, so ALL validation happens here:
15512
+ // that it is an object at all, and that `gas`, `gasPrice`, and `fee` are
15513
+ // each parseable integer strings (including `fee`, which the SDK contract
15514
+ // requires be a numeric base-unit string). Any failure — a wrong type
15515
+ // (`fees: 123`), a missing field, or a non-numeric value — degrades the
15516
+ // whole entry to a `fees: null` soft failure rather than surfacing a
15517
+ // malformed "successful" estimate or rejecting the quote.
15518
+ try {
15519
+ if (typeof entry.fees !== 'object') {
15520
+ throw new TypeError(`gas fees must be an object (got ${typeof entry.fees})`);
15521
+ }
15522
+ const { gas, gasPrice, fee } = entry.fees;
15523
+ return {
15524
+ ...base,
15525
+ fees: {
15526
+ gas: toBigInt('gas', gas),
15527
+ gasPrice: toBigInt('gasPrice', gasPrice),
15528
+ fee: toBigInt('fee', fee).toString()
15529
+ }
15530
+ };
15531
+ } catch (error) {
15532
+ return {
15533
+ ...base,
15534
+ fees: null,
15535
+ error: getErrorMessage(error)
15536
+ };
15537
+ }
15538
+ });
15539
+ }
15540
+ /**
15541
+ * Parse an unknown value into a `bigint`, rejecting anything that is not a
15542
+ * non-empty integer string. `BigInt` alone is too permissive for this path —
15543
+ * it accepts numbers, booleans, and empty strings — so guard the type first.
15544
+ *
15545
+ * @param field - Field name, used in the thrown error message.
15546
+ * @param value - Raw value from the backend gas entry.
15547
+ * @returns The parsed `bigint`.
15548
+ * @throws {TypeError} When `value` is not a non-empty integer string.
15549
+ */ function toBigInt(field, value) {
15550
+ if (typeof value !== 'string' || value.trim() === '') {
15551
+ throw new TypeError(`gas fee field "${field}" must be an integer string`);
15552
+ }
15553
+ try {
15554
+ // BigInt throws on non-integer strings (e.g. "1.5", "not-a-number").
15555
+ return BigInt(value);
15556
+ } catch {
15557
+ throw new Error(`gas fee field "${field}" is not a valid integer string: ${value}`);
15558
+ }
15559
+ }
15560
+
15561
+ function toDepositQuoteInfo(data, chain) {
14773
15562
  const fees = (data.fees ?? []).map(({ token: feeTokenSymbol, ...fee })=>{
14774
15563
  // Earn Service returns fee.token as a display symbol, for example "USDC".
14775
15564
  return {
@@ -14798,7 +15587,10 @@ function toDepositQuoteInfo(data) {
14798
15587
  sharePrice: data.sharePrice,
14799
15588
  currentApy: data.currentApy,
14800
15589
  fees,
14801
- gasFees: []
15590
+ // The Earn Service estimates gas server-side; the chain fills token/blockchain.
15591
+ // Cross-chain quotes resolve no local chain definition, so gasFees stays
15592
+ // empty there (unchanged behavior).
15593
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain)
14802
15594
  };
14803
15595
  }
14804
15596
  /**
@@ -14833,7 +15625,7 @@ function toDepositQuoteInfo(data) {
14833
15625
  };
14834
15626
  try {
14835
15627
  const response = await pollApiPost(url.toString(), requestBody, isDepositQuoteResponse, pollingConfig);
14836
- return toDepositQuoteInfo(response.data);
15628
+ return toDepositQuoteInfo(response.data, params.chainDefinition);
14837
15629
  } catch (error) {
14838
15630
  throw parseEarnApiError(error, {
14839
15631
  operation: 'getDepositQuote'
@@ -14841,7 +15633,7 @@ function toDepositQuoteInfo(data) {
14841
15633
  }
14842
15634
  }
14843
15635
 
14844
- function toWithdrawalQuoteInfo(data) {
15636
+ function toWithdrawalQuoteInfo(data, chain) {
14845
15637
  return {
14846
15638
  vaultAddress: data.vaultAddress,
14847
15639
  vaultName: data.vaultName,
@@ -14869,7 +15661,7 @@ function toWithdrawalQuoteInfo(data) {
14869
15661
  status
14870
15662
  }
14871
15663
  })),
14872
- gasFees: [],
15664
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain),
14873
15665
  // Wire format uses `warnings`, but the SDK surface uses
14874
15666
  // `earnKitWarnings` to match the precedent set by `VaultInfo` —
14875
15667
  // `warnings` is reserved for the structured `VaultWarning` shape.
@@ -14901,7 +15693,7 @@ function toWithdrawalQuoteInfo(data) {
14901
15693
  };
14902
15694
  try {
14903
15695
  const response = await pollApiPost(url.toString(), requestBody, isWithdrawalQuoteResponse, pollingConfig);
14904
- return toWithdrawalQuoteInfo(response.data);
15696
+ return toWithdrawalQuoteInfo(response.data, params.chainDefinition);
14905
15697
  } catch (error) {
14906
15698
  throw parseEarnApiError(error, {
14907
15699
  operation: 'getWithdrawalQuote'
@@ -14937,6 +15729,8 @@ function toWithdrawalQuoteInfo(data) {
14937
15729
  amount: Amount.fromJSON(r.amount),
14938
15730
  address: r.token
14939
15731
  })),
15732
+ // The claimRewards/quote response does not carry a gas estimate (unlike
15733
+ // deposit/withdrawal quotes), so there is nothing to surface here.
14940
15734
  gasFees: []
14941
15735
  };
14942
15736
  } catch (error) {
@@ -14946,6 +15740,83 @@ function toWithdrawalQuoteInfo(data) {
14946
15740
  }
14947
15741
  }
14948
15742
 
15743
+ /**
15744
+ * Build the native gas triple the backend expects for `gasUsed`.
15745
+ *
15746
+ * Returns `undefined` unless both receipt components are present, so the
15747
+ * caller can omit the field entirely — the Earn Service treats a missing
15748
+ * triple as "skip the gas cache write, still return 200".
15749
+ *
15750
+ * @param gasUsed - Receipt gas units used.
15751
+ * @param effectiveGasPrice - Receipt effective gas price.
15752
+ * @returns The `{ gas, gasPrice, fee }` triple, or `undefined` when either
15753
+ * component is missing.
15754
+ *
15755
+ * @example
15756
+ * ```typescript
15757
+ * buildReportedGasUsed(362454n, 29466364605n)
15758
+ * // { gas: '362454', gasPrice: '29466364605', fee: '10680201716540670' }
15759
+ * ```
15760
+ *
15761
+ * @internal
15762
+ */ function buildReportedGasUsed(gasUsed, effectiveGasPrice) {
15763
+ if (gasUsed === undefined || effectiveGasPrice === undefined) {
15764
+ return undefined;
15765
+ }
15766
+ return {
15767
+ gas: gasUsed.toString(),
15768
+ gasPrice: effectiveGasPrice.toString(),
15769
+ fee: (gasUsed * effectiveGasPrice).toString()
15770
+ };
15771
+ }
15772
+ /**
15773
+ * Report the outcome of an SDK-submitted same-chain Earn transaction.
15774
+ *
15775
+ * @param params - Transaction report parameters.
15776
+ * @throws {@link KitError} When the API call fails.
15777
+ *
15778
+ * @internal
15779
+ */ async function reportEarnTransaction(params) {
15780
+ const { pollingConfig, baseUrl } = buildConfig(params.config);
15781
+ const url = new URL(`${EARN_KIT_API_PREFIX}/transactions/report`, baseUrl);
15782
+ // The report endpoint is not idempotent: success reports refresh the gas
15783
+ // cache and failure reports increment counts. If the first request succeeds
15784
+ // server-side but the client times out or sees a transient 5xx, retrying
15785
+ // would duplicate the report (double-writing an outcome or inflating failure
15786
+ // counts). Reporting is best-effort (see the fire-and-forget caller), so
15787
+ // make exactly one attempt and never retry — a single dropped report is
15788
+ // preferable to a duplicated one. `maxRetries` here is the total attempt
15789
+ // count in pollApiWithValidation (loop runs `attempt <= maxRetries`), so 1
15790
+ // means one request with no retry; 0 would skip the request entirely.
15791
+ const reportConfig = {
15792
+ ...pollingConfig,
15793
+ maxRetries: 1
15794
+ };
15795
+ const gasUsed = buildReportedGasUsed(params.gasUsed, params.effectiveGasPrice);
15796
+ const requestBody = {
15797
+ execId: params.execId,
15798
+ chain: params.chain,
15799
+ status: params.status,
15800
+ action: params.action,
15801
+ ...params.txHash !== undefined && {
15802
+ txHash: params.txHash
15803
+ },
15804
+ ...gasUsed !== undefined && {
15805
+ gasUsed
15806
+ },
15807
+ ...params.errorCode !== undefined && {
15808
+ errorCode: params.errorCode
15809
+ }
15810
+ };
15811
+ try {
15812
+ await pollApiPost(url.toString(), requestBody, isTransactionReportResponse, reportConfig);
15813
+ } catch (error) {
15814
+ throw parseEarnApiError(error, {
15815
+ operation: 'transactionReport'
15816
+ });
15817
+ }
15818
+ }
15819
+
14949
15820
  /**
14950
15821
  * Sum the amounts across every token input to size the allowance approval.
14951
15822
  *
@@ -15056,6 +15927,59 @@ function toWithdrawalQuoteInfo(data) {
15056
15927
  // Intentionally built-ins-only: Earn bridge support is limited to SDK-known
15057
15928
  // token contracts plus the explicit ERC-3009 domain allowlist below.
15058
15929
  const TOKEN_REGISTRY = createTokenRegistry();
15930
+ function submitTransactionReport(reportContext, action, status, details) {
15931
+ void reportEarnTransaction({
15932
+ execId: reportContext.execId,
15933
+ chain: reportContext.chain,
15934
+ config: reportContext.config,
15935
+ action,
15936
+ status,
15937
+ ...details
15938
+ }).catch(()=>undefined);
15939
+ }
15940
+ function reportTransactionSuccess(reportContext, action, result) {
15941
+ if (result === undefined) {
15942
+ return;
15943
+ }
15944
+ submitTransactionReport(reportContext, action, 'success', {
15945
+ txHash: result.txHash,
15946
+ gasUsed: result.gasUsed,
15947
+ effectiveGasPrice: result.effectiveGasPrice
15948
+ });
15949
+ }
15950
+ function reportTransactionFailure(reportContext, action, error) {
15951
+ submitTransactionReport(reportContext, action, 'failure', {
15952
+ txHash: transactionReportTxHash(error),
15953
+ errorCode: transactionReportErrorCode(error)
15954
+ });
15955
+ }
15956
+ function transactionReportErrorCode(error) {
15957
+ if (isKitError(error)) {
15958
+ return error.name;
15959
+ }
15960
+ const message = getErrorMessage(error);
15961
+ if (/user (rejected|denied)|rejected by user/i.test(message)) {
15962
+ return 'USER_REJECTED';
15963
+ }
15964
+ if (/insufficient funds/i.test(message)) {
15965
+ return 'INSUFFICIENT_FUNDS';
15966
+ }
15967
+ if (/timeout|timed out/i.test(message)) {
15968
+ return 'TIMEOUT';
15969
+ }
15970
+ return 'UNKNOWN_ERROR';
15971
+ }
15972
+ function transactionReportTxHash(error) {
15973
+ if (!isKitError(error)) {
15974
+ return undefined;
15975
+ }
15976
+ const trace = error.cause?.trace;
15977
+ if (typeof trace !== 'object' || trace === null) {
15978
+ return undefined;
15979
+ }
15980
+ const txHash = trace['txHash'];
15981
+ return typeof txHash === 'string' && txHash !== '' ? txHash : undefined;
15982
+ }
15059
15983
  /**
15060
15984
  * Build the typed error raised when a cross-chain wait is cancelled via its
15061
15985
  * `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
@@ -15377,7 +16301,7 @@ function finishElapsedWait(lastStatus, lastError) {
15377
16301
  const adapterContractAddress = requireAdapterContract(chain);
15378
16302
  const { adapter } = params.from;
15379
16303
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15380
- const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
16304
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
15381
16305
  vaultAddress,
15382
16306
  amount: params.amount,
15383
16307
  address,
@@ -15385,32 +16309,55 @@ function finishElapsedWait(lastStatus, lastError) {
15385
16309
  config
15386
16310
  }), ()=>undefined);
15387
16311
  validateExecutionDeadline(executionParams);
16312
+ const transactionReportContext = {
16313
+ execId,
16314
+ chain: apiChain,
16315
+ config
16316
+ };
15388
16317
  const approvalToken = resolveEarnApprovalToken(executionParams);
15389
16318
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15390
16319
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15391
16320
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
15392
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
16321
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16322
+ try {
16323
+ const approval = await approveAllowanceIfNeeded({
16324
+ adapter,
16325
+ chain,
16326
+ tokenAddress: approvalToken,
16327
+ delegate: adapterContractAddress,
16328
+ address,
16329
+ requiredAllowance,
16330
+ revertMessage: 'Earn deposit token approval reverted on-chain'
16331
+ });
16332
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
16333
+ return approval;
16334
+ } catch (error) {
16335
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
16336
+ throw error;
16337
+ }
16338
+ }, (approval)=>approval?.txHash);
16339
+ }
16340
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
16341
+ try {
16342
+ const result = await executeEarnAction({
15393
16343
  adapter,
15394
16344
  chain,
15395
- tokenAddress: approvalToken,
15396
- delegate: adapterContractAddress,
15397
16345
  address,
15398
- requiredAllowance,
15399
- revertMessage: 'Earn deposit token approval reverted on-chain'
15400
- }), (txHash)=>txHash);
15401
- }
15402
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>executeEarnAction({
15403
- adapter,
15404
- chain,
15405
- address,
15406
- actionKey: 'earn.deposit',
15407
- actionParams: {
15408
- executeParams: executionParams,
15409
- tokenInputs,
15410
- signature
15411
- },
15412
- revertMessage: 'Earn deposit reverted on-chain'
15413
- }), ({ txHash })=>txHash);
16346
+ actionKey: 'earn.deposit',
16347
+ actionParams: {
16348
+ executeParams: executionParams,
16349
+ tokenInputs,
16350
+ signature
16351
+ },
16352
+ revertMessage: 'Earn deposit reverted on-chain'
16353
+ });
16354
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
16355
+ return result;
16356
+ } catch (error) {
16357
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
16358
+ throw error;
16359
+ }
16360
+ }, ({ txHash })=>txHash);
15414
16361
  return {
15415
16362
  kind: 'same-chain',
15416
16363
  txHash,
@@ -15490,7 +16437,13 @@ function finishElapsedWait(lastStatus, lastError) {
15490
16437
  amount: params.amount,
15491
16438
  sourceChain: sourceChain.chain,
15492
16439
  destinationChain: destinationChain.chain,
15493
- expiresAt: prepared.expiresAt
16440
+ expiresAt: prepared.expiresAt,
16441
+ ...prepared.quoteIssuedAt !== undefined && {
16442
+ quoteIssuedAt: prepared.quoteIssuedAt
16443
+ },
16444
+ ...prepared.quoteExpiry !== undefined && {
16445
+ quoteExpiry: prepared.quoteExpiry
16446
+ }
15494
16447
  };
15495
16448
  }
15496
16449
  /** {@inheritdoc} */ async withdraw(params) {
@@ -15511,7 +16464,7 @@ function finishElapsedWait(lastStatus, lastError) {
15511
16464
  const adapterContractAddress = requireAdapterContract(chain);
15512
16465
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15513
16466
  const { adapter } = params.from;
15514
- const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
16467
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
15515
16468
  vaultAddress,
15516
16469
  amount: params.amount,
15517
16470
  address,
@@ -15519,32 +16472,55 @@ function finishElapsedWait(lastStatus, lastError) {
15519
16472
  config
15520
16473
  }), ()=>undefined);
15521
16474
  validateExecutionDeadline(executionParams);
16475
+ const transactionReportContext = {
16476
+ execId,
16477
+ chain: apiChain,
16478
+ config
16479
+ };
15522
16480
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
15523
16481
  const approvalToken = tokenInputs[0]?.token;
15524
16482
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15525
16483
  if (!options.skipApprove && approvalToken !== undefined) {
15526
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
16484
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16485
+ try {
16486
+ const approval = await approveAllowanceIfNeeded({
16487
+ adapter,
16488
+ chain,
16489
+ tokenAddress: approvalToken,
16490
+ delegate: adapterContractAddress,
16491
+ address,
16492
+ requiredAllowance,
16493
+ revertMessage: 'Vault share token approval reverted on-chain'
16494
+ });
16495
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
16496
+ return approval;
16497
+ } catch (error) {
16498
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
16499
+ throw error;
16500
+ }
16501
+ }, (approval)=>approval?.txHash);
16502
+ }
16503
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
16504
+ try {
16505
+ const result = await executeEarnAction({
15527
16506
  adapter,
15528
16507
  chain,
15529
- tokenAddress: approvalToken,
15530
- delegate: adapterContractAddress,
15531
16508
  address,
15532
- requiredAllowance,
15533
- revertMessage: 'Vault share token approval reverted on-chain'
15534
- }), (txHash)=>txHash);
15535
- }
15536
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>executeEarnAction({
15537
- adapter,
15538
- chain,
15539
- address,
15540
- actionKey: 'earn.withdraw',
15541
- actionParams: {
15542
- executeParams: executionParams,
15543
- tokenInputs,
15544
- signature
15545
- },
15546
- revertMessage: 'Earn withdraw reverted on-chain'
15547
- }), ({ txHash })=>txHash);
16509
+ actionKey: 'earn.withdraw',
16510
+ actionParams: {
16511
+ executeParams: executionParams,
16512
+ tokenInputs,
16513
+ signature
16514
+ },
16515
+ revertMessage: 'Earn withdraw reverted on-chain'
16516
+ });
16517
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
16518
+ return result;
16519
+ } catch (error) {
16520
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
16521
+ throw error;
16522
+ }
16523
+ }, ({ txHash })=>txHash);
15548
16524
  return {
15549
16525
  txHash,
15550
16526
  explorerUrl,
@@ -15678,141 +16654,6 @@ function finishElapsedWait(lastStatus, lastError) {
15678
16654
  }
15679
16655
  }
15680
16656
  }
15681
- gasEstimateFailure(name, chain, error) {
15682
- return {
15683
- name,
15684
- token: chain.nativeCurrency.symbol,
15685
- blockchain: chain.chain,
15686
- fees: null,
15687
- error: getErrorMessage(error)
15688
- };
15689
- }
15690
- async estimateDepositQuoteGasFees(params) {
15691
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
15692
- try {
15693
- const adapterContractAddress = requireAdapterContract(chain);
15694
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15695
- const { executionParams, signature } = await fetchDeposit({
15696
- vaultAddress: normalizedVaultAddress,
15697
- amount,
15698
- address,
15699
- chain: apiChain,
15700
- config
15701
- });
15702
- validateExecutionDeadline(executionParams);
15703
- const approvalToken = resolveEarnApprovalToken(executionParams);
15704
- const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15705
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15706
- return await estimateEarnQuoteGasFees({
15707
- adapter,
15708
- chain,
15709
- address,
15710
- actionName: 'Deposit',
15711
- actionKey: 'earn.deposit',
15712
- actionParams: {
15713
- executeParams: executionParams,
15714
- tokenInputs,
15715
- signature
15716
- },
15717
- approval: approvalToken !== undefined && requiredAllowance > 0n ? {
15718
- token: approvalToken,
15719
- delegate: adapterContractAddress,
15720
- requiredAllowance
15721
- } : undefined
15722
- });
15723
- } catch (error) {
15724
- return [
15725
- this.gasEstimateFailure('Deposit', chain, error)
15726
- ];
15727
- }
15728
- }
15729
- async estimateWithdrawalQuoteGasFees(params) {
15730
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
15731
- try {
15732
- const adapterContractAddress = requireAdapterContract(chain);
15733
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15734
- const { executionParams, signature } = await fetchWithdraw({
15735
- vaultAddress: normalizedVaultAddress,
15736
- amount,
15737
- address,
15738
- chain: apiChain,
15739
- config
15740
- });
15741
- validateExecutionDeadline(executionParams);
15742
- const tokenInputs = buildEarnTokenInputs(executionParams, normalizedVaultAddress);
15743
- const approvalToken = tokenInputs[0]?.token;
15744
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15745
- return await estimateEarnQuoteGasFees({
15746
- adapter,
15747
- chain,
15748
- address,
15749
- actionName: 'Withdraw',
15750
- actionKey: 'earn.withdraw',
15751
- actionParams: {
15752
- executeParams: executionParams,
15753
- tokenInputs,
15754
- signature
15755
- },
15756
- approval: approvalToken !== undefined ? {
15757
- token: approvalToken,
15758
- delegate: adapterContractAddress,
15759
- requiredAllowance
15760
- } : undefined
15761
- });
15762
- } catch (error) {
15763
- return [
15764
- this.gasEstimateFailure('Withdraw', chain, error)
15765
- ];
15766
- }
15767
- }
15768
- async estimateClaimRewardsQuoteGasFees(params) {
15769
- const { adapter, chain, apiChain, address, vaultAddress, config } = params;
15770
- try {
15771
- requireAdapterContract(chain);
15772
- const { rewards, executionParams, signature } = await fetchClaimRewards({
15773
- address,
15774
- chain: apiChain,
15775
- vaultAddress,
15776
- config
15777
- });
15778
- if (rewards.length === 0) {
15779
- return [];
15780
- }
15781
- const missingExecutionParams = executionParams === undefined;
15782
- const missingSignature = signature === undefined;
15783
- if (missingExecutionParams || missingSignature) {
15784
- throw new KitError({
15785
- ...EarnError.INTERNAL_ERROR,
15786
- recoverability: 'RETRYABLE',
15787
- message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
15788
- cause: {
15789
- trace: {
15790
- rewardsCount: rewards.length,
15791
- missingExecutionParams,
15792
- missingSignature
15793
- }
15794
- }
15795
- });
15796
- }
15797
- validateExecutionDeadline(executionParams);
15798
- return await estimateEarnQuoteGasFees({
15799
- adapter,
15800
- chain,
15801
- address,
15802
- actionName: 'Claim Rewards',
15803
- actionKey: 'earn.claimRewards',
15804
- actionParams: {
15805
- executeParams: executionParams,
15806
- tokenInputs: [],
15807
- signature
15808
- }
15809
- });
15810
- } catch (error) {
15811
- return [
15812
- this.gasEstimateFailure('Claim Rewards', chain, error)
15813
- ];
15814
- }
15815
- }
15816
16657
  /** {@inheritdoc} */ async getDepositQuote(params) {
15817
16658
  const config = this.resolveConfig(params.config);
15818
16659
  if (hasQuoteDestinationChain(params)) {
@@ -15835,96 +16676,43 @@ function finishElapsedWait(lastStatus, lastError) {
15835
16676
  throw createValidationFailedError('chain', destinationChain.chain, 'chain is only supported for cross-chain Earn deposit quotes; omit chain/address when quoting on the source chain');
15836
16677
  }
15837
16678
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
15838
- // The quote fetch and the gas estimation share no data, so run them
15839
- // concurrently. The estimator never rejects (failures fold into
15840
- // `{ fees: null }` entries), so only a quote failure can throw here.
15841
- const [quote, gasFees] = await Promise.all([
15842
- fetchDepositQuote({
15843
- vaultAddress: params.vaultAddress,
15844
- amount: params.amount,
15845
- address,
15846
- chain,
15847
- config
15848
- }),
15849
- this.estimateDepositQuoteGasFees({
15850
- adapter: params.from.adapter,
15851
- chain: chainDefinition,
15852
- apiChain: chain,
15853
- address,
15854
- vaultAddress: params.vaultAddress,
15855
- amount: params.amount,
15856
- config
15857
- })
15858
- ]);
15859
- return {
15860
- ...quote,
15861
- gasFees
15862
- };
16679
+ // Gas is estimated server-side by the Earn Service and returned on the quote, so the
16680
+ // SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
16681
+ // the native token symbol / blockchain on each gas entry.
16682
+ return fetchDepositQuote({
16683
+ vaultAddress: params.vaultAddress,
16684
+ amount: params.amount,
16685
+ address,
16686
+ chain,
16687
+ config,
16688
+ chainDefinition
16689
+ });
15863
16690
  }
15864
16691
  /** {@inheritdoc} */ async getWithdrawalQuote(params) {
15865
16692
  const config = this.resolveConfig(params.config);
15866
16693
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
15867
- // The quote fetch and the gas estimation share no data, so run them
15868
- // concurrently. The estimator never rejects (failures fold into
15869
- // `{ fees: null }` entries), so only a quote failure can throw here.
15870
- const [quote, gasFees] = await Promise.all([
15871
- fetchWithdrawalQuote({
15872
- vaultAddress: params.vaultAddress,
15873
- amount: params.amount,
15874
- address,
15875
- chain,
15876
- config
15877
- }),
15878
- this.estimateWithdrawalQuoteGasFees({
15879
- adapter: params.from.adapter,
15880
- chain: chainDefinition,
15881
- apiChain: chain,
15882
- address,
15883
- vaultAddress: params.vaultAddress,
15884
- amount: params.amount,
15885
- config
15886
- })
15887
- ]);
15888
- return {
15889
- ...quote,
15890
- gasFees
15891
- };
16694
+ // Gas is estimated server-side by the Earn Service and returned on the quote.
16695
+ return fetchWithdrawalQuote({
16696
+ vaultAddress: params.vaultAddress,
16697
+ amount: params.amount,
16698
+ address,
16699
+ chain,
16700
+ config,
16701
+ chainDefinition
16702
+ });
15892
16703
  }
15893
16704
  /** {@inheritdoc} */ async getClaimRewardsQuote(params) {
15894
16705
  const config = this.resolveConfig(params.config);
15895
- const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
15896
- const quote = await fetchClaimRewardsQuote({
16706
+ const { address, chain } = await resolveAdapterContext(params.from);
16707
+ // The claimRewards/quote response carries no gas estimate (unlike
16708
+ // deposit/withdrawal quotes), and the SDK no longer estimates gas locally,
16709
+ // so gasFees is always empty for claim rewards.
16710
+ return fetchClaimRewardsQuote({
15897
16711
  vaultAddress: params.vaultAddress,
15898
16712
  address,
15899
16713
  chain,
15900
16714
  config
15901
16715
  });
15902
- // No claimable rewards means there is nothing to execute, so there is no
15903
- // gas to estimate. Short-circuit on the already-fetched quote rather than
15904
- // calling the (heavier) claim execution endpoint again — this also keeps
15905
- // `gasFees` empty as documented, instead of risking a `{ fees: null }`
15906
- // estimation-error entry when the adapter/RPC is unavailable. This
15907
- // short-circuit is why the claim path stays sequential instead of using
15908
- // the Promise.all pattern of the deposit/withdrawal quotes: estimating in
15909
- // parallel would hit the signing endpoint even when nothing is claimable.
15910
- if (quote.rewards.length === 0) {
15911
- return {
15912
- ...quote,
15913
- gasFees: []
15914
- };
15915
- }
15916
- const gasFees = await this.estimateClaimRewardsQuoteGasFees({
15917
- adapter: params.from.adapter,
15918
- chain: chainDefinition,
15919
- apiChain: chain,
15920
- address,
15921
- vaultAddress: params.vaultAddress,
15922
- config
15923
- });
15924
- return {
15925
- ...quote,
15926
- gasFees
15927
- };
15928
16716
  }
15929
16717
  }
15930
16718
  function hasDepositDestination(params) {
@@ -16103,6 +16891,12 @@ function createUnsupportedCrossChainDepositError(chain, reason) {
16103
16891
  if (config.providers !== undefined && !Array.isArray(config.providers)) {
16104
16892
  throw createValidationFailedError('config.providers', config.providers, 'providers must be an array of earn providers when provided');
16105
16893
  }
16894
+ if (config.disableAnalytics !== undefined && typeof config.disableAnalytics !== 'boolean') {
16895
+ throw createValidationFailedError('config.disableAnalytics', config.disableAnalytics, 'disableAnalytics must be a boolean when provided');
16896
+ }
16897
+ if (config.disableErrorReporting !== undefined && typeof config.disableErrorReporting !== 'boolean') {
16898
+ throw createValidationFailedError('config.disableErrorReporting', config.disableErrorReporting, 'disableErrorReporting must be a boolean when provided');
16899
+ }
16106
16900
  const defaultProviders = getDefaultProviders();
16107
16901
  const providers = [
16108
16902
  ...config.providers ?? [],
@@ -16114,6 +16908,31 @@ function createUnsupportedCrossChainDepositError(chain, reason) {
16114
16908
  return context;
16115
16909
  }
16116
16910
 
16911
+ /**
16912
+ * Register Earn Kit telemetry event type strings with the shared registry so
16913
+ * error telemetry helpers remain compile-time checked.
16914
+ *
16915
+ * @internal
16916
+ */ /**
16917
+ * Telemetry event type identifiers for Earn Kit operations.
16918
+ *
16919
+ * @internal
16920
+ */ const EARN_EVENT_TYPES = {
16921
+ GET_VAULTS: 'earn_get_vaults',
16922
+ EXPLORE_VAULTS: 'earn_explore_vaults',
16923
+ GET_POSITION: 'earn_get_position',
16924
+ GET_CROSS_CHAIN_DEPOSIT_STATUS: 'earn_get_cross_chain_deposit_status',
16925
+ WAIT_FOR_CROSS_CHAIN_DEPOSIT: 'earn_wait_for_cross_chain_deposit',
16926
+ DEPOSIT: 'earn_deposit',
16927
+ CROSS_CHAIN_DEPOSIT: 'earn_cross_chain_deposit',
16928
+ WITHDRAW: 'earn_withdraw',
16929
+ CLAIM_REWARDS: 'earn_claim_rewards',
16930
+ GET_DEPOSIT_QUOTE: 'earn_get_deposit_quote',
16931
+ GET_WITHDRAWAL_QUOTE: 'earn_get_withdrawal_quote',
16932
+ GET_CLAIM_REWARDS_QUOTE: 'earn_get_claim_rewards_quote',
16933
+ RETRY: 'earn_retry'
16934
+ };
16935
+
16117
16936
  /**
16118
16937
  * Format a provider amount object as a human-readable decimal string.
16119
16938
  *
@@ -16162,11 +16981,27 @@ function formatPositionPnL(pnl) {
16162
16981
  * @param vault - Provider vault info with raw amount objects
16163
16982
  * @returns Vault info with total deposits and liquidity formatted as strings
16164
16983
  */ function formatVaultInfo(vault) {
16165
- const { totalDeposits, liquidity, ...rest } = vault;
16984
+ // The flat `totalDeposits`/`liquidity` are deprecated aliases that are
16985
+ // intentionally dual-read through the migration window so existing
16986
+ // consumers keep receiving them until Contract.
16987
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
16988
+ const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
16989
+ // `liquidityProfile` is `.optional()` in the response schema during the
16990
+ // expand/contract window (an old backend that predates the nested facets
16991
+ // omits it), so only format and re-attach it when present — matching the
16992
+ // provider-side `toVaultInfo` mapper.
16166
16993
  return {
16167
16994
  ...rest,
16168
16995
  totalDeposits: formatAmount(totalDeposits),
16169
- liquidity: formatAmount(liquidity)
16996
+ liquidity: formatAmount(liquidity),
16997
+ ...liquidityProfile !== undefined && {
16998
+ liquidityProfile: {
16999
+ ...liquidityProfile,
17000
+ totalDeposits: formatAmount(liquidityProfile.totalDeposits),
17001
+ available: formatAmount(liquidityProfile.available),
17002
+ totalSupply: formatAmount(liquidityProfile.totalSupply)
17003
+ }
17004
+ }
16170
17005
  };
16171
17006
  }
16172
17007
  /**
@@ -17613,6 +18448,14 @@ function hasCrossChainDestination(params) {
17613
18448
  return formatClaimRewardsQuoteInfo(result);
17614
18449
  }
17615
18450
 
18451
+ /** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg$1.name);
18452
+ /**
18453
+ * Determine whether deposit parameters target a destination chain.
18454
+ *
18455
+ * @internal
18456
+ */ function isCrossChainDeposit(params) {
18457
+ return 'to' in params && params.to !== undefined;
18458
+ }
17616
18459
  function formatRetryResult(operation, result) {
17617
18460
  switch(operation){
17618
18461
  case 'deposit':
@@ -17627,6 +18470,70 @@ function formatRetryResult(operation, result) {
17627
18470
  }
17628
18471
  }
17629
18472
  }
18473
+ /**
18474
+ * Emit the success event corresponding to a completed retry.
18475
+ *
18476
+ * @internal
18477
+ */ function emitRetrySuccessTelemetry(trace, result, config) {
18478
+ switch(trace.operation){
18479
+ case 'deposit':
18480
+ {
18481
+ const sourceChain = resolveChainName(trace.params.from.chain);
18482
+ if ('to' in trace.params && trace.params.to !== undefined) {
18483
+ const destinationChain = resolveChainName(trace.params.to.chain);
18484
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CROSS_CHAIN_DEPOSIT, config, {
18485
+ ...sourceChain != null && {
18486
+ sourceChain
18487
+ },
18488
+ ...destinationChain != null && {
18489
+ destinationChain
18490
+ }
18491
+ });
18492
+ return;
18493
+ }
18494
+ emitSuccessTelemetry(EARN_EVENT_TYPES.DEPOSIT, config, {
18495
+ ...sourceChain != null && {
18496
+ sourceChain
18497
+ },
18498
+ ...'txHash' in result && {
18499
+ txHash: result.txHash
18500
+ }
18501
+ });
18502
+ return;
18503
+ }
18504
+ case 'withdraw':
18505
+ {
18506
+ const sourceChain = resolveChainName(trace.params.from.chain);
18507
+ emitSuccessTelemetry(EARN_EVENT_TYPES.WITHDRAW, config, {
18508
+ ...sourceChain != null && {
18509
+ sourceChain
18510
+ },
18511
+ ...'txHash' in result && {
18512
+ txHash: result.txHash
18513
+ }
18514
+ });
18515
+ return;
18516
+ }
18517
+ case 'claimRewards':
18518
+ {
18519
+ if ('rewards' in result && result.status === 'claimed') {
18520
+ const sourceChain = resolveChainName(trace.params.from.chain);
18521
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CLAIM_REWARDS, config, {
18522
+ ...sourceChain != null && {
18523
+ sourceChain
18524
+ },
18525
+ txHash: result.txHash
18526
+ });
18527
+ }
18528
+ return;
18529
+ }
18530
+ default:
18531
+ {
18532
+ const exhaustive = trace;
18533
+ throw createValidationFailedError('error.cause.trace', exhaustive, 'EarnKit.retry() does not support this earn operation');
18534
+ }
18535
+ }
18536
+ }
17630
18537
  /**
17631
18538
  * A high-level class-based interface for DeFi lending vault operations.
17632
18539
  *
@@ -17685,6 +18592,8 @@ function formatRetryResult(operation, result) {
17685
18592
  * ```
17686
18593
  */ class EarnKit {
17687
18594
  context;
18595
+ /** Per-kit identity and opt-out state for error telemetry. */ telemetryConfig;
18596
+ /** Per-kit identity and opt-out state for success telemetry. */ analyticsTelemetryConfig;
17688
18597
  /**
17689
18598
  * Event dispatcher for step-level events emitted during multi-phase earn
17690
18599
  * operations. Prefer {@link EarnKit.on} / {@link EarnKit.off} over using
@@ -17709,6 +18618,16 @@ function formatRetryResult(operation, result) {
17709
18618
  */ constructor(config = {}){
17710
18619
  this.context = createEarnKitContext(config);
17711
18620
  this.actionDispatcher = new Actionable();
18621
+ this.telemetryConfig = {
18622
+ sdkName: SDK_NAME,
18623
+ sdkVersion: pkg$1.version,
18624
+ disabled: config.disableErrorReporting === true
18625
+ };
18626
+ this.analyticsTelemetryConfig = {
18627
+ sdkName: SDK_NAME,
18628
+ sdkVersion: pkg$1.version,
18629
+ disabled: config.disableAnalytics === true
18630
+ };
17712
18631
  for (const provider of this.context.providers){
17713
18632
  provider.registerDispatcher(this.actionDispatcher);
17714
18633
  }
@@ -17769,29 +18688,36 @@ function formatRetryResult(operation, result) {
17769
18688
  * }
17770
18689
  * ```
17771
18690
  */ async retry(error) {
17772
- if (!isKitError(error)) {
17773
- throw createValidationFailedError('error', error, 'EarnKit.retry() requires a KitError thrown by a previous earn operation');
17774
- }
17775
- if (!isRetryableError$1(error)) {
17776
- throw createValidationFailedError('error.recoverability', error.recoverability, 'EarnKit.retry() requires a retryable or resumable error — check isRetryableError(error) first');
17777
- }
17778
- const trace = error.cause?.trace;
17779
- if (!isEarnErrorTrace(trace)) {
17780
- throw createValidationFailedError('error.cause.trace', trace, 'EarnKit.retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
17781
- }
17782
- const provider = this.context.providers.find((candidate)=>candidate.name === trace.provider);
17783
- if (provider === undefined) {
17784
- throw createValidationFailedError('error.cause.trace.provider', trace.provider, `No earn provider named "${trace.provider}" is registered with this kit`);
17785
- }
17786
- const result = await provider.retry(error);
17787
- // `provider.retry` returns a flat result union with no compile-time link to
17788
- // `trace.operation`, so narrow the operation here to select the matching
17789
- // overload. The result cast in each branch is sound: the provider always
17790
- // returns the result type corresponding to the resumed operation.
17791
- if (trace.operation === 'claimRewards') {
18691
+ const result = await withErrorTelemetry(async ()=>{
18692
+ if (!isKitError(error)) {
18693
+ throw createValidationFailedError('error', error, 'EarnKit.retry() requires a KitError thrown by a previous earn operation');
18694
+ }
18695
+ if (!isRetryableError$1(error)) {
18696
+ throw createValidationFailedError('error.recoverability', error.recoverability, 'EarnKit.retry() requires a retryable or resumable error — check isRetryableError(error) first');
18697
+ }
18698
+ const trace = error.cause?.trace;
18699
+ if (!isEarnErrorTrace(trace)) {
18700
+ throw createValidationFailedError('error.cause.trace', trace, 'EarnKit.retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
18701
+ }
18702
+ const provider = this.context.providers.find((candidate)=>candidate.name === trace.provider);
18703
+ if (provider === undefined) {
18704
+ throw createValidationFailedError('error.cause.trace.provider', trace.provider, `No earn provider named "${trace.provider}" is registered with this kit`);
18705
+ }
18706
+ const result = await provider.retry(error);
18707
+ // `provider.retry` returns a flat result union with no compile-time link to
18708
+ // `trace.operation`, so narrow the operation here to select the matching
18709
+ // overload. The result cast in each branch is sound: the provider always
18710
+ // returns the result type corresponding to the resumed operation.
18711
+ if (trace.operation === 'claimRewards') {
18712
+ return formatRetryResult(trace.operation, result);
18713
+ }
17792
18714
  return formatRetryResult(trace.operation, result);
18715
+ }, EARN_EVENT_TYPES.RETRY, this.telemetryConfig);
18716
+ const trace = isKitError(error) ? error.cause?.trace : undefined;
18717
+ if (isEarnErrorTrace(trace)) {
18718
+ emitRetrySuccessTelemetry(trace, result, this.analyticsTelemetryConfig);
17793
18719
  }
17794
- return formatRetryResult(trace.operation, result);
18720
+ return result;
17795
18721
  }
17796
18722
  /**
17797
18723
  * Return the chains supported by configured earn providers.
@@ -17825,7 +18751,9 @@ function formatRetryResult(operation, result) {
17825
18751
  * result.vaults.forEach(v => console.log(`${v.name}: ${(v.currentApy * 100).toFixed(2)}% APY`))
17826
18752
  * ```
17827
18753
  */ async getVaults(params) {
17828
- return getVaults$1(this.context, params);
18754
+ const result = await withErrorTelemetry(async ()=>getVaults$1(this.context, params), EARN_EVENT_TYPES.GET_VAULTS, this.telemetryConfig);
18755
+ emitSuccessTelemetry(EARN_EVENT_TYPES.GET_VAULTS, this.analyticsTelemetryConfig, {});
18756
+ return result;
17829
18757
  }
17830
18758
  /**
17831
18759
  * Discover vaults available on a chain.
@@ -17850,7 +18778,12 @@ function formatRetryResult(operation, result) {
17850
18778
  * const guarded = result.vaults.filter(v => v.circleGuarded) // Circle-guarded vaults only
17851
18779
  * ```
17852
18780
  */ async exploreVaults(params) {
17853
- return exploreVaults$1(this.context, params);
18781
+ const context = {
18782
+ sourceChain: resolveChainName(params.chain)
18783
+ };
18784
+ const result = await withErrorTelemetry(async ()=>exploreVaults$1(this.context, params), EARN_EVENT_TYPES.EXPLORE_VAULTS, this.telemetryConfig, context);
18785
+ emitSuccessTelemetry(EARN_EVENT_TYPES.EXPLORE_VAULTS, this.analyticsTelemetryConfig, context);
18786
+ return result;
17854
18787
  }
17855
18788
  /**
17856
18789
  * Lazily iterate every vault available on a chain.
@@ -17897,7 +18830,9 @@ function formatRetryResult(operation, result) {
17897
18830
  * }
17898
18831
  * ```
17899
18832
  */ async getPosition(params) {
17900
- return getPosition$1(this.context, params);
18833
+ return withErrorTelemetry(async ()=>getPosition$1(this.context, params), EARN_EVENT_TYPES.GET_POSITION, this.telemetryConfig, {
18834
+ sourceChain: resolveChainName(params.from.chain)
18835
+ });
17901
18836
  }
17902
18837
  /**
17903
18838
  * Fetch the current status of a cross-chain deposit by execution ID.
@@ -17922,7 +18857,7 @@ function formatRetryResult(operation, result) {
17922
18857
  * console.log(`Bridge ${status.execId} is ${status.status}`)
17923
18858
  * ```
17924
18859
  */ async getCrossChainDepositStatus(params) {
17925
- return getCrossChainDepositStatus$1(this.context, params);
18860
+ return withErrorTelemetry(async ()=>getCrossChainDepositStatus$1(this.context, params), EARN_EVENT_TYPES.GET_CROSS_CHAIN_DEPOSIT_STATUS, this.telemetryConfig);
17926
18861
  }
17927
18862
  /**
17928
18863
  * Poll a cross-chain deposit until it reaches a terminal bridge state.
@@ -17949,10 +18884,30 @@ function formatRetryResult(operation, result) {
17949
18884
  * console.log(`Bridge ended as ${result.outcome}`)
17950
18885
  * ```
17951
18886
  */ async waitForCrossChainDeposit(params) {
17952
- return waitForCrossChainDeposit$1(this.context, params);
18887
+ return withErrorTelemetry(async ()=>waitForCrossChainDeposit$1(this.context, params), EARN_EVENT_TYPES.WAIT_FOR_CROSS_CHAIN_DEPOSIT, this.telemetryConfig);
17953
18888
  }
17954
18889
  async deposit(params) {
17955
- return deposit$1(this.context, params);
18890
+ const isCrossChain = isCrossChainDeposit(params);
18891
+ const context = {
18892
+ sourceChain: resolveChainName(params.from.chain),
18893
+ ...isCrossChain && {
18894
+ destinationChain: resolveChainName(params.to.chain)
18895
+ }
18896
+ };
18897
+ const eventType = isCrossChain ? EARN_EVENT_TYPES.CROSS_CHAIN_DEPOSIT : EARN_EVENT_TYPES.DEPOSIT;
18898
+ const result = await withErrorTelemetry(async ()=>deposit$1(this.context, params), eventType, this.telemetryConfig, context);
18899
+ if (result.kind === 'cross-chain') {
18900
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CROSS_CHAIN_DEPOSIT, this.analyticsTelemetryConfig, {
18901
+ sourceChain: resolveChainName(result.sourceChain),
18902
+ destinationChain: resolveChainName(result.destinationChain)
18903
+ });
18904
+ } else {
18905
+ emitSuccessTelemetry(EARN_EVENT_TYPES.DEPOSIT, this.analyticsTelemetryConfig, {
18906
+ ...context,
18907
+ txHash: result.txHash
18908
+ });
18909
+ }
18910
+ return result;
17956
18911
  }
17957
18912
  /**
17958
18913
  * Execute a withdrawal from a DeFi lending vault.
@@ -17978,7 +18933,15 @@ function formatRetryResult(operation, result) {
17978
18933
  * console.log(`Withdrew ${result.amount} from ${result.vaultAddress}, tx: ${result.txHash}`)
17979
18934
  * ```
17980
18935
  */ async withdraw(params) {
17981
- return withdraw$1(this.context, params);
18936
+ const context = {
18937
+ sourceChain: resolveChainName(params.from.chain)
18938
+ };
18939
+ const result = await withErrorTelemetry(async ()=>withdraw$1(this.context, params), EARN_EVENT_TYPES.WITHDRAW, this.telemetryConfig, context);
18940
+ emitSuccessTelemetry(EARN_EVENT_TYPES.WITHDRAW, this.analyticsTelemetryConfig, {
18941
+ ...context,
18942
+ txHash: result.txHash
18943
+ });
18944
+ return result;
17982
18945
  }
17983
18946
  /**
17984
18947
  * Claim rewards from earn vaults.
@@ -18005,7 +18968,17 @@ function formatRetryResult(operation, result) {
18005
18968
  *
18006
18969
  * @internal
18007
18970
  */ async claimRewards(params) {
18008
- return claimRewards$1(this.context, params);
18971
+ const context = {
18972
+ sourceChain: resolveChainName(params.from.chain)
18973
+ };
18974
+ const result = await withErrorTelemetry(async ()=>claimRewards$1(this.context, params), EARN_EVENT_TYPES.CLAIM_REWARDS, this.telemetryConfig, context);
18975
+ if (result.status === 'claimed') {
18976
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CLAIM_REWARDS, this.analyticsTelemetryConfig, {
18977
+ ...context,
18978
+ txHash: result.txHash
18979
+ });
18980
+ }
18981
+ return result;
18009
18982
  }
18010
18983
  /**
18011
18984
  * Get an informational quote for a deposit into a vault.
@@ -18027,7 +19000,9 @@ function formatRetryResult(operation, result) {
18027
19000
  * console.log(`Expected shares: ${quote.expectedShares.amount}`)
18028
19001
  * ```
18029
19002
  */ async getDepositQuote(params) {
18030
- return getDepositQuote$1(this.context, params);
19003
+ return withErrorTelemetry(async ()=>getDepositQuote$1(this.context, params), EARN_EVENT_TYPES.GET_DEPOSIT_QUOTE, this.telemetryConfig, {
19004
+ sourceChain: resolveChainName(params.from.chain)
19005
+ });
18031
19006
  }
18032
19007
  /**
18033
19008
  * Get an informational quote for a withdrawal from a vault.
@@ -18049,7 +19024,9 @@ function formatRetryResult(operation, result) {
18049
19024
  * console.log(`Shares to redeem: ${quote.sharesToRedeem.amount}`)
18050
19025
  * ```
18051
19026
  */ async getWithdrawalQuote(params) {
18052
- return getWithdrawalQuote$1(this.context, params);
19027
+ return withErrorTelemetry(async ()=>getWithdrawalQuote$1(this.context, params), EARN_EVENT_TYPES.GET_WITHDRAWAL_QUOTE, this.telemetryConfig, {
19028
+ sourceChain: resolveChainName(params.from.chain)
19029
+ });
18053
19030
  }
18054
19031
  /**
18055
19032
  * Get an informational quote for claiming rewards.
@@ -18071,34 +19048,111 @@ function formatRetryResult(operation, result) {
18071
19048
  *
18072
19049
  * @internal
18073
19050
  */ async getClaimRewardsQuote(params) {
18074
- return getClaimRewardsQuote$1(this.context, params);
19051
+ return withErrorTelemetry(async ()=>getClaimRewardsQuote$1(this.context, params), EARN_EVENT_TYPES.GET_CLAIM_REWARDS_QUOTE, this.telemetryConfig, {
19052
+ sourceChain: resolveChainName(params.from.chain)
19053
+ });
18075
19054
  }
18076
19055
  }
18077
19056
 
18078
19057
  // Auto-register this kit for user agent tracking
18079
19058
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
18080
19059
 
19060
+ /**
19061
+ * Register event handlers from a context actions map to a kit instance.
19062
+ *
19063
+ * This utility function registers event handlers stored in a context actions map
19064
+ * with a kit instance that supports event handling via an `on` method. It handles
19065
+ * wildcard handlers ('*') and prefixed action handlers, stripping the prefix
19066
+ * before registration.
19067
+ *
19068
+ * The function is designed to be reusable across different operation types
19069
+ * (bridge, swap, stake, etc.) by accepting a configurable prefix parameter.
19070
+ *
19071
+ * @param kit - The kit instance to register handlers with (must have an `on` method)
19072
+ * @param handlers - Map of action names to arrays of handler functions
19073
+ * @param prefix - Optional prefix to strip from action names (e.g., 'bridge.')
19074
+ *
19075
+ * @example
19076
+ * ```typescript
19077
+ * import { registerActionHandlers } from '@circle-fin/app-kit/utils'
19078
+ * import { BridgeKit } from '@circle-fin/bridge-kit'
19079
+ *
19080
+ * const kit = new BridgeKit()
19081
+ * const handlers = {
19082
+ * '*': [(payload) => console.log('All actions:', payload)],
19083
+ * 'bridge.approve': [(payload) => console.log('Approved:', payload)],
19084
+ * 'bridge.burn': [(payload) => console.log('Burned:', payload)],
19085
+ * }
19086
+ *
19087
+ * registerActionHandlers(kit, handlers, 'bridge.')
19088
+ * ```
19089
+ *
19090
+ * @example
19091
+ * ```typescript
19092
+ * import { registerActionHandlers } from '@circle-fin/app-kit/utils'
19093
+ * import { SwapKit } from '@circle-fin/swap-kit'
19094
+ *
19095
+ * const kit = new SwapKit()
19096
+ * const handlers = {
19097
+ * 'swap.initiate': [(payload) => console.log('Swap initiated:', payload)],
19098
+ * }
19099
+ *
19100
+ * registerActionHandlers(kit, handlers, 'swap.')
19101
+ * ```
19102
+ */ const registerActionHandlers = (kit, handlers, prefix = '')=>{
19103
+ for (const [action, handlerArray] of Object.entries(handlers)){
19104
+ // Register all handlers for this action
19105
+ for (const handler of handlerArray){
19106
+ if (action === '*') {
19107
+ // Wildcard handlers are registered as-is
19108
+ kit.on('*', handler);
19109
+ } else if (prefix && action.startsWith(prefix)) {
19110
+ // Remove prefix to get the actual kit action name
19111
+ const kitAction = action.split('.').at(1);
19112
+ if (kitAction) {
19113
+ kit.on(kitAction, handler);
19114
+ }
19115
+ } else if (!prefix) {
19116
+ // No prefix configured, register action as-is
19117
+ kit.on(action, handler);
19118
+ }
19119
+ // Actions that don't match the prefix are silently ignored
19120
+ }
19121
+ }
19122
+ };
19123
+
18081
19124
  /**
18082
19125
  * Create an EarnKit instance for AppKit earn operations.
18083
19126
  *
18084
- * @remarks The context parameter is reserved for future EarnKit wiring.
18085
- * EarnKit does not currently support AppKit developer fee hooks, so the
18086
- * factory does not read fee callbacks from the context. Earn custom fees remain
18087
- * reserved until EarnKit fee support ships.
19127
+ * Attaches any earn event handlers previously registered on the AppKit
19128
+ * context (via `kit.on('earn.*', …)` or `kit.on('*', …)`) so step events
19129
+ * fire during the returned kit's earn operations.
18088
19130
  *
18089
- * When EarnKit supports developer fee hooks, this factory can wire AppKit context through.
19131
+ * @remarks Developer fee hooks from the AppKit context are not applied.
19132
+ * EarnKit does not yet support custom fee policies.
18090
19133
  *
18091
- * @param context - AppKit context reserved for future EarnKit wiring
18092
- * @returns A new EarnKit instance
19134
+ * @param context - AppKit context with earn event handlers and kit options
19135
+ * @returns An EarnKit instance ready for AppKit earn operations
18093
19136
  *
18094
19137
  * @example
18095
19138
  * ```typescript
18096
19139
  * const earnKit = createEarnKit(context)
18097
19140
  * ```
18098
- */ const createEarnKit = ()=>new EarnKit();
19141
+ */ const createEarnKit = (context)=>{
19142
+ const kit = new EarnKit({
19143
+ ...context.disableErrorReporting != null && {
19144
+ disableErrorReporting: context.disableErrorReporting
19145
+ },
19146
+ ...context.disableAnalytics != null && {
19147
+ disableAnalytics: context.disableAnalytics
19148
+ }
19149
+ });
19150
+ registerActionHandlers(kit, context.actions.earn, 'earn');
19151
+ return kit;
19152
+ };
18099
19153
 
18100
19154
  async function deposit(context, params) {
18101
- return createEarnKit().deposit(params);
19155
+ return createEarnKit(context).deposit(params);
18102
19156
  }
18103
19157
  /**
18104
19158
  * Execute an earn withdrawal operation.
@@ -18122,7 +19176,7 @@ async function deposit(context, params) {
18122
19176
  * })
18123
19177
  * ```
18124
19178
  */ async function withdraw(context, params) {
18125
- return createEarnKit().withdraw(params);
19179
+ return createEarnKit(context).withdraw(params);
18126
19180
  }
18127
19181
  /**
18128
19182
  * Claim earn rewards.
@@ -18145,7 +19199,7 @@ async function deposit(context, params) {
18145
19199
  * })
18146
19200
  * ```
18147
19201
  */ async function claimRewards(context, params) {
18148
- return createEarnKit().claimRewards(params);
19202
+ return createEarnKit(context).claimRewards(params);
18149
19203
  }
18150
19204
  /**
18151
19205
  * Fetch vault information.
@@ -18167,7 +19221,7 @@ async function deposit(context, params) {
18167
19221
  * })
18168
19222
  * ```
18169
19223
  */ async function getVaults(context, params) {
18170
- return createEarnKit().getVaults(params);
19224
+ return createEarnKit(context).getVaults(params);
18171
19225
  }
18172
19226
  /**
18173
19227
  * Discover vaults available on a chain.
@@ -18191,7 +19245,7 @@ async function deposit(context, params) {
18191
19245
  * })
18192
19246
  * ```
18193
19247
  */ async function exploreVaults(context, params) {
18194
- return createEarnKit().exploreVaults(params);
19248
+ return createEarnKit(context).exploreVaults(params);
18195
19249
  }
18196
19250
  /**
18197
19251
  * Lazily iterate every vault available on a chain.
@@ -18215,7 +19269,7 @@ async function deposit(context, params) {
18215
19269
  * }
18216
19270
  * ```
18217
19271
  */ function exploreVaultsIterator(context, params) {
18218
- return createEarnKit().exploreVaultsIterator(params);
19272
+ return createEarnKit(context).exploreVaultsIterator(params);
18219
19273
  }
18220
19274
  /**
18221
19275
  * Fetch a wallet position in a vault.
@@ -18238,7 +19292,7 @@ async function deposit(context, params) {
18238
19292
  * })
18239
19293
  * ```
18240
19294
  */ async function getPosition(context, params) {
18241
- return createEarnKit().getPosition(params);
19295
+ return createEarnKit(context).getPosition(params);
18242
19296
  }
18243
19297
  /**
18244
19298
  * Fetch the current status of a cross-chain Earn deposit.
@@ -18260,7 +19314,7 @@ async function deposit(context, params) {
18260
19314
  * console.log(status.status)
18261
19315
  * ```
18262
19316
  */ async function getCrossChainDepositStatus(context, params) {
18263
- return createEarnKit().getCrossChainDepositStatus(params);
19317
+ return createEarnKit(context).getCrossChainDepositStatus(params);
18264
19318
  }
18265
19319
  /**
18266
19320
  * Poll a cross-chain Earn deposit until it reaches a terminal bridge state.
@@ -18283,7 +19337,7 @@ async function deposit(context, params) {
18283
19337
  * console.log(result.outcome)
18284
19338
  * ```
18285
19339
  */ async function waitForCrossChainDeposit(context, params) {
18286
- return createEarnKit().waitForCrossChainDeposit(params);
19340
+ return createEarnKit(context).waitForCrossChainDeposit(params);
18287
19341
  }
18288
19342
  /**
18289
19343
  * Fetch a deposit quote.
@@ -18307,7 +19361,7 @@ async function deposit(context, params) {
18307
19361
  * })
18308
19362
  * ```
18309
19363
  */ async function getDepositQuote(context, params) {
18310
- return createEarnKit().getDepositQuote(params);
19364
+ return createEarnKit(context).getDepositQuote(params);
18311
19365
  }
18312
19366
  /**
18313
19367
  * Fetch a withdrawal quote.
@@ -18331,7 +19385,7 @@ async function deposit(context, params) {
18331
19385
  * })
18332
19386
  * ```
18333
19387
  */ async function getWithdrawalQuote(context, params) {
18334
- return createEarnKit().getWithdrawalQuote(params);
19388
+ return createEarnKit(context).getWithdrawalQuote(params);
18335
19389
  }
18336
19390
  /**
18337
19391
  * Fetch a claim rewards quote.
@@ -18354,7 +19408,43 @@ async function deposit(context, params) {
18354
19408
  * })
18355
19409
  * ```
18356
19410
  */ async function getClaimRewardsQuote(context, params) {
18357
- return createEarnKit().getClaimRewardsQuote(params);
19411
+ return createEarnKit(context).getClaimRewardsQuote(params);
19412
+ }
19413
+ /**
19414
+ * Resume a multi-phase earn operation that previously failed.
19415
+ *
19416
+ * Pass the {@link KitError} caught from `deposit`, `withdraw`, or
19417
+ * `claimRewards`. Completed phases can be skipped when the error carries
19418
+ * earn retry context. Call `isRetryableError(error)` first.
19419
+ *
19420
+ * @remarks
19421
+ * Retry re-fetches execution params and may re-submit the execute
19422
+ * transaction. Treat this as best-effort recovery if a prior execute
19423
+ * broadcast may still be in flight.
19424
+ *
19425
+ * @param context - AppKit context
19426
+ * @param error - The error caught from a previous multi-phase earn operation
19427
+ * @returns Promise resolving to the result of the resumed operation
19428
+ * @throws If the error is not retryable or lacks earn retry context
19429
+ *
19430
+ * @example
19431
+ * ```typescript
19432
+ * import { isRetryableError } from '@circle-fin/app-kit'
19433
+ * import { createContext } from '@circle-fin/app-kit/context'
19434
+ * import { retry } from '@circle-fin/app-kit/earn'
19435
+ *
19436
+ * const context = createContext()
19437
+ *
19438
+ * try {
19439
+ * await deposit(context, params)
19440
+ * } catch (error) {
19441
+ * if (isRetryableError(error)) {
19442
+ * const result = await retry(context, error)
19443
+ * }
19444
+ * }
19445
+ * ```
19446
+ */ async function retry(context, error) {
19447
+ return createEarnKit(context).retry(error);
18358
19448
  }
18359
19449
 
18360
19450
  exports.claimRewards = claimRewards;
@@ -18367,6 +19457,7 @@ exports.getDepositQuote = getDepositQuote;
18367
19457
  exports.getPosition = getPosition;
18368
19458
  exports.getVaults = getVaults;
18369
19459
  exports.getWithdrawalQuote = getWithdrawalQuote;
19460
+ exports.retry = retry;
18370
19461
  exports.waitForCrossChainDeposit = waitForCrossChainDeposit;
18371
19462
  exports.withdraw = withdraw;
18372
19463
  //# sourceMappingURL=earn.cjs.map