@circle-fin/app-kit 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/earn.mjs CHANGED
@@ -16,6 +16,17 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ // Buffer polyfill setup - executes before any other code
20
+ // Ensures globalThis.Buffer is available for Solana libraries
21
+ import { Buffer } from 'buffer';
22
+ if (typeof globalThis !== 'undefined' && typeof globalThis.Buffer === 'undefined') {
23
+ globalThis.Buffer = Buffer;
24
+ }
25
+ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
26
+ window.Buffer = Buffer;
27
+ }
28
+
29
+
19
30
  import { z } from 'zod';
20
31
  import 'pino';
21
32
  import '@ethersproject/bytes';
@@ -26,8 +37,9 @@ import 'bn.js';
26
37
  import '@coral-xyz/anchor';
27
38
  import 'bs58';
28
39
  import '@noble/curves/ed25519';
29
- import { keccak256 } from '@ethersproject/keccak256';
40
+ import { decodeFunctionData } from 'viem';
30
41
  import { formatUnits as formatUnits$1 } from '@ethersproject/units';
42
+ import { keccak256 } from '@ethersproject/keccak256';
31
43
 
32
44
  // Import global type declarations
33
45
  /**
@@ -44,6 +56,51 @@ import { formatUnits as formatUnits$1 } from '@ethersproject/units';
44
56
  * }
45
57
  * ```
46
58
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
59
+ /**
60
+ * Check whether the current runtime exposes a browser DOM.
61
+ *
62
+ * @remarks
63
+ * This intentionally does not treat every non-Node runtime as a browser.
64
+ * Server-side edge runtimes such as Cloudflare Workers, Deno, and Supabase
65
+ * Edge Functions do not expose Node globals but can safely use server
66
+ * credentials. A Node.js runtime remains server-side even when a test or SSR
67
+ * environment provides a DOM shim.
68
+ *
69
+ * @returns `true` when running in a browser window, `false` otherwise.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * import { isBrowserEnvironment } from '@core/utils'
74
+ *
75
+ * if (isBrowserEnvironment()) {
76
+ * throw new Error('Server-only secrets must not be used in the browser')
77
+ * }
78
+ * ```
79
+ */ const isBrowserEnvironment = ()=>{
80
+ const browserWindow = globalThis.window;
81
+ return !isNodeEnvironment() && browserWindow?.document !== undefined;
82
+ };
83
+ /**
84
+ * Return the SDK User-Agent request header only when running in Node.js.
85
+ *
86
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
87
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
88
+ * attribution header because they cannot set it reliably.
89
+ *
90
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * import { getNodeUserAgentHeader } from '@core/utils'
95
+ *
96
+ * const headers = {
97
+ * 'Content-Type': 'application/json',
98
+ * ...getNodeUserAgentHeader(),
99
+ * }
100
+ * ```
101
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
102
+ 'User-Agent': getUserAgent()
103
+ } : {};
47
104
  /**
48
105
  * Detect the runtime environment and return a shortened identifier.
49
106
  *
@@ -621,6 +678,11 @@ class KitError extends Error {
621
678
  name: 'INPUT_INSUFFICIENT_SWAP_AMOUNT',
622
679
  type: 'INPUT'
623
680
  },
681
+ /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
682
+ code: 1008,
683
+ name: 'INPUT_UNSUPPORTED_ACTION',
684
+ type: 'INPUT'
685
+ },
624
686
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
625
687
  code: 1009,
626
688
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -685,6 +747,29 @@ class KitError extends Error {
685
747
  type: 'LIQUIDITY'
686
748
  }
687
749
  };
750
+ /**
751
+ * Standardized error definitions for RPC type errors.
752
+ *
753
+ * RPC errors occur when communicating with blockchain RPC providers,
754
+ * including endpoint failures, invalid responses, and provider-specific issues.
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * import { RpcError } from '@core/errors'
759
+ *
760
+ * const error = new KitError({
761
+ * ...RpcError.ENDPOINT_ERROR,
762
+ * recoverability: 'RETRYABLE',
763
+ * message: 'RPC endpoint unavailable on Ethereum',
764
+ * cause: { trace: { endpoint: 'https://mainnet.infura.io' } }
765
+ * })
766
+ * ```
767
+ */ const RpcError = {
768
+ /** RPC endpoint returned error or is unavailable */ ENDPOINT_ERROR: {
769
+ code: 4001,
770
+ name: 'RPC_ENDPOINT_ERROR',
771
+ type: 'RPC'
772
+ }};
688
773
  /**
689
774
  * Standardized error definitions for NETWORK type errors.
690
775
  *
@@ -2284,6 +2369,8 @@ function getOptionalString(value) {
2284
2369
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2285
2370
  Blockchain["XDC"] = "XDC";
2286
2371
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2372
+ Blockchain["X_Layer"] = "X_Layer";
2373
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2287
2374
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2288
2375
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2289
2376
  })(Blockchain || (Blockchain = {}));
@@ -2337,6 +2424,7 @@ var BridgeChain;
2337
2424
  BridgeChain["Unichain"] = "Unichain";
2338
2425
  BridgeChain["World_Chain"] = "World_Chain";
2339
2426
  BridgeChain["XDC"] = "XDC";
2427
+ BridgeChain["X_Layer"] = "X_Layer";
2340
2428
  // Testnet chains with CCTPv2 support
2341
2429
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2342
2430
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2362,6 +2450,7 @@ var BridgeChain;
2362
2450
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2363
2451
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2364
2452
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2453
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2365
2454
  })(BridgeChain || (BridgeChain = {}));
2366
2455
  var UnifiedBalanceChain;
2367
2456
  (function(UnifiedBalanceChain) {
@@ -4899,7 +4988,8 @@ var EarnChain;
4899
4988
  isTestnet: true,
4900
4989
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4901
4990
  rpcEndpoints: [
4902
- 'https://rpc-amoy.polygon.technology'
4991
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4992
+ 'https://polygon-amoy.drpc.org'
4903
4993
  ],
4904
4994
  eurcAddress: null,
4905
4995
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5764,6 +5854,104 @@ var EarnChain;
5764
5854
  }
5765
5855
  });
5766
5856
 
5857
+ /**
5858
+ * X Layer Mainnet chain definition
5859
+ * @remarks
5860
+ * This represents the official production network for the X Layer blockchain.
5861
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5862
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5863
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5864
+ */ const XLayer = defineChain({
5865
+ type: 'evm',
5866
+ chain: Blockchain.X_Layer,
5867
+ name: 'X Layer',
5868
+ title: 'X Layer Mainnet',
5869
+ nativeCurrency: {
5870
+ name: 'OKB',
5871
+ symbol: 'OKB',
5872
+ decimals: 18
5873
+ },
5874
+ chainId: 196,
5875
+ isTestnet: false,
5876
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5877
+ rpcEndpoints: [
5878
+ 'https://xlayerrpc.okx.com'
5879
+ ],
5880
+ eurcAddress: null,
5881
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5882
+ usdtAddress: null,
5883
+ cctp: {
5884
+ domain: 37,
5885
+ contracts: {
5886
+ v2: {
5887
+ type: 'split',
5888
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5889
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5890
+ confirmations: 65,
5891
+ fastConfirmations: 1
5892
+ }
5893
+ },
5894
+ forwarderSupported: {
5895
+ source: false,
5896
+ destination: false
5897
+ }
5898
+ },
5899
+ kitContracts: {
5900
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5901
+ }
5902
+ });
5903
+
5904
+ /**
5905
+ * X Layer Testnet chain definition
5906
+ * @remarks
5907
+ * This represents the official test network for the X Layer blockchain.
5908
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5909
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5910
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5911
+ */ const XLayerTestnet = defineChain({
5912
+ type: 'evm',
5913
+ chain: Blockchain.X_Layer_Testnet,
5914
+ name: 'X Layer Testnet',
5915
+ title: 'X Layer Testnet',
5916
+ nativeCurrency: {
5917
+ name: 'OKB',
5918
+ symbol: 'OKB',
5919
+ decimals: 18
5920
+ },
5921
+ chainId: 1952,
5922
+ isTestnet: true,
5923
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5924
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5925
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5926
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5927
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5928
+ rpcEndpoints: [
5929
+ 'https://testrpc.xlayer.tech'
5930
+ ],
5931
+ eurcAddress: null,
5932
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5933
+ usdtAddress: null,
5934
+ cctp: {
5935
+ domain: 37,
5936
+ contracts: {
5937
+ v2: {
5938
+ type: 'split',
5939
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5940
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5941
+ confirmations: 65,
5942
+ fastConfirmations: 1
5943
+ }
5944
+ },
5945
+ forwarderSupported: {
5946
+ source: false,
5947
+ destination: false
5948
+ }
5949
+ },
5950
+ kitContracts: {
5951
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5952
+ }
5953
+ });
5954
+
5767
5955
  /**
5768
5956
  * ZKSync Era Mainnet chain definition
5769
5957
  * @remarks
@@ -5883,6 +6071,8 @@ var Chains = /*#__PURE__*/Object.freeze({
5883
6071
  WorldChainSepolia: WorldChainSepolia,
5884
6072
  XDC: XDC,
5885
6073
  XDCApothem: XDCApothem,
6074
+ XLayer: XLayer,
6075
+ XLayerTestnet: XLayerTestnet,
5886
6076
  ZKSyncEra: ZKSyncEra,
5887
6077
  ZKSyncEraSepolia: ZKSyncEraSepolia
5888
6078
  });
@@ -6392,6 +6582,39 @@ const swapTokenEnumSchema = z.enum([
6392
6582
  throw new Error(`Invalid chain identifier type: ${typeof chainIdentifier}. Expected ChainDefinition object, Blockchain enum, or string literal.`);
6393
6583
  }
6394
6584
 
6585
+ /**
6586
+ * Resolve a chain identifier to a plain chain-name string.
6587
+ *
6588
+ * Accept a string literal (`'Ethereum'`), a `ChainDefinition`-like
6589
+ * object (`{ chain: 'Ethereum' }`), or `null`/`undefined` and return
6590
+ * the chain name as a string. Return `undefined` when the value
6591
+ * cannot be resolved.
6592
+ *
6593
+ * @remarks
6594
+ * Unlike `resolveChainIdentifier` (which returns a full `ChainDefinition`
6595
+ * and throws on invalid input), this helper is intentionally lenient and
6596
+ * never throws — it is safe to call in error-handling and telemetry paths.
6597
+ *
6598
+ * @param value - A string, chain-definition object, or nullish value.
6599
+ * @returns The chain name string, or `undefined`.
6600
+ *
6601
+ * @example
6602
+ * ```typescript
6603
+ * import { resolveChainName } from '@core/chains'
6604
+ *
6605
+ * resolveChainName('Ethereum') // 'Ethereum'
6606
+ * resolveChainName({ chain: 'Ethereum' }) // 'Ethereum'
6607
+ * resolveChainName(undefined) // undefined
6608
+ * ```
6609
+ */ function resolveChainName(value) {
6610
+ if (value == null) return undefined;
6611
+ if (typeof value === 'string') return value;
6612
+ if (typeof value === 'object' && 'chain' in value && typeof value.chain === 'string') {
6613
+ return value.chain;
6614
+ }
6615
+ return undefined;
6616
+ }
6617
+
6395
6618
  /**
6396
6619
  * Extracts chain information including name, display name, and expected address format.
6397
6620
  *
@@ -6776,13 +6999,12 @@ const swapTokenEnumSchema = z.enum([
6776
6999
  headers: {
6777
7000
  ...DEFAULT_CONFIG$1.headers,
6778
7001
  ...config.headers ?? {},
6779
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
6780
- // This is why we use the 'X-User-Agent' header instead.
6781
- ...typeof window === 'undefined' ? {
6782
- 'User-Agent': getUserAgent()
6783
- } : {
6784
- 'X-User-Agent': getUserAgent()
6785
- }
7002
+ // Browsers forbid setting a user-agent request header, and the custom
7003
+ // fallback header the SDK used instead trips CORS preflight against the
7004
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
7005
+ // blocking the request. So send the SDK user agent only in Node;
7006
+ // browsers omit it entirely.
7007
+ ...getNodeUserAgentHeader()
6786
7008
  }
6787
7009
  };
6788
7010
  let lastError;
@@ -7294,6 +7516,7 @@ const swapTokenEnumSchema = z.enum([
7294
7516
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7295
7517
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7296
7518
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7519
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7297
7520
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7298
7521
  // =========================================================================
7299
7522
  // Testnets (alphabetically sorted)
@@ -7328,6 +7551,7 @@ const swapTokenEnumSchema = z.enum([
7328
7551
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7329
7552
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7330
7553
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
7554
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7331
7555
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7332
7556
  }
7333
7557
  };
@@ -8042,134 +8266,544 @@ const swapTokenEnumSchema = z.enum([
8042
8266
  }
8043
8267
 
8044
8268
  /**
8045
- * CCTP forwarding magic bytes prefix.
8269
+ * Assert that a value has type `never` (exhaustive switch helper).
8046
8270
  *
8047
- * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
8048
- * This prefix is right-padded to 24 bytes in the final hookData.
8049
- */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
8050
-
8051
- /**
8052
- * Strip the `@circle-fin/` scope from a kit package name to produce the
8053
- * short SDK name used in telemetry payloads.
8271
+ * @remarks
8272
+ * Use in the `default` branch of a switch over a discriminated union.
8273
+ * If all union members are handled, the default is unreachable and TypeScript
8274
+ * narrows the parameter to `never`. If a member is missed, the compiler errors.
8054
8275
  *
8055
- * @param pkgName - The full npm package name (e.g. `@circle-fin/bridge-kit`).
8056
- * @returns The unscoped kit name (e.g. `bridge-kit`).
8276
+ * @param _x - The value (typed as `never` when switch is exhaustive).
8277
+ * @returns Never returns; always throws.
8278
+ * @throws Error when the switch is not exhaustive.
8057
8279
  *
8058
8280
  * @example
8059
8281
  * ```typescript
8060
- * import pkg from '../../package.json'
8061
- * import { resolveKitSdkName } from '@core/utils'
8282
+ * type Foo = { type: 'a'; x: number } | { type: 'b'; y: string }
8062
8283
  *
8063
- * const SDK_NAME = resolveKitSdkName(pkg.name) // 'bridge-kit'
8284
+ * function handle(foo: Foo): string {
8285
+ * switch (foo.type) {
8286
+ * case 'a': return String(foo.x)
8287
+ * case 'b': return foo.y
8288
+ * default: return assertNever(foo)
8289
+ * }
8290
+ * }
8064
8291
  * ```
8065
- */ function resolveKitSdkName(pkgName) {
8066
- return pkgName.replace('@circle-fin/', '');
8292
+ */ function assertNever$2(x) {
8293
+ // Plain `String(x)` collapses non-primitive union members (objects, arrays)
8294
+ // to `'[object Object]'`, which is useless when triaging which discriminant
8295
+ // was missed. Attempt `JSON.stringify` first so the thrown message preserves
8296
+ // the offending shape. Fall back to a minimal `typeof`-based label if
8297
+ // serialization fails (`BigInt` member, circular references, host objects).
8298
+ //
8299
+ // `x` is statically typed as `never` (the whole point of this helper), but
8300
+ // at runtime callers may still pass an unexpected value when the switch is
8301
+ // not actually exhaustive — that's exactly the bug we want to surface. Cast
8302
+ // through `unknown` so the runtime defence is not stripped by the compiler.
8303
+ const value = x;
8304
+ let stringified;
8305
+ try {
8306
+ const json = JSON.stringify(value);
8307
+ stringified = typeof json === 'string' ? json : `<${typeof value}>`;
8308
+ } catch {
8309
+ stringified = `<unstringifiable ${typeof value}>`;
8310
+ }
8311
+ throw new Error(`Unhandled switch case: ${stringified}`);
8067
8312
  }
8068
8313
 
8069
- var name$3 = "@circle-fin/bridge-kit";
8070
- var version$3 = "1.12.1";
8071
- var pkg$3 = {
8072
- name: name$3,
8073
- version: version$3};
8074
-
8075
8314
  /**
8076
- * Schema for validating BridgeKit custom fee policy.
8315
+ * CCTP forwarding magic bytes prefix.
8077
8316
  *
8078
- * Validates the shape of {@link CustomFeePolicy}, which lets SDK consumers
8079
- * provide custom fee calculation and fee-recipient resolution logic.
8317
+ * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
8318
+ * This prefix is right-padded to 24 bytes in the final hookData.
8319
+ */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
8320
+
8321
+ /**
8322
+ * Project an arbitrary payload onto the exact set of fields the telemetry
8323
+ * endpoint accepts.
8080
8324
  *
8081
- * - computeFee: optional function (recommended) that receives human-readable amounts
8082
- * and returns a fee as a string (or Promise<string>).
8083
- * - calculateFee: optional function (deprecated) that receives smallest-unit amounts
8084
- * and returns a fee as a string (or Promise<string>).
8085
- * - resolveFeeRecipientAddress: required function that returns a recipient address as a
8086
- * string (or Promise<string>).
8325
+ * @remarks
8326
+ * Defense-in-depth before the last network hop: rather than
8327
+ * `JSON.stringify`-ing the caller's object verbatim, only the
8328
+ * allowlisted {@link ClientLogPayload} fields (and the allowlisted
8329
+ * sub-fields of `errorDetails` / `clientContext`) are copied across.
8330
+ * A regressing upstream mapper — or a plain-JS caller that bypasses the
8331
+ * type — therefore cannot exfiltrate stray properties (secrets, PII,
8332
+ * raw error stacks) through the analytics channel. Optional fields are
8333
+ * only included when present so the serialised shape matches the
8334
+ * server's strict schema.
8087
8335
  *
8088
- * Exactly one of `computeFee` or `calculateFee` must be provided (not both).
8336
+ * @internal
8337
+ */ function toSafePayload(payload) {
8338
+ const clientContext = {
8339
+ platform: payload.clientContext.platform,
8340
+ os: payload.clientContext.os,
8341
+ runtimeName: payload.clientContext.runtimeName
8342
+ };
8343
+ const safe = {
8344
+ sdkName: payload.sdkName,
8345
+ sdkVersion: payload.sdkVersion,
8346
+ eventType: payload.eventType,
8347
+ timestamp: payload.timestamp,
8348
+ clientContext
8349
+ };
8350
+ if (payload.sourceChain !== undefined) safe['sourceChain'] = payload.sourceChain;
8351
+ if (payload.destinationChain !== undefined) safe['destinationChain'] = payload.destinationChain;
8352
+ if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
8353
+ if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
8354
+ if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
8355
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
8356
+ if (payload.errorDetails !== undefined) {
8357
+ const errorDetails = {
8358
+ ...payload.errorDetails.errorCode !== undefined && {
8359
+ errorCode: payload.errorDetails.errorCode
8360
+ },
8361
+ ...payload.errorDetails.errorType !== undefined && {
8362
+ errorType: payload.errorDetails.errorType
8363
+ }
8364
+ };
8365
+ safe['errorDetails'] = errorDetails;
8366
+ }
8367
+ return safe;
8368
+ }
8369
+ /**
8370
+ * Default telemetry endpoint.
8089
8371
  *
8090
- * This schema only ensures the presence and return types of the functions; it
8091
- * does not validate their argument types.
8372
+ * Override via the `STABLECOIN_KITS_TELEMETRY_URL` environment variable
8373
+ * (e.g. for staging or local development).
8092
8374
  *
8093
- * @example
8094
- * ```ts
8095
- * const config = {
8096
- * computeFee: async () => '1', // 1 USDC (human-readable)
8097
- * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
8098
- * }
8099
- * const result = customFeePolicySchema.safeParse(config)
8100
- * // result.success === true
8101
- * ```
8102
- */ z.object({
8103
- computeFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
8104
- calculateFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
8105
- resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
8106
- }).strict().refine((data)=>{
8107
- const hasComputeFee = data.computeFee !== undefined;
8108
- const hasCalculateFee = data.calculateFee !== undefined;
8109
- // XOR: exactly one must be provided
8110
- return hasComputeFee !== hasCalculateFee;
8111
- }, {
8112
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
8113
- });
8114
-
8375
+ * @internal
8376
+ */ const DEFAULT_LOGS_URL = 'https://api.circle.com/v1/stablecoinKits/logs';
8115
8377
  /**
8116
- * Schema for validating hexadecimal strings with '0x' prefix.
8378
+ * Resolve the telemetry endpoint URL.
8117
8379
  *
8118
- * This schema validates that a string:
8119
- * - Is a string type
8120
- * - Is not empty after trimming
8121
- * - Starts with '0x'
8122
- * - Contains only valid hexadecimal characters (0-9, a-f, A-F) after '0x'
8380
+ * @internal
8381
+ */ function getLogsUrl() {
8382
+ if (isNodeEnvironment() && typeof process.env['STABLECOIN_KITS_TELEMETRY_URL'] === 'string' && process.env['STABLECOIN_KITS_TELEMETRY_URL'].length > 0) {
8383
+ return process.env['STABLECOIN_KITS_TELEMETRY_URL'];
8384
+ }
8385
+ return DEFAULT_LOGS_URL;
8386
+ }
8387
+ /**
8388
+ * Send a telemetry event to the proxy service.
8123
8389
  *
8124
8390
  * @remarks
8125
- * This schema does not validate length, making it suitable for various hex string types
8126
- * like addresses, transaction hashes, and other hex-encoded data.
8391
+ * Fire-and-forget: the returned promise is intentionally not awaited
8392
+ * by the caller. A fetch failure (network error, non-2xx, timeout)
8393
+ * is silently swallowed so telemetry never blocks or fails user
8394
+ * operations.
8127
8395
  *
8128
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
8396
+ * @param payload - The structured log payload matching the server schema.
8129
8397
  *
8130
8398
  * @example
8131
8399
  * ```typescript
8132
- * import { hexStringSchema } from '@core/adapter'
8400
+ * import { emitAnalyticsLog } from '@core/utils'
8133
8401
  *
8134
- * const validAddress = '0x1234567890123456789012345678901234567890'
8135
- * const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
8136
- *
8137
- * const addressResult = hexStringSchema.safeParse(validAddress)
8138
- * const txHashResult = hexStringSchema.safeParse(validTxHash)
8139
- * console.log(addressResult.success) // true
8140
- * console.log(txHashResult.success) // true
8402
+ * // Fire-and-forget do not await
8403
+ * void emitAnalyticsLog(payload)
8141
8404
  * ```
8142
- */ const hexStringSchema = z.string().min(1, 'Hex string is required').refine((value)=>value.trim().length > 0, 'Hex string cannot be empty').refine((value)=>value.startsWith('0x'), 'Hex string must start with 0x prefix').refine((value)=>{
8143
- const hexPattern = /^0x[0-9a-fA-F]+$/;
8144
- return hexPattern.test(value);
8145
- }, 'Hex string contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x');
8405
+ */ async function emitAnalyticsLog(payload) {
8406
+ // Hand-rolled timeout via `AbortController` + `setTimeout` rather than
8407
+ // `AbortSignal.timeout(...)` so we can `clearTimeout` the handle in a
8408
+ // `finally`. `AbortSignal.timeout` registers a timer that stays on the
8409
+ // event loop until it fires even if the fetch already settled, which
8410
+ // manifests as spurious `TimeoutError` unhandled rejections during
8411
+ // process teardown (notably between e2e test fork lifecycles). See
8412
+ // nodejs/node#48298 for the underlying issue.
8413
+ const controller = new AbortController();
8414
+ const timeoutHandle = setTimeout(()=>{
8415
+ controller.abort(new DOMException('Telemetry request timed out', 'TimeoutError'));
8416
+ }, 5_000);
8417
+ // Don't let the timer keep the Node event loop alive in short-lived
8418
+ // CLIs / test processes; telemetry is best-effort and must never
8419
+ // block clean process exit. `unref` only exists on Node's `Timeout`
8420
+ // object, not on the `number` returned by the browser's `setTimeout`,
8421
+ // so we feature-detect rather than call unconditionally.
8422
+ if (typeof timeoutHandle.unref === 'function') {
8423
+ timeoutHandle.unref();
8424
+ }
8425
+ try {
8426
+ await fetch(getLogsUrl(), {
8427
+ method: 'POST',
8428
+ headers: {
8429
+ 'Content-Type': 'application/json',
8430
+ // Browsers forbid setting a user-agent request header, and the custom
8431
+ // fallback header the SDK used instead trips CORS preflight (it isn't
8432
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
8433
+ // it only in Node; browsers omit it entirely.
8434
+ ...getNodeUserAgentHeader()
8435
+ },
8436
+ body: JSON.stringify(toSafePayload(payload)),
8437
+ signal: controller.signal
8438
+ });
8439
+ } catch {
8440
+ // Silently swallow — telemetry must never break user operations.
8441
+ } finally{
8442
+ clearTimeout(timeoutHandle);
8443
+ }
8444
+ }
8445
+
8146
8446
  /**
8147
- * Schema for validating EVM addresses.
8447
+ * Build the `clientContext` object for telemetry payloads.
8148
8448
  *
8149
- * This schema validates that a string is a properly formatted EVM address:
8150
- * - Must be a valid hex string with '0x' prefix
8151
- * - Must be exactly 42 characters long (0x + 40 hex characters)
8449
+ * @remarks
8450
+ * Use the exported `getRuntime()` and `isNodeEnvironment()` from
8451
+ * `@core/utils` to detect the runtime environment. The returned
8452
+ * string is parsed into the structured `ClientContext` fields
8453
+ * expected by the server schema.
8152
8454
  *
8153
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
8455
+ * @returns A {@link ClientContext} with platform, OS, and runtime name
8456
+ * populated from the current environment.
8154
8457
  *
8155
8458
  * @example
8156
8459
  * ```typescript
8157
- * import { evmAddressSchema } from '@core/adapter'
8158
- *
8159
- * const validAddress = '0x1234567890123456789012345678901234567890'
8460
+ * import { buildClientContext } from '@core/utils'
8160
8461
  *
8161
- * const result = evmAddressSchema.safeParse(validAddress)
8162
- * console.log(result.success) // true
8462
+ * const ctx = buildClientContext()
8463
+ * // Node: { platform: 'node', os: 'darwin', runtimeName: null }
8464
+ * // Browser: { platform: 'browser', os: null, runtimeName: 'chrome' }
8163
8465
  * ```
8164
- */ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
8466
+ */ function buildClientContext() {
8467
+ const runtime = getRuntime();
8468
+ if (runtime.startsWith('browser/')) {
8469
+ return {
8470
+ platform: 'browser',
8471
+ os: null,
8472
+ runtimeName: runtime.slice('browser/'.length).toLowerCase()
8473
+ };
8474
+ }
8475
+ if (runtime.startsWith('node/')) {
8476
+ return {
8477
+ platform: 'node',
8478
+ os: isNodeEnvironment() ? process.platform : null,
8479
+ runtimeName: null
8480
+ };
8481
+ }
8482
+ return {
8483
+ platform: 'node',
8484
+ os: null,
8485
+ runtimeName: null
8486
+ };
8487
+ }
8488
+
8165
8489
  /**
8166
- * Schema for validating transaction hashes.
8490
+ * Extract structured error details from an unknown error value.
8167
8491
  *
8168
- * This schema validates that a string is a properly formatted transaction hash:
8169
- * - Must be a valid hex string with '0x' prefix
8170
- * - Must be exactly 66 characters long (0x + 64 hex characters)
8492
+ * @remarks
8493
+ * Handle three cases:
8494
+ * - `KitError` extract `code` and `name`.
8495
+ * - `Error` — extract `name`.
8496
+ * - Anything else — return empty details.
8171
8497
  *
8172
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
8498
+ * Only structured, bounded fields (`errorCode`, `errorType`) are
8499
+ * included. Free-text fields (`message`, `stack`) are intentionally
8500
+ * omitted to avoid leaking secrets or PII through vendor telemetry.
8501
+ *
8502
+ * @param error - The thrown value to extract details from.
8503
+ * @returns A {@link ErrorDetails} object suitable for telemetry payloads.
8504
+ *
8505
+ * @example
8506
+ * ```typescript
8507
+ * import { extractErrorDetails } from '@core/utils'
8508
+ *
8509
+ * try {
8510
+ * await riskyOperation()
8511
+ * } catch (error) {
8512
+ * const details = extractErrorDetails(error)
8513
+ * // { errorCode: '1001', errorType: 'INPUT_NETWORK_MISMATCH' }
8514
+ * }
8515
+ * ```
8516
+ */ function extractErrorDetails(error) {
8517
+ if (error instanceof KitError) {
8518
+ return {
8519
+ errorCode: String(error.code),
8520
+ errorType: error.name
8521
+ };
8522
+ }
8523
+ if (error instanceof Error) {
8524
+ return {
8525
+ errorType: error.name
8526
+ };
8527
+ }
8528
+ return {};
8529
+ }
8530
+
8531
+ /**
8532
+ * Strip the `@circle-fin/` scope from a kit package name to produce the
8533
+ * short SDK name used in telemetry payloads.
8534
+ *
8535
+ * @param pkgName - The full npm package name (e.g. `@circle-fin/bridge-kit`).
8536
+ * @returns The unscoped kit name (e.g. `bridge-kit`).
8537
+ *
8538
+ * @example
8539
+ * ```typescript
8540
+ * import pkg from '../../package.json'
8541
+ * import { resolveKitSdkName } from '@core/utils'
8542
+ *
8543
+ * const SDK_NAME = resolveKitSdkName(pkg.name) // 'bridge-kit'
8544
+ * ```
8545
+ */ function resolveKitSdkName(pkgName) {
8546
+ return pkgName.replace('@circle-fin/', '');
8547
+ }
8548
+
8549
+ /**
8550
+ * Soft signal for the case where building or emitting a telemetry payload
8551
+ * threw — for example, a buggy `TelemetryContextResolver`, a regression in
8552
+ * `extractErrorDetails`, or a synchronous failure inside `emitAnalyticsLog`
8553
+ * before it could swallow the error itself. Logged with a stable prefix so
8554
+ * consumers can grep for it. We deliberately do not re-throw: the caller's
8555
+ * original operation error must always win.
8556
+ *
8557
+ * @internal
8558
+ */ function warnTelemetryDrop(eventType, cause) {
8559
+ try {
8560
+ // Pass `cause` as the second console.warn argument rather than
8561
+ // string-coercing it. `String(err)` (and `err.message` alone)
8562
+ // discards the stack trace, nested `cause`, and any custom Error
8563
+ // properties — exactly the context an on-call needs when a
8564
+ // resolver-closure regression triggers this path.
8565
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
8566
+ } catch {
8567
+ // console.warn itself throwing is the user's environment; nothing more we
8568
+ // can do without risking the original operation error.
8569
+ }
8570
+ }
8571
+ /**
8572
+ * Build a telemetry payload from common fields.
8573
+ *
8574
+ * @internal
8575
+ */ function buildPayload(config, eventType, errorDetails, context) {
8576
+ return {
8577
+ sdkName: config.sdkName,
8578
+ sdkVersion: config.sdkVersion,
8579
+ eventType,
8580
+ timestamp: new Date().toISOString(),
8581
+ ...errorDetails !== undefined && {
8582
+ errorDetails
8583
+ },
8584
+ clientContext: buildClientContext(),
8585
+ ...context?.sourceChain != null && {
8586
+ sourceChain: context.sourceChain
8587
+ },
8588
+ ...context?.destinationChain != null && {
8589
+ destinationChain: context.destinationChain
8590
+ },
8591
+ ...context?.tokenIn != null && {
8592
+ tokenIn: context.tokenIn
8593
+ },
8594
+ ...context?.tokenOut != null && {
8595
+ tokenOut: context.tokenOut
8596
+ },
8597
+ ...context?.txHash != null && {
8598
+ txHash: context.txHash
8599
+ },
8600
+ ...context?.correlationId != null && {
8601
+ correlationId: context.correlationId
8602
+ }
8603
+ };
8604
+ }
8605
+ /**
8606
+ * Emit telemetry for a completed operation without affecting its caller.
8607
+ *
8608
+ * No-ops when `config.disabled` is `true`. Like {@link withErrorTelemetry},
8609
+ * failures while constructing or submitting the telemetry payload are reported
8610
+ * as a soft warning and never change a completed operation's result.
8611
+ *
8612
+ * @param eventType - The telemetry event type for the completed operation.
8613
+ * @param config - Per-kit SDK identity and disabled flag.
8614
+ * @param context - Optional chain, token, and transaction context.
8615
+ * @returns Nothing.
8616
+ * @throws Never — telemetry failures are reported as warnings.
8617
+ *
8618
+ * @example
8619
+ * ```typescript
8620
+ * import { emitSuccessTelemetry } from '@core/utils'
8621
+ *
8622
+ * emitSuccessTelemetry(
8623
+ * 'bridge_bridge',
8624
+ * { sdkName: 'bridge-kit', sdkVersion: '1.0.0', disabled: false },
8625
+ * { sourceChain: 'Ethereum', destinationChain: 'Base', tokenIn: 'USDC' },
8626
+ * )
8627
+ * ```
8628
+ */ function emitSuccessTelemetry(eventType, config, context) {
8629
+ if (config.disabled) {
8630
+ return;
8631
+ }
8632
+ try {
8633
+ void emitAnalyticsLog(buildPayload(config, eventType, undefined, context));
8634
+ } catch (telemetryError) {
8635
+ warnTelemetryDrop(eventType, telemetryError);
8636
+ }
8637
+ }
8638
+ /**
8639
+ * Wrap an async operation with error telemetry.
8640
+ *
8641
+ * Execute `fn` and, if it throws, emit an error telemetry payload
8642
+ * before re-throwing. No-ops when `config.disabled` is `true`.
8643
+ *
8644
+ * `context` may be a static {@link TelemetryContext} or a
8645
+ * {@link TelemetryContextResolver}. The resolver is invoked in the
8646
+ * catch branch, so it can read state — most importantly `txHash` —
8647
+ * that the wrapped operation set after a successful broadcast. The
8648
+ * resolver must close over per-call locals only; passing instance
8649
+ * state would break isolation between concurrent invocations.
8650
+ *
8651
+ * @param fn - The async operation to execute.
8652
+ * @param eventType - The telemetry event type for this operation.
8653
+ * @param config - Per-kit SDK identity and disabled flag.
8654
+ * @param context - Optional context, static or lazily resolved.
8655
+ * @returns The result of the operation.
8656
+ * @throws Re-throws any error after emitting telemetry.
8657
+ *
8658
+ * @example
8659
+ * ```typescript
8660
+ * import { withErrorTelemetry } from '@core/utils'
8661
+ *
8662
+ * let txHash: string | undefined
8663
+ * const result = await withErrorTelemetry(
8664
+ * () => provider.swap(params, h => { txHash = h }),
8665
+ * 'swap_swap',
8666
+ * { sdkName: 'swap-kit', sdkVersion: '1.0.0', disabled: false },
8667
+ * () => ({
8668
+ * sourceChain: 'Ethereum',
8669
+ * tokenIn: 'USDC',
8670
+ * tokenOut: 'EURC',
8671
+ * ...(txHash != null && { txHash }),
8672
+ * }),
8673
+ * )
8674
+ * ```
8675
+ */ async function withErrorTelemetry(fn, eventType, config, context) {
8676
+ try {
8677
+ return await fn();
8678
+ } catch (error) {
8679
+ if (!config.disabled) {
8680
+ try {
8681
+ const resolved = typeof context === 'function' ? context() : context;
8682
+ void emitAnalyticsLog(buildPayload(config, eventType, extractErrorDetails(error), resolved));
8683
+ } catch (telemetryError) {
8684
+ // Never let telemetry emission mask the original operation error.
8685
+ // But surface a soft signal so silent telemetry drops are
8686
+ // discoverable (e.g. a regression in a resolver closure or in
8687
+ // `extractErrorDetails`) instead of vanishing without any trace.
8688
+ warnTelemetryDrop(eventType, telemetryError);
8689
+ }
8690
+ }
8691
+ throw error;
8692
+ }
8693
+ }
8694
+
8695
+ var name$3 = "@circle-fin/bridge-kit";
8696
+ var version$3 = "1.13.0";
8697
+ var pkg$3 = {
8698
+ name: name$3,
8699
+ version: version$3};
8700
+
8701
+ /**
8702
+ * Schema for validating BridgeKit custom fee policy.
8703
+ *
8704
+ * Validates the shape of {@link CustomFeePolicy}, which lets SDK consumers
8705
+ * provide custom fee calculation and fee-recipient resolution logic.
8706
+ *
8707
+ * - computeFee: optional function (recommended) that receives human-readable amounts
8708
+ * and returns a fee as a string (or Promise<string>).
8709
+ * - calculateFee: optional function (deprecated) that receives smallest-unit amounts
8710
+ * and returns a fee as a string (or Promise<string>).
8711
+ * - resolveFeeRecipientAddress: required function that returns a recipient address as a
8712
+ * string (or Promise<string>).
8713
+ *
8714
+ * Exactly one of `computeFee` or `calculateFee` must be provided (not both).
8715
+ *
8716
+ * This schema only ensures the presence and return types of the functions; it
8717
+ * does not validate their argument types.
8718
+ *
8719
+ * @example
8720
+ * ```ts
8721
+ * const config = {
8722
+ * computeFee: async () => '1', // 1 USDC (human-readable)
8723
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
8724
+ * }
8725
+ * const result = customFeePolicySchema.safeParse(config)
8726
+ * // result.success === true
8727
+ * ```
8728
+ */ z.object({
8729
+ computeFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
8730
+ calculateFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
8731
+ resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
8732
+ }).strict().superRefine((data, ctx)=>{
8733
+ const hasComputeFee = data.computeFee !== undefined;
8734
+ const hasCalculateFee = data.calculateFee !== undefined;
8735
+ if (hasComputeFee && hasCalculateFee) {
8736
+ ctx.addIssue({
8737
+ code: z.ZodIssueCode.custom,
8738
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
8739
+ });
8740
+ }
8741
+ if (!hasComputeFee && !hasCalculateFee) {
8742
+ ctx.addIssue({
8743
+ code: z.ZodIssueCode.custom,
8744
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
8745
+ });
8746
+ }
8747
+ });
8748
+
8749
+ /**
8750
+ * Schema for validating hexadecimal strings with '0x' prefix.
8751
+ *
8752
+ * This schema validates that a string:
8753
+ * - Is a string type
8754
+ * - Is not empty after trimming
8755
+ * - Starts with '0x'
8756
+ * - Contains only valid hexadecimal characters (0-9, a-f, A-F) after '0x'
8757
+ *
8758
+ * @remarks
8759
+ * This schema does not validate length, making it suitable for various hex string types
8760
+ * like addresses, transaction hashes, and other hex-encoded data.
8761
+ *
8762
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
8763
+ *
8764
+ * @example
8765
+ * ```typescript
8766
+ * import { hexStringSchema } from '@core/adapter'
8767
+ *
8768
+ * const validAddress = '0x1234567890123456789012345678901234567890'
8769
+ * const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
8770
+ *
8771
+ * const addressResult = hexStringSchema.safeParse(validAddress)
8772
+ * const txHashResult = hexStringSchema.safeParse(validTxHash)
8773
+ * console.log(addressResult.success) // true
8774
+ * console.log(txHashResult.success) // true
8775
+ * ```
8776
+ */ const hexStringSchema = z.string().min(1, 'Hex string is required').refine((value)=>value.trim().length > 0, 'Hex string cannot be empty').refine((value)=>value.startsWith('0x'), 'Hex string must start with 0x prefix').refine((value)=>{
8777
+ const hexPattern = /^0x[0-9a-fA-F]+$/;
8778
+ return hexPattern.test(value);
8779
+ }, 'Hex string contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x');
8780
+ /**
8781
+ * Schema for validating EVM addresses.
8782
+ *
8783
+ * This schema validates that a string is a properly formatted EVM address:
8784
+ * - Must be a valid hex string with '0x' prefix
8785
+ * - Must be exactly 42 characters long (0x + 40 hex characters)
8786
+ *
8787
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
8788
+ *
8789
+ * @example
8790
+ * ```typescript
8791
+ * import { evmAddressSchema } from '@core/adapter'
8792
+ *
8793
+ * const validAddress = '0x1234567890123456789012345678901234567890'
8794
+ *
8795
+ * const result = evmAddressSchema.safeParse(validAddress)
8796
+ * console.log(result.success) // true
8797
+ * ```
8798
+ */ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
8799
+ /**
8800
+ * Schema for validating transaction hashes.
8801
+ *
8802
+ * This schema validates that a string is a properly formatted transaction hash:
8803
+ * - Must be a valid hex string with '0x' prefix
8804
+ * - Must be exactly 66 characters long (0x + 64 hex characters)
8805
+ *
8806
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
8173
8807
  *
8174
8808
  * @example
8175
8809
  * ```typescript
@@ -8895,7 +9529,13 @@ var TransferSpeed;
8895
9529
  /**
8896
9530
  * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
8897
9531
  * hookData must start with.
8898
- */ Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
9532
+ *
9533
+ * Encoded with `TextEncoder` (a browser-safe Web API) rather than `Buffer.from`
9534
+ * so this module-level constant does not reference the Node `Buffer` global at
9535
+ * import time. App Kit inlines this provider into its bundle without a Buffer polyfill, and a
9536
+ * bare `Buffer` here crashes browser bundles (e.g. Vite) on load — even for apps
9537
+ * that never touch the prepaid FORWARD path. Mirrors `buildForwardingHookData`.
9538
+ */ Array.from(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX)).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
8899
9539
 
8900
9540
  /**
8901
9541
  * The minimum finality threshold for CCTPv2 transfers.
@@ -8929,7 +9569,7 @@ var TransferSpeed;
8929
9569
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
8930
9570
 
8931
9571
  var name$2 = "@circle-fin/swap-kit";
8932
- var version$2 = "1.4.0";
9572
+ var version$2 = "1.5.1";
8933
9573
  var pkg$2 = {
8934
9574
  name: name$2,
8935
9575
  version: version$2};
@@ -9670,6 +10310,16 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9670
10310
  required_error: 'estimatedAmount is required',
9671
10311
  invalid_type_error: 'estimatedAmount must be a string'
9672
10312
  }).min(1, 'estimatedAmount must be a non-empty string'),
10313
+ // Per-swap join key echoed back verbatim on success telemetry. Optional so a
10314
+ // not-yet-upgraded service (no field) still validates during rollout. A
10315
+ // malformed/non-UUID value is coerced to `undefined` (no telemetry id) rather
10316
+ // than throwing: this is a telemetry-only field (stripped from the developer
10317
+ // result, never used for control flow), so it must not be able to abort the
10318
+ // swap via `parseCreateSwapResponse().parse()`. Mirrors the best-effort,
10319
+ // never-throw contract of the rest of the telemetry stack. Implemented with
10320
+ // `preprocess` rather than Zod's `.catch()` because static analysis misreads
10321
+ // `.catch` on the schema chain as an unhandled Promise (S7785).
10322
+ correlationId: z.preprocess((value)=>z.string().uuid().safeParse(value).success ? value : undefined, z.string().optional()),
9673
10323
  config: createSwapRequestBaseSchema.shape.config.optional(),
9674
10324
  fees: createSwapFeesSchema.optional(),
9675
10325
  transaction: createSwapTransactionSchema
@@ -9704,16 +10354,321 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9704
10354
  * const isValid = isValidApiKey('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
9705
10355
  * console.log(isValid) // true
9706
10356
  *
9707
- * ```
9708
- */ const isValidApiKey = (apiKey)=>{
9709
- // Handle invalid input types
9710
- if (typeof apiKey !== 'string') {
9711
- return false;
10357
+ * ```
10358
+ */ const isValidApiKey = (apiKey)=>{
10359
+ // Handle invalid input types
10360
+ if (typeof apiKey !== 'string') {
10361
+ return false;
10362
+ }
10363
+ const apiKeyPattern = /^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/;
10364
+ // Validate without trimming - any whitespace will cause validation to fail
10365
+ return apiKeyPattern.test(apiKey);
10366
+ };
10367
+
10368
+ /**
10369
+ * IAdapter contract ABI.
10370
+ *
10371
+ * Shared ABI for the on-chain Adapter contract used by multiple kits
10372
+ * (swap, earn) for executing signed instruction sets. The `execute()`
10373
+ * function accepts EIP-712 signed execution parameters, token inputs,
10374
+ * and a signature, then executes the corresponding on-chain
10375
+ * instructions.
10376
+ */ const adapterContractAbi = [
10377
+ {
10378
+ type: 'function',
10379
+ name: 'execute',
10380
+ inputs: [
10381
+ {
10382
+ name: 'params',
10383
+ type: 'tuple',
10384
+ internalType: 'struct IAdapter.ExecutionParams',
10385
+ components: [
10386
+ {
10387
+ name: 'instructions',
10388
+ type: 'tuple[]',
10389
+ internalType: 'struct IAdapter.Instruction[]',
10390
+ components: [
10391
+ {
10392
+ name: 'target',
10393
+ type: 'address',
10394
+ internalType: 'address'
10395
+ },
10396
+ {
10397
+ name: 'data',
10398
+ type: 'bytes',
10399
+ internalType: 'bytes'
10400
+ },
10401
+ {
10402
+ name: 'value',
10403
+ type: 'uint256',
10404
+ internalType: 'uint256'
10405
+ },
10406
+ {
10407
+ name: 'tokenIn',
10408
+ type: 'address',
10409
+ internalType: 'address'
10410
+ },
10411
+ {
10412
+ name: 'amountToApprove',
10413
+ type: 'uint256',
10414
+ internalType: 'uint256'
10415
+ },
10416
+ {
10417
+ name: 'tokenOut',
10418
+ type: 'address',
10419
+ internalType: 'address'
10420
+ },
10421
+ {
10422
+ name: 'minTokenOut',
10423
+ type: 'uint256',
10424
+ internalType: 'uint256'
10425
+ }
10426
+ ]
10427
+ },
10428
+ {
10429
+ name: 'tokens',
10430
+ type: 'tuple[]',
10431
+ internalType: 'struct IAdapter.TokenRecipient[]',
10432
+ components: [
10433
+ {
10434
+ name: 'token',
10435
+ type: 'address',
10436
+ internalType: 'address'
10437
+ },
10438
+ {
10439
+ name: 'beneficiary',
10440
+ type: 'address',
10441
+ internalType: 'address'
10442
+ }
10443
+ ]
10444
+ },
10445
+ {
10446
+ name: 'execId',
10447
+ type: 'uint256',
10448
+ internalType: 'uint256'
10449
+ },
10450
+ {
10451
+ name: 'deadline',
10452
+ type: 'uint256',
10453
+ internalType: 'uint256'
10454
+ },
10455
+ {
10456
+ name: 'metadata',
10457
+ type: 'bytes',
10458
+ internalType: 'bytes'
10459
+ }
10460
+ ]
10461
+ },
10462
+ {
10463
+ name: 'tokenInputs',
10464
+ type: 'tuple[]',
10465
+ internalType: 'struct IAdapter.TokenInput[]',
10466
+ components: [
10467
+ {
10468
+ name: 'permitType',
10469
+ type: 'uint8',
10470
+ internalType: 'enum IAdapter.PermitType'
10471
+ },
10472
+ {
10473
+ name: 'token',
10474
+ type: 'address',
10475
+ internalType: 'address'
10476
+ },
10477
+ {
10478
+ name: 'amount',
10479
+ type: 'uint256',
10480
+ internalType: 'uint256'
10481
+ },
10482
+ {
10483
+ name: 'permitCalldata',
10484
+ type: 'bytes',
10485
+ internalType: 'bytes'
10486
+ }
10487
+ ]
10488
+ },
10489
+ {
10490
+ name: 'signature',
10491
+ type: 'bytes',
10492
+ internalType: 'bytes'
10493
+ }
10494
+ ],
10495
+ outputs: [],
10496
+ stateMutability: 'payable'
10497
+ }
10498
+ ];
10499
+
10500
+ /**
10501
+ * Minimal ERC-4626 tokenized-vault ABI.
10502
+ *
10503
+ * Covers only the mutating methods EarnKit bundles as inner instructions inside
10504
+ * an Adapter `execute()` call: `deposit`, `withdraw`, and `redeem`. It exists so
10505
+ * clients can decode the inner instruction calldata into a human-readable
10506
+ * summary of what a signer is authorizing (asset amount, receiver, owner)
10507
+ * rather than showing opaque bytes. The 4-byte selectors match the calldata the
10508
+ * earn service signs (`deposit(uint256,address)` = `0x6e553f65`,
10509
+ * `withdraw(uint256,address,address)` = `0xb460af94`,
10510
+ * `redeem(uint256,address,address)` = `0xba087652`).
10511
+ */ const erc4626VaultAbi = [
10512
+ {
10513
+ type: 'function',
10514
+ name: 'deposit',
10515
+ stateMutability: 'nonpayable',
10516
+ inputs: [
10517
+ {
10518
+ name: 'assets',
10519
+ type: 'uint256',
10520
+ internalType: 'uint256'
10521
+ },
10522
+ {
10523
+ name: 'receiver',
10524
+ type: 'address',
10525
+ internalType: 'address'
10526
+ }
10527
+ ],
10528
+ outputs: [
10529
+ {
10530
+ name: 'shares',
10531
+ type: 'uint256',
10532
+ internalType: 'uint256'
10533
+ }
10534
+ ]
10535
+ },
10536
+ {
10537
+ type: 'function',
10538
+ name: 'withdraw',
10539
+ stateMutability: 'nonpayable',
10540
+ inputs: [
10541
+ {
10542
+ name: 'assets',
10543
+ type: 'uint256',
10544
+ internalType: 'uint256'
10545
+ },
10546
+ {
10547
+ name: 'receiver',
10548
+ type: 'address',
10549
+ internalType: 'address'
10550
+ },
10551
+ {
10552
+ name: 'owner',
10553
+ type: 'address',
10554
+ internalType: 'address'
10555
+ }
10556
+ ],
10557
+ outputs: [
10558
+ {
10559
+ name: 'shares',
10560
+ type: 'uint256',
10561
+ internalType: 'uint256'
10562
+ }
10563
+ ]
10564
+ },
10565
+ {
10566
+ type: 'function',
10567
+ name: 'redeem',
10568
+ stateMutability: 'nonpayable',
10569
+ inputs: [
10570
+ {
10571
+ name: 'shares',
10572
+ type: 'uint256',
10573
+ internalType: 'uint256'
10574
+ },
10575
+ {
10576
+ name: 'receiver',
10577
+ type: 'address',
10578
+ internalType: 'address'
10579
+ },
10580
+ {
10581
+ name: 'owner',
10582
+ type: 'address',
10583
+ internalType: 'address'
10584
+ }
10585
+ ],
10586
+ outputs: [
10587
+ {
10588
+ name: 'assets',
10589
+ type: 'uint256',
10590
+ internalType: 'uint256'
10591
+ }
10592
+ ]
10593
+ }
10594
+ ];
10595
+
10596
+ /**
10597
+ * Minimal FeeTaker ABI.
10598
+ *
10599
+ * The earn service appends a `takeFeeERC20` instruction to withdraw bundles
10600
+ * when Circle charges a withdrawal fee. This ABI decodes that inner instruction
10601
+ * so the fee (token, beneficiary, amount) is visible in the signing summary
10602
+ * instead of appearing as opaque calldata alongside the redeem/withdraw call.
10603
+ */ const feeTakerAbi = [
10604
+ {
10605
+ type: 'function',
10606
+ name: 'takeFeeERC20',
10607
+ stateMutability: 'nonpayable',
10608
+ inputs: [
10609
+ {
10610
+ name: 'token',
10611
+ type: 'address',
10612
+ internalType: 'address'
10613
+ },
10614
+ {
10615
+ name: 'beneficiary',
10616
+ type: 'address',
10617
+ internalType: 'address'
10618
+ },
10619
+ {
10620
+ name: 'fee',
10621
+ type: 'uint256',
10622
+ internalType: 'uint256'
10623
+ },
10624
+ {
10625
+ name: 'kitType',
10626
+ type: 'bytes8',
10627
+ internalType: 'bytes8'
10628
+ }
10629
+ ],
10630
+ outputs: []
10631
+ }
10632
+ ];
10633
+
10634
+ /**
10635
+ * Minimal Merkl Distributor ABI.
10636
+ *
10637
+ * EarnKit claim-rewards bundles a single `claim` instruction targeting the
10638
+ * Merkl Distributor, batching one entry per reward token. This ABI decodes that
10639
+ * inner instruction so the claimed tokens and amounts are visible in the signing
10640
+ * summary. `claim` uses dynamic array arguments, which is why a real ABI decoder
10641
+ * (rather than fixed-word slicing) is required for the reward instruction.
10642
+ */ const merklDistributorAbi = [
10643
+ {
10644
+ type: 'function',
10645
+ name: 'claim',
10646
+ stateMutability: 'nonpayable',
10647
+ inputs: [
10648
+ {
10649
+ name: 'users',
10650
+ type: 'address[]',
10651
+ internalType: 'address[]'
10652
+ },
10653
+ {
10654
+ name: 'tokens',
10655
+ type: 'address[]',
10656
+ internalType: 'address[]'
10657
+ },
10658
+ {
10659
+ name: 'amounts',
10660
+ type: 'uint256[]',
10661
+ internalType: 'uint256[]'
10662
+ },
10663
+ {
10664
+ name: 'proofs',
10665
+ type: 'bytes32[][]',
10666
+ internalType: 'bytes32[][]'
10667
+ }
10668
+ ],
10669
+ outputs: []
9712
10670
  }
9713
- const apiKeyPattern = /^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/;
9714
- // Validate without trimming - any whitespace will cause validation to fail
9715
- return apiKeyPattern.test(apiKey);
9716
- };
10671
+ ];
9717
10672
 
9718
10673
  /**
9719
10674
  * Zod schema for validating EVM adapter capabilities.
@@ -12228,7 +13183,7 @@ new Set(Object.values(Blockchain));
12228
13183
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
12229
13184
 
12230
13185
  var name$1 = "@circle-fin/earn-kit";
12231
- var version$1 = "1.3.0";
13186
+ var version$1 = "1.5.0";
12232
13187
  var pkg$1 = {
12233
13188
  name: name$1,
12234
13189
  version: version$1};
@@ -12844,31 +13799,670 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12844
13799
  return tokenInputs;
12845
13800
  }
12846
13801
  /**
12847
- * Resolve the single token that needs adapter allowance from signed Earn
12848
- * instructions.
13802
+ * Resolve the single token that needs adapter allowance from signed Earn
13803
+ * instructions.
13804
+ *
13805
+ * @param executionParams - Service-signed `ExecutionParams` forwarded to the adapter.
13806
+ * @returns The token requested by positive approval instructions, or `undefined`.
13807
+ * @throws {@link KitError} If positive approval instructions reference multiple tokens.
13808
+ *
13809
+ * @internal
13810
+ */ function resolveEarnApprovalToken(executionParams) {
13811
+ let approvedToken;
13812
+ executionParams.instructions.forEach((instruction, index)=>{
13813
+ const amount = BigInt(instruction.amountToApprove);
13814
+ if (amount <= 0n) {
13815
+ return;
13816
+ }
13817
+ const { tokenIn } = instruction;
13818
+ if (approvedToken === undefined) {
13819
+ approvedToken = tokenIn;
13820
+ return;
13821
+ }
13822
+ if (!isSameAddress(tokenIn, approvedToken)) {
13823
+ throw createValidationFailedError(`executionParams.instructions[${index.toString()}].tokenIn`, tokenIn, 'tokenIn must match the token approved for adapter spending');
13824
+ }
13825
+ });
13826
+ return approvedToken;
13827
+ }
13828
+
13829
+ /**
13830
+ * Combined ABI of every inner instruction EarnKit can bundle inside an Adapter
13831
+ * `execute()` call. `decodeFunctionData` matches an instruction's calldata to
13832
+ * one of these functions by its 4-byte selector.
13833
+ */ const earnInstructionAbi = [
13834
+ ...erc4626VaultAbi,
13835
+ ...feeTakerAbi,
13836
+ ...merklDistributorAbi
13837
+ ];
13838
+ /**
13839
+ * Extract and shallow-validate the `instructions` array from loosely-typed
13840
+ * signed execution params.
13841
+ *
13842
+ * The earn service schema validates `tokenIn`/`amountToApprove` and passes the
13843
+ * remaining instruction fields through untyped, so the params arrive as a plain
13844
+ * record; each accessed field is narrowed at runtime.
13845
+ */ function requireInstructions(executionParams) {
13846
+ const instructions = executionParams['instructions'];
13847
+ if (!Array.isArray(instructions)) {
13848
+ throw decodeMismatchError('execution params are missing an instructions array', {
13849
+ instructions
13850
+ });
13851
+ }
13852
+ return instructions.map((instruction, index)=>{
13853
+ if (typeof instruction !== 'object' || instruction === null) {
13854
+ throw decodeMismatchError(`instructions[${index.toString()}] is not an object`, {
13855
+ index
13856
+ });
13857
+ }
13858
+ return instruction;
13859
+ });
13860
+ }
13861
+ /**
13862
+ * Build a fail-closed {@link KitError} for an earn decode or review failure.
13863
+ *
13864
+ * Marked non-recoverable: a mismatch between what would be shown and what would
13865
+ * be signed is never safe to retry, so the operation fails fast rather than
13866
+ * presenting misleading decoded data. `messagePrefix` names the failing stage
13867
+ * (calldata decode vs. review construction); callers bind it once and pass the
13868
+ * specific failure as `message`.
13869
+ */ function failClosedEarnError(messagePrefix, message, trace) {
13870
+ return new KitError({
13871
+ ...EarnError.INTERNAL_ERROR,
13872
+ recoverability: 'FATAL',
13873
+ message: `${messagePrefix}: ${message}`,
13874
+ cause: {
13875
+ trace
13876
+ }
13877
+ });
13878
+ }
13879
+ /**
13880
+ * Build a {@link KitError} for a decode or consistency failure.
13881
+ *
13882
+ * Thin wrapper over {@link failClosedEarnError} bound to the decode-stage
13883
+ * message prefix.
13884
+ */ function decodeMismatchError(message, trace) {
13885
+ return failClosedEarnError('Unable to decode earn transaction', message, trace);
13886
+ }
13887
+ /**
13888
+ * Narrow an untyped value to a 0x-prefixed hex string, or fail fast.
13889
+ */ function requireHex(value, path) {
13890
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value)) {
13891
+ return value;
13892
+ }
13893
+ throw decodeMismatchError(`${path} is not a hex string`, {
13894
+ path,
13895
+ value
13896
+ });
13897
+ }
13898
+ /**
13899
+ * Narrow an untyped value to a 20-byte EVM address, or fail fast.
13900
+ */ function requireAddress(value, path) {
13901
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) {
13902
+ return value;
13903
+ }
13904
+ throw decodeMismatchError(`${path} is not an address`, {
13905
+ path,
13906
+ value
13907
+ });
13908
+ }
13909
+ /**
13910
+ * Narrow an untyped `uint256`-like value (decimal string, bigint, or integer)
13911
+ * to a bigint, or fail fast.
13912
+ */ function requireUint(value, path) {
13913
+ if (typeof value === 'bigint') {
13914
+ return value;
13915
+ }
13916
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
13917
+ return BigInt(value);
13918
+ }
13919
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
13920
+ return BigInt(value);
13921
+ }
13922
+ throw decodeMismatchError(`${path} is not a uint256 value`, {
13923
+ path,
13924
+ value
13925
+ });
13926
+ }
13927
+ /**
13928
+ * Decode inner instruction calldata against the earn instruction ABI, mapping a
13929
+ * viem decode failure (unknown selector, malformed args) to a fail-fast error.
13930
+ */ function decodeEarnInstructionData(data, index) {
13931
+ try {
13932
+ return decodeFunctionData({
13933
+ abi: earnInstructionAbi,
13934
+ data
13935
+ });
13936
+ } catch (error) {
13937
+ throw decodeMismatchError(`instructions[${index.toString()}] calldata is not a recognized earn instruction`, {
13938
+ index,
13939
+ selector: data.slice(0, 10),
13940
+ error: String(error)
13941
+ });
13942
+ }
13943
+ }
13944
+ /**
13945
+ * Decode Adapter `execute()` calldata, mapping a viem decode failure to a
13946
+ * fail-fast error.
13947
+ */ function decodeExecuteCalldata(calldata) {
13948
+ try {
13949
+ return decodeFunctionData({
13950
+ abi: adapterContractAbi,
13951
+ data: calldata
13952
+ });
13953
+ } catch (error) {
13954
+ throw decodeMismatchError('encoded calldata is not a valid Adapter execute() call', {
13955
+ error: String(error)
13956
+ });
13957
+ }
13958
+ }
13959
+ /**
13960
+ * Decode one inner instruction's calldata into a typed {@link
13961
+ * DecodedEarnInstruction}.
13962
+ */ function decodeInstruction(instruction, index) {
13963
+ const target = requireAddress(instruction['target'], `instructions[${index.toString()}].target`);
13964
+ const data = requireHex(instruction['data'], `instructions[${index.toString()}].data`);
13965
+ const decoded = decodeEarnInstructionData(data, index);
13966
+ switch(decoded.functionName){
13967
+ case 'deposit':
13968
+ {
13969
+ const [assets, receiver] = decoded.args;
13970
+ return {
13971
+ method: 'deposit',
13972
+ vault: target,
13973
+ assets: assets.toString(),
13974
+ receiver
13975
+ };
13976
+ }
13977
+ case 'withdraw':
13978
+ {
13979
+ const [assets, receiver, owner] = decoded.args;
13980
+ return {
13981
+ method: 'withdraw',
13982
+ vault: target,
13983
+ assets: assets.toString(),
13984
+ receiver,
13985
+ owner
13986
+ };
13987
+ }
13988
+ case 'redeem':
13989
+ {
13990
+ const [shares, receiver, owner] = decoded.args;
13991
+ return {
13992
+ method: 'redeem',
13993
+ vault: target,
13994
+ shares: shares.toString(),
13995
+ receiver,
13996
+ owner
13997
+ };
13998
+ }
13999
+ case 'takeFeeERC20':
14000
+ {
14001
+ const [token, beneficiary, fee, kitType] = decoded.args;
14002
+ return {
14003
+ method: 'takeFeeERC20',
14004
+ feeTaker: target,
14005
+ token,
14006
+ beneficiary,
14007
+ fee: fee.toString(),
14008
+ kitType
14009
+ };
14010
+ }
14011
+ case 'claim':
14012
+ {
14013
+ const users = decoded.args[0];
14014
+ const tokens = decoded.args[1];
14015
+ const amounts = decoded.args[2];
14016
+ // Merkl claim(users, tokens, amounts, proofs) carries parallel arrays,
14017
+ // one entry per reward. Reject any length skew rather than padding with
14018
+ // zero amounts or dropping trailing entries, so the preview can never
14019
+ // misstate what is claimed or for whom.
14020
+ //
14021
+ // Note: Merkl `amounts` are the *cumulative lifetime* total claimable per
14022
+ // (user, token); the Distributor transfers only `amount - alreadyClaimed`.
14023
+ // This decode faithfully surfaces the signed cumulative value, which is
14024
+ // what `DecodedRewardClaim.amount` documents. See that type's doc.
14025
+ if (new Set([
14026
+ users.length,
14027
+ tokens.length,
14028
+ amounts.length
14029
+ ]).size !== 1) {
14030
+ throw decodeMismatchError(`instructions[${index.toString()}] claim has mismatched recipient/token/amount lengths`, {
14031
+ index,
14032
+ users: users.length,
14033
+ tokens: tokens.length,
14034
+ amounts: amounts.length
14035
+ });
14036
+ }
14037
+ const rewards = tokens.map((token, rewardIndex)=>({
14038
+ recipient: requireAddress(users[rewardIndex], `instructions[${index.toString()}].claim.users[${rewardIndex.toString()}]`),
14039
+ address: token,
14040
+ amount: requireUint(amounts[rewardIndex], `instructions[${index.toString()}].claim.amounts[${rewardIndex.toString()}]`).toString()
14041
+ }));
14042
+ return {
14043
+ method: 'claim',
14044
+ distributor: target,
14045
+ rewards
14046
+ };
14047
+ }
14048
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
14049
+ return assertNever$2(decoded);
14050
+ }
14051
+ }
14052
+ /**
14053
+ * Lift the primary values a wallet prompt cares about out of the decoded
14054
+ * instructions into a flat summary.
14055
+ */ function buildSummary(instructions, envelope) {
14056
+ const summary = {};
14057
+ instructions.forEach((instruction, index)=>{
14058
+ switch(instruction.method){
14059
+ case 'deposit':
14060
+ case 'withdraw':
14061
+ case 'redeem':
14062
+ {
14063
+ // The summary lifts a single primary token movement to the top level.
14064
+ // An earn bundle carries exactly one deposit/withdraw/redeem today;
14065
+ // fail fast rather than silently overwriting an earlier one, which
14066
+ // would drop it from the wallet-facing preview.
14067
+ if (summary.token !== undefined) {
14068
+ throw decodeMismatchError('multiple deposit/withdraw/redeem instructions cannot be summarized into a single preview', {
14069
+ index
14070
+ });
14071
+ }
14072
+ // Pair the amount with the token it is actually denominated in so the
14073
+ // preview never folds two units into one entry:
14074
+ // - deposit: `assets` of the underlying asset pulled in (`tokenIn`)
14075
+ // - redeem: `shares` of the vault-share token burned (`tokenIn`)
14076
+ // - withdraw: `assets` of the underlying asset paid out (`tokenOut`).
14077
+ // `withdraw(assets)` counts the underlying received, not the shares
14078
+ // burned to produce it, so `tokenIn` (the share token) would misstate
14079
+ // the unit; the underlying is the instruction's `tokenOut`.
14080
+ const amount = instruction.method === 'redeem' ? instruction.shares : instruction.assets;
14081
+ const tokenField = instruction.method === 'withdraw' ? 'tokenOut' : 'tokenIn';
14082
+ summary.vault = instruction.vault;
14083
+ summary.receiver = instruction.receiver;
14084
+ summary.token = {
14085
+ address: requireAddress(envelope[index]?.[tokenField], `instructions[${index.toString()}].${tokenField}`),
14086
+ amount
14087
+ };
14088
+ break;
14089
+ }
14090
+ case 'takeFeeERC20':
14091
+ {
14092
+ // As with the vault case, a second fee would silently overwrite the
14093
+ // first and understate what is charged; fail fast instead.
14094
+ if (summary.fee !== undefined) {
14095
+ throw decodeMismatchError('multiple fee instructions cannot be summarized into a single preview', {
14096
+ index
14097
+ });
14098
+ }
14099
+ summary.fee = {
14100
+ address: instruction.token,
14101
+ amount: instruction.fee
14102
+ };
14103
+ break;
14104
+ }
14105
+ case 'claim':
14106
+ {
14107
+ // A second claim would silently drop the first from the preview
14108
+ // (rewards are already batched inside one Merkl claim); fail fast.
14109
+ if (summary.rewards !== undefined) {
14110
+ throw decodeMismatchError('multiple claim instructions cannot be summarized into a single preview', {
14111
+ index
14112
+ });
14113
+ }
14114
+ summary.rewards = instruction.rewards;
14115
+ break;
14116
+ }
14117
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
14118
+ assertNever$2(instruction);
14119
+ }
14120
+ });
14121
+ return summary;
14122
+ }
14123
+ /**
14124
+ * Vault/claim instruction methods each declared action may decode to. The
14125
+ * mapping is many-to-one: a full withdrawal decodes to `redeem`, and any action
14126
+ * may carry an auxiliary `takeFeeERC20` alongside its primary instruction.
14127
+ */ const ACTION_ALLOWED_METHODS = {
14128
+ deposit: new Set([
14129
+ 'deposit'
14130
+ ]),
14131
+ withdraw: new Set([
14132
+ 'withdraw',
14133
+ 'redeem'
14134
+ ]),
14135
+ claimRewards: new Set([
14136
+ 'claim'
14137
+ ])
14138
+ };
14139
+ /**
14140
+ * Fail fast when the caller-declared `action` disagrees with the decoded
14141
+ * instructions, so the preview's headline can never mislabel what is signed
14142
+ * (e.g. a `deposit`-labeled call handed withdraw params). `takeFeeERC20` is an
14143
+ * auxiliary Circle-fee instruction and is allowed alongside any action.
14144
+ */ function assertActionMatchesInstructions(action, instructions) {
14145
+ const allowed = ACTION_ALLOWED_METHODS[action];
14146
+ instructions.forEach((instruction, index)=>{
14147
+ if (instruction.method === 'takeFeeERC20') {
14148
+ return;
14149
+ }
14150
+ if (!allowed.has(instruction.method)) {
14151
+ throw decodeMismatchError(`decoded instruction method '${instruction.method}' does not match the '${action}' action`, {
14152
+ action,
14153
+ method: instruction.method,
14154
+ index
14155
+ });
14156
+ }
14157
+ });
14158
+ }
14159
+ /**
14160
+ * Decode a same-chain earn `execute()` bundle into a human-readable summary.
14161
+ *
14162
+ * Decodes every inner instruction in the service-signed `executionParams` — the
14163
+ * same object the SDK ABI-encodes into the transaction — so the returned decode
14164
+ * is a faithful, drift-free view of what the signer is authorizing: input token
14165
+ * and amount, target vault, receiver, any Circle fee, and claimed rewards. Fails
14166
+ * fast with a non-recoverable {@link KitError} if any instruction cannot be
14167
+ * decoded, rather than returning misleading data.
14168
+ *
14169
+ * @param input - Action, chain, adapter, and the signed execution params.
14170
+ * @returns The decoded transaction summary.
14171
+ * @throws {@link KitError} If an instruction's calldata cannot be decoded.
14172
+ *
14173
+ * @example
14174
+ * ```typescript
14175
+ * const decoded = decodeEarnExecute({
14176
+ * action: 'deposit',
14177
+ * chain: 'Arc_Testnet',
14178
+ * adapter: '0x7fb8c7260b63934d8da38af902f87ae6e284a845',
14179
+ * executionParams,
14180
+ * })
14181
+ * // decoded.summary -> { token: { address, amount }, vault, receiver }
14182
+ * ```
14183
+ *
14184
+ * @internal
14185
+ */ function decodeEarnExecute(input) {
14186
+ const { action, chain, adapter, executionParams } = input;
14187
+ const envelope = requireInstructions(executionParams);
14188
+ const instructions = envelope.map((instruction, index)=>decodeInstruction(instruction, index));
14189
+ assertActionMatchesInstructions(action, instructions);
14190
+ return {
14191
+ action,
14192
+ chain,
14193
+ adapter,
14194
+ instructions,
14195
+ summary: buildSummary(instructions, envelope)
14196
+ };
14197
+ }
14198
+ /**
14199
+ * Assert that ABI-encoded Adapter `execute()` calldata encodes the same
14200
+ * instruction set as the service-signed execution params.
14201
+ *
14202
+ * Fail-fast preview check: the SDK encodes `execute(executeParams, ...)` locally,
14203
+ * so decoding those final bytes and comparing every field of each instruction
14204
+ * against the signed params proves the previewed instruction set matches what
14205
+ * will be signed. It compares `instructions[]` only — the outer `tokens`,
14206
+ * `execId`, `deadline`, and `metadata` are not re-compared here. The
14207
+ * authoritative integrity guarantee for the full signed struct is the on-chain
14208
+ * EIP-712 signature verification, which reverts if any signed field is altered.
14209
+ *
14210
+ * @param calldata - Encoded `execute()` calldata about to be signed.
14211
+ * @param executionParams - Service-signed execution params.
14212
+ * @throws {@link KitError} If the calldata is not an `execute()` call or any
14213
+ * instruction field differs from the signed params.
14214
+ *
14215
+ * @example
14216
+ * ```typescript
14217
+ * assertEarnCalldataMatchesExecuteParams(
14218
+ * prepared.getCallData().data,
14219
+ * executionParams,
14220
+ * )
14221
+ * ```
14222
+ *
14223
+ * @internal
14224
+ */ function assertEarnCalldataMatchesExecuteParams(calldata, executionParams) {
14225
+ // adapterContractAbi declares only `execute`, so a successful decode is always
14226
+ // the execute() call; a non-execute selector throws inside
14227
+ // decodeExecuteCalldata above.
14228
+ const decoded = decodeExecuteCalldata(calldata);
14229
+ const encoded = decoded.args[0].instructions;
14230
+ const signed = requireInstructions(executionParams);
14231
+ // Compare each encoded instruction against its signed counterpart. Iterating
14232
+ // the encoded instructions and indexing the signed set keeps both mismatch
14233
+ // branches reachable: a signed set that is too short trips the guard below,
14234
+ // and one that is too long trips the post-loop check.
14235
+ encoded.forEach((instruction, index)=>{
14236
+ const signedInstruction = signed[index];
14237
+ if (signedInstruction === undefined) {
14238
+ throw decodeMismatchError(`signed params are missing instruction ${index.toString()}`, {
14239
+ index,
14240
+ encoded: encoded.length,
14241
+ signed: signed.length
14242
+ });
14243
+ }
14244
+ const path = `instructions[${index.toString()}]`;
14245
+ assertHexEqual(instruction.target, signedInstruction['target'], `${path}.target`);
14246
+ assertHexEqual(instruction.data, signedInstruction['data'], `${path}.data`);
14247
+ assertUintEqual(instruction.value, signedInstruction['value'], `${path}.value`);
14248
+ assertHexEqual(instruction.tokenIn, signedInstruction['tokenIn'], `${path}.tokenIn`);
14249
+ assertUintEqual(instruction.amountToApprove, signedInstruction['amountToApprove'], `${path}.amountToApprove`);
14250
+ assertHexEqual(instruction.tokenOut, signedInstruction['tokenOut'], `${path}.tokenOut`);
14251
+ assertUintEqual(instruction.minTokenOut, signedInstruction['minTokenOut'], `${path}.minTokenOut`);
14252
+ });
14253
+ if (signed.length > encoded.length) {
14254
+ throw decodeMismatchError('signed params contain more instructions than the encoded calldata', {
14255
+ encoded: encoded.length,
14256
+ signed: signed.length
14257
+ });
14258
+ }
14259
+ }
14260
+ /**
14261
+ * Assert two hex values are equal, case-insensitively (addresses and calldata).
14262
+ */ function assertHexEqual(encoded, signed, path) {
14263
+ const signedHex = requireHex(signed, path);
14264
+ if (encoded.toLowerCase() !== signedHex.toLowerCase()) {
14265
+ throw decodeMismatchError(`${path} differs from signed params`, {
14266
+ path,
14267
+ encoded,
14268
+ signed: signedHex
14269
+ });
14270
+ }
14271
+ }
14272
+ /**
14273
+ * Assert an encoded bigint equals a signed `uint256`-like value.
14274
+ */ function assertUintEqual(encoded, signed, path) {
14275
+ const signedUint = requireUint(signed, path);
14276
+ if (encoded !== signedUint) {
14277
+ throw decodeMismatchError(`${path} differs from signed params`, {
14278
+ path,
14279
+ encoded: encoded.toString(),
14280
+ signed: signedUint.toString()
14281
+ });
14282
+ }
14283
+ }
14284
+
14285
+ /**
14286
+ * Namespaced discriminator for the EarnKit authorization review.
14287
+ *
14288
+ * Applications match on this in an adapter `onBeforeAuthorize` hook to decide
14289
+ * whether the request carries EarnKit semantic data. Prefer the
14290
+ * {@link isEarnExecuteReview} type guard over comparing this string directly.
14291
+ *
14292
+ * @example
14293
+ * ```typescript
14294
+ * if (review?.kind === EARN_EXECUTE_REVIEW_KIND) { … }
14295
+ * ```
14296
+ */ const EARN_EXECUTE_REVIEW_KIND = 'earn.execute';
14297
+
14298
+ /**
14299
+ * Build a fail-closed {@link KitError} for a review-construction failure.
14300
+ *
14301
+ * Marked non-recoverable: a review that cannot prove the calldata matches the
14302
+ * signed operation must abort authorization, never retry with misleading data.
14303
+ */ function reviewError(message, trace) {
14304
+ return failClosedEarnError('Unable to build earn authorization review', message, trace);
14305
+ }
14306
+ /**
14307
+ * Narrow the canonical authorization payload to the single Adapter `execute()`
14308
+ * call a same-chain earn operation authorizes.
14309
+ *
14310
+ * Same-chain deposit, withdraw, and claim-rewards each authorize exactly one
14311
+ * `evm-calls` payload carrying one call. Anything else (typed data, a batch,
14312
+ * an empty call list) means this descriptor was attached to the wrong
14313
+ * authorization unit, so fail closed rather than decode misleading data.
14314
+ */ function assertSingleEvmCallPayload(payload) {
14315
+ if (payload.type !== 'evm-calls') {
14316
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
14317
+ type: payload.type
14318
+ });
14319
+ }
14320
+ const [call, ...rest] = payload.calls;
14321
+ if (call === undefined) {
14322
+ throw reviewError('the evm-calls payload contains no calls to review', {
14323
+ callCount: payload.calls.length
14324
+ });
14325
+ }
14326
+ if (rest.length > 0) {
14327
+ throw reviewError('a same-chain earn operation authorizes exactly one Adapter execute() call', {
14328
+ callCount: payload.calls.length
14329
+ });
14330
+ }
14331
+ return call;
14332
+ }
14333
+ /**
14334
+ * Select the final earn `execute()` call from an atomic Earn batch.
14335
+ *
14336
+ * Same-chain batched deposit/withdraw authorizes either `[execute]` when the
14337
+ * current allowance is sufficient, or `[approve, execute]` when a top-up is
14338
+ * required. Any other shape means the descriptor was attached to an
14339
+ * unexpected authorization unit, so fail closed.
14340
+ */ function assertBatchedEarnExecuteCall(payload) {
14341
+ if (payload.type !== 'evm-calls') {
14342
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
14343
+ type: payload.type
14344
+ });
14345
+ }
14346
+ if (payload.calls.length !== 1 && payload.calls.length !== 2) {
14347
+ throw reviewError('a batched earn operation authorizes [execute] or [approve, execute]', {
14348
+ callCount: payload.calls.length
14349
+ });
14350
+ }
14351
+ const executeCall = payload.calls.at(-1);
14352
+ if (executeCall === undefined) {
14353
+ throw reviewError('the earn batch contains no execute call to review', {
14354
+ callCount: payload.calls.length
14355
+ });
14356
+ }
14357
+ return executeCall;
14358
+ }
14359
+ /**
14360
+ * Map a canonical {@link EvmCall} to the {@link EarnEncodedTransaction} preview
14361
+ * shape, failing closed when the earn `execute()` call carries no calldata.
14362
+ */ function toEarnEncodedTransaction(call) {
14363
+ if (call.data === undefined) {
14364
+ throw reviewError('the earn execute() call is missing calldata', {
14365
+ to: call.to
14366
+ });
14367
+ }
14368
+ return {
14369
+ to: call.to,
14370
+ data: call.data,
14371
+ ...call.value !== undefined && {
14372
+ value: call.value
14373
+ }
14374
+ };
14375
+ }
14376
+ /**
14377
+ * Create an Earn authorization descriptor using the supplied canonical-payload
14378
+ * call selector.
14379
+ *
14380
+ * @param input - The action, chain, and service-signed execution params.
14381
+ * @param selectCall - Fail-closed selector for the execute call under review.
14382
+ * @returns A lazy descriptor that decodes and verifies the selected call.
14383
+ *
14384
+ * @internal
14385
+ */ function createEarnExecuteDescriptor(input, selectCall) {
14386
+ const { action, chain, executionParams } = input;
14387
+ const createReview = (payload)=>{
14388
+ const call = selectCall(payload);
14389
+ const encoded = toEarnEncodedTransaction(call);
14390
+ const decoded = decodeEarnExecute({
14391
+ action,
14392
+ chain,
14393
+ adapter: encoded.to,
14394
+ executionParams
14395
+ });
14396
+ // Prove the calldata about to be signed encodes the same instruction set as
14397
+ // the service-signed params. Throwing here aborts before the wallet prompt.
14398
+ assertEarnCalldataMatchesExecuteParams(encoded.data, executionParams);
14399
+ const review = {
14400
+ kind: EARN_EXECUTE_REVIEW_KIND,
14401
+ data: {
14402
+ encoded,
14403
+ decoded
14404
+ }
14405
+ };
14406
+ return review;
14407
+ };
14408
+ return {
14409
+ createReview
14410
+ };
14411
+ }
14412
+ /**
14413
+ * Build the lazy `earn.execute` authorization descriptor for a same-chain earn
14414
+ * action.
14415
+ *
14416
+ * The returned descriptor carries only a `createReview` factory — no intent
14417
+ * override, because the adapter's action system supplies the intent from the
14418
+ * action key. The factory is evaluated at most once, and only when the
14419
+ * application configured an adapter `onBeforeAuthorize` hook. When it runs it:
14420
+ *
14421
+ * 1. narrows the canonical payload to its single Adapter `execute()` call;
14422
+ * 2. lifts that call into an {@link EarnEncodedTransaction};
14423
+ * 3. decodes it into a `DecodedEarnTx`; and
14424
+ * 4. asserts the decoded calldata matches the service-signed params, throwing
14425
+ * (aborting authorization before the wallet or signer) on any mismatch.
14426
+ *
14427
+ * @param input - The action, chain, and service-signed execution params.
14428
+ * @returns An authorization descriptor to pass as the fourth `prepareAction`
14429
+ * argument for the final earn action only (never the allowance approval).
14430
+ * @throws {@link KitError} From the review factory when the payload is not a
14431
+ * single earn `execute()` call or the calldata diverges from the signed
14432
+ * params. The throw surfaces through the adapter gate before authorization.
14433
+ *
14434
+ * @example
14435
+ * ```typescript
14436
+ * const descriptor = buildEarnExecuteDescriptor({
14437
+ * action: 'deposit',
14438
+ * chain: 'Arc_Testnet',
14439
+ * executionParams,
14440
+ * })
14441
+ * await adapter.prepareAction('earn.deposit', actionParams, ctx, {
14442
+ * authorization: descriptor,
14443
+ * })
14444
+ * ```
14445
+ *
14446
+ * @internal
14447
+ */ function buildEarnExecuteDescriptor(input) {
14448
+ return createEarnExecuteDescriptor(input, assertSingleEvmCallPayload);
14449
+ }
14450
+ /**
14451
+ * Build a lazy `earn.execute` authorization descriptor for an atomic Earn
14452
+ * batch containing either `[execute]` or `[approve, execute]`.
12849
14453
  *
12850
- * @param executionParams - Service-signed `ExecutionParams` forwarded to the adapter.
12851
- * @returns The token requested by positive approval instructions, or `undefined`.
12852
- * @throws {@link KitError} If positive approval instructions reference multiple tokens.
14454
+ * The review always decodes and verifies the final call against the
14455
+ * service-signed execution params. Unexpected payload types and call counts
14456
+ * fail closed before wallet authorization.
14457
+ *
14458
+ * @param input - The action, chain, and service-signed execution params.
14459
+ * @returns A descriptor suitable for `batchExecute` authorization options.
14460
+ * @throws {@link KitError} From the lazy review factory when the batch shape or
14461
+ * final execute calldata cannot be verified.
12853
14462
  *
12854
14463
  * @internal
12855
- */ function resolveEarnApprovalToken(executionParams) {
12856
- let approvedToken;
12857
- executionParams.instructions.forEach((instruction, index)=>{
12858
- const amount = BigInt(instruction.amountToApprove);
12859
- if (amount <= 0n) {
12860
- return;
12861
- }
12862
- const { tokenIn } = instruction;
12863
- if (approvedToken === undefined) {
12864
- approvedToken = tokenIn;
12865
- return;
12866
- }
12867
- if (!isSameAddress(tokenIn, approvedToken)) {
12868
- throw createValidationFailedError(`executionParams.instructions[${index.toString()}].tokenIn`, tokenIn, 'tokenIn must match the token approved for adapter spending');
12869
- }
12870
- });
12871
- return approvedToken;
14464
+ */ function buildBatchedEarnExecuteDescriptor(input) {
14465
+ return createEarnExecuteDescriptor(input, assertBatchedEarnExecuteCall);
12872
14466
  }
12873
14467
 
12874
14468
  /**
@@ -12897,16 +14491,37 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12897
14491
  * address,
12898
14492
  * actionKey: 'earn.deposit',
12899
14493
  * actionParams: { executeParams, tokenInputs, signature },
14494
+ * action: 'deposit',
14495
+ * executionParams,
12900
14496
  * revertMessage: 'Earn deposit reverted on-chain',
12901
14497
  * })
12902
14498
  * ```
12903
14499
  *
12904
14500
  * @internal
12905
14501
  */ async function executeEarnAction(params) {
12906
- const { adapter, chain, address, actionKey, actionParams, revertMessage } = params;
14502
+ const { adapter, chain, address, actionKey, actionParams, action, executionParams, revertMessage } = params;
14503
+ // Attach the lazy `earn.execute` review to the final earn action only (never
14504
+ // the allowance approval, which runs on a separate path). The adapter's
14505
+ // action system supplies the intent from `actionKey`, so the descriptor
14506
+ // carries only the review factory. The factory is evaluated at most once,
14507
+ // and only when the application configured an `onBeforeAuthorize` hook.
14508
+ const authorization = buildEarnExecuteDescriptor({
14509
+ action,
14510
+ // The provider validates the chain is Earn-supported in
14511
+ // `resolveAdapterContext` before reaching execute, so the concrete chain
14512
+ // identifier is a valid `EarnChainIdentifier`. It is carried through to the
14513
+ // decoded preview's display `chain` field only.
14514
+ chain: chain.chain,
14515
+ executionParams
14516
+ });
14517
+ // The abstract `Adapter.prepareAction` is 3-arg; the fourth authorization
14518
+ // argument lives on the `withLegacyCompat` wrapper that produced the concrete
14519
+ // adapter passed here. Narrow the single seam that threads the descriptor.
12907
14520
  const prepared = await adapter.prepareAction(actionKey, actionParams, {
12908
14521
  chain,
12909
14522
  address
14523
+ }, {
14524
+ authorization
12910
14525
  });
12911
14526
  const gasLimitOverride = await estimateBufferedGasLimit(prepared);
12912
14527
  const txHash = prepared.type === 'evm' && gasLimitOverride !== undefined ? await prepared.execute({
@@ -12931,6 +14546,278 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12931
14546
  };
12932
14547
  }
12933
14548
 
14549
+ /**
14550
+ * Decide whether a same-chain earn action should be submitted as a single
14551
+ * atomic batch.
14552
+ *
14553
+ * Returns `true` only when the consumer has not opted out
14554
+ * (`batchTransactions !== false`), the source chain is EVM, the adapter
14555
+ * structurally exposes the shared batch methods, and the wallet reports atomic
14556
+ * batch support. `address` is forwarded as `fromAddress` so developer-controlled
14557
+ * adapters can probe the specific wallet. Any thrown capability probe is
14558
+ * treated as "no support".
14559
+ *
14560
+ * @param params - Adapter, chain, address, and the resolved `batchTransactions` flag.
14561
+ * @returns `true` when batched execution should be attempted.
14562
+ *
14563
+ * @example
14564
+ * ```typescript
14565
+ * if (await shouldUseBatchedEarnAction({ adapter, chain, address, batchTransactions })) {
14566
+ * // take the batched approve + execute path
14567
+ * }
14568
+ * ```
14569
+ *
14570
+ * @internal
14571
+ */ async function shouldUseBatchedEarnAction(params) {
14572
+ const { adapter, chain, address, batchTransactions } = params;
14573
+ if (batchTransactions === false) {
14574
+ return false;
14575
+ }
14576
+ if (chain.type !== 'evm') {
14577
+ return false;
14578
+ }
14579
+ const candidate = adapter;
14580
+ if (typeof candidate.supportsAtomicBatch !== 'function' || typeof candidate.batchExecute !== 'function') {
14581
+ return false;
14582
+ }
14583
+ try {
14584
+ return await candidate.supportsAtomicBatch(chain, {
14585
+ fromAddress: address
14586
+ });
14587
+ } catch {
14588
+ return false;
14589
+ }
14590
+ }
14591
+ async function buildSuccessfulBatchResult(adapter, chain, receipt, batchId, revertMessage) {
14592
+ const transaction = {
14593
+ txHash: receipt.txHash,
14594
+ explorerUrl: buildExplorerUrl(chain, receipt.txHash)
14595
+ };
14596
+ let confirmed;
14597
+ try {
14598
+ confirmed = await adapter.waitForTransaction(receipt.txHash, {
14599
+ confirmations: 1
14600
+ }, chain);
14601
+ } catch {
14602
+ // The batch adapter already confirmed success. Receipt enrichment is
14603
+ // telemetry-only, so an additional RPC failure must not turn an accepted
14604
+ // money-moving operation into a retryable business failure.
14605
+ return transaction;
14606
+ }
14607
+ if (confirmed.status === 'reverted') {
14608
+ throw createTransactionRevertedError(chain.name, revertMessage, {
14609
+ batchId
14610
+ }, receipt.txHash, transaction.explorerUrl);
14611
+ }
14612
+ return {
14613
+ ...transaction,
14614
+ ...confirmed.gasUsed !== undefined && {
14615
+ gasUsed: confirmed.gasUsed
14616
+ },
14617
+ ...confirmed.effectiveGasPrice !== undefined && {
14618
+ effectiveGasPrice: confirmed.effectiveGasPrice
14619
+ }
14620
+ };
14621
+ }
14622
+ function throwBatchFailure(result, executeReceipt, chain, actionKey, revertMessage) {
14623
+ const cause = result.error;
14624
+ if (result.statusCode === 400) {
14625
+ throw new KitError({
14626
+ ...RpcError.ENDPOINT_ERROR,
14627
+ recoverability: 'RETRYABLE',
14628
+ message: `Batched earn ${actionKey} failed off-chain before inclusion (batch ${result.batchId}).`,
14629
+ cause: {
14630
+ trace: {
14631
+ batchId: result.batchId,
14632
+ statusCode: result.statusCode,
14633
+ cause
14634
+ }
14635
+ }
14636
+ });
14637
+ }
14638
+ const causeTrace = cause instanceof KitError && typeof cause.cause?.trace === 'object' && cause.cause.trace !== null ? cause.cause.trace : undefined;
14639
+ if (cause instanceof KitError && causeTrace?.['kind'] === 'failed_offchain') {
14640
+ throw cause;
14641
+ }
14642
+ const isConfirmedRevert = cause instanceof KitError && cause.name === OnchainError.TRANSACTION_REVERTED.name || result.statusCode === 500 || result.statusCode === 600 || result.statusCode === undefined && cause === undefined && executeReceipt?.status === 'error' && executeReceipt.txHash !== '';
14643
+ if (isConfirmedRevert) {
14644
+ throw createTransactionRevertedError(chain.name, revertMessage, {
14645
+ batchId: result.batchId,
14646
+ error: cause
14647
+ });
14648
+ }
14649
+ throw new KitError({
14650
+ ...NetworkError.TIMEOUT,
14651
+ recoverability: 'FATAL',
14652
+ message: `Batched earn ${actionKey} was submitted (batch ${result.batchId}) but its outcome could not be confirmed; check the transaction status before retrying.`,
14653
+ cause: {
14654
+ trace: {
14655
+ batchId: result.batchId,
14656
+ cause
14657
+ }
14658
+ }
14659
+ });
14660
+ }
14661
+ /**
14662
+ * Execute the `approve` and `execute` steps of a same-chain earn action as a
14663
+ * single atomic batch.
14664
+ *
14665
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw call
14666
+ * data via `getCallData()`, then submit both through the adapter's shared
14667
+ * `batchExecute`. `address` is forwarded as `opts.fromAddress` so
14668
+ * developer-controlled adapters batch on behalf of the right wallet;
14669
+ * `idempotencyKey` is forwarded for adapters that deduplicate ambiguous
14670
+ * submissions (the Circle developer-controlled adapter reuses the Earn
14671
+ * execution id); other adapters may ignore either option. Reused by both the
14672
+ * deposit and withdraw flows via the `actionKey` parameter.
14673
+ *
14674
+ * @param params - Adapter, chain, action key, signed payload, and approval inputs.
14675
+ * @returns The confirmed execute transaction hash, explorer URL, and receipt
14676
+ * gas data when the adapter can retrieve it.
14677
+ * @throws {@link KitError} when the source chain is not EVM.
14678
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not
14679
+ * supported by the prepared requests.
14680
+ * @throws {@link KitError} when the batch reverts on-chain (a confirmed
14681
+ * terminal revert), carrying `batchId`.
14682
+ * @throws {@link KitError} RETRYABLE when EIP-5792 reports an off-chain
14683
+ * failure carrying `batchId` and status code `400`; no call was included.
14684
+ * @throws {@link KitError} FATAL `NetworkError.TIMEOUT` when the batch was
14685
+ * submitted but its outcome could not be confirmed (poll timeout or any
14686
+ * other non-revert post-submission failure); carries `batchId` so the caller
14687
+ * can check transaction status before retrying.
14688
+ * @remarks
14689
+ * Once the batch has been submitted this function does not fall back to the
14690
+ * sequential path — the batch is already on its way, so a fallback would risk
14691
+ * double-spend. Post-submission failures surface through the adapter's batch
14692
+ * result: a confirmed on-chain revert (Circle: a `TRANSACTION_REVERTED` cause;
14693
+ * Viem: status code `500`/`600`) is thrown as a revert error, status code `400`
14694
+ * is reported as a retryable off-chain failure, and any other unconfirmed
14695
+ * outcome is thrown as a FATAL timeout error carrying `batchId`.
14696
+ *
14697
+ * @example
14698
+ * ```typescript
14699
+ * const { txHash, explorerUrl } = await executeBatchedEarnAction({
14700
+ * adapter,
14701
+ * chain,
14702
+ * address,
14703
+ * actionKey: 'earn.deposit',
14704
+ * executeParams,
14705
+ * tokenInputs,
14706
+ * signature,
14707
+ * approvalToken: usdcAddress,
14708
+ * delegate: adapterContractAddress,
14709
+ * requiredAllowance: 1_000_000n,
14710
+ * idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
14711
+ * revertMessage: 'Earn deposit reverted on-chain',
14712
+ * })
14713
+ * ```
14714
+ *
14715
+ * @internal
14716
+ */ async function executeBatchedEarnAction(params) {
14717
+ const { adapter, chain, address, actionKey, executeParams, tokenInputs, signature, approvalToken, delegate, requiredAllowance, idempotencyKey, revertMessage } = params;
14718
+ if (chain.type !== 'evm') {
14719
+ throw new KitError({
14720
+ ...InputError.INVALID_CHAIN,
14721
+ recoverability: 'FATAL',
14722
+ message: 'Batched earn execution is only supported on EVM chains.'
14723
+ });
14724
+ }
14725
+ const evmChain = chain;
14726
+ const batchAdapter = adapter;
14727
+ // Read the current allowance so the approval tops up only the missing amount.
14728
+ // When the existing allowance already covers the payload, skip the approve
14729
+ // call and batch only the execute — this mirrors the sequential
14730
+ // approveAllowanceIfNeeded guard and avoids an increaseAllowance underflow
14731
+ // (requiredAllowance - currentAllowance would be negative, which reverts as
14732
+ // an out-of-range uint256).
14733
+ const allowancePrepared = await adapter.prepareAction('token.allowance', {
14734
+ tokenAddress: approvalToken,
14735
+ delegate
14736
+ }, {
14737
+ chain,
14738
+ address
14739
+ });
14740
+ const currentAllowance = parseAllowanceResponse(await allowancePrepared.execute());
14741
+ const approvalNeeded = currentAllowance < requiredAllowance;
14742
+ const executePrepared = await adapter.prepareAction(actionKey, {
14743
+ executeParams,
14744
+ tokenInputs,
14745
+ signature
14746
+ }, {
14747
+ chain,
14748
+ address
14749
+ });
14750
+ const approvePrepared = approvalNeeded ? await prepareApprovalAction({
14751
+ adapter,
14752
+ chain,
14753
+ address,
14754
+ tokenAddress: approvalToken,
14755
+ delegate,
14756
+ currentAllowance,
14757
+ requiredAllowance
14758
+ }) : undefined;
14759
+ if (executePrepared.type !== 'evm' || !executePrepared.getCallData) {
14760
+ throw new KitError({
14761
+ ...InputError.UNSUPPORTED_ACTION,
14762
+ recoverability: 'FATAL',
14763
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
14764
+ });
14765
+ }
14766
+ if (approvePrepared !== undefined && (approvePrepared.type !== 'evm' || !approvePrepared.getCallData)) {
14767
+ throw new KitError({
14768
+ ...InputError.UNSUPPORTED_ACTION,
14769
+ recoverability: 'FATAL',
14770
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
14771
+ });
14772
+ }
14773
+ const executeCallData = executePrepared.getCallData();
14774
+ // Prepend the approve call only when an allowance top-up is required.
14775
+ const calls = approvePrepared?.type === 'evm' && approvePrepared.getCallData ? [
14776
+ approvePrepared.getCallData(),
14777
+ executeCallData
14778
+ ] : [
14779
+ executeCallData
14780
+ ];
14781
+ const authorization = buildBatchedEarnExecuteDescriptor({
14782
+ action: actionKey === 'earn.deposit' ? 'deposit' : 'withdraw',
14783
+ chain: evmChain.chain,
14784
+ executionParams: executeParams
14785
+ });
14786
+ const result = await batchAdapter.batchExecute(calls, evmChain, {
14787
+ fromAddress: address,
14788
+ idempotencyKey,
14789
+ atomicRequired: true,
14790
+ authorization
14791
+ });
14792
+ // Success fans one confirmed hash across every receipt; the execute call is
14793
+ // the last one (approve, if present, precedes it). On failure a confirming
14794
+ // adapter returns no receipts, so a missing/non-success last receipt — or a
14795
+ // populated `result.error` — means the batch failed after submission (point
14796
+ // of no return). We never fall back, which would double-spend.
14797
+ const receiptCountMatches = result.receipts.length === calls.length;
14798
+ const executeReceipt = receiptCountMatches ? result.receipts[calls.length - 1] : undefined;
14799
+ const succeeded = receiptCountMatches && (result.statusCode === undefined || result.statusCode === 200) && result.error === undefined && executeReceipt?.status === 'success' && executeReceipt.txHash !== '';
14800
+ if (succeeded) {
14801
+ return buildSuccessfulBatchResult(adapter, evmChain, executeReceipt, result.batchId, revertMessage);
14802
+ }
14803
+ // Distinguish an off-chain rejection, a confirmed on-chain revert, and an
14804
+ // unknown outcome across both adapter conventions that share this contract:
14805
+ // - Circle SCA: no receipts + `error`; its trace kind identifies an
14806
+ // off-chain rejection, confirmed revert, or unconfirmed outcome.
14807
+ // - Viem EIP-5792: statusCode 500/600 explicitly confirms an on-chain
14808
+ // full/partial revert.
14809
+ // - Legacy/string-status wallets: a real-hash error receipt with no cause
14810
+ // is the best available confirmed-revert signal.
14811
+ // statusCode 400 is terminal but off-chain: the wallet confirms no call was
14812
+ // included, so it must not be labeled as a revert or unknown outcome.
14813
+ // Anything else — a poll timeout or any other post-submission failure with no
14814
+ // confirmed-revert signal — means the batch was submitted but its fate is
14815
+ // unconfirmed. Surface that as a FATAL (non-auto-retry) error carrying
14816
+ // `batchId` so the caller checks status before retrying, rather than
14817
+ // mislabeling it a revert.
14818
+ return throwBatchFailure(result, executeReceipt, evmChain, actionKey, revertMessage);
14819
+ }
14820
+
12934
14821
  /**
12935
14822
  * Validate that a service-signed execution payload has not expired before
12936
14823
  * the SDK asks the wallet to broadcast a transaction.
@@ -14227,7 +16114,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
14227
16114
  }
14228
16115
 
14229
16116
  var name = "@circle-fin/provider-earn-service";
14230
- var version = "1.3.0";
16117
+ var version = "1.4.0";
14231
16118
  var pkg = {
14232
16119
  name: name,
14233
16120
  version: version};
@@ -14289,15 +16176,25 @@ var pkg = {
14289
16176
  *
14290
16177
  * @internal
14291
16178
  */ function buildConfig(serviceConfig) {
16179
+ // The kit key is a server-only secret. Reject it in the browser so it cannot
16180
+ // leak into a client bundle (no-op in Node.js). Keyless usage stays allowed.
16181
+ if (serviceConfig?.kitKey !== undefined && isBrowserEnvironment()) {
16182
+ 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');
16183
+ }
14292
16184
  const baseUrl = serviceConfig?.baseUrl ?? EARN_SERVICE_BASE_URL;
14293
- const sdkVersion = resolveSdkVersionHeader();
16185
+ // The API CORS policy does not allow this custom header. Keep the existing
16186
+ // per-request version attribution for Node callers, but omit it in browsers
16187
+ // so public EarnKit endpoints do not fail at CORS preflight.
16188
+ const sdkVersionHeader = isNodeEnvironment() ? {
16189
+ [SDK_VERSION_HEADER]: resolveSdkVersionHeader()
16190
+ } : {};
14294
16191
  if (serviceConfig?.kitKey === undefined) {
14295
16192
  return {
14296
16193
  pollingConfig: {
14297
16194
  ...DEFAULT_CONFIG,
14298
16195
  headers: {
14299
16196
  ...DEFAULT_CONFIG.headers,
14300
- [SDK_VERSION_HEADER]: sdkVersion
16197
+ ...sdkVersionHeader
14301
16198
  }
14302
16199
  },
14303
16200
  baseUrl
@@ -14315,7 +16212,7 @@ var pkg = {
14315
16212
  ...DEFAULT_CONFIG,
14316
16213
  headers: {
14317
16214
  ...DEFAULT_CONFIG.headers,
14318
- [SDK_VERSION_HEADER]: sdkVersion,
16215
+ ...sdkVersionHeader,
14319
16216
  Authorization: `Bearer ${serviceConfig.kitKey}`
14320
16217
  }
14321
16218
  },
@@ -15845,6 +17742,46 @@ function finishElapsedWait(lastStatus, lastError) {
15845
17742
  const approvalToken = resolveEarnApprovalToken(executionParams);
15846
17743
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15847
17744
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
17745
+ const approvalNeeded = !options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n;
17746
+ // Batch-capable wallets bundle approve + deposit into one atomic
17747
+ // submission. Only attempt this when an approval is actually needed.
17748
+ if (approvalNeeded && approvalToken !== undefined && await shouldUseBatchedEarnAction({
17749
+ adapter,
17750
+ chain,
17751
+ address,
17752
+ batchTransactions: config?.batchTransactions
17753
+ })) {
17754
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
17755
+ try {
17756
+ const result = await executeBatchedEarnAction({
17757
+ adapter,
17758
+ chain,
17759
+ address,
17760
+ actionKey: 'earn.deposit',
17761
+ executeParams: executionParams,
17762
+ tokenInputs,
17763
+ signature,
17764
+ approvalToken,
17765
+ delegate: adapterContractAddress,
17766
+ requiredAllowance,
17767
+ idempotencyKey: execId,
17768
+ revertMessage: 'Earn deposit reverted on-chain'
17769
+ });
17770
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
17771
+ return result;
17772
+ } catch (error) {
17773
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
17774
+ throw error;
17775
+ }
17776
+ }, ({ txHash })=>txHash);
17777
+ return {
17778
+ kind: 'same-chain',
17779
+ txHash,
17780
+ explorerUrl,
17781
+ vaultAddress,
17782
+ amount: params.amount
17783
+ };
17784
+ }
15848
17785
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
15849
17786
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
15850
17787
  try {
@@ -15877,6 +17814,8 @@ function finishElapsedWait(lastStatus, lastError) {
15877
17814
  tokenInputs,
15878
17815
  signature
15879
17816
  },
17817
+ action: 'deposit',
17818
+ executionParams,
15880
17819
  revertMessage: 'Earn deposit reverted on-chain'
15881
17820
  });
15882
17821
  reportTransactionSuccess(transactionReportContext, 'Deposit', result);
@@ -16008,6 +17947,44 @@ function finishElapsedWait(lastStatus, lastError) {
16008
17947
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
16009
17948
  const approvalToken = tokenInputs[0]?.token;
16010
17949
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
17950
+ // Batch-capable wallets bundle approve + withdraw into one atomic
17951
+ // submission. Only attempt this when an approval is actually needed.
17952
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n && await shouldUseBatchedEarnAction({
17953
+ adapter,
17954
+ chain,
17955
+ address,
17956
+ batchTransactions: config?.batchTransactions
17957
+ })) {
17958
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
17959
+ try {
17960
+ const result = await executeBatchedEarnAction({
17961
+ adapter,
17962
+ chain,
17963
+ address,
17964
+ actionKey: 'earn.withdraw',
17965
+ executeParams: executionParams,
17966
+ tokenInputs,
17967
+ signature,
17968
+ approvalToken,
17969
+ delegate: adapterContractAddress,
17970
+ requiredAllowance,
17971
+ idempotencyKey: execId,
17972
+ revertMessage: 'Earn withdraw reverted on-chain'
17973
+ });
17974
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
17975
+ return result;
17976
+ } catch (error) {
17977
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
17978
+ throw error;
17979
+ }
17980
+ }, ({ txHash })=>txHash);
17981
+ return {
17982
+ txHash,
17983
+ explorerUrl,
17984
+ vaultAddress,
17985
+ amount: params.amount
17986
+ };
17987
+ }
16011
17988
  if (!options.skipApprove && approvalToken !== undefined) {
16012
17989
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16013
17990
  try {
@@ -16040,6 +18017,8 @@ function finishElapsedWait(lastStatus, lastError) {
16040
18017
  tokenInputs,
16041
18018
  signature
16042
18019
  },
18020
+ action: 'withdraw',
18021
+ executionParams,
16043
18022
  revertMessage: 'Earn withdraw reverted on-chain'
16044
18023
  });
16045
18024
  reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
@@ -16119,6 +18098,8 @@ function finishElapsedWait(lastStatus, lastError) {
16119
18098
  tokenInputs: [],
16120
18099
  signature
16121
18100
  },
18101
+ action: 'claimRewards',
18102
+ executionParams,
16122
18103
  revertMessage: 'Earn claim rewards reverted on-chain'
16123
18104
  }), ({ txHash })=>txHash);
16124
18105
  return {
@@ -16419,6 +18400,12 @@ function createUnsupportedCrossChainDepositError(chain, reason) {
16419
18400
  if (config.providers !== undefined && !Array.isArray(config.providers)) {
16420
18401
  throw createValidationFailedError('config.providers', config.providers, 'providers must be an array of earn providers when provided');
16421
18402
  }
18403
+ if (config.disableAnalytics !== undefined && typeof config.disableAnalytics !== 'boolean') {
18404
+ throw createValidationFailedError('config.disableAnalytics', config.disableAnalytics, 'disableAnalytics must be a boolean when provided');
18405
+ }
18406
+ if (config.disableErrorReporting !== undefined && typeof config.disableErrorReporting !== 'boolean') {
18407
+ throw createValidationFailedError('config.disableErrorReporting', config.disableErrorReporting, 'disableErrorReporting must be a boolean when provided');
18408
+ }
16422
18409
  const defaultProviders = getDefaultProviders();
16423
18410
  const providers = [
16424
18411
  ...config.providers ?? [],
@@ -16430,6 +18417,31 @@ function createUnsupportedCrossChainDepositError(chain, reason) {
16430
18417
  return context;
16431
18418
  }
16432
18419
 
18420
+ /**
18421
+ * Register Earn Kit telemetry event type strings with the shared registry so
18422
+ * error telemetry helpers remain compile-time checked.
18423
+ *
18424
+ * @internal
18425
+ */ /**
18426
+ * Telemetry event type identifiers for Earn Kit operations.
18427
+ *
18428
+ * @internal
18429
+ */ const EARN_EVENT_TYPES = {
18430
+ GET_VAULTS: 'earn_get_vaults',
18431
+ EXPLORE_VAULTS: 'earn_explore_vaults',
18432
+ GET_POSITION: 'earn_get_position',
18433
+ GET_CROSS_CHAIN_DEPOSIT_STATUS: 'earn_get_cross_chain_deposit_status',
18434
+ WAIT_FOR_CROSS_CHAIN_DEPOSIT: 'earn_wait_for_cross_chain_deposit',
18435
+ DEPOSIT: 'earn_deposit',
18436
+ CROSS_CHAIN_DEPOSIT: 'earn_cross_chain_deposit',
18437
+ WITHDRAW: 'earn_withdraw',
18438
+ CLAIM_REWARDS: 'earn_claim_rewards',
18439
+ GET_DEPOSIT_QUOTE: 'earn_get_deposit_quote',
18440
+ GET_WITHDRAWAL_QUOTE: 'earn_get_withdrawal_quote',
18441
+ GET_CLAIM_REWARDS_QUOTE: 'earn_get_claim_rewards_quote',
18442
+ RETRY: 'earn_retry'
18443
+ };
18444
+
16433
18445
  /**
16434
18446
  * Format a provider amount object as a human-readable decimal string.
16435
18447
  *
@@ -16771,11 +18783,16 @@ const sourceAdapterContextSchema = z.object({
16771
18783
  *
16772
18784
  * Validate the optional Kit Key field using the standard `apiKeySchema`
16773
18785
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
16774
- * operates in permissionless mode.
18786
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
18787
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
18788
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
18789
+ * schema must accept them or a valid config object is rejected.
16775
18790
  *
16776
18791
  * @internal
16777
18792
  */ const earnConfigSchema = z.object({
16778
- kitKey: apiKeySchema.optional()
18793
+ kitKey: apiKeySchema.optional(),
18794
+ baseUrl: z.string().optional(),
18795
+ batchTransactions: z.boolean().optional()
16779
18796
  }).strict();
16780
18797
  /**
16781
18798
  * Canonical decimal form: a leading digit with no leading zeros (a single
@@ -17945,6 +19962,14 @@ function hasCrossChainDestination(params) {
17945
19962
  return formatClaimRewardsQuoteInfo(result);
17946
19963
  }
17947
19964
 
19965
+ /** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg$1.name);
19966
+ /**
19967
+ * Determine whether deposit parameters target a destination chain.
19968
+ *
19969
+ * @internal
19970
+ */ function isCrossChainDeposit(params) {
19971
+ return 'to' in params && params.to !== undefined;
19972
+ }
17948
19973
  function formatRetryResult(operation, result) {
17949
19974
  switch(operation){
17950
19975
  case 'deposit':
@@ -17959,6 +19984,70 @@ function formatRetryResult(operation, result) {
17959
19984
  }
17960
19985
  }
17961
19986
  }
19987
+ /**
19988
+ * Emit the success event corresponding to a completed retry.
19989
+ *
19990
+ * @internal
19991
+ */ function emitRetrySuccessTelemetry(trace, result, config) {
19992
+ switch(trace.operation){
19993
+ case 'deposit':
19994
+ {
19995
+ const sourceChain = resolveChainName(trace.params.from.chain);
19996
+ if ('to' in trace.params && trace.params.to !== undefined) {
19997
+ const destinationChain = resolveChainName(trace.params.to.chain);
19998
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CROSS_CHAIN_DEPOSIT, config, {
19999
+ ...sourceChain != null && {
20000
+ sourceChain
20001
+ },
20002
+ ...destinationChain != null && {
20003
+ destinationChain
20004
+ }
20005
+ });
20006
+ return;
20007
+ }
20008
+ emitSuccessTelemetry(EARN_EVENT_TYPES.DEPOSIT, config, {
20009
+ ...sourceChain != null && {
20010
+ sourceChain
20011
+ },
20012
+ ...'txHash' in result && {
20013
+ txHash: result.txHash
20014
+ }
20015
+ });
20016
+ return;
20017
+ }
20018
+ case 'withdraw':
20019
+ {
20020
+ const sourceChain = resolveChainName(trace.params.from.chain);
20021
+ emitSuccessTelemetry(EARN_EVENT_TYPES.WITHDRAW, config, {
20022
+ ...sourceChain != null && {
20023
+ sourceChain
20024
+ },
20025
+ ...'txHash' in result && {
20026
+ txHash: result.txHash
20027
+ }
20028
+ });
20029
+ return;
20030
+ }
20031
+ case 'claimRewards':
20032
+ {
20033
+ if ('rewards' in result && result.status === 'claimed') {
20034
+ const sourceChain = resolveChainName(trace.params.from.chain);
20035
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CLAIM_REWARDS, config, {
20036
+ ...sourceChain != null && {
20037
+ sourceChain
20038
+ },
20039
+ txHash: result.txHash
20040
+ });
20041
+ }
20042
+ return;
20043
+ }
20044
+ default:
20045
+ {
20046
+ const exhaustive = trace;
20047
+ throw createValidationFailedError('error.cause.trace', exhaustive, 'EarnKit.retry() does not support this earn operation');
20048
+ }
20049
+ }
20050
+ }
17962
20051
  /**
17963
20052
  * A high-level class-based interface for DeFi lending vault operations.
17964
20053
  *
@@ -18017,6 +20106,8 @@ function formatRetryResult(operation, result) {
18017
20106
  * ```
18018
20107
  */ class EarnKit {
18019
20108
  context;
20109
+ /** Per-kit identity and opt-out state for error telemetry. */ telemetryConfig;
20110
+ /** Per-kit identity and opt-out state for success telemetry. */ analyticsTelemetryConfig;
18020
20111
  /**
18021
20112
  * Event dispatcher for step-level events emitted during multi-phase earn
18022
20113
  * operations. Prefer {@link EarnKit.on} / {@link EarnKit.off} over using
@@ -18041,6 +20132,16 @@ function formatRetryResult(operation, result) {
18041
20132
  */ constructor(config = {}){
18042
20133
  this.context = createEarnKitContext(config);
18043
20134
  this.actionDispatcher = new Actionable();
20135
+ this.telemetryConfig = {
20136
+ sdkName: SDK_NAME,
20137
+ sdkVersion: pkg$1.version,
20138
+ disabled: config.disableErrorReporting === true
20139
+ };
20140
+ this.analyticsTelemetryConfig = {
20141
+ sdkName: SDK_NAME,
20142
+ sdkVersion: pkg$1.version,
20143
+ disabled: config.disableAnalytics === true
20144
+ };
18044
20145
  for (const provider of this.context.providers){
18045
20146
  provider.registerDispatcher(this.actionDispatcher);
18046
20147
  }
@@ -18101,29 +20202,36 @@ function formatRetryResult(operation, result) {
18101
20202
  * }
18102
20203
  * ```
18103
20204
  */ async retry(error) {
18104
- if (!isKitError(error)) {
18105
- throw createValidationFailedError('error', error, 'EarnKit.retry() requires a KitError thrown by a previous earn operation');
18106
- }
18107
- if (!isRetryableError$1(error)) {
18108
- throw createValidationFailedError('error.recoverability', error.recoverability, 'EarnKit.retry() requires a retryable or resumable error — check isRetryableError(error) first');
18109
- }
18110
- const trace = error.cause?.trace;
18111
- if (!isEarnErrorTrace(trace)) {
18112
- throw createValidationFailedError('error.cause.trace', trace, 'EarnKit.retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
18113
- }
18114
- const provider = this.context.providers.find((candidate)=>candidate.name === trace.provider);
18115
- if (provider === undefined) {
18116
- throw createValidationFailedError('error.cause.trace.provider', trace.provider, `No earn provider named "${trace.provider}" is registered with this kit`);
18117
- }
18118
- const result = await provider.retry(error);
18119
- // `provider.retry` returns a flat result union with no compile-time link to
18120
- // `trace.operation`, so narrow the operation here to select the matching
18121
- // overload. The result cast in each branch is sound: the provider always
18122
- // returns the result type corresponding to the resumed operation.
18123
- if (trace.operation === 'claimRewards') {
20205
+ const result = await withErrorTelemetry(async ()=>{
20206
+ if (!isKitError(error)) {
20207
+ throw createValidationFailedError('error', error, 'EarnKit.retry() requires a KitError thrown by a previous earn operation');
20208
+ }
20209
+ if (!isRetryableError$1(error)) {
20210
+ throw createValidationFailedError('error.recoverability', error.recoverability, 'EarnKit.retry() requires a retryable or resumable error — check isRetryableError(error) first');
20211
+ }
20212
+ const trace = error.cause?.trace;
20213
+ if (!isEarnErrorTrace(trace)) {
20214
+ throw createValidationFailedError('error.cause.trace', trace, 'EarnKit.retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
20215
+ }
20216
+ const provider = this.context.providers.find((candidate)=>candidate.name === trace.provider);
20217
+ if (provider === undefined) {
20218
+ throw createValidationFailedError('error.cause.trace.provider', trace.provider, `No earn provider named "${trace.provider}" is registered with this kit`);
20219
+ }
20220
+ const result = await provider.retry(error);
20221
+ // `provider.retry` returns a flat result union with no compile-time link to
20222
+ // `trace.operation`, so narrow the operation here to select the matching
20223
+ // overload. The result cast in each branch is sound: the provider always
20224
+ // returns the result type corresponding to the resumed operation.
20225
+ if (trace.operation === 'claimRewards') {
20226
+ return formatRetryResult(trace.operation, result);
20227
+ }
18124
20228
  return formatRetryResult(trace.operation, result);
20229
+ }, EARN_EVENT_TYPES.RETRY, this.telemetryConfig);
20230
+ const trace = isKitError(error) ? error.cause?.trace : undefined;
20231
+ if (isEarnErrorTrace(trace)) {
20232
+ emitRetrySuccessTelemetry(trace, result, this.analyticsTelemetryConfig);
18125
20233
  }
18126
- return formatRetryResult(trace.operation, result);
20234
+ return result;
18127
20235
  }
18128
20236
  /**
18129
20237
  * Return the chains supported by configured earn providers.
@@ -18157,7 +20265,9 @@ function formatRetryResult(operation, result) {
18157
20265
  * result.vaults.forEach(v => console.log(`${v.name}: ${(v.currentApy * 100).toFixed(2)}% APY`))
18158
20266
  * ```
18159
20267
  */ async getVaults(params) {
18160
- return getVaults$1(this.context, params);
20268
+ const result = await withErrorTelemetry(async ()=>getVaults$1(this.context, params), EARN_EVENT_TYPES.GET_VAULTS, this.telemetryConfig);
20269
+ emitSuccessTelemetry(EARN_EVENT_TYPES.GET_VAULTS, this.analyticsTelemetryConfig, {});
20270
+ return result;
18161
20271
  }
18162
20272
  /**
18163
20273
  * Discover vaults available on a chain.
@@ -18182,7 +20292,12 @@ function formatRetryResult(operation, result) {
18182
20292
  * const guarded = result.vaults.filter(v => v.circleGuarded) // Circle-guarded vaults only
18183
20293
  * ```
18184
20294
  */ async exploreVaults(params) {
18185
- return exploreVaults$1(this.context, params);
20295
+ const context = {
20296
+ sourceChain: resolveChainName(params.chain)
20297
+ };
20298
+ const result = await withErrorTelemetry(async ()=>exploreVaults$1(this.context, params), EARN_EVENT_TYPES.EXPLORE_VAULTS, this.telemetryConfig, context);
20299
+ emitSuccessTelemetry(EARN_EVENT_TYPES.EXPLORE_VAULTS, this.analyticsTelemetryConfig, context);
20300
+ return result;
18186
20301
  }
18187
20302
  /**
18188
20303
  * Lazily iterate every vault available on a chain.
@@ -18229,7 +20344,9 @@ function formatRetryResult(operation, result) {
18229
20344
  * }
18230
20345
  * ```
18231
20346
  */ async getPosition(params) {
18232
- return getPosition$1(this.context, params);
20347
+ return withErrorTelemetry(async ()=>getPosition$1(this.context, params), EARN_EVENT_TYPES.GET_POSITION, this.telemetryConfig, {
20348
+ sourceChain: resolveChainName(params.from.chain)
20349
+ });
18233
20350
  }
18234
20351
  /**
18235
20352
  * Fetch the current status of a cross-chain deposit by execution ID.
@@ -18254,7 +20371,7 @@ function formatRetryResult(operation, result) {
18254
20371
  * console.log(`Bridge ${status.execId} is ${status.status}`)
18255
20372
  * ```
18256
20373
  */ async getCrossChainDepositStatus(params) {
18257
- return getCrossChainDepositStatus$1(this.context, params);
20374
+ return withErrorTelemetry(async ()=>getCrossChainDepositStatus$1(this.context, params), EARN_EVENT_TYPES.GET_CROSS_CHAIN_DEPOSIT_STATUS, this.telemetryConfig);
18258
20375
  }
18259
20376
  /**
18260
20377
  * Poll a cross-chain deposit until it reaches a terminal bridge state.
@@ -18281,10 +20398,30 @@ function formatRetryResult(operation, result) {
18281
20398
  * console.log(`Bridge ended as ${result.outcome}`)
18282
20399
  * ```
18283
20400
  */ async waitForCrossChainDeposit(params) {
18284
- return waitForCrossChainDeposit$1(this.context, params);
20401
+ return withErrorTelemetry(async ()=>waitForCrossChainDeposit$1(this.context, params), EARN_EVENT_TYPES.WAIT_FOR_CROSS_CHAIN_DEPOSIT, this.telemetryConfig);
18285
20402
  }
18286
20403
  async deposit(params) {
18287
- return deposit$1(this.context, params);
20404
+ const isCrossChain = isCrossChainDeposit(params);
20405
+ const context = {
20406
+ sourceChain: resolveChainName(params.from.chain),
20407
+ ...isCrossChain && {
20408
+ destinationChain: resolveChainName(params.to.chain)
20409
+ }
20410
+ };
20411
+ const eventType = isCrossChain ? EARN_EVENT_TYPES.CROSS_CHAIN_DEPOSIT : EARN_EVENT_TYPES.DEPOSIT;
20412
+ const result = await withErrorTelemetry(async ()=>deposit$1(this.context, params), eventType, this.telemetryConfig, context);
20413
+ if (result.kind === 'cross-chain') {
20414
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CROSS_CHAIN_DEPOSIT, this.analyticsTelemetryConfig, {
20415
+ sourceChain: resolveChainName(result.sourceChain),
20416
+ destinationChain: resolveChainName(result.destinationChain)
20417
+ });
20418
+ } else {
20419
+ emitSuccessTelemetry(EARN_EVENT_TYPES.DEPOSIT, this.analyticsTelemetryConfig, {
20420
+ ...context,
20421
+ txHash: result.txHash
20422
+ });
20423
+ }
20424
+ return result;
18288
20425
  }
18289
20426
  /**
18290
20427
  * Execute a withdrawal from a DeFi lending vault.
@@ -18310,7 +20447,15 @@ function formatRetryResult(operation, result) {
18310
20447
  * console.log(`Withdrew ${result.amount} from ${result.vaultAddress}, tx: ${result.txHash}`)
18311
20448
  * ```
18312
20449
  */ async withdraw(params) {
18313
- return withdraw$1(this.context, params);
20450
+ const context = {
20451
+ sourceChain: resolveChainName(params.from.chain)
20452
+ };
20453
+ const result = await withErrorTelemetry(async ()=>withdraw$1(this.context, params), EARN_EVENT_TYPES.WITHDRAW, this.telemetryConfig, context);
20454
+ emitSuccessTelemetry(EARN_EVENT_TYPES.WITHDRAW, this.analyticsTelemetryConfig, {
20455
+ ...context,
20456
+ txHash: result.txHash
20457
+ });
20458
+ return result;
18314
20459
  }
18315
20460
  /**
18316
20461
  * Claim rewards from earn vaults.
@@ -18337,7 +20482,17 @@ function formatRetryResult(operation, result) {
18337
20482
  *
18338
20483
  * @internal
18339
20484
  */ async claimRewards(params) {
18340
- return claimRewards$1(this.context, params);
20485
+ const context = {
20486
+ sourceChain: resolveChainName(params.from.chain)
20487
+ };
20488
+ const result = await withErrorTelemetry(async ()=>claimRewards$1(this.context, params), EARN_EVENT_TYPES.CLAIM_REWARDS, this.telemetryConfig, context);
20489
+ if (result.status === 'claimed') {
20490
+ emitSuccessTelemetry(EARN_EVENT_TYPES.CLAIM_REWARDS, this.analyticsTelemetryConfig, {
20491
+ ...context,
20492
+ txHash: result.txHash
20493
+ });
20494
+ }
20495
+ return result;
18341
20496
  }
18342
20497
  /**
18343
20498
  * Get an informational quote for a deposit into a vault.
@@ -18359,7 +20514,9 @@ function formatRetryResult(operation, result) {
18359
20514
  * console.log(`Expected shares: ${quote.expectedShares.amount}`)
18360
20515
  * ```
18361
20516
  */ async getDepositQuote(params) {
18362
- return getDepositQuote$1(this.context, params);
20517
+ return withErrorTelemetry(async ()=>getDepositQuote$1(this.context, params), EARN_EVENT_TYPES.GET_DEPOSIT_QUOTE, this.telemetryConfig, {
20518
+ sourceChain: resolveChainName(params.from.chain)
20519
+ });
18363
20520
  }
18364
20521
  /**
18365
20522
  * Get an informational quote for a withdrawal from a vault.
@@ -18381,7 +20538,9 @@ function formatRetryResult(operation, result) {
18381
20538
  * console.log(`Shares to redeem: ${quote.sharesToRedeem.amount}`)
18382
20539
  * ```
18383
20540
  */ async getWithdrawalQuote(params) {
18384
- return getWithdrawalQuote$1(this.context, params);
20541
+ return withErrorTelemetry(async ()=>getWithdrawalQuote$1(this.context, params), EARN_EVENT_TYPES.GET_WITHDRAWAL_QUOTE, this.telemetryConfig, {
20542
+ sourceChain: resolveChainName(params.from.chain)
20543
+ });
18385
20544
  }
18386
20545
  /**
18387
20546
  * Get an informational quote for claiming rewards.
@@ -18403,7 +20562,9 @@ function formatRetryResult(operation, result) {
18403
20562
  *
18404
20563
  * @internal
18405
20564
  */ async getClaimRewardsQuote(params) {
18406
- return getClaimRewardsQuote$1(this.context, params);
20565
+ return withErrorTelemetry(async ()=>getClaimRewardsQuote$1(this.context, params), EARN_EVENT_TYPES.GET_CLAIM_REWARDS_QUOTE, this.telemetryConfig, {
20566
+ sourceChain: resolveChainName(params.from.chain)
20567
+ });
18407
20568
  }
18408
20569
  }
18409
20570
 
@@ -18492,7 +20653,14 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
18492
20653
  * const earnKit = createEarnKit(context)
18493
20654
  * ```
18494
20655
  */ const createEarnKit = (context)=>{
18495
- const kit = new EarnKit();
20656
+ const kit = new EarnKit({
20657
+ ...context.disableErrorReporting != null && {
20658
+ disableErrorReporting: context.disableErrorReporting
20659
+ },
20660
+ ...context.disableAnalytics != null && {
20661
+ disableAnalytics: context.disableAnalytics
20662
+ }
20663
+ });
18496
20664
  registerActionHandlers(kit, context.actions.earn, 'earn');
18497
20665
  return kit;
18498
20666
  };