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