@circle-fin/app-kit 1.9.0 → 1.10.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
@@ -18,6 +18,9 @@
18
18
 
19
19
  import { z } from 'zod';
20
20
  import 'pino';
21
+ import '@ethersproject/bytes';
22
+ import '@ethersproject/abi';
23
+ import '@ethersproject/address';
21
24
  import { PublicKey } from '@solana/web3.js';
22
25
  import 'bn.js';
23
26
  import '@coral-xyz/anchor';
@@ -1759,14 +1762,14 @@ class KitError extends Error {
1759
1762
  }
1760
1763
 
1761
1764
  /**
1762
- * Standardized error definitions for Earn/Zenith operations.
1765
+ * Standardized error definitions for Earn operations.
1763
1766
  *
1764
1767
  * These error codes provide fine-grained categorization of failures
1765
- * from the Zenith earn service, enabling SDK consumers to distinguish
1768
+ * from the Earn service, enabling SDK consumers to distinguish
1766
1769
  * between input errors (fix your request) and service errors (retry later).
1767
1770
  *
1768
1771
  * Error code ranges:
1769
- * - 1100-1105: INPUT errors — invalid inputs, unsupported configurations
1772
+ * - 1100-1106: INPUT errors — invalid, unsupported, or stale request state
1770
1773
  * - 8100-8105: SERVICE errors — retryable backend/provider failures
1771
1774
  *
1772
1775
  * @example
@@ -1819,6 +1822,14 @@ class KitError extends Error {
1819
1822
  name: 'EARN_UNSUPPORTED_BRIDGE_ROUTE',
1820
1823
  type: 'INPUT'
1821
1824
  },
1825
+ /**
1826
+ * The bridge quote expired. This is an INPUT error because the prepared
1827
+ * request is stale and must be replaced instead of retried.
1828
+ */ BRIDGE_QUOTE_EXPIRED: {
1829
+ code: 1106,
1830
+ name: 'EARN_BRIDGE_QUOTE_EXPIRED',
1831
+ type: 'INPUT'
1832
+ },
1822
1833
  /** The proxy signing call failed — retryable. */ SIGNING_FAILED: {
1823
1834
  code: 8100,
1824
1835
  name: 'EARN_SIGNING_FAILED',
@@ -1878,6 +1889,9 @@ function getOptionalString(value) {
1878
1889
  * internal-error, vault-refresh-busy, off-chain-paused, position-PnL-pending,
1879
1890
  * bridge failures/status lookup failures
1880
1891
  *
1892
+ * Quote expiry is INPUT/FATAL because callers must start a fresh bridge prepare
1893
+ * flow rather than retry the stale prepared bundle.
1894
+ *
1881
1895
  * Unrecognized codes fall through to `parseApiError` for HTTP-status-based
1882
1896
  * handling.
1883
1897
  *
@@ -2111,6 +2125,13 @@ function getOptionalString(value) {
2111
2125
  errorDef: EarnError.PROVIDER_ERROR,
2112
2126
  recoverability: 'FATAL'
2113
2127
  }
2128
+ ],
2129
+ [
2130
+ 380506,
2131
+ {
2132
+ errorDef: EarnError.BRIDGE_QUOTE_EXPIRED,
2133
+ recoverability: 'FATAL'
2134
+ }
2114
2135
  ]
2115
2136
  ]);
2116
2137
  /**
@@ -2834,7 +2855,10 @@ var EarnChain;
2834
2855
  contracts: {
2835
2856
  v1: {
2836
2857
  wallet: GATEWAY_WALLET_EVM_TESTNET,
2837
- minter: GATEWAY_MINTER_EVM_TESTNET
2858
+ minter: GATEWAY_MINTER_EVM_TESTNET,
2859
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
2860
+ // deposit into the GatewayWallet above.
2861
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
2838
2862
  }
2839
2863
  },
2840
2864
  forwarderSupported: {
@@ -5902,7 +5926,10 @@ var Chains = /*#__PURE__*/Object.freeze({
5902
5926
  minter: z.string({
5903
5927
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
5904
5928
  invalid_type_error: 'Gateway minter address must be a string.'
5905
- }).min(1, 'Gateway minter address cannot be empty.')
5929
+ }).min(1, 'Gateway minter address cannot be empty.'),
5930
+ depositForHandler: z.string({
5931
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
5932
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
5906
5933
  }).strict() // Reject any additional properties not defined in the schema
5907
5934
  ;
5908
5935
  /**
@@ -8014,6 +8041,13 @@ const swapTokenEnumSchema = z.enum([
8014
8041
  return explorerUrl;
8015
8042
  }
8016
8043
 
8044
+ /**
8045
+ * CCTP forwarding magic bytes prefix.
8046
+ *
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
+
8017
8051
  /**
8018
8052
  * Strip the `@circle-fin/` scope from a kit package name to produce the
8019
8053
  * short SDK name used in telemetry payloads.
@@ -8033,7 +8067,7 @@ const swapTokenEnumSchema = z.enum([
8033
8067
  }
8034
8068
 
8035
8069
  var name$3 = "@circle-fin/bridge-kit";
8036
- var version$3 = "1.12.0";
8070
+ var version$3 = "1.12.1";
8037
8071
  var pkg$3 = {
8038
8072
  name: name$3,
8039
8073
  version: version$3};
@@ -8858,6 +8892,11 @@ var TransferSpeed;
8858
8892
  clock: z.any().optional()
8859
8893
  }).passthrough();
8860
8894
 
8895
+ /**
8896
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
8897
+ * hookData must start with.
8898
+ */ Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
8899
+
8861
8900
  /**
8862
8901
  * The minimum finality threshold for CCTPv2 transfers.
8863
8902
  *
@@ -8890,7 +8929,7 @@ var TransferSpeed;
8890
8929
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
8891
8930
 
8892
8931
  var name$2 = "@circle-fin/swap-kit";
8893
- var version$2 = "1.3.2";
8932
+ var version$2 = "1.4.0";
8894
8933
  var pkg$2 = {
8895
8934
  name: name$2,
8896
8935
  version: version$2};
@@ -8955,7 +8994,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
8955
8994
  }).min(1, 'kitKey must be a non-empty string').optional(),
8956
8995
  provider: z.string({
8957
8996
  invalid_type_error: 'provider must be a string'
8958
- }).min(1, 'provider must be a non-empty string').optional()
8997
+ }).min(1, 'provider must be a non-empty string').optional(),
8998
+ batchTransactions: z.boolean({
8999
+ invalid_type_error: 'batchTransactions must be a boolean'
9000
+ }).optional()
8959
9001
  });
8960
9002
  /**
8961
9003
  * Zod schema for adapter context.
@@ -9394,7 +9436,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9394
9436
  /**
9395
9437
  * Circle Stablecoin Service API Key.
9396
9438
  * Must be a valid API key format.
9397
- */ apiKey: apiKeySchema
9439
+ */ apiKey: apiKeySchema.optional()
9398
9440
  }).superRefine(requireCrossChainQuoteToAddress);
9399
9441
  /**
9400
9442
  * Zod schema for validating CreateSwapRequest parameters.
@@ -9452,7 +9494,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9452
9494
  /**
9453
9495
  * Circle Stablecoin Service API Key.
9454
9496
  * Must be a valid API key format.
9455
- */ apiKey: apiKeySchema
9497
+ */ apiKey: apiKeySchema.optional()
9456
9498
  });
9457
9499
  /**
9458
9500
  * Zod schema for validating GetSwapStatusResponse data.
@@ -9488,7 +9530,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9488
9530
  toChain: z.string({
9489
9531
  invalid_type_error: 'toChain must be a string'
9490
9532
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
9491
- apiKey: apiKeySchema
9533
+ apiKey: apiKeySchema.optional()
9492
9534
  });
9493
9535
  /**
9494
9536
  * Zod schema for validating CreateSwapResponse payloads.
@@ -9497,13 +9539,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9497
9539
  required_error: 'fee token is required',
9498
9540
  invalid_type_error: 'fee token must be a string'
9499
9541
  }).min(1, 'fee token must be a non-empty string'),
9500
- amount: feeAmountSchema
9542
+ amount: feeAmountSchema,
9543
+ decimals: z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
9544
+ symbol: z.string({
9545
+ invalid_type_error: 'fee token symbol must be a string'
9546
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
9501
9547
  });
9502
9548
  /**
9503
9549
  * Developer fee item schema with basis field.
9504
- */ const createSwapDeveloperFeeItemSchema = z.object({
9505
- token: z.string().min(1, 'fee token must be a non-empty string'),
9506
- amount: feeAmountSchema,
9550
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
9507
9551
  basis: z.enum([
9508
9552
  'inputAmount',
9509
9553
  'estimatedAmount'
@@ -9595,7 +9639,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
9595
9639
  addresses: z.array(z.string({
9596
9640
  invalid_type_error: 'addresses entries must be strings'
9597
9641
  }).min(1, 'addresses entries must be non-empty strings')).min(1, 'addresses must contain at least one entry when provided').max(MAX_RATE_ADDRESSES_PER_REQUEST, `addresses supports at most ${String(MAX_RATE_ADDRESSES_PER_REQUEST)} values per request`).optional(),
9598
- apiKey: apiKeySchema
9642
+ apiKey: apiKeySchema.optional()
9599
9643
  });
9600
9644
  /**
9601
9645
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -12184,7 +12228,7 @@ new Set(Object.values(Blockchain));
12184
12228
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
12185
12229
 
12186
12230
  var name$1 = "@circle-fin/earn-kit";
12187
- var version$1 = "1.2.2";
12231
+ var version$1 = "1.3.0";
12188
12232
  var pkg$1 = {
12189
12233
  name: name$1,
12190
12234
  version: version$1};
@@ -12651,7 +12695,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12651
12695
  *
12652
12696
  * @param params - Adapter, chain, token/delegate/wallet addresses, the required
12653
12697
  * allowance for the signed payload, and a revert message for on-chain failure.
12654
- * @returns The approval transaction hash when an approval was submitted, or
12698
+ * @returns The approval transaction result when an approval was submitted, or
12655
12699
  * `undefined` when the existing allowance already covers `requiredAllowance`
12656
12700
  * (or `requiredAllowance` is zero).
12657
12701
  * @throws {@link KitError} If the `token.allowance` response is malformed.
@@ -12659,7 +12703,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12659
12703
  *
12660
12704
  * @example
12661
12705
  * ```typescript
12662
- * const txHash = await approveAllowanceIfNeeded({
12706
+ * const approval = await approveAllowanceIfNeeded({
12663
12707
  * adapter,
12664
12708
  * chain,
12665
12709
  * tokenAddress: usdcAddress,
@@ -12721,7 +12765,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12721
12765
  maxAttempts: params.allowancePropagation?.maxAttempts ?? DEFAULT_PROPAGATION_ATTEMPTS,
12722
12766
  delayMs: params.allowancePropagation?.delayMs ?? DEFAULT_PROPAGATION_DELAY_MS
12723
12767
  });
12724
- return approvalTxHash;
12768
+ return {
12769
+ txHash: approvalTxHash,
12770
+ ...approvalReceipt.gasUsed !== undefined && {
12771
+ gasUsed: approvalReceipt.gasUsed
12772
+ },
12773
+ ...approvalReceipt.effectiveGasPrice !== undefined && {
12774
+ effectiveGasPrice: approvalReceipt.effectiveGasPrice
12775
+ }
12776
+ };
12725
12777
  }
12726
12778
 
12727
12779
  /** @internal */ function isSameAddress(actual, expected) {
@@ -12869,7 +12921,13 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12869
12921
  }
12870
12922
  return {
12871
12923
  txHash,
12872
- explorerUrl
12924
+ explorerUrl,
12925
+ ...receipt.gasUsed !== undefined && {
12926
+ gasUsed: receipt.gasUsed
12927
+ },
12928
+ ...receipt.effectiveGasPrice !== undefined && {
12929
+ effectiveGasPrice: receipt.effectiveGasPrice
12930
+ }
12873
12931
  };
12874
12932
  }
12875
12933
 
@@ -13181,112 +13239,6 @@ const EARN_OPERATIONS = new Set([
13181
13239
  return hasEarnServiceParamsShape(operation, candidate['params']);
13182
13240
  }
13183
13241
 
13184
- function buildGasFeeBase(name, chain) {
13185
- return {
13186
- name,
13187
- token: chain.nativeCurrency.symbol,
13188
- blockchain: chain.chain
13189
- };
13190
- }
13191
- function buildGasFeeSuccess(name, chain, fees) {
13192
- return {
13193
- ...buildGasFeeBase(name, chain),
13194
- fees
13195
- };
13196
- }
13197
- function buildGasFeeFailure(name, chain, error) {
13198
- return {
13199
- ...buildGasFeeBase(name, chain),
13200
- fees: null,
13201
- error: getErrorMessage(error)
13202
- };
13203
- }
13204
- async function estimatePreparedGasFee(name, chain, prepared) {
13205
- try {
13206
- const estimate = bufferEstimatedGas(await prepared.estimate());
13207
- if (estimate.gas <= 0n) {
13208
- throw createValidationFailedError('estimate.gas', estimate.gas.toString(), 'gas estimate must be greater than zero');
13209
- }
13210
- return buildGasFeeSuccess(name, chain, estimate);
13211
- } catch (error) {
13212
- return buildGasFeeFailure(name, chain, error);
13213
- }
13214
- }
13215
- async function estimateApprovalGasFeeIfNeeded(params) {
13216
- const { adapter, chain, address, tokenAddress, delegate, requiredAllowance } = params;
13217
- if (requiredAllowance <= 0n) {
13218
- return undefined;
13219
- }
13220
- try {
13221
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
13222
- tokenAddress,
13223
- delegate
13224
- }, {
13225
- chain,
13226
- address
13227
- });
13228
- const allowanceRaw = await allowancePrepared.execute();
13229
- const currentAllowance = parseAllowanceResponse(allowanceRaw);
13230
- if (currentAllowance >= requiredAllowance) {
13231
- return undefined;
13232
- }
13233
- // Reuse the execute path's approval builder so the estimate simulates the
13234
- // exact approval (action, amount, and WARM_SLOT_RESIDUAL) that
13235
- // approveAllowanceIfNeeded later submits.
13236
- const approvalPrepared = await prepareApprovalAction({
13237
- adapter,
13238
- chain,
13239
- address,
13240
- tokenAddress,
13241
- delegate,
13242
- currentAllowance,
13243
- requiredAllowance
13244
- });
13245
- return await estimatePreparedGasFee('Approve', chain, approvalPrepared);
13246
- } catch (error) {
13247
- return buildGasFeeFailure('Approve', chain, error);
13248
- }
13249
- }
13250
- /**
13251
- * Estimate gas fee entries for an earn quote without submitting transactions.
13252
- *
13253
- * Each entry is produced by simulating the prepared transaction against
13254
- * current chain state. When an approval is required (allowance below the
13255
- * signed payload's required amount), the subsequent action simulation runs
13256
- * without that approval in place and is expected to revert — the action entry
13257
- * then carries `fees: null` with the revert message while the approval entry
13258
- * still estimates normally. Quote consumers must treat that as "estimate
13259
- * pending approval", not a hard failure.
13260
- *
13261
- * @internal
13262
- */ async function estimateEarnQuoteGasFees(params) {
13263
- const { adapter, chain, address, actionName, actionKey, actionParams, approval } = params;
13264
- const gasFees = [];
13265
- if (approval !== undefined) {
13266
- const approvalEstimate = await estimateApprovalGasFeeIfNeeded({
13267
- adapter,
13268
- chain,
13269
- address,
13270
- tokenAddress: approval.token,
13271
- delegate: approval.delegate,
13272
- requiredAllowance: approval.requiredAllowance
13273
- });
13274
- if (approvalEstimate !== undefined) {
13275
- gasFees.push(approvalEstimate);
13276
- }
13277
- }
13278
- try {
13279
- const actionPrepared = await adapter.prepareAction(actionKey, actionParams, {
13280
- chain,
13281
- address
13282
- });
13283
- gasFees.push(await estimatePreparedGasFee(actionName, chain, actionPrepared));
13284
- } catch (error) {
13285
- gasFees.push(buildGasFeeFailure(actionName, chain, error));
13286
- }
13287
- return gasFees;
13288
- }
13289
-
13290
13242
  // ---------------------------------------------------------------------------
13291
13243
  // Shared primitives
13292
13244
  // ---------------------------------------------------------------------------
@@ -13364,7 +13316,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
13364
13316
  asset: z.string(),
13365
13317
  assetAddress: z.string(),
13366
13318
  lltv: z.number(),
13367
- supplyUsd: z.number()
13319
+ supplyUsd: z.number(),
13320
+ // Optional during the expand/contract window (a backend that predates the
13321
+ // field omits the key), mirroring the `.optional()` facets on the base
13322
+ // schema; `null` when the product exposes no per-market allocation (V2).
13323
+ allocationPct: z.number().nullable().optional()
13368
13324
  });
13369
13325
  /**
13370
13326
  * Zod schema for a Morpho vault warning in the API response.
@@ -13378,7 +13334,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
13378
13334
  ])
13379
13335
  });
13380
13336
  /**
13381
- * Zod schema for a single vault info object in the API response.
13337
+ * Zod schema for the manager (curator) facet in the API response.
13338
+ *
13339
+ * @internal
13340
+ */ const managerSchema = z.object({
13341
+ name: z.string(),
13342
+ address: z.string().optional(),
13343
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
13344
+ // are added here as the providers that emit them land, rather than shipped
13345
+ // speculatively.
13346
+ type: z.enum([
13347
+ 'curator'
13348
+ ])
13349
+ });
13350
+ /**
13351
+ * Zod schema for the APY profile facet in the API response.
13352
+ *
13353
+ * @internal
13354
+ */ const apyProfileSchema = z.object({
13355
+ current: z.number(),
13356
+ native: z.number().nullable(),
13357
+ d7: z.number().nullable(),
13358
+ d30: z.number().nullable(),
13359
+ d90: z.number().nullable(),
13360
+ rewardShare: z.number().nullable(),
13361
+ source: z.string().optional(),
13362
+ asOf: z.string().optional()
13363
+ });
13364
+ /**
13365
+ * Zod schema for the fee split facet in the API response.
13366
+ *
13367
+ * @internal
13368
+ */ const feeInfoSchema = z.object({
13369
+ performance: z.number().nullable(),
13370
+ management: z.number().nullable()
13371
+ });
13372
+ /**
13373
+ * Zod schema for the liquidity profile facet in the API response.
13374
+ *
13375
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
13376
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
13377
+ *
13378
+ * @internal
13379
+ */ const liquidityProfileSchema = z.object({
13380
+ totalDeposits: amountJsonSchema,
13381
+ available: amountJsonSchema,
13382
+ totalSupply: amountJsonSchema,
13383
+ status: z.enum([
13384
+ 'active',
13385
+ 'low_liquidity'
13386
+ ])
13387
+ });
13388
+ /**
13389
+ * Zod schema for the risk signals facet in the API response.
13390
+ *
13391
+ * @internal
13392
+ */ const riskSignalsSchema = z.object({
13393
+ circleSentinel: z.boolean(),
13394
+ warnings: z.array(vaultWarningSchema).optional(),
13395
+ earnKitWarnings: z.array(z.string()).optional()
13396
+ });
13397
+ /**
13398
+ * Zod schema for the universal earn-opportunity base in the API response.
13399
+ *
13400
+ * Retains every existing deprecated flat field (kept validated through the
13401
+ * expand/contract window so default-strip does not drop them) and adds the
13402
+ * new nested facets. The nested facets are `.optional()` during the
13403
+ * transition so the SDK still validates against a not-yet-fully-deployed
13404
+ * backend; they become required after Expand ships.
13382
13405
  *
13383
13406
  * @internal
13384
13407
  */ const vaultInfoResponseSchema = z.object({
@@ -13403,6 +13426,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
13403
13426
  warnings: z.array(vaultWarningSchema).optional(),
13404
13427
  earnKitWarnings: z.array(z.string()).optional()
13405
13428
  });
13429
+ /**
13430
+ * Shared base schema: existing flat fields (kept) plus the new nested
13431
+ * facets and neutral identity. Facets are `.optional()` during the
13432
+ * transition; flip to required once the backend is confirmed emitting.
13433
+ *
13434
+ * @internal
13435
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
13436
+ address: z.string().optional(),
13437
+ asOf: z.string().optional(),
13438
+ manager: managerSchema.nullable().optional(),
13439
+ apyProfile: apyProfileSchema.optional(),
13440
+ fee: feeInfoSchema.optional(),
13441
+ liquidityProfile: liquidityProfileSchema.optional(),
13442
+ riskSignals: riskSignalsSchema.optional()
13443
+ });
13444
+ /**
13445
+ * Zod schema for the `vault` opportunity variant.
13446
+ *
13447
+ * @internal
13448
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
13449
+ productType: z.literal('vault'),
13450
+ collateral: z.array(collateralSchema)
13451
+ });
13452
+ /**
13453
+ * Discriminated union over `productType`. Add union members here as new
13454
+ * product types (e.g. `lending_market`, `rwa_token`) land.
13455
+ *
13456
+ * @internal
13457
+ */ const earnOpportunityVariants = [
13458
+ vaultOpportunitySchema
13459
+ ];
13460
+ /** @internal */ const earnOpportunitySchema = z.discriminatedUnion('productType', earnOpportunityVariants);
13461
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
13462
+ /**
13463
+ * Tolerant list parser for earn opportunities.
13464
+ *
13465
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
13466
+ * `z.array` fails the whole array if any element fails. Two migration-window
13467
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
13468
+ *
13469
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
13470
+ * only opportunity type then, so default a missing discriminant to `'vault'`
13471
+ * rather than dropping every vault the backend returns.
13472
+ * - A future backend adds a *second* `productType` this SDK version does not
13473
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
13474
+ * of rejecting the whole list.
13475
+ *
13476
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
13477
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
13478
+ * primitives, or an object whose `productType` is malformed — is passed through
13479
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
13480
+ * validation failure. It is deliberately not silently dropped (which would hide
13481
+ * malformed backend data) and never throws here (an unguarded property read on
13482
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
13483
+ * `ZodError`).
13484
+ *
13485
+ * @internal
13486
+ */ const earnOpportunityListSchema = z.preprocess((raw)=>{
13487
+ if (!Array.isArray(raw)) {
13488
+ return raw;
13489
+ }
13490
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
13491
+ // map/filter chain stays type-safe and no `any` leaks into the return.
13492
+ const entries = raw;
13493
+ return entries.map((entry)=>{
13494
+ // Only touch plain objects; non-objects fall through to fail validation.
13495
+ if (typeof entry !== 'object' || entry === null) {
13496
+ return entry;
13497
+ }
13498
+ const record = entry;
13499
+ // Older backend predating productType: default to the only type then.
13500
+ return record.productType === undefined ? {
13501
+ ...record,
13502
+ productType: 'vault'
13503
+ } : record;
13504
+ }).filter((entry)=>{
13505
+ // Drop ONLY a present-but-unknown string discriminant (a future
13506
+ // productType this SDK version doesn't know). Everything else —
13507
+ // non-objects, a non-string productType — flows through to
13508
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
13509
+ if (typeof entry !== 'object' || entry === null) {
13510
+ return true;
13511
+ }
13512
+ const productType = entry.productType;
13513
+ if (typeof productType !== 'string') {
13514
+ return true;
13515
+ }
13516
+ return knownProductTypes.has(productType);
13517
+ });
13518
+ }, z.array(earnOpportunitySchema));
13406
13519
  // ---------------------------------------------------------------------------
13407
13520
  // Position response schema
13408
13521
  // ---------------------------------------------------------------------------
@@ -13532,6 +13645,7 @@ const positionPnlSchema = z.discriminatedUnion('status', [
13532
13645
  *
13533
13646
  * @internal
13534
13647
  */ const depositPayloadSchema = z.object({
13648
+ execId: bridgeDepositExecIdSchema,
13535
13649
  executionParams: depositExecutionParamsSchema,
13536
13650
  signature: hexSignatureSchema
13537
13651
  });
@@ -13623,6 +13737,21 @@ const bridgeDepositPrepareReviewSchema = z.object({
13623
13737
  amount: amountJsonSchema,
13624
13738
  vaultAddress: hexAddressSchema
13625
13739
  }).passthrough();
13740
+ /** @internal */ const bridgeQuoteExpirySchema = z.discriminatedUnion('mode', [
13741
+ z.object({
13742
+ mode: z.literal('TIMESTAMP'),
13743
+ expiresAt: z.string().datetime({
13744
+ offset: true
13745
+ })
13746
+ }),
13747
+ z.object({
13748
+ mode: z.literal('BLOCK_NUMBER'),
13749
+ expiresAtBlock: z.number().int(),
13750
+ blockEstimatedAt: z.string().datetime({
13751
+ offset: true
13752
+ }).optional()
13753
+ })
13754
+ ]).optional().catch(undefined);
13626
13755
  /**
13627
13756
  * Zod schema for the bridge deposit prepare payload.
13628
13757
  *
@@ -13634,6 +13763,10 @@ const bridgeDepositPrepareReviewSchema = z.object({
13634
13763
  execId: bridgeDepositExecIdSchema,
13635
13764
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
13636
13765
  expiresAt: z.string().datetime(),
13766
+ quoteIssuedAt: z.string().datetime({
13767
+ offset: true
13768
+ }).optional().catch(undefined),
13769
+ quoteExpiry: bridgeQuoteExpirySchema,
13637
13770
  review: bridgeDepositPrepareReviewSchema
13638
13771
  });
13639
13772
  /**
@@ -13699,6 +13832,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
13699
13832
  *
13700
13833
  * @internal
13701
13834
  */ const withdrawPayloadSchema = z.object({
13835
+ execId: bridgeDepositExecIdSchema,
13702
13836
  executionParams: withdrawExecutionParamsSchema,
13703
13837
  signature: hexSignatureSchema
13704
13838
  });
@@ -13712,6 +13846,27 @@ const bridgeDepositPrepareReviewSchema = z.object({
13712
13846
  data: withdrawPayloadSchema
13713
13847
  });
13714
13848
  // ---------------------------------------------------------------------------
13849
+ // Transaction report response schema
13850
+ // ---------------------------------------------------------------------------
13851
+ /**
13852
+ * Zod schema for the transaction report payload inside the API `data` envelope.
13853
+ *
13854
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
13855
+ * schema accepts any object shape and does not require specific fields.
13856
+ *
13857
+ * @internal
13858
+ */ const transactionReportPayloadSchema = z.object({}).passthrough();
13859
+ /**
13860
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
13861
+ *
13862
+ * The Earn Service API wraps the transaction report payload in a `data`
13863
+ * envelope.
13864
+ *
13865
+ * @internal
13866
+ */ const transactionReportResponseSchema = z.object({
13867
+ data: transactionReportPayloadSchema
13868
+ });
13869
+ // ---------------------------------------------------------------------------
13715
13870
  // Claim rewards response schema
13716
13871
  // ---------------------------------------------------------------------------
13717
13872
  /**
@@ -13772,6 +13927,30 @@ const bridgeDepositPrepareReviewSchema = z.object({
13772
13927
  token: z.string(),
13773
13928
  amount: amountJsonSchema
13774
13929
  });
13930
+ /**
13931
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
13932
+ *
13933
+ * The Earn Service backend estimates gas server-side and returns one entry per
13934
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
13935
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
13936
+ * integer string in the chain's native base units. When the backend cannot
13937
+ * estimate an action it returns `fees: null` with an `error` message instead.
13938
+ *
13939
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
13940
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
13941
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
13942
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
13943
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
13944
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
13945
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
13946
+ * `fee`) must never fail Zod validation and reject the entire quote.
13947
+ *
13948
+ * @internal
13949
+ */ const quoteGasFeeSchema = z.object({
13950
+ name: z.string().optional(),
13951
+ fees: z.unknown(),
13952
+ error: z.string().optional()
13953
+ }).passthrough();
13775
13954
  /**
13776
13955
  * Zod schema for the inner deposit quote payload.
13777
13956
  *
@@ -13787,7 +13966,8 @@ const bridgeDepositPrepareReviewSchema = z.object({
13787
13966
  expectedShares: amountJsonSchema,
13788
13967
  sharePrice: z.string(),
13789
13968
  currentApy: z.number(),
13790
- fees: z.array(feeSchema).optional()
13969
+ fees: z.array(feeSchema).optional(),
13970
+ gasFees: z.array(quoteGasFeeSchema).optional()
13791
13971
  });
13792
13972
  /**
13793
13973
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -13814,6 +13994,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
13814
13994
  sharePrice: z.string(),
13815
13995
  maxWithdrawable: amountJsonSchema,
13816
13996
  fees: z.array(feeSchema),
13997
+ gasFees: z.array(quoteGasFeeSchema).optional(),
13817
13998
  warnings: z.array(z.string()).optional()
13818
13999
  });
13819
14000
  /**
@@ -13871,7 +14052,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
13871
14052
  *
13872
14053
  * @internal
13873
14054
  */ const getVaultsPayloadSchema = z.object({
13874
- vaults: z.array(vaultInfoResponseSchema),
14055
+ vaults: earnOpportunityListSchema,
13875
14056
  errors: z.array(vaultErrorSchema)
13876
14057
  });
13877
14058
  /**
@@ -13901,7 +14082,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
13901
14082
  *
13902
14083
  * @internal
13903
14084
  */ const exploreVaultsPayloadSchema = z.object({
13904
- vaults: z.array(vaultInfoResponseSchema),
14085
+ vaults: earnOpportunityListSchema,
13905
14086
  pagination: explorePaginationSchema
13906
14087
  });
13907
14088
  /**
@@ -13994,6 +14175,16 @@ const bridgeDepositPrepareReviewSchema = z.object({
13994
14175
  */ function isWithdrawResponse(value) {
13995
14176
  return withdrawResponseSchema.safeParse(value).success;
13996
14177
  }
14178
+ /**
14179
+ * Type guard for the transaction report API response.
14180
+ *
14181
+ * @param value - Unknown response value to validate
14182
+ * @returns True when the value matches the transaction report response shape
14183
+ *
14184
+ * @internal
14185
+ */ function isTransactionReportResponse(value) {
14186
+ return transactionReportResponseSchema.safeParse(value).success;
14187
+ }
13997
14188
  /**
13998
14189
  * Type guard for the claim rewards API response.
13999
14190
  *
@@ -14036,7 +14227,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
14036
14227
  }
14037
14228
 
14038
14229
  var name = "@circle-fin/provider-earn-service";
14039
- var version = "1.2.2";
14230
+ var version = "1.3.0";
14040
14231
  var pkg = {
14041
14232
  name: name,
14042
14233
  version: version};
@@ -14156,7 +14347,7 @@ var pkg = {
14156
14347
  }
14157
14348
 
14158
14349
  /**
14159
- * Convert an API vault info object into the SDK {@link VaultInfo} shape.
14350
+ * Convert an API vault info object into the SDK {@link EarnOpportunity} shape.
14160
14351
  *
14161
14352
  * Map the API chain code back to the SDK chain identifier and hydrate the
14162
14353
  * amount payloads into {@link Amount} instances.
@@ -14167,16 +14358,29 @@ var pkg = {
14167
14358
  *
14168
14359
  * @internal
14169
14360
  */ function toVaultInfo(data) {
14170
- const { totalDeposits, liquidity, ...vault } = data;
14361
+ const { totalDeposits, liquidity, liquidityProfile, ...vault } = data;
14171
14362
  const chain = toSdkChain(vault.chain);
14172
14363
  if (chain === undefined) {
14173
14364
  throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
14174
14365
  }
14366
+ // The nested facets are `.optional()` in the schema (a backend that predates
14367
+ // them omits them) and are typed optional on `EarnOpportunity` to match.
14368
+ // Convert the nested liquidity amounts when present and pass the remaining
14369
+ // facets straight through; each absent facet stays absent rather than being
14370
+ // asserted present by a cast.
14175
14371
  return {
14176
14372
  ...vault,
14177
14373
  chain,
14178
14374
  totalDeposits: Amount.fromJSON(totalDeposits),
14179
- liquidity: Amount.fromJSON(liquidity)
14375
+ liquidity: Amount.fromJSON(liquidity),
14376
+ ...liquidityProfile !== undefined && {
14377
+ liquidityProfile: {
14378
+ ...liquidityProfile,
14379
+ totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
14380
+ available: Amount.fromJSON(liquidityProfile.available),
14381
+ totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
14382
+ }
14383
+ }
14180
14384
  };
14181
14385
  }
14182
14386
 
@@ -14211,8 +14415,11 @@ function toVaultError(error) {
14211
14415
  }
14212
14416
  try {
14213
14417
  const response = await pollApiGet(url.toString(), isGetVaultsResponse, pollingConfig);
14418
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
14419
+ // does not run the schema's preprocess. Parse explicitly so unknown
14420
+ // `productType` values are dropped before `toVaultInfo`.
14214
14421
  return {
14215
- vaults: response.data.vaults.map(toVaultInfo),
14422
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
14216
14423
  errors: response.data.errors.map(toVaultError)
14217
14424
  };
14218
14425
  } catch (error) {
@@ -14261,8 +14468,11 @@ function toVaultError(error) {
14261
14468
  }
14262
14469
  try {
14263
14470
  const response = await pollApiGet(url.toString(), isExploreVaultsResponse, pollingConfig);
14471
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
14472
+ // does not run the schema's preprocess. Parse explicitly so unknown
14473
+ // `productType` values are dropped before `toVaultInfo`.
14264
14474
  return {
14265
- vaults: response.data.vaults.map(toVaultInfo),
14475
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
14266
14476
  pagination: response.data.pagination
14267
14477
  };
14268
14478
  } catch (error) {
@@ -14426,6 +14636,12 @@ function toPositionInfo(data) {
14426
14636
  execId: response.data.execId,
14427
14637
  preparedBundle,
14428
14638
  expiresAt: response.data.expiresAt,
14639
+ ...response.data.quoteIssuedAt !== undefined && {
14640
+ quoteIssuedAt: response.data.quoteIssuedAt
14641
+ },
14642
+ ...response.data.quoteExpiry !== undefined && {
14643
+ quoteExpiry: response.data.quoteExpiry
14644
+ },
14429
14645
  review: response.data.review
14430
14646
  };
14431
14647
  } catch (error) {
@@ -14767,7 +14983,110 @@ function toClaimedAmount(reward) {
14767
14983
  }
14768
14984
  }
14769
14985
 
14770
- function toDepositQuoteInfo(data) {
14986
+ /**
14987
+ * Map the Earn Service's server-side quote gas estimates into the SDK
14988
+ * {@link EarnGasFeeEstimate} shape.
14989
+ *
14990
+ * The Earn Service estimates gas for each action (`Approve`, `Deposit`,
14991
+ * `Withdraw`) and returns `{ name, fees: { gas, gasPrice, fee } }` with raw
14992
+ * integer strings.
14993
+ * The SDK type additionally carries `token` (the chain's native currency
14994
+ * symbol) and `blockchain`, which are filled in here from the chain
14995
+ * definition.
14996
+ *
14997
+ * Gas reporting is best-effort: a malformed entry (e.g. a non-integer string
14998
+ * that fails `BigInt` parsing) degrades to a `{ fees: null, error }` estimate
14999
+ * rather than throwing, so one bad entry never fails the whole quote.
15000
+ *
15001
+ * @param gasFees - Backend gas-fee entries from the quote response, if any.
15002
+ * @param chain - Chain definition, used for the native token symbol and
15003
+ * blockchain identifier.
15004
+ * @returns One {@link EarnGasFeeEstimate} per backend entry (empty when the
15005
+ * backend returned none).
15006
+ *
15007
+ * @example
15008
+ * ```typescript
15009
+ * toQuoteGasFees(
15010
+ * [{ name: 'Deposit', fees: { gas: '364142', gasPrice: '21000000000', fee: '7646982000000000' } }],
15011
+ * arcTestnet,
15012
+ * )
15013
+ * // [{ name: 'Deposit', token: 'USDC', blockchain: 'Arc_Testnet',
15014
+ * // fees: { gas: 364142n, gasPrice: 21000000000n, fee: '7646982000000000' } }]
15015
+ * ```
15016
+ *
15017
+ * @internal
15018
+ */ function toQuoteGasFees(gasFees, chain) {
15019
+ if (gasFees === undefined) {
15020
+ return [];
15021
+ }
15022
+ return gasFees.map((entry)=>{
15023
+ const base = {
15024
+ // `name` is optional on the wire; label an unnamed entry rather than
15025
+ // emitting `name: undefined`.
15026
+ name: entry.name ?? 'Unknown',
15027
+ token: chain.nativeCurrency.symbol,
15028
+ blockchain: chain.chain
15029
+ };
15030
+ // The Earn Service itself reports a failed estimate as `fees: null` with
15031
+ // an error; propagate that soft failure verbatim.
15032
+ if (entry.fees === null || entry.fees === undefined) {
15033
+ return {
15034
+ ...base,
15035
+ fees: null,
15036
+ error: entry.error ?? 'gas estimate unavailable'
15037
+ };
15038
+ }
15039
+ // `fees` is `unknown` at the schema layer, so ALL validation happens here:
15040
+ // that it is an object at all, and that `gas`, `gasPrice`, and `fee` are
15041
+ // each parseable integer strings (including `fee`, which the SDK contract
15042
+ // requires be a numeric base-unit string). Any failure — a wrong type
15043
+ // (`fees: 123`), a missing field, or a non-numeric value — degrades the
15044
+ // whole entry to a `fees: null` soft failure rather than surfacing a
15045
+ // malformed "successful" estimate or rejecting the quote.
15046
+ try {
15047
+ if (typeof entry.fees !== 'object') {
15048
+ throw new TypeError(`gas fees must be an object (got ${typeof entry.fees})`);
15049
+ }
15050
+ const { gas, gasPrice, fee } = entry.fees;
15051
+ return {
15052
+ ...base,
15053
+ fees: {
15054
+ gas: toBigInt('gas', gas),
15055
+ gasPrice: toBigInt('gasPrice', gasPrice),
15056
+ fee: toBigInt('fee', fee).toString()
15057
+ }
15058
+ };
15059
+ } catch (error) {
15060
+ return {
15061
+ ...base,
15062
+ fees: null,
15063
+ error: getErrorMessage(error)
15064
+ };
15065
+ }
15066
+ });
15067
+ }
15068
+ /**
15069
+ * Parse an unknown value into a `bigint`, rejecting anything that is not a
15070
+ * non-empty integer string. `BigInt` alone is too permissive for this path —
15071
+ * it accepts numbers, booleans, and empty strings — so guard the type first.
15072
+ *
15073
+ * @param field - Field name, used in the thrown error message.
15074
+ * @param value - Raw value from the backend gas entry.
15075
+ * @returns The parsed `bigint`.
15076
+ * @throws {TypeError} When `value` is not a non-empty integer string.
15077
+ */ function toBigInt(field, value) {
15078
+ if (typeof value !== 'string' || value.trim() === '') {
15079
+ throw new TypeError(`gas fee field "${field}" must be an integer string`);
15080
+ }
15081
+ try {
15082
+ // BigInt throws on non-integer strings (e.g. "1.5", "not-a-number").
15083
+ return BigInt(value);
15084
+ } catch {
15085
+ throw new Error(`gas fee field "${field}" is not a valid integer string: ${value}`);
15086
+ }
15087
+ }
15088
+
15089
+ function toDepositQuoteInfo(data, chain) {
14771
15090
  const fees = (data.fees ?? []).map(({ token: feeTokenSymbol, ...fee })=>{
14772
15091
  // Earn Service returns fee.token as a display symbol, for example "USDC".
14773
15092
  return {
@@ -14796,7 +15115,10 @@ function toDepositQuoteInfo(data) {
14796
15115
  sharePrice: data.sharePrice,
14797
15116
  currentApy: data.currentApy,
14798
15117
  fees,
14799
- gasFees: []
15118
+ // The Earn Service estimates gas server-side; the chain fills token/blockchain.
15119
+ // Cross-chain quotes resolve no local chain definition, so gasFees stays
15120
+ // empty there (unchanged behavior).
15121
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain)
14800
15122
  };
14801
15123
  }
14802
15124
  /**
@@ -14831,7 +15153,7 @@ function toDepositQuoteInfo(data) {
14831
15153
  };
14832
15154
  try {
14833
15155
  const response = await pollApiPost(url.toString(), requestBody, isDepositQuoteResponse, pollingConfig);
14834
- return toDepositQuoteInfo(response.data);
15156
+ return toDepositQuoteInfo(response.data, params.chainDefinition);
14835
15157
  } catch (error) {
14836
15158
  throw parseEarnApiError(error, {
14837
15159
  operation: 'getDepositQuote'
@@ -14839,7 +15161,7 @@ function toDepositQuoteInfo(data) {
14839
15161
  }
14840
15162
  }
14841
15163
 
14842
- function toWithdrawalQuoteInfo(data) {
15164
+ function toWithdrawalQuoteInfo(data, chain) {
14843
15165
  return {
14844
15166
  vaultAddress: data.vaultAddress,
14845
15167
  vaultName: data.vaultName,
@@ -14867,7 +15189,7 @@ function toWithdrawalQuoteInfo(data) {
14867
15189
  status
14868
15190
  }
14869
15191
  })),
14870
- gasFees: [],
15192
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain),
14871
15193
  // Wire format uses `warnings`, but the SDK surface uses
14872
15194
  // `earnKitWarnings` to match the precedent set by `VaultInfo` —
14873
15195
  // `warnings` is reserved for the structured `VaultWarning` shape.
@@ -14899,7 +15221,7 @@ function toWithdrawalQuoteInfo(data) {
14899
15221
  };
14900
15222
  try {
14901
15223
  const response = await pollApiPost(url.toString(), requestBody, isWithdrawalQuoteResponse, pollingConfig);
14902
- return toWithdrawalQuoteInfo(response.data);
15224
+ return toWithdrawalQuoteInfo(response.data, params.chainDefinition);
14903
15225
  } catch (error) {
14904
15226
  throw parseEarnApiError(error, {
14905
15227
  operation: 'getWithdrawalQuote'
@@ -14935,6 +15257,8 @@ function toWithdrawalQuoteInfo(data) {
14935
15257
  amount: Amount.fromJSON(r.amount),
14936
15258
  address: r.token
14937
15259
  })),
15260
+ // The claimRewards/quote response does not carry a gas estimate (unlike
15261
+ // deposit/withdrawal quotes), so there is nothing to surface here.
14938
15262
  gasFees: []
14939
15263
  };
14940
15264
  } catch (error) {
@@ -14944,6 +15268,83 @@ function toWithdrawalQuoteInfo(data) {
14944
15268
  }
14945
15269
  }
14946
15270
 
15271
+ /**
15272
+ * Build the native gas triple the backend expects for `gasUsed`.
15273
+ *
15274
+ * Returns `undefined` unless both receipt components are present, so the
15275
+ * caller can omit the field entirely — the Earn Service treats a missing
15276
+ * triple as "skip the gas cache write, still return 200".
15277
+ *
15278
+ * @param gasUsed - Receipt gas units used.
15279
+ * @param effectiveGasPrice - Receipt effective gas price.
15280
+ * @returns The `{ gas, gasPrice, fee }` triple, or `undefined` when either
15281
+ * component is missing.
15282
+ *
15283
+ * @example
15284
+ * ```typescript
15285
+ * buildReportedGasUsed(362454n, 29466364605n)
15286
+ * // { gas: '362454', gasPrice: '29466364605', fee: '10680201716540670' }
15287
+ * ```
15288
+ *
15289
+ * @internal
15290
+ */ function buildReportedGasUsed(gasUsed, effectiveGasPrice) {
15291
+ if (gasUsed === undefined || effectiveGasPrice === undefined) {
15292
+ return undefined;
15293
+ }
15294
+ return {
15295
+ gas: gasUsed.toString(),
15296
+ gasPrice: effectiveGasPrice.toString(),
15297
+ fee: (gasUsed * effectiveGasPrice).toString()
15298
+ };
15299
+ }
15300
+ /**
15301
+ * Report the outcome of an SDK-submitted same-chain Earn transaction.
15302
+ *
15303
+ * @param params - Transaction report parameters.
15304
+ * @throws {@link KitError} When the API call fails.
15305
+ *
15306
+ * @internal
15307
+ */ async function reportEarnTransaction(params) {
15308
+ const { pollingConfig, baseUrl } = buildConfig(params.config);
15309
+ const url = new URL(`${EARN_KIT_API_PREFIX}/transactions/report`, baseUrl);
15310
+ // The report endpoint is not idempotent: success reports refresh the gas
15311
+ // cache and failure reports increment counts. If the first request succeeds
15312
+ // server-side but the client times out or sees a transient 5xx, retrying
15313
+ // would duplicate the report (double-writing an outcome or inflating failure
15314
+ // counts). Reporting is best-effort (see the fire-and-forget caller), so
15315
+ // make exactly one attempt and never retry — a single dropped report is
15316
+ // preferable to a duplicated one. `maxRetries` here is the total attempt
15317
+ // count in pollApiWithValidation (loop runs `attempt <= maxRetries`), so 1
15318
+ // means one request with no retry; 0 would skip the request entirely.
15319
+ const reportConfig = {
15320
+ ...pollingConfig,
15321
+ maxRetries: 1
15322
+ };
15323
+ const gasUsed = buildReportedGasUsed(params.gasUsed, params.effectiveGasPrice);
15324
+ const requestBody = {
15325
+ execId: params.execId,
15326
+ chain: params.chain,
15327
+ status: params.status,
15328
+ action: params.action,
15329
+ ...params.txHash !== undefined && {
15330
+ txHash: params.txHash
15331
+ },
15332
+ ...gasUsed !== undefined && {
15333
+ gasUsed
15334
+ },
15335
+ ...params.errorCode !== undefined && {
15336
+ errorCode: params.errorCode
15337
+ }
15338
+ };
15339
+ try {
15340
+ await pollApiPost(url.toString(), requestBody, isTransactionReportResponse, reportConfig);
15341
+ } catch (error) {
15342
+ throw parseEarnApiError(error, {
15343
+ operation: 'transactionReport'
15344
+ });
15345
+ }
15346
+ }
15347
+
14947
15348
  /**
14948
15349
  * Sum the amounts across every token input to size the allowance approval.
14949
15350
  *
@@ -15054,6 +15455,59 @@ function toWithdrawalQuoteInfo(data) {
15054
15455
  // Intentionally built-ins-only: Earn bridge support is limited to SDK-known
15055
15456
  // token contracts plus the explicit ERC-3009 domain allowlist below.
15056
15457
  const TOKEN_REGISTRY = createTokenRegistry();
15458
+ function submitTransactionReport(reportContext, action, status, details) {
15459
+ void reportEarnTransaction({
15460
+ execId: reportContext.execId,
15461
+ chain: reportContext.chain,
15462
+ config: reportContext.config,
15463
+ action,
15464
+ status,
15465
+ ...details
15466
+ }).catch(()=>undefined);
15467
+ }
15468
+ function reportTransactionSuccess(reportContext, action, result) {
15469
+ if (result === undefined) {
15470
+ return;
15471
+ }
15472
+ submitTransactionReport(reportContext, action, 'success', {
15473
+ txHash: result.txHash,
15474
+ gasUsed: result.gasUsed,
15475
+ effectiveGasPrice: result.effectiveGasPrice
15476
+ });
15477
+ }
15478
+ function reportTransactionFailure(reportContext, action, error) {
15479
+ submitTransactionReport(reportContext, action, 'failure', {
15480
+ txHash: transactionReportTxHash(error),
15481
+ errorCode: transactionReportErrorCode(error)
15482
+ });
15483
+ }
15484
+ function transactionReportErrorCode(error) {
15485
+ if (isKitError(error)) {
15486
+ return error.name;
15487
+ }
15488
+ const message = getErrorMessage(error);
15489
+ if (/user (rejected|denied)|rejected by user/i.test(message)) {
15490
+ return 'USER_REJECTED';
15491
+ }
15492
+ if (/insufficient funds/i.test(message)) {
15493
+ return 'INSUFFICIENT_FUNDS';
15494
+ }
15495
+ if (/timeout|timed out/i.test(message)) {
15496
+ return 'TIMEOUT';
15497
+ }
15498
+ return 'UNKNOWN_ERROR';
15499
+ }
15500
+ function transactionReportTxHash(error) {
15501
+ if (!isKitError(error)) {
15502
+ return undefined;
15503
+ }
15504
+ const trace = error.cause?.trace;
15505
+ if (typeof trace !== 'object' || trace === null) {
15506
+ return undefined;
15507
+ }
15508
+ const txHash = trace['txHash'];
15509
+ return typeof txHash === 'string' && txHash !== '' ? txHash : undefined;
15510
+ }
15057
15511
  /**
15058
15512
  * Build the typed error raised when a cross-chain wait is cancelled via its
15059
15513
  * `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
@@ -15375,7 +15829,7 @@ function finishElapsedWait(lastStatus, lastError) {
15375
15829
  const adapterContractAddress = requireAdapterContract(chain);
15376
15830
  const { adapter } = params.from;
15377
15831
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15378
- const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
15832
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
15379
15833
  vaultAddress,
15380
15834
  amount: params.amount,
15381
15835
  address,
@@ -15383,32 +15837,55 @@ function finishElapsedWait(lastStatus, lastError) {
15383
15837
  config
15384
15838
  }), ()=>undefined);
15385
15839
  validateExecutionDeadline(executionParams);
15840
+ const transactionReportContext = {
15841
+ execId,
15842
+ chain: apiChain,
15843
+ config
15844
+ };
15386
15845
  const approvalToken = resolveEarnApprovalToken(executionParams);
15387
15846
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15388
15847
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15389
15848
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
15390
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
15849
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
15850
+ try {
15851
+ const approval = await approveAllowanceIfNeeded({
15852
+ adapter,
15853
+ chain,
15854
+ tokenAddress: approvalToken,
15855
+ delegate: adapterContractAddress,
15856
+ address,
15857
+ requiredAllowance,
15858
+ revertMessage: 'Earn deposit token approval reverted on-chain'
15859
+ });
15860
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
15861
+ return approval;
15862
+ } catch (error) {
15863
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
15864
+ throw error;
15865
+ }
15866
+ }, (approval)=>approval?.txHash);
15867
+ }
15868
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
15869
+ try {
15870
+ const result = await executeEarnAction({
15391
15871
  adapter,
15392
15872
  chain,
15393
- tokenAddress: approvalToken,
15394
- delegate: adapterContractAddress,
15395
15873
  address,
15396
- requiredAllowance,
15397
- revertMessage: 'Earn deposit token approval reverted on-chain'
15398
- }), (txHash)=>txHash);
15399
- }
15400
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>executeEarnAction({
15401
- adapter,
15402
- chain,
15403
- address,
15404
- actionKey: 'earn.deposit',
15405
- actionParams: {
15406
- executeParams: executionParams,
15407
- tokenInputs,
15408
- signature
15409
- },
15410
- revertMessage: 'Earn deposit reverted on-chain'
15411
- }), ({ txHash })=>txHash);
15874
+ actionKey: 'earn.deposit',
15875
+ actionParams: {
15876
+ executeParams: executionParams,
15877
+ tokenInputs,
15878
+ signature
15879
+ },
15880
+ revertMessage: 'Earn deposit reverted on-chain'
15881
+ });
15882
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
15883
+ return result;
15884
+ } catch (error) {
15885
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
15886
+ throw error;
15887
+ }
15888
+ }, ({ txHash })=>txHash);
15412
15889
  return {
15413
15890
  kind: 'same-chain',
15414
15891
  txHash,
@@ -15488,7 +15965,13 @@ function finishElapsedWait(lastStatus, lastError) {
15488
15965
  amount: params.amount,
15489
15966
  sourceChain: sourceChain.chain,
15490
15967
  destinationChain: destinationChain.chain,
15491
- expiresAt: prepared.expiresAt
15968
+ expiresAt: prepared.expiresAt,
15969
+ ...prepared.quoteIssuedAt !== undefined && {
15970
+ quoteIssuedAt: prepared.quoteIssuedAt
15971
+ },
15972
+ ...prepared.quoteExpiry !== undefined && {
15973
+ quoteExpiry: prepared.quoteExpiry
15974
+ }
15492
15975
  };
15493
15976
  }
15494
15977
  /** {@inheritdoc} */ async withdraw(params) {
@@ -15509,7 +15992,7 @@ function finishElapsedWait(lastStatus, lastError) {
15509
15992
  const adapterContractAddress = requireAdapterContract(chain);
15510
15993
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15511
15994
  const { adapter } = params.from;
15512
- const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
15995
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
15513
15996
  vaultAddress,
15514
15997
  amount: params.amount,
15515
15998
  address,
@@ -15517,32 +16000,55 @@ function finishElapsedWait(lastStatus, lastError) {
15517
16000
  config
15518
16001
  }), ()=>undefined);
15519
16002
  validateExecutionDeadline(executionParams);
16003
+ const transactionReportContext = {
16004
+ execId,
16005
+ chain: apiChain,
16006
+ config
16007
+ };
15520
16008
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
15521
16009
  const approvalToken = tokenInputs[0]?.token;
15522
16010
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15523
16011
  if (!options.skipApprove && approvalToken !== undefined) {
15524
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
16012
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16013
+ try {
16014
+ const approval = await approveAllowanceIfNeeded({
16015
+ adapter,
16016
+ chain,
16017
+ tokenAddress: approvalToken,
16018
+ delegate: adapterContractAddress,
16019
+ address,
16020
+ requiredAllowance,
16021
+ revertMessage: 'Vault share token approval reverted on-chain'
16022
+ });
16023
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
16024
+ return approval;
16025
+ } catch (error) {
16026
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
16027
+ throw error;
16028
+ }
16029
+ }, (approval)=>approval?.txHash);
16030
+ }
16031
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
16032
+ try {
16033
+ const result = await executeEarnAction({
15525
16034
  adapter,
15526
16035
  chain,
15527
- tokenAddress: approvalToken,
15528
- delegate: adapterContractAddress,
15529
16036
  address,
15530
- requiredAllowance,
15531
- revertMessage: 'Vault share token approval reverted on-chain'
15532
- }), (txHash)=>txHash);
15533
- }
15534
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>executeEarnAction({
15535
- adapter,
15536
- chain,
15537
- address,
15538
- actionKey: 'earn.withdraw',
15539
- actionParams: {
15540
- executeParams: executionParams,
15541
- tokenInputs,
15542
- signature
15543
- },
15544
- revertMessage: 'Earn withdraw reverted on-chain'
15545
- }), ({ txHash })=>txHash);
16037
+ actionKey: 'earn.withdraw',
16038
+ actionParams: {
16039
+ executeParams: executionParams,
16040
+ tokenInputs,
16041
+ signature
16042
+ },
16043
+ revertMessage: 'Earn withdraw reverted on-chain'
16044
+ });
16045
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
16046
+ return result;
16047
+ } catch (error) {
16048
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
16049
+ throw error;
16050
+ }
16051
+ }, ({ txHash })=>txHash);
15546
16052
  return {
15547
16053
  txHash,
15548
16054
  explorerUrl,
@@ -15676,141 +16182,6 @@ function finishElapsedWait(lastStatus, lastError) {
15676
16182
  }
15677
16183
  }
15678
16184
  }
15679
- gasEstimateFailure(name, chain, error) {
15680
- return {
15681
- name,
15682
- token: chain.nativeCurrency.symbol,
15683
- blockchain: chain.chain,
15684
- fees: null,
15685
- error: getErrorMessage(error)
15686
- };
15687
- }
15688
- async estimateDepositQuoteGasFees(params) {
15689
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
15690
- try {
15691
- const adapterContractAddress = requireAdapterContract(chain);
15692
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15693
- const { executionParams, signature } = await fetchDeposit({
15694
- vaultAddress: normalizedVaultAddress,
15695
- amount,
15696
- address,
15697
- chain: apiChain,
15698
- config
15699
- });
15700
- validateExecutionDeadline(executionParams);
15701
- const approvalToken = resolveEarnApprovalToken(executionParams);
15702
- const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15703
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15704
- return await estimateEarnQuoteGasFees({
15705
- adapter,
15706
- chain,
15707
- address,
15708
- actionName: 'Deposit',
15709
- actionKey: 'earn.deposit',
15710
- actionParams: {
15711
- executeParams: executionParams,
15712
- tokenInputs,
15713
- signature
15714
- },
15715
- approval: approvalToken !== undefined && requiredAllowance > 0n ? {
15716
- token: approvalToken,
15717
- delegate: adapterContractAddress,
15718
- requiredAllowance
15719
- } : undefined
15720
- });
15721
- } catch (error) {
15722
- return [
15723
- this.gasEstimateFailure('Deposit', chain, error)
15724
- ];
15725
- }
15726
- }
15727
- async estimateWithdrawalQuoteGasFees(params) {
15728
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
15729
- try {
15730
- const adapterContractAddress = requireAdapterContract(chain);
15731
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15732
- const { executionParams, signature } = await fetchWithdraw({
15733
- vaultAddress: normalizedVaultAddress,
15734
- amount,
15735
- address,
15736
- chain: apiChain,
15737
- config
15738
- });
15739
- validateExecutionDeadline(executionParams);
15740
- const tokenInputs = buildEarnTokenInputs(executionParams, normalizedVaultAddress);
15741
- const approvalToken = tokenInputs[0]?.token;
15742
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15743
- return await estimateEarnQuoteGasFees({
15744
- adapter,
15745
- chain,
15746
- address,
15747
- actionName: 'Withdraw',
15748
- actionKey: 'earn.withdraw',
15749
- actionParams: {
15750
- executeParams: executionParams,
15751
- tokenInputs,
15752
- signature
15753
- },
15754
- approval: approvalToken !== undefined ? {
15755
- token: approvalToken,
15756
- delegate: adapterContractAddress,
15757
- requiredAllowance
15758
- } : undefined
15759
- });
15760
- } catch (error) {
15761
- return [
15762
- this.gasEstimateFailure('Withdraw', chain, error)
15763
- ];
15764
- }
15765
- }
15766
- async estimateClaimRewardsQuoteGasFees(params) {
15767
- const { adapter, chain, apiChain, address, vaultAddress, config } = params;
15768
- try {
15769
- requireAdapterContract(chain);
15770
- const { rewards, executionParams, signature } = await fetchClaimRewards({
15771
- address,
15772
- chain: apiChain,
15773
- vaultAddress,
15774
- config
15775
- });
15776
- if (rewards.length === 0) {
15777
- return [];
15778
- }
15779
- const missingExecutionParams = executionParams === undefined;
15780
- const missingSignature = signature === undefined;
15781
- if (missingExecutionParams || missingSignature) {
15782
- throw new KitError({
15783
- ...EarnError.INTERNAL_ERROR,
15784
- recoverability: 'RETRYABLE',
15785
- message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
15786
- cause: {
15787
- trace: {
15788
- rewardsCount: rewards.length,
15789
- missingExecutionParams,
15790
- missingSignature
15791
- }
15792
- }
15793
- });
15794
- }
15795
- validateExecutionDeadline(executionParams);
15796
- return await estimateEarnQuoteGasFees({
15797
- adapter,
15798
- chain,
15799
- address,
15800
- actionName: 'Claim Rewards',
15801
- actionKey: 'earn.claimRewards',
15802
- actionParams: {
15803
- executeParams: executionParams,
15804
- tokenInputs: [],
15805
- signature
15806
- }
15807
- });
15808
- } catch (error) {
15809
- return [
15810
- this.gasEstimateFailure('Claim Rewards', chain, error)
15811
- ];
15812
- }
15813
- }
15814
16185
  /** {@inheritdoc} */ async getDepositQuote(params) {
15815
16186
  const config = this.resolveConfig(params.config);
15816
16187
  if (hasQuoteDestinationChain(params)) {
@@ -15833,96 +16204,43 @@ function finishElapsedWait(lastStatus, lastError) {
15833
16204
  throw createValidationFailedError('chain', destinationChain.chain, 'chain is only supported for cross-chain Earn deposit quotes; omit chain/address when quoting on the source chain');
15834
16205
  }
15835
16206
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
15836
- // The quote fetch and the gas estimation share no data, so run them
15837
- // concurrently. The estimator never rejects (failures fold into
15838
- // `{ fees: null }` entries), so only a quote failure can throw here.
15839
- const [quote, gasFees] = await Promise.all([
15840
- fetchDepositQuote({
15841
- vaultAddress: params.vaultAddress,
15842
- amount: params.amount,
15843
- address,
15844
- chain,
15845
- config
15846
- }),
15847
- this.estimateDepositQuoteGasFees({
15848
- adapter: params.from.adapter,
15849
- chain: chainDefinition,
15850
- apiChain: chain,
15851
- address,
15852
- vaultAddress: params.vaultAddress,
15853
- amount: params.amount,
15854
- config
15855
- })
15856
- ]);
15857
- return {
15858
- ...quote,
15859
- gasFees
15860
- };
16207
+ // Gas is estimated server-side by the Earn Service and returned on the quote, so the
16208
+ // SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
16209
+ // the native token symbol / blockchain on each gas entry.
16210
+ return fetchDepositQuote({
16211
+ vaultAddress: params.vaultAddress,
16212
+ amount: params.amount,
16213
+ address,
16214
+ chain,
16215
+ config,
16216
+ chainDefinition
16217
+ });
15861
16218
  }
15862
16219
  /** {@inheritdoc} */ async getWithdrawalQuote(params) {
15863
16220
  const config = this.resolveConfig(params.config);
15864
16221
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
15865
- // The quote fetch and the gas estimation share no data, so run them
15866
- // concurrently. The estimator never rejects (failures fold into
15867
- // `{ fees: null }` entries), so only a quote failure can throw here.
15868
- const [quote, gasFees] = await Promise.all([
15869
- fetchWithdrawalQuote({
15870
- vaultAddress: params.vaultAddress,
15871
- amount: params.amount,
15872
- address,
15873
- chain,
15874
- config
15875
- }),
15876
- this.estimateWithdrawalQuoteGasFees({
15877
- adapter: params.from.adapter,
15878
- chain: chainDefinition,
15879
- apiChain: chain,
15880
- address,
15881
- vaultAddress: params.vaultAddress,
15882
- amount: params.amount,
15883
- config
15884
- })
15885
- ]);
15886
- return {
15887
- ...quote,
15888
- gasFees
15889
- };
16222
+ // Gas is estimated server-side by the Earn Service and returned on the quote.
16223
+ return fetchWithdrawalQuote({
16224
+ vaultAddress: params.vaultAddress,
16225
+ amount: params.amount,
16226
+ address,
16227
+ chain,
16228
+ config,
16229
+ chainDefinition
16230
+ });
15890
16231
  }
15891
16232
  /** {@inheritdoc} */ async getClaimRewardsQuote(params) {
15892
16233
  const config = this.resolveConfig(params.config);
15893
- const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
15894
- const quote = await fetchClaimRewardsQuote({
16234
+ const { address, chain } = await resolveAdapterContext(params.from);
16235
+ // The claimRewards/quote response carries no gas estimate (unlike
16236
+ // deposit/withdrawal quotes), and the SDK no longer estimates gas locally,
16237
+ // so gasFees is always empty for claim rewards.
16238
+ return fetchClaimRewardsQuote({
15895
16239
  vaultAddress: params.vaultAddress,
15896
16240
  address,
15897
16241
  chain,
15898
16242
  config
15899
16243
  });
15900
- // No claimable rewards means there is nothing to execute, so there is no
15901
- // gas to estimate. Short-circuit on the already-fetched quote rather than
15902
- // calling the (heavier) claim execution endpoint again — this also keeps
15903
- // `gasFees` empty as documented, instead of risking a `{ fees: null }`
15904
- // estimation-error entry when the adapter/RPC is unavailable. This
15905
- // short-circuit is why the claim path stays sequential instead of using
15906
- // the Promise.all pattern of the deposit/withdrawal quotes: estimating in
15907
- // parallel would hit the signing endpoint even when nothing is claimable.
15908
- if (quote.rewards.length === 0) {
15909
- return {
15910
- ...quote,
15911
- gasFees: []
15912
- };
15913
- }
15914
- const gasFees = await this.estimateClaimRewardsQuoteGasFees({
15915
- adapter: params.from.adapter,
15916
- chain: chainDefinition,
15917
- apiChain: chain,
15918
- address,
15919
- vaultAddress: params.vaultAddress,
15920
- config
15921
- });
15922
- return {
15923
- ...quote,
15924
- gasFees
15925
- };
15926
16244
  }
15927
16245
  }
15928
16246
  function hasDepositDestination(params) {
@@ -16160,11 +16478,27 @@ function formatPositionPnL(pnl) {
16160
16478
  * @param vault - Provider vault info with raw amount objects
16161
16479
  * @returns Vault info with total deposits and liquidity formatted as strings
16162
16480
  */ function formatVaultInfo(vault) {
16163
- const { totalDeposits, liquidity, ...rest } = vault;
16481
+ // The flat `totalDeposits`/`liquidity` are deprecated aliases that are
16482
+ // intentionally dual-read through the migration window so existing
16483
+ // consumers keep receiving them until Contract.
16484
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
16485
+ const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
16486
+ // `liquidityProfile` is `.optional()` in the response schema during the
16487
+ // expand/contract window (an old backend that predates the nested facets
16488
+ // omits it), so only format and re-attach it when present — matching the
16489
+ // provider-side `toVaultInfo` mapper.
16164
16490
  return {
16165
16491
  ...rest,
16166
16492
  totalDeposits: formatAmount(totalDeposits),
16167
- liquidity: formatAmount(liquidity)
16493
+ liquidity: formatAmount(liquidity),
16494
+ ...liquidityProfile !== undefined && {
16495
+ liquidityProfile: {
16496
+ ...liquidityProfile,
16497
+ totalDeposits: formatAmount(liquidityProfile.totalDeposits),
16498
+ available: formatAmount(liquidityProfile.available),
16499
+ totalSupply: formatAmount(liquidityProfile.totalSupply)
16500
+ }
16501
+ }
16168
16502
  };
16169
16503
  }
16170
16504
  /**
@@ -18076,27 +18410,95 @@ function formatRetryResult(operation, result) {
18076
18410
  // Auto-register this kit for user agent tracking
18077
18411
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
18078
18412
 
18413
+ /**
18414
+ * Register event handlers from a context actions map to a kit instance.
18415
+ *
18416
+ * This utility function registers event handlers stored in a context actions map
18417
+ * with a kit instance that supports event handling via an `on` method. It handles
18418
+ * wildcard handlers ('*') and prefixed action handlers, stripping the prefix
18419
+ * before registration.
18420
+ *
18421
+ * The function is designed to be reusable across different operation types
18422
+ * (bridge, swap, stake, etc.) by accepting a configurable prefix parameter.
18423
+ *
18424
+ * @param kit - The kit instance to register handlers with (must have an `on` method)
18425
+ * @param handlers - Map of action names to arrays of handler functions
18426
+ * @param prefix - Optional prefix to strip from action names (e.g., 'bridge.')
18427
+ *
18428
+ * @example
18429
+ * ```typescript
18430
+ * import { registerActionHandlers } from '@circle-fin/app-kit/utils'
18431
+ * import { BridgeKit } from '@circle-fin/bridge-kit'
18432
+ *
18433
+ * const kit = new BridgeKit()
18434
+ * const handlers = {
18435
+ * '*': [(payload) => console.log('All actions:', payload)],
18436
+ * 'bridge.approve': [(payload) => console.log('Approved:', payload)],
18437
+ * 'bridge.burn': [(payload) => console.log('Burned:', payload)],
18438
+ * }
18439
+ *
18440
+ * registerActionHandlers(kit, handlers, 'bridge.')
18441
+ * ```
18442
+ *
18443
+ * @example
18444
+ * ```typescript
18445
+ * import { registerActionHandlers } from '@circle-fin/app-kit/utils'
18446
+ * import { SwapKit } from '@circle-fin/swap-kit'
18447
+ *
18448
+ * const kit = new SwapKit()
18449
+ * const handlers = {
18450
+ * 'swap.initiate': [(payload) => console.log('Swap initiated:', payload)],
18451
+ * }
18452
+ *
18453
+ * registerActionHandlers(kit, handlers, 'swap.')
18454
+ * ```
18455
+ */ const registerActionHandlers = (kit, handlers, prefix = '')=>{
18456
+ for (const [action, handlerArray] of Object.entries(handlers)){
18457
+ // Register all handlers for this action
18458
+ for (const handler of handlerArray){
18459
+ if (action === '*') {
18460
+ // Wildcard handlers are registered as-is
18461
+ kit.on('*', handler);
18462
+ } else if (prefix && action.startsWith(prefix)) {
18463
+ // Remove prefix to get the actual kit action name
18464
+ const kitAction = action.split('.').at(1);
18465
+ if (kitAction) {
18466
+ kit.on(kitAction, handler);
18467
+ }
18468
+ } else if (!prefix) {
18469
+ // No prefix configured, register action as-is
18470
+ kit.on(action, handler);
18471
+ }
18472
+ // Actions that don't match the prefix are silently ignored
18473
+ }
18474
+ }
18475
+ };
18476
+
18079
18477
  /**
18080
18478
  * Create an EarnKit instance for AppKit earn operations.
18081
18479
  *
18082
- * @remarks The context parameter is reserved for future EarnKit wiring.
18083
- * EarnKit does not currently support AppKit developer fee hooks, so the
18084
- * factory does not read fee callbacks from the context. Earn custom fees remain
18085
- * reserved until EarnKit fee support ships.
18480
+ * Attaches any earn event handlers previously registered on the AppKit
18481
+ * context (via `kit.on('earn.*', …)` or `kit.on('*', …)`) so step events
18482
+ * fire during the returned kit's earn operations.
18086
18483
  *
18087
- * When EarnKit supports developer fee hooks, this factory can wire AppKit context through.
18484
+ * @remarks Developer fee hooks from the AppKit context are not applied.
18485
+ * EarnKit does not yet support custom fee policies.
18088
18486
  *
18089
- * @param context - AppKit context reserved for future EarnKit wiring
18090
- * @returns A new EarnKit instance
18487
+ * @param context - AppKit context with earn event handlers and kit options
18488
+ * @returns An EarnKit instance ready for AppKit earn operations
18091
18489
  *
18092
18490
  * @example
18093
18491
  * ```typescript
18094
18492
  * const earnKit = createEarnKit(context)
18095
18493
  * ```
18096
- */ const createEarnKit = ()=>new EarnKit();
18494
+ */ const createEarnKit = (context)=>{
18495
+ const kit = new EarnKit();
18496
+ registerActionHandlers(kit, context.actions.earn, 'earn');
18497
+ return kit;
18498
+ };
18097
18499
 
18098
18500
  async function deposit(context, params) {
18099
- return createEarnKit().deposit(params);
18501
+ return createEarnKit(context).deposit(params);
18100
18502
  }
18101
18503
  /**
18102
18504
  * Execute an earn withdrawal operation.
@@ -18120,7 +18522,7 @@ async function deposit(context, params) {
18120
18522
  * })
18121
18523
  * ```
18122
18524
  */ async function withdraw(context, params) {
18123
- return createEarnKit().withdraw(params);
18525
+ return createEarnKit(context).withdraw(params);
18124
18526
  }
18125
18527
  /**
18126
18528
  * Claim earn rewards.
@@ -18143,7 +18545,7 @@ async function deposit(context, params) {
18143
18545
  * })
18144
18546
  * ```
18145
18547
  */ async function claimRewards(context, params) {
18146
- return createEarnKit().claimRewards(params);
18548
+ return createEarnKit(context).claimRewards(params);
18147
18549
  }
18148
18550
  /**
18149
18551
  * Fetch vault information.
@@ -18165,7 +18567,7 @@ async function deposit(context, params) {
18165
18567
  * })
18166
18568
  * ```
18167
18569
  */ async function getVaults(context, params) {
18168
- return createEarnKit().getVaults(params);
18570
+ return createEarnKit(context).getVaults(params);
18169
18571
  }
18170
18572
  /**
18171
18573
  * Discover vaults available on a chain.
@@ -18189,7 +18591,7 @@ async function deposit(context, params) {
18189
18591
  * })
18190
18592
  * ```
18191
18593
  */ async function exploreVaults(context, params) {
18192
- return createEarnKit().exploreVaults(params);
18594
+ return createEarnKit(context).exploreVaults(params);
18193
18595
  }
18194
18596
  /**
18195
18597
  * Lazily iterate every vault available on a chain.
@@ -18213,7 +18615,7 @@ async function deposit(context, params) {
18213
18615
  * }
18214
18616
  * ```
18215
18617
  */ function exploreVaultsIterator(context, params) {
18216
- return createEarnKit().exploreVaultsIterator(params);
18618
+ return createEarnKit(context).exploreVaultsIterator(params);
18217
18619
  }
18218
18620
  /**
18219
18621
  * Fetch a wallet position in a vault.
@@ -18236,7 +18638,7 @@ async function deposit(context, params) {
18236
18638
  * })
18237
18639
  * ```
18238
18640
  */ async function getPosition(context, params) {
18239
- return createEarnKit().getPosition(params);
18641
+ return createEarnKit(context).getPosition(params);
18240
18642
  }
18241
18643
  /**
18242
18644
  * Fetch the current status of a cross-chain Earn deposit.
@@ -18258,7 +18660,7 @@ async function deposit(context, params) {
18258
18660
  * console.log(status.status)
18259
18661
  * ```
18260
18662
  */ async function getCrossChainDepositStatus(context, params) {
18261
- return createEarnKit().getCrossChainDepositStatus(params);
18663
+ return createEarnKit(context).getCrossChainDepositStatus(params);
18262
18664
  }
18263
18665
  /**
18264
18666
  * Poll a cross-chain Earn deposit until it reaches a terminal bridge state.
@@ -18281,7 +18683,7 @@ async function deposit(context, params) {
18281
18683
  * console.log(result.outcome)
18282
18684
  * ```
18283
18685
  */ async function waitForCrossChainDeposit(context, params) {
18284
- return createEarnKit().waitForCrossChainDeposit(params);
18686
+ return createEarnKit(context).waitForCrossChainDeposit(params);
18285
18687
  }
18286
18688
  /**
18287
18689
  * Fetch a deposit quote.
@@ -18305,7 +18707,7 @@ async function deposit(context, params) {
18305
18707
  * })
18306
18708
  * ```
18307
18709
  */ async function getDepositQuote(context, params) {
18308
- return createEarnKit().getDepositQuote(params);
18710
+ return createEarnKit(context).getDepositQuote(params);
18309
18711
  }
18310
18712
  /**
18311
18713
  * Fetch a withdrawal quote.
@@ -18329,7 +18731,7 @@ async function deposit(context, params) {
18329
18731
  * })
18330
18732
  * ```
18331
18733
  */ async function getWithdrawalQuote(context, params) {
18332
- return createEarnKit().getWithdrawalQuote(params);
18734
+ return createEarnKit(context).getWithdrawalQuote(params);
18333
18735
  }
18334
18736
  /**
18335
18737
  * Fetch a claim rewards quote.
@@ -18352,8 +18754,44 @@ async function deposit(context, params) {
18352
18754
  * })
18353
18755
  * ```
18354
18756
  */ async function getClaimRewardsQuote(context, params) {
18355
- return createEarnKit().getClaimRewardsQuote(params);
18757
+ return createEarnKit(context).getClaimRewardsQuote(params);
18758
+ }
18759
+ /**
18760
+ * Resume a multi-phase earn operation that previously failed.
18761
+ *
18762
+ * Pass the {@link KitError} caught from `deposit`, `withdraw`, or
18763
+ * `claimRewards`. Completed phases can be skipped when the error carries
18764
+ * earn retry context. Call `isRetryableError(error)` first.
18765
+ *
18766
+ * @remarks
18767
+ * Retry re-fetches execution params and may re-submit the execute
18768
+ * transaction. Treat this as best-effort recovery if a prior execute
18769
+ * broadcast may still be in flight.
18770
+ *
18771
+ * @param context - AppKit context
18772
+ * @param error - The error caught from a previous multi-phase earn operation
18773
+ * @returns Promise resolving to the result of the resumed operation
18774
+ * @throws If the error is not retryable or lacks earn retry context
18775
+ *
18776
+ * @example
18777
+ * ```typescript
18778
+ * import { isRetryableError } from '@circle-fin/app-kit'
18779
+ * import { createContext } from '@circle-fin/app-kit/context'
18780
+ * import { retry } from '@circle-fin/app-kit/earn'
18781
+ *
18782
+ * const context = createContext()
18783
+ *
18784
+ * try {
18785
+ * await deposit(context, params)
18786
+ * } catch (error) {
18787
+ * if (isRetryableError(error)) {
18788
+ * const result = await retry(context, error)
18789
+ * }
18790
+ * }
18791
+ * ```
18792
+ */ async function retry(context, error) {
18793
+ return createEarnKit(context).retry(error);
18356
18794
  }
18357
18795
 
18358
- export { claimRewards, deposit, exploreVaults, exploreVaultsIterator, getClaimRewardsQuote, getCrossChainDepositStatus, getDepositQuote, getPosition, getVaults, getWithdrawalQuote, waitForCrossChainDeposit, withdraw };
18796
+ export { claimRewards, deposit, exploreVaults, exploreVaultsIterator, getClaimRewardsQuote, getCrossChainDepositStatus, getDepositQuote, getPosition, getVaults, getWithdrawalQuote, retry, waitForCrossChainDeposit, withdraw };
18359
18797
  //# sourceMappingURL=earn.mjs.map