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