@circle-fin/app-kit 1.15.1 → 1.15.3

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
@@ -704,6 +704,29 @@ class KitError extends Error {
704
704
  name: 'INPUT_VALIDATION_FAILED',
705
705
  type: 'INPUT'
706
706
  }};
707
+ /**
708
+ * Standardized error definitions for BALANCE type errors.
709
+ *
710
+ * BALANCE errors indicate insufficient funds or allowance issues
711
+ * that prevent transaction execution.
712
+ *
713
+ * @example
714
+ * ```typescript
715
+ * import { BalanceError } from '@core/errors'
716
+ *
717
+ * const error = new KitError({
718
+ * ...BalanceError.INSUFFICIENT_TOKEN,
719
+ * recoverability: 'FATAL',
720
+ * message: 'Insufficient USDC balance on Ethereum',
721
+ * cause: { trace: { required: '100', available: '50' } }
722
+ * })
723
+ * ```
724
+ */ const BalanceError = {
725
+ /** Insufficient token balance for transaction */ INSUFFICIENT_TOKEN: {
726
+ code: 9001,
727
+ name: 'BALANCE_INSUFFICIENT_TOKEN',
728
+ type: 'BALANCE'
729
+ }};
707
730
  /**
708
731
  * Standardized error definitions for ONCHAIN type errors.
709
732
  *
@@ -9135,7 +9158,7 @@ const swapTokenEnumSchema = zod.z.enum([
9135
9158
  }
9136
9159
 
9137
9160
  var name$3 = "@circle-fin/bridge-kit";
9138
- var version$3 = "1.15.0";
9161
+ var version$3 = "1.15.1";
9139
9162
  var pkg$3 = {
9140
9163
  name: name$3,
9141
9164
  version: version$3};
@@ -9223,7 +9246,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
9223
9246
  *
9224
9247
  * @internal
9225
9248
  */ function isReadActionKey(action) {
9226
- return READ_ACTION_KEY_SET.has(action);
9249
+ return typeof action === 'string' && READ_ACTION_KEY_SET.has(action);
9227
9250
  }
9228
9251
 
9229
9252
  /**
@@ -12547,6 +12570,86 @@ const abiParameterSchema = zod.z.object({
12547
12570
  };
12548
12571
  }
12549
12572
 
12573
+ /**
12574
+ * EIP-7702 delegation indicator. A 7702-delegated EOA's bytecode is
12575
+ * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12576
+ * The underlying secp256k1 key still produces `ecrecover`-verifiable
12577
+ * signatures, so for Gateway's purposes a 7702-delegated address is
12578
+ * an EOA, not a contract signer.
12579
+ *
12580
+ * Spec: https://eips.ethereum.org/EIPS/eip-7702
12581
+ */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12582
+ /**
12583
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
12584
+ * rather than as an EOA.
12585
+ *
12586
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
12587
+ * check for EOAs, and — for requests that carry `contractSigner: true` —
12588
+ * an offchain `isValidSignature` simulation against the signing contract
12589
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
12590
+ * declare it. This detects the contract case from on-chain bytecode.
12591
+ *
12592
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
12593
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
12594
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
12595
+ * stays correct for them.
12596
+ *
12597
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
12598
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
12599
+ * genuine contract signer misreported this way is rejected by Gateway with
12600
+ * an invalid-signature error rather than silently mis-attested.
12601
+ *
12602
+ * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12603
+ * @param address - Signer address to classify.
12604
+ * @param chain - EVM chain where the signer lives.
12605
+ * @returns `true` when the signer is a contract account and the transfer
12606
+ * request must set `contractSigner: true`; `false` otherwise.
12607
+ *
12608
+ * @example
12609
+ * ```typescript
12610
+ * import { isContractSigner } from '@core/adapter-evm'
12611
+ * import { Ethereum } from '@core/chains'
12612
+ *
12613
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12614
+ * ```
12615
+ */ async function isContractSigner(adapter, address, chain) {
12616
+ let code;
12617
+ try {
12618
+ code = await adapter.readBytecode(address, chain);
12619
+ } catch (err) {
12620
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12621
+ return false;
12622
+ }
12623
+ if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12624
+ return false;
12625
+ }
12626
+ return true;
12627
+ }
12628
+
12629
+ /**
12630
+ * Duck-type test for an EVM adapter that exposes `readBytecode`.
12631
+ *
12632
+ * Used as a structural guard at package boundaries (e.g. the Circle Wallets
12633
+ * hybrid adapter calling into a chain adapter, or the Gateway sign path
12634
+ * receiving an arbitrary adapter implementation) where `instanceof EvmAdapter`
12635
+ * is unreliable because each consumer bundles its own copy of the base class.
12636
+ *
12637
+ * @param value - The value to test.
12638
+ * @returns `true` when `value` is an object exposing a callable
12639
+ * `readBytecode` method, narrowed to {@link EvmAdapterLike}.
12640
+ *
12641
+ * @example
12642
+ * ```typescript
12643
+ * import { isEvmAdapterLike } from '@core/adapter-evm'
12644
+ *
12645
+ * if (isEvmAdapterLike(adapter)) {
12646
+ * const code = await adapter.readBytecode('0xabc...', chain)
12647
+ * }
12648
+ * ```
12649
+ */ function isEvmAdapterLike(value) {
12650
+ return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12651
+ }
12652
+
12550
12653
  const hexBytesSchema$1 = zod.z.string().regex(/^0x([0-9a-fA-F]{2})*$/, {
12551
12654
  message: 'value must be a 0x-prefixed even-length hex string'
12552
12655
  }).transform((value)=>value);
@@ -14441,7 +14544,7 @@ new Set(Object.values(Blockchain));
14441
14544
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
14442
14545
 
14443
14546
  var name$1 = "@circle-fin/earn-kit";
14444
- var version$1 = "1.7.0";
14547
+ var version$1 = "1.8.0";
14445
14548
  var pkg$1 = {
14446
14549
  name: name$1,
14447
14550
  version: version$1};
@@ -14980,6 +15083,142 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
14980
15083
  };
14981
15084
  }
14982
15085
 
15086
+ /**
15087
+ * Fallback gas reserve expressed in ERC-20 deposit token base units when no
15088
+ * quote gas fees are available. 50_000 = 0.05 USDC at the 6-decimal ERC-20
15089
+ * USDC scale used on Arc (and the scale of `balance` / `depositAmountRaw`).
15090
+ *
15091
+ * @internal
15092
+ */ const DEFAULT_GAS_RESERVE = 50_000n;
15093
+ /**
15094
+ * Standard ERC-20 decimal precision for USDC and EURC. Both tokens use 6
15095
+ * decimals on all EVM chains, regardless of how many decimals the native
15096
+ * gas-token representation uses.
15097
+ *
15098
+ * @internal
15099
+ */ const STABLECOIN_ERC20_DECIMALS = 6;
15100
+ /**
15101
+ * Compute the gas reserve in ERC-20 deposit token base units.
15102
+ *
15103
+ * `EstimatedGas.fee` is denominated in the chain's native currency base units
15104
+ * (e.g. 18-decimal USDC on Arc). The deposit token balance and
15105
+ * `depositAmountRaw` are in ERC-20 token base units (e.g. 6-decimal USDC).
15106
+ * This function converts the native fee total to the same scale so both sides
15107
+ * of the `balance < required` comparison are commensurable.
15108
+ *
15109
+ * Conversion: `fee_token_units = fee_native_units / 10^(nativeDecimals - tokenDecimals)`
15110
+ *
15111
+ * Falls back to {@link DEFAULT_GAS_RESERVE} when:
15112
+ * - `gasFees` is absent or empty
15113
+ * - all fee entries have `fees: null` (estimation failed)
15114
+ * - the converted total rounds to zero (native fee smaller than 1 token unit)
15115
+ *
15116
+ * @internal
15117
+ */ function computeGasReserve(gasFees, nativeDecimals, tokenDecimals) {
15118
+ if (gasFees === undefined || gasFees.length === 0) {
15119
+ return DEFAULT_GAS_RESERVE;
15120
+ }
15121
+ let totalNative = 0n;
15122
+ let hasNullEntry = false;
15123
+ for (const entry of gasFees){
15124
+ if (entry.fees === null) {
15125
+ hasNullEntry = true;
15126
+ } else {
15127
+ totalNative += BigInt(entry.fees.fee);
15128
+ }
15129
+ }
15130
+ const decimalDiff = nativeDecimals - tokenDecimals;
15131
+ // Use ceiling division when reducing precision so we never under-estimate.
15132
+ const scale = decimalDiff > 0 ? 10n ** BigInt(decimalDiff) : 1n;
15133
+ let knownToken;
15134
+ if (totalNative === 0n) {
15135
+ knownToken = 0n;
15136
+ } else if (decimalDiff > 0) {
15137
+ knownToken = (totalNative + scale - 1n) / scale;
15138
+ } else {
15139
+ knownToken = totalNative * 10n ** BigInt(-decimalDiff);
15140
+ }
15141
+ // When any estimate is missing the true total is unknown, so reserve at
15142
+ // least DEFAULT_GAS_RESERVE on top of whatever was already known.
15143
+ // BigInt is not supported by Math.max, so we use an explicit comparison.
15144
+ if (hasNullEntry) {
15145
+ return knownToken > DEFAULT_GAS_RESERVE ? knownToken : DEFAULT_GAS_RESERVE // NOSONAR
15146
+ ;
15147
+ }
15148
+ return knownToken > 0n ? knownToken : DEFAULT_GAS_RESERVE;
15149
+ }
15150
+ /**
15151
+ * Build a symbol → ERC-20 address map from the chain's known token addresses.
15152
+ * Used to resolve whether the native gas token corresponds to the deposit
15153
+ * token's address.
15154
+ *
15155
+ * @internal
15156
+ */ function buildSymbolAddressMap(chain) {
15157
+ const map = new Map();
15158
+ if (chain.usdcAddress != null) {
15159
+ map.set('USDC', chain.usdcAddress.toLowerCase());
15160
+ }
15161
+ if (chain.eurcAddress != null) {
15162
+ map.set('EURC', chain.eurcAddress.toLowerCase());
15163
+ }
15164
+ return map;
15165
+ }
15166
+ /**
15167
+ * Pre-broadcast balance check for EOA wallets on chains where the native gas
15168
+ * token and the deposit token are the same asset (e.g. Arc: USDC gas + USDC
15169
+ * deposit).
15170
+ *
15171
+ * When the deposit would consume the entire balance, the adapter locks gas
15172
+ * before `transferFrom`, causing an `ERC20: transfer amount exceeds balance`
15173
+ * revert on-chain. This function reads the wallet balance before broadcast and
15174
+ * throws {@link BalanceError.INSUFFICIENT_TOKEN} (code 9001) when
15175
+ * `balance < depositAmount + gasReserve`, giving callers a clean pre-broadcast
15176
+ * error instead of an opaque chain revert.
15177
+ *
15178
+ * The check is skipped when the chain's native gas currency symbol does not
15179
+ * match the deposit token symbol (derived by address lookup). Examples of
15180
+ * chains where the check is skipped: EURC deposit on Arc, any ETH-gas chain.
15181
+ *
15182
+ * Gas fees from the quote (`gasFees`) are in native-currency base units
15183
+ * (18-decimal on Arc) and are scaled down to ERC-20 token units (6-decimal)
15184
+ * before being added to `depositAmountRaw`.
15185
+ *
15186
+ * @param params - Adapter context, chain, deposit token, amount, and optional
15187
+ * quote gas fees.
15188
+ * @throws {@link KitError} (code 9001) when the balance is insufficient to
15189
+ * cover both the deposit and the gas reserve.
15190
+ *
15191
+ * @internal
15192
+ */ async function checkGasReserveForDeposit(params) {
15193
+ const { adapter, chain, address, depositTokenAddress, depositAmountRaw, gasFees } = params;
15194
+ const nativeSymbol = chain.nativeCurrency.symbol.toUpperCase();
15195
+ const symbolAddressMap = buildSymbolAddressMap(chain);
15196
+ const nativeTokenAddress = symbolAddressMap.get(nativeSymbol);
15197
+ // Only applies when native gas currency and deposit token are the same asset.
15198
+ if (nativeTokenAddress === undefined || depositTokenAddress.toLowerCase() !== nativeTokenAddress) {
15199
+ return;
15200
+ }
15201
+ const rawBalance = await executeAdapterReadAction(adapter, 'token.balanceOf', {
15202
+ tokenAddress: depositTokenAddress,
15203
+ walletAddress: address
15204
+ }, {
15205
+ chain,
15206
+ address
15207
+ });
15208
+ const balance = BigInt(String(rawBalance));
15209
+ // Both USDC and EURC use 6 ERC-20 decimals; the check only reaches here when
15210
+ // the deposit token is the chain's native gas asset (USDC or EURC).
15211
+ const gasReserve = computeGasReserve(gasFees, chain.nativeCurrency.decimals, STABLECOIN_ERC20_DECIMALS);
15212
+ const required = depositAmountRaw + gasReserve;
15213
+ if (balance < required) {
15214
+ throw new KitError({
15215
+ ...BalanceError.INSUFFICIENT_TOKEN,
15216
+ recoverability: 'FATAL',
15217
+ message: `Insufficient ${chain.nativeCurrency.symbol} balance to cover deposit and gas. Balance: ${balance.toString()}, required: ${required.toString()} (deposit: ${depositAmountRaw.toString()} + gas reserve: ${gasReserve.toString()})`
15218
+ });
15219
+ }
15220
+ }
15221
+
14983
15222
  /** @internal */ function isSameAddress(actual, expected) {
14984
15223
  return actual.toLowerCase() === expected.toLowerCase();
14985
15224
  }
@@ -16331,12 +16570,19 @@ const EARN_OPERATIONS = new Set([
16331
16570
  writable: true,
16332
16571
  configurable: true
16333
16572
  });
16573
+ // Surface the tx hash in the message so callers can look it up without
16574
+ // digging into cause.trace. Only applies to on-chain reverts — other errors
16575
+ // either have no tx hash or already embed it in their message.
16576
+ let message = base.message;
16577
+ if (base.name === 'ONCHAIN_TRANSACTION_REVERTED' && typeof existingTrace['txHash'] === 'string' && existingTrace['txHash'] !== '') {
16578
+ message = `${message} (txHash: ${existingTrace['txHash']})`;
16579
+ }
16334
16580
  return new KitError({
16335
16581
  code: base.code,
16336
16582
  name: base.name,
16337
16583
  type: base.type,
16338
16584
  recoverability,
16339
- message: base.message,
16585
+ message,
16340
16586
  cause: {
16341
16587
  trace
16342
16588
  }
@@ -16452,10 +16698,8 @@ const bridgeFeeTokenSchema = hexAddressSchema;
16452
16698
  assetAddress: zod.z.string(),
16453
16699
  lltv: zod.z.number(),
16454
16700
  supplyUsd: zod.z.number(),
16455
- // Optional during the expand/contract window (a backend that predates the
16456
- // field omits the key), mirroring the `.optional()` facets on the base
16457
- // schema; `null` when the product exposes no per-market allocation (V2).
16458
- allocationPct: zod.z.number().nullable().optional()
16701
+ // `null` when the product exposes no per-market allocation (V2).
16702
+ allocationPct: zod.z.number().nullable()
16459
16703
  });
16460
16704
  /**
16461
16705
  * Zod schema for a Morpho vault warning in the API response.
@@ -16562,19 +16806,20 @@ const bridgeFeeTokenSchema = hexAddressSchema;
16562
16806
  earnKitWarnings: zod.z.array(zod.z.string()).optional()
16563
16807
  });
16564
16808
  /**
16565
- * Shared base schema: existing flat fields (kept) plus the new nested
16566
- * facets and neutral identity. Facets are `.optional()` during the
16567
- * transition; flip to required once the backend is confirmed emitting.
16809
+ * Shared base schema: existing flat fields (kept as deprecated aliases) plus
16810
+ * the nested facets. Zenith emits all facets on every response; required here
16811
+ * so a partial emission is caught at parse time rather than surfacing as a
16812
+ * runtime `undefined` access.
16568
16813
  *
16569
16814
  * @internal
16570
16815
  */ const earnBaseSchema = vaultInfoResponseSchema.extend({
16571
- address: zod.z.string().optional(),
16572
- asOf: zod.z.string().optional(),
16573
- manager: managerSchema.nullable().optional(),
16574
- apyProfile: apyProfileSchema.optional(),
16575
- fee: feeInfoSchema.optional(),
16576
- liquidityProfile: liquidityProfileSchema.optional(),
16577
- riskSignals: riskSignalsSchema.optional()
16816
+ address: zod.z.string(),
16817
+ asOf: zod.z.string(),
16818
+ manager: managerSchema.nullable(),
16819
+ apyProfile: apyProfileSchema,
16820
+ fee: feeInfoSchema,
16821
+ liquidityProfile: liquidityProfileSchema,
16822
+ riskSignals: riskSignalsSchema
16578
16823
  });
16579
16824
  /**
16580
16825
  * Zod schema for the `vault` opportunity variant.
@@ -17362,7 +17607,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
17362
17607
  }
17363
17608
 
17364
17609
  var name = "@circle-fin/provider-earn-service";
17365
- var version = "1.6.0";
17610
+ var version = "1.7.0";
17366
17611
  var pkg = {
17367
17612
  name: name,
17368
17613
  version: version};
@@ -17510,23 +17755,16 @@ var pkg = {
17510
17755
  if (chain === undefined) {
17511
17756
  throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
17512
17757
  }
17513
- // The nested facets are `.optional()` in the schema (a backend that predates
17514
- // them omits them) and are typed optional on `EarnOpportunity` to match.
17515
- // Convert the nested liquidity amounts when present and pass the remaining
17516
- // facets straight through; each absent facet stays absent rather than being
17517
- // asserted present by a cast.
17518
17758
  return {
17519
17759
  ...vault,
17520
17760
  chain,
17521
17761
  totalDeposits: Amount.fromJSON(totalDeposits),
17522
17762
  liquidity: Amount.fromJSON(liquidity),
17523
- ...liquidityProfile !== undefined && {
17524
- liquidityProfile: {
17525
- ...liquidityProfile,
17526
- totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
17527
- available: Amount.fromJSON(liquidityProfile.available),
17528
- totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
17529
- }
17763
+ liquidityProfile: {
17764
+ ...liquidityProfile,
17765
+ totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
17766
+ available: Amount.fromJSON(liquidityProfile.available),
17767
+ totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
17530
17768
  }
17531
17769
  };
17532
17770
  }
@@ -19032,6 +19270,24 @@ function finishElapsedWait(lastStatus, lastError) {
19032
19270
  amount: params.amount
19033
19271
  };
19034
19272
  }
19273
+ // Only EOA wallets pay native gas from their own balance — SCAs have
19274
+ // their gas sponsored externally. Detect EOA by checking that the
19275
+ // adapter supports readBytecode and the wallet address has no on-chain
19276
+ // bytecode (empty code ⟹ EOA; non-empty ⟹ smart-contract account).
19277
+ // EIP-7702-delegated addresses are treated as EOA by isContractSigner.
19278
+ // If the adapter does not expose readBytecode (non-EVM adapter) the
19279
+ // check is skipped conservatively.
19280
+ if (approvalToken !== undefined && requiredAllowance > 0n && chain.type === 'evm' && isEvmAdapterLike(adapter) && !await isContractSigner(adapter, address, chain)) {
19281
+ const sameChainParams = params;
19282
+ await checkGasReserveForDeposit({
19283
+ adapter,
19284
+ chain,
19285
+ address,
19286
+ depositTokenAddress: approvalToken,
19287
+ depositAmountRaw: requiredAllowance,
19288
+ gasFees: sameChainParams.gasFees
19289
+ });
19290
+ }
19035
19291
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
19036
19292
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
19037
19293
  try {
@@ -19438,7 +19694,7 @@ function finishElapsedWait(lastStatus, lastError) {
19438
19694
  // Gas is estimated server-side by the Earn Service and returned on the quote, so the
19439
19695
  // SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
19440
19696
  // the native token symbol / blockchain on each gas entry.
19441
- return fetchDepositQuote({
19697
+ const quote = await fetchDepositQuote({
19442
19698
  vaultAddress: params.vaultAddress,
19443
19699
  amount: params.amount,
19444
19700
  address,
@@ -19446,6 +19702,34 @@ function finishElapsedWait(lastStatus, lastError) {
19446
19702
  config,
19447
19703
  chainDefinition
19448
19704
  });
19705
+ // Run the same pre-broadcast gas reserve check here so callers get an early
19706
+ // warning at quote time — before they call deposit() and hit the real check.
19707
+ // Only applies on stablecoin-gas chains (e.g. Arc) where the deposit token
19708
+ // is also the native gas token, and only for EOA wallets that pay native gas
19709
+ // from their own balance. Skips silently on all other chains and wallet types.
19710
+ if (chainDefinition?.type === 'evm' && isEvmAdapterLike(params.from.adapter) && !await isContractSigner(params.from.adapter, address, chainDefinition)) {
19711
+ const symbol = quote.deposit.symbol.toUpperCase();
19712
+ let depositTokenAddress = null;
19713
+ if (symbol === 'USDC') {
19714
+ depositTokenAddress = chainDefinition.usdcAddress;
19715
+ } else if (symbol === 'EURC') {
19716
+ depositTokenAddress = chainDefinition.eurcAddress;
19717
+ }
19718
+ if (depositTokenAddress != null) {
19719
+ const amountObj = Amount.parse(params.amount, {
19720
+ decimals: 6
19721
+ });
19722
+ await checkGasReserveForDeposit({
19723
+ adapter: params.from.adapter,
19724
+ chain: chainDefinition,
19725
+ address,
19726
+ depositTokenAddress,
19727
+ depositAmountRaw: amountObj.raw,
19728
+ gasFees: quote.gasFees
19729
+ });
19730
+ }
19731
+ }
19732
+ return quote;
19449
19733
  }
19450
19734
  /** {@inheritdoc} */ async getWithdrawalQuote(params) {
19451
19735
  const config = this.resolveConfig(params.config);
@@ -19745,21 +20029,15 @@ function formatPositionPnL(pnl) {
19745
20029
  // consumers keep receiving them until Contract.
19746
20030
  // eslint-disable-next-line @typescript-eslint/no-deprecated
19747
20031
  const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
19748
- // `liquidityProfile` is `.optional()` in the response schema during the
19749
- // expand/contract window (an old backend that predates the nested facets
19750
- // omits it), so only format and re-attach it when present — matching the
19751
- // provider-side `toVaultInfo` mapper.
19752
20032
  return {
19753
20033
  ...rest,
19754
20034
  totalDeposits: formatAmount(totalDeposits),
19755
20035
  liquidity: formatAmount(liquidity),
19756
- ...liquidityProfile !== undefined && {
19757
- liquidityProfile: {
19758
- ...liquidityProfile,
19759
- totalDeposits: formatAmount(liquidityProfile.totalDeposits),
19760
- available: formatAmount(liquidityProfile.available),
19761
- totalSupply: formatAmount(liquidityProfile.totalSupply)
19762
- }
20036
+ liquidityProfile: {
20037
+ ...liquidityProfile,
20038
+ totalDeposits: formatAmount(liquidityProfile.totalDeposits),
20039
+ available: formatAmount(liquidityProfile.available),
20040
+ totalSupply: formatAmount(liquidityProfile.totalSupply)
19763
20041
  }
19764
20042
  };
19765
20043
  }
@@ -20245,6 +20523,49 @@ const sourceAdapterContextSchema = zod.z.object({
20245
20523
  vaultAddress: vaultAddressSchema,
20246
20524
  config: earnConfigSchema.optional()
20247
20525
  });
20526
+ /**
20527
+ * Schema for a successful gas fee entry (fees is non-null).
20528
+ *
20529
+ * Validates the kit shape — {@link EarnEstimatedGas} uses decimal strings for
20530
+ * `gas`, `gasPrice`, and `fee`. Only digits are accepted so that the deposit
20531
+ * operation can safely convert them to `bigint` before passing them to the
20532
+ * provider's `computeGasReserve`.
20533
+ *
20534
+ * @internal
20535
+ */ const earnGasFeeEstimateSuccessSchema = zod.z.object({
20536
+ name: zod.z.string(),
20537
+ token: zod.z.string(),
20538
+ blockchain: zod.z.string(),
20539
+ fees: zod.z.object({
20540
+ gas: zod.z.string().regex(/^\d+$/, 'gas must be a non-negative integer string'),
20541
+ gasPrice: zod.z.string().regex(/^\d+$/, 'gasPrice must be a non-negative integer string'),
20542
+ fee: zod.z.string().regex(/^\d+$/, 'fee must be a non-negative integer string')
20543
+ }),
20544
+ error: zod.z.undefined().optional()
20545
+ }).strict();
20546
+ /**
20547
+ * Schema for a failed gas fee entry (fees is null, error is a string).
20548
+ *
20549
+ * @internal
20550
+ */ const earnGasFeeEstimateFailureSchema = zod.z.object({
20551
+ name: zod.z.string(),
20552
+ token: zod.z.string(),
20553
+ blockchain: zod.z.string(),
20554
+ fees: zod.z.null(),
20555
+ error: zod.z.string()
20556
+ }).strict();
20557
+ /**
20558
+ * Schema for a single kit-level gas fee estimate entry.
20559
+ *
20560
+ * Rejects arbitrary inputs (null, numbers, empty objects, non-numeric fee
20561
+ * strings) so callers receive a descriptive validation error instead of a
20562
+ * raw `BigInt` conversion error inside `computeGasReserve`.
20563
+ *
20564
+ * @internal
20565
+ */ const earnGasFeeEstimateSchema = zod.z.union([
20566
+ earnGasFeeEstimateSuccessSchema,
20567
+ earnGasFeeEstimateFailureSchema
20568
+ ]);
20248
20569
  /**
20249
20570
  * Validation schema for the same-chain variant of DepositParams.
20250
20571
  *
@@ -20257,6 +20578,7 @@ const sourceAdapterContextSchema = zod.z.object({
20257
20578
  to: zod.z.undefined().optional(),
20258
20579
  vaultAddress: vaultAddressSchema,
20259
20580
  amount: amountSchema,
20581
+ gasFees: zod.z.array(earnGasFeeEstimateSchema).optional(),
20260
20582
  config: earnConfigSchema.optional()
20261
20583
  }).strict();
20262
20584
  const crossChainDepositDestinationSchema = zod.z.object({
@@ -20877,6 +21199,9 @@ async function deposit$1(context, params) {
20877
21199
  },
20878
21200
  vaultAddress: params.vaultAddress,
20879
21201
  amount: params.amount,
21202
+ ...!hasExplicitDestination && params.gasFees !== undefined && {
21203
+ gasFees: toProviderGasFees(params.gasFees)
21204
+ },
20880
21205
  ...params.config !== undefined && {
20881
21206
  config: params.config
20882
21207
  }
@@ -20899,6 +21224,21 @@ async function deposit$1(context, params) {
20899
21224
  }
20900
21225
  return provider.deposit(baseParams);
20901
21226
  }
21227
+ function toProviderGasFees(gasFees) {
21228
+ return gasFees.map((entry)=>{
21229
+ if (entry.fees === null) {
21230
+ return entry; // NOSONAR
21231
+ }
21232
+ return {
21233
+ ...entry,
21234
+ fees: {
21235
+ ...entry.fees,
21236
+ gas: BigInt(entry.fees.gas),
21237
+ gasPrice: BigInt(entry.fees.gasPrice)
21238
+ }
21239
+ };
21240
+ });
21241
+ }
20902
21242
  function hasCrossChainDestination(params) {
20903
21243
  // Runs before schema validation, so guard against non-object params from
20904
21244
  // untyped callers; assertEarnParams reports those with a structured error.
package/earn.d.cts CHANGED
@@ -6872,12 +6872,9 @@ interface CollateralInfo {
6872
6872
  * (e.g., 0.6 = 60%).
6873
6873
  *
6874
6874
  * Populated for Morpho V1 vaults; `null` when the underlying product
6875
- * exposes no per-market allocation (e.g. Morpho V2). Optional for now — a
6876
- * backend that predates this field omits it entirely, matching the other
6877
- * optional facets on {@link EarnOpportunityBase}; a future release makes it
6878
- * required once every backend emits it.
6875
+ * exposes no per-market allocation (e.g. Morpho V2).
6879
6876
  */
6880
- readonly allocationPct?: number | null | undefined;
6877
+ readonly allocationPct: number | null;
6881
6878
  }
6882
6879
  /**
6883
6880
  * Vault warning from the underlying earn protocol.
@@ -7047,19 +7044,19 @@ interface EarnOpportunityBase {
7047
7044
  /** Reward tokens distributed by this opportunity. */
7048
7045
  readonly rewards: readonly VaultRewardInfo[];
7049
7046
  /** Primary on-chain address (protocol-neutral; replaces vaultAddress). */
7050
- readonly address?: string | undefined;
7047
+ readonly address: string;
7051
7048
  /** RFC3339 freshness timestamp: provider state ts, else cache sync time. */
7052
- readonly asOf?: string | undefined;
7049
+ readonly asOf: string;
7053
7050
  /** Manager/curator identity; `null` when the product has no manager. */
7054
- readonly manager?: ManagerInfo | null | undefined;
7051
+ readonly manager: ManagerInfo | null;
7055
7052
  /** Yield profile including trailing averages. */
7056
- readonly apyProfile?: ApyProfile | undefined;
7053
+ readonly apyProfile: ApyProfile;
7057
7054
  /** Fee split. */
7058
- readonly fee?: FeeInfo | undefined;
7055
+ readonly fee: FeeInfo;
7059
7056
  /** Liquidity profile. */
7060
- readonly liquidityProfile?: LiquidityProfile | undefined;
7057
+ readonly liquidityProfile: LiquidityProfile;
7061
7058
  /** Risk signals. */
7062
- readonly riskSignals?: RiskSignals | undefined;
7059
+ readonly riskSignals: RiskSignals;
7063
7060
  /** @deprecated use {@link EarnOpportunityBase.address} */
7064
7061
  readonly vaultAddress: string;
7065
7062
  /** @deprecated use {@link ApyProfile.current} via apyProfile */
@@ -7126,13 +7123,9 @@ interface VaultOpportunity extends EarnOpportunityBase {
7126
7123
  /**
7127
7124
  * Universal discriminator identifying the opportunity shape.
7128
7125
  *
7129
- * Optional for now — a backend that predates the field omits it, matching
7130
- * the other optional facets — so this release stays source-compatible for
7131
- * code that constructs the type. A future release makes it required once
7132
- * every backend emits it. Always present on responses from an emitting
7133
- * backend; narrow on it before reading product-specific fields.
7126
+ * Narrow on `productType` before reading product-specific fields.
7134
7127
  */
7135
- readonly productType?: 'vault';
7128
+ readonly productType: 'vault';
7136
7129
  /** Collateral markets backing this vault. */
7137
7130
  readonly collateral: readonly CollateralInfo[];
7138
7131
  }
@@ -8158,6 +8151,12 @@ interface SameChainDepositParams<TFromAdapterCapabilities extends AdapterCapabil
8158
8151
  * @example '100.50' for 100.50 USDC
8159
8152
  */
8160
8153
  readonly amount: string;
8154
+ /**
8155
+ * Gas fee estimates from a prior {@link getDepositQuote} call. When provided,
8156
+ * the pre-broadcast gas reserve check uses the actual estimated cost instead
8157
+ * of the conservative fallback reserve.
8158
+ */
8159
+ readonly gasFees?: readonly EarnGasFeeEstimate[] | undefined;
8161
8160
  /** Optional earn configuration. */
8162
8161
  readonly config?: EarnConfig | undefined;
8163
8162
  }
@@ -8488,14 +8487,8 @@ type EarnVaultInfo = DistributiveOmit<EarnOpportunity, 'totalDeposits' | 'liquid
8488
8487
  readonly totalDeposits: string;
8489
8488
  /** Available liquidity in the vault in human-readable decimal format. */
8490
8489
  readonly liquidity: string;
8491
- /**
8492
- * Liquidity profile with amounts as human-readable decimal strings.
8493
- *
8494
- * Optional during the expand/contract migration window: a backend that
8495
- * predates the nested facets omits it, so it is absent until the response
8496
- * carries it.
8497
- */
8498
- readonly liquidityProfile?: EarnLiquidityProfile;
8490
+ /** Liquidity profile with amounts as human-readable decimal strings. */
8491
+ readonly liquidityProfile: EarnLiquidityProfile;
8499
8492
  };
8500
8493
  /** Result of a batch vault lookup. */
8501
8494
  type EarnGetVaultsResult = Omit<GetVaultsResult, 'vaults'> & {