import { PublicClient, Address, Hex } from 'viem'; import { Lender } from '@1delta/lender-registry'; export { hasCrossMarginRisk, isAaveType, isAaveV2Type, isAaveV32Type, isAaveV3Type, isCompoundV3, isCompoundV3Type, isInit, isMorphoType, isMultiMarket, isYLDR } from '@1delta/lender-registry'; import { DebitData, LenderDebitData, LendingMode as LendingMode$1, LstAcceptedInput } from '@1delta/calldata-sdk'; import { RelayProxyConfig } from '@1delta/proxy-fetch'; import { TermMarketConfig, LiquityBranchConfig, LiquityConfigChain, RiverMarketConfig, RiverConfigChain, RiverChainData, InverseMarketConfig, InverseConfigChain, InverseChainData, FraxlendConfigChain, ResupplyConfigChain, CoolerConfigChain, LlamaLendMarketConfig, LlamaLendConfigChain, LlamaLendChainData, UsddMarketConfig, UsddConfigChain, UsddChainData, FrankencoinMarketConfig, FrankencoinConfigChain, FrankencoinChainData, TellerPoolConfig, MorphoTypeVaultEntry } from '@1delta/data-sdk'; export { MorphoLensAbi } from '@1delta/abis'; interface GenericCurrency { /** chainId as string */ chainId: string; /** lower case address */ address: string; /** name */ name?: string; /** symbol */ symbol?: string; /** decimal number */ decimals: number; /** extensible */ [k: string]: any; } type GenericTokenList = { [addressLowerCase: string]: GenericCurrency; }; interface BaseYields$1 { variableBorrowRate: number; stableBorrowRate: number; depositRate: number; } interface RewardEntry$1 extends BaseYields$1 { asset: string; } type RewardsList$1 = RewardEntry$1[]; /** * The knob the BORROWER turns at open, when this config's numbers depend on one. * * Absent on every protocol whose factors are constants (Aave, Morpho, Compound…). * Present where a per-loan choice moves them — LlamaLend's band count `N` moves * the collateral factor; Liquity's chosen interest rate moves the cost. * * This describes the DOMAIN only. The value a given position actually chose is * per-position and lives in the user-data `modes[posId]` slot — it cannot live * here, because a lender with sub-accounts (Liquity troves, TermMax GTs) has * several live values in one market at once. * * `kind` is what tells a consumer how to READ that number: without it a UI * renders a band count of 15 as "e-mode 15", since `modes` historically only * ever carried e-mode categories. * * See POSITION_PARAMETERS_PLAN.md. */ interface OpenParameter { /** Discriminator — how to interpret the matching `modes[posId]` value. */ /** * Discriminator — how to interpret the matching `modes[posId]` value. * * `twyne-liq-ltv` is the IDENTITY case: the borrower's chosen value IS the * liquidation threshold (1e4 on chain, a fraction here), so no `curve` is * needed to price a position at its own parameter — see `identityMapping`. */ kind: 'llamalend-bands' | 'interest-rate' | 'twyne-liq-ltv'; /** Which of this config's numbers moves with the parameter. */ dimension: 'collateralFactor' | 'rate'; /** Allowed values: a continuous range, or a discrete set. */ domain: { min: number; max: number; } | { values: number[]; }; /** * The value THIS config's numbers were computed at. A consumer that quotes a * different value must recompute — it must not reuse `collateralFactor`. */ default: number; /** * `true` ⇒ fixed for the life of the loan; changing it means close & reopen * (LlamaLend — `_add_collateral_borrow` reuses the tick width). * `false` ⇒ adjustable in place (Liquity), subject to the friction below. */ immutableAfterOpen: boolean; /** Adjustment cooldown, when mutable (Liquity `interestRateAdjCooldownSeconds`). */ adjustCooldownSeconds?: number; /** * The parameter→factor map, when the moved dimension is derivable per value — * LlamaLend's full `bandLtv` curve (`curve["10"]` = the collateral factor at * `N = 10`). This is what lets a GENERIC consumer price a position at its * ACTUAL parameter value instead of the default's: `modes[posId]` carries the * value, and `curve[String(value)]` is the factor it implies. Without it, * every consumer that keyed `configs[mode]` fell back to factor 1 for any * borrower off the default — reporting borrow capacity a Controller would * refuse. Absent where the mapping is not a pure function of the value * (`interest-rate`, where the chosen rate moves cost, not a factor). */ curve?: { [value: string]: number; }; /** * `true` ⇒ the parameter VALUE IS the moved dimension, so a consumer prices a * position by using the value directly and needs neither a `curve` nor the * default's numbers (Twyne: the borrower's chosen `twyneLiqLTV` IS the * liquidation threshold). Without this flag a curve-less declaration falls * back to the DEFAULT point, which for Twyne is the band's FLOOR — i.e. it * would price every position as if it had bought no extra LTV at all, which * is the entire product missing. */ identityMapping?: boolean; } interface ConfigEntry { category: number; borrowCollateralFactor: number; collateralFactor: number; borrowFactor: number; /** * Liquidation penalty for this mode, as a fraction of the repaid debt that * the liquidator receives on top of par (e.g. `0.05` = 5% bonus). Mode-/ * e-mode-specific where the protocol supports it (Aave e-modes, Dolomite * categories, Euler vaults). `0` when the asset cannot be liquidated in this * mode or the protocol does not expose a penalty. */ liquidationPenalty: number; /** * Max fraction of debt repayable in a single liquidation (0..1). Mirrors the * pool-level {@link PoolData.closeFactor}; not e-mode-specific in any * supported protocol, so this is the same value across every mode of a * market — duplicated here for per-mode consumer convenience. */ closeFactor: number; /** * Liquidation target health factor (e.g. 1.05). Set only by protocols whose * liquidation engine repays just enough debt to restore a position to a * target HF instead of using a fixed close factor (Aave V4, spoke-level). * Mirrors {@link PoolData.targetHealthFactor}; `undefined` where the protocol * has no such parameter. */ targetHealthFactor?: number; collateralDisabled?: boolean; debtDisabled?: boolean; /** Borrower-chosen open-time parameter this config's numbers depend on. */ openParameter?: OpenParameter; } interface PoolConfig { [category: string]: ConfigEntry; } /** * The market's supply-side ACCUMULATOR at this snapshot — the monotone * quantity such that one unit deposited at t₀ is worth `index(t₁)/index(t₀)` * units of the same asset at t₁. Two samples give the exact realized return * between them with no compounding assumption, which is what a quoted rate * series cannot do (a rate is a point sample; an index is an integral). * * Read from the SAME multicall the public-data fetcher already makes — it * costs no extra RPC — and persisted by yield-tracer into * `market_index_snapshots` (`source = 'live-cron'`). * * Decimal STRINGS, never floats: only the RATIO between two samples matters, * and it depends on digits float64 discards. Scale per `kind`: * - `ray` — Aave `liquidityIndex` / `variableBorrowIndex` ÷ 1e27 (≈ 1.x) * - `exchange_rate` — Compound V2 `exchangeRateStored` ÷ 1e18 (underlying per cToken, ×10^(dec−8)) * - `assets_per_share` — Morpho `totalSupplyAssets / totalSupplyShares` (raw ÷ raw, 30 dp — matches the `lending-owners` backfill exactly, so live and replayed rows splice) * - `share_price` — ERC-4626-style `convertToAssets(1 share)` in raw units (Euler) */ interface MarketAccumulator { supplyIndex: string; borrowIndex?: string; kind: 'ray' | 'exchange_rate' | 'assets_per_share' | 'share_price'; } interface PoolData { poolId: string; /** Supply/borrow accumulator — see {@link MarketAccumulator}. Absent where * the family's fetch does not read one. */ accumulator?: MarketAccumulator; underlying: string; asset: GenericCurrency; totalDeposits: string; totalDebtStable: string; totalDebt: string; totalLiquidity: number; /** * Cap-adjusted borrow liquidity in token units. * `min(totalLiquidity, max(0, borrowCap - totalDebt))` when `borrowCap > 0`. * Equals `totalLiquidity` when no borrow cap is active (`borrowCap === 0`). */ borrowLiquidity: number; totalDepositsUSD: number; totalDebtStableUSD: number; totalDebtUSD: number; totalLiquidityUSD: number; borrowLiquidityUSD: number; /** * Borrow utilization as a 0..1 fraction — the IRM input for this market. * Pool protocols: `totalDebt / totalDeposits` of the market itself. * Shared-liquidity protocols (Fluid): the Liquidity-layer utilization of * this row's token, since rates are set at that layer — never a * vault-level ratio. */ utilization: number; /** * Fluid collateral rows only: share of the vault's collateral currently * locked below the Liquidity-layer withdrawal limit * (`withdrawLimit / totalSupplyVault`, 0..1). */ lockupRatio?: number; /** * Totals in the IRM's own utilization basis (token units), for protocols * where the rate curve is defined over a larger pool than this row: * Aave V4 hub (`liquidity + drawn + swept` / `drawn`), Fluid Liquidity * layer, Gearbox pool. `irmTotalDebt / irmTotalDeposits == utilization`. * Rate-impact simulations must shift these, not the row totals. */ irmTotalDeposits?: number; irmTotalDebt?: number; depositRate: number; variableBorrowRate: number; stableBorrowRate: number; intrinsicYield: number; /** * Gearbox per-collateral quota rate (percent APR), a borrow-side carrying * cost on quoted collateral. Already netted into `intrinsicYield` on the * collateral market — surfaced for attribution/borrow-side display; do not * subtract again. Absent for lenders without quotas. */ quotaRate?: number; rewards: RewardsList$1; decimals: number; config: PoolConfig; collateralActive: boolean; borrowingEnabled: boolean; depositsEnabled: boolean; hasStable: boolean; isActive: boolean; isFrozen: boolean; borrowCap: number; supplyCap: number; debtCeiling: string; /** * Max fraction of a borrower's debt that can be repaid in a single * liquidation (0..1). Protocol-level constant for most lenders: * `0.5` for Aave (rises to `1` once health factor drops below the * close-factor threshold) and Compound V2's `closeFactorMantissa`. * `1` (full liquidation) for isolated / credit-account protocols * (Compound V3, Morpho, Euler, Fluid, Gearbox, Dolomite, Silo). */ closeFactor: number; /** * Liquidation target health factor (e.g. 1.05) for protocols that liquidate * to a target HF rather than by a fixed close factor (Aave V4, spoke-level). * `undefined` where the protocol has no such parameter. */ targetHealthFactor?: number; } type LenderData = { [chainId: string]: { data: { [lender: string]: { data: { [poolId: string]: PoolData; }; chainId: string; }; }; lastFetched: number; }; }; interface ParsedResponse { price: number; time: number; } interface ChainLinkResponse extends ParsedResponse { roundId: number; } interface LenderRewardsEntry { deposit: number; borrow: number; } type LenderRewardsMap = { [key: string]: LenderRewardsEntry; }; type NumberMap = { [key: string]: number; }; type FullLenderRewardsMap = { [chainId: string]: { [lender: string]: LenderRewardsMap; }; }; type AdditionalYields = { intrinsicYields: NumberMap; lenderRewards: FullLenderRewardsMap; loaded: boolean; }; declare interface GeneralCall { address: string; name: string; params?: any[]; /** * Per-call ABI override. The multicall layer already honours this * (`call.abi ?? abi` in `getLenderUserDataResult`) — it was simply untyped, so * every builder that needs it declared its array as `any[]` and lost checking * on the rest of the call shape too. */ abi?: any; } type TokenList = { [address: string]: { decimals: number; assetGroup?: string; }; }; interface MulticallRetryParams { chain: string; calls: any[]; abi: any; batchSize?: number; maxRetries?: number; providerId?: number; allowFailure?: boolean; overrdies?: Record; logErrors?: boolean; } type MulticallRetryFunction = (params: MulticallRetryParams) => Promise; /** * Options a caller may ask the client factory for. * * `timeoutMs` is optional in both directions: a factory that ignores it still * satisfies this type (TypeScript accepts a 2-arg function here), and every * existing implementation does exactly that. Only the sharded multicall passes * it, and only because viem's 10s default turns one stalled request into a 10s * response for the whole batch. */ interface GetEvmClientOptions { timeoutMs?: number; } type GetEvmClientFunction = (chain: string, rpcId?: number, options?: GetEvmClientOptions) => PublicClient; type SerializedBigNumber = string; interface LenderUserQuery { /** the lender enum (Note that for multi-market ones, it is not the lender but the group) */ lender: any; /** user address */ account: string; /** custom parameters for fetching e.g. multi-market lenders */ params?: any[]; /** custom parameters for spceifying assets */ assets?: any[]; } /** * Collapse per-market queries whose builder + RPC call are identical * across every market key into a single query. Without this, a chain * with N Gearbox CMs (or N Morpho markets) would fan out into N * identical RPC calls — all returning the same owner-scoped data. * * - **Morpho** (`MORPHO_BLUE_*` / `LISTA_DAO_*`): one query per family, * `params` carries the original per-market lender keys. * - **Gearbox V3** (`GEARBOX_V3_*`): one `Lender.GEARBOX_V3` query per * chain; `AccountCompressor.getCreditAccounts` already scopes by * `configurators` + `owner`, so we only need to run the call once. * `params` carries the requested per-CM lender keys in case the * parser wants to emit empty buckets for CMs the user has no CAs * in (currently it only emits populated ones — same outcome, but * without the 24-duplicate-RPC-call penalty). */ declare function organizeUserQueries(queries: LenderUserQuery[]): LenderUserQuery[]; interface UserLendingPosition { deposits: string; debt: string; debtStable: number; depositsUSD: number; debtUSD: number; debtStableUSD: number; collateralEnabled: boolean; claimableRewards: number; } type UserRewardEntry = { asset: string; totalRewards: number; claimableRewards: number; }[]; interface LenderUserResponse { chainId: string; account: string; lendingPositions: { [lender: string]: { [marketUid: string]: UserLendingPosition; }; }; rewards: UserRewardEntry; } interface AaveV2UserReserveResponse { chainId: string; account: string; lendingPositions: { [marketUid: string]: BaseLendingPositions; }; rewards: UserRewardEntry; } interface BaseLendingPositions { marketUid: string; /** * Optional loan binding. Absent ⇒ the position applies to ALL loans (e.g. shared collateral); * present ⇒ the position is strictly tied to that loan. (Distinct from sub-account `accountId`.) */ loanId?: string; deposits: string; debt: string; debtStable: string; depositsUSD: number; debtUSD: number; debtStableUSD: number; depositsUSDOracle?: number; debtUSDOracle?: number; debtStableUSDOracle?: number; collateralEnabled: boolean; claimableRewards: number; /** max withdrawable token amount (capped at deposit balance) */ withdrawable?: string; /** max borrowable token amount */ borrowable?: string; /** remaining deposit capacity in token units (null = uncapped) */ depositable?: string; /** Underlying asset info (asset metadata, prices, oracle price) */ underlyingInfo?: { asset: any; oraclePrice?: any; prices: any; }; /** Standard collateral deposit amount (borrowable, accrues interest) */ depositsStandard?: string; /** Protected collateral deposit amount (non-borrowable, no interest accrual) */ depositsProtected?: string; /** Collateral share exchange rate: assets per 10^decimals shares */ collateralRate?: string; } /** * Early-repayment policy for a fixed-term market. `none` = a borrower can exit * any time at the current market price with no penalty (Morpho Midnight buys the * debt units back on the order book). `penalty` = a per-loan penalty applies * (Lista); the concrete amount is position-level, on `ListaTermLoan.earlyRepayPenalty`. * `discount` = repaying early costs LESS than face value (Exactly: the pool's * unassigned earnings are rebated to the early repayer — the exact amount comes * from `previewRepayAtMaturity` at repay time). Note that a `discount` lender * can still have a LATE-repay penalty (see `fees.latePenaltyApr`). */ type FixedTermEarlyRepay = { kind: 'none' | 'penalty' | 'discount'; }; /** * Who fronts a fixed term — the cross-protocol answer to "who is offering these * borrow/lend terms". Consistent across lenders even though the shape differs: * - `broker`: a single market broker sets the term (Lista LendingBroker proxy). * `address` is that broker contract — one per market, stable, public. * - `orderbook`: the term is an aggregate of many signed maker offers (Morpho * Midnight) or continuous repo-token listings (Term Finance secondary * market). There is no single provider at the market level, so `address` is * omitted; the concrete maker(s) are per-offer and only known at quote time. * - `auction`: the term is discovered by a periodic sealed-bid auction (Term * Finance primary market). `address` is the per-repo auction/servicer venue. * - `pool`: the term is fronted by a passive liquidity pool with a * utilization-curve rate (Exactly fixed pools backed by the floating pool). * `address` is the Market contract. */ interface FixedTermProvider { kind: 'broker' | 'orderbook' | 'auction' | 'pool'; /** The single counterparty/venue contract, when there is one (Lista broker, Term servicer). */ address?: string; } /** * Origination window for a fixed-term market whose terms are only obtainable * during a bounded round rather than continuously (`provider.kind: 'auction'` * — Term Finance). * * This is the difference between "the rate card is empty right now" and "this * market is dead": between rounds a Term repo still has a maturity, collateral * params and a last-cleared rate, but nothing can be borrowed until the next * round is listed. Without it every closed repo renders as an ordinary * borrowable market whose action silently cannot be built. * * `status` is a snapshot at fetch time; the timestamps are raw so a consumer * can re-derive it live (and drive a countdown) against a cached response. */ interface FixedTermAuction { /** * Round lifecycle at fetch time: * - `upcoming` — listed but not yet accepting submissions (`now < startTime`) * - `open` — accepting sealed bids/offers (`startTime ≤ now < revealTime`) * - `revealing` — submissions closed, prices revealing / clearing pending * (`revealTime ≤ now < endTime`) * - `closed` — no round is currently listed for this market. Borrowing is * unavailable until the next one; lending may still be * possible on the secondary repo-token book. */ status: 'upcoming' | 'open' | 'revealing' | 'closed'; /** * Can a NEW borrow be opened right now? True only inside an open round — * Term borrow origination is a sealed bid, so there is no other entry point. * * Consume this rather than re-deriving from `status`: it is the single flag * a borrow CTA should gate on, and it stays correct if more statuses appear. * It is NOT the same as `canLend` — see below. */ canBorrow: boolean; /** * Can a NEW lend position be opened right now? Deliberately decoupled from * `canBorrow`: the primary auction is only one of two lend surfaces, and * buying repo tokens on the secondary book works between rounds. So a closed * round leaves the market lend-only rather than fully inert, and a UI that * greys out the whole market would be wrong. */ canLend: boolean; /** * Seconds until submissions close (`revealTime − now`), or undefined when no * round is open. A snapshot — for a live countdown, derive from `revealTime`. */ secondsUntilClose?: number; /** * Ready-to-display consequences of this market's origination model, most * important first. Mirrors `params.market.teller.implications`: auction * mechanics are unusual enough that a UI showing only a rate misleads. */ implications?: string[]; /** Round id. Absent when `status: 'closed'`. */ id?: string; /** Submissions open (unix seconds). Absent when `status: 'closed'`. */ startTime?: number; /** Submissions CLOSE / reveal begins (unix seconds). Absent when closed. */ revealTime?: number; /** Round clears (unix seconds). Absent when closed. */ endTime?: number; /** * Minimum submission size in loan-token base units (raw). Term rounds carry a * real floor (e.g. 1000 USDC) — an amount below it cannot be submitted at all, * so it belongs next to the terms rather than surfacing as a failed action. */ minBorrowAmount?: string; minLendAmount?: string; } /** * Canonical fixed-term market descriptor, emitted on `params.market.fixedTerm` * for EVERY fixed-rate / fixed-maturity market (Lista brokered + Morpho * Midnight) so consumers read ONE shape instead of branching per protocol. The * rate-card menu stays on `params.market.terms`; this carries the cross-protocol * maturity + fee + early-repay facts. Absent on non-fixed-term markets. */ interface FixedTermInfo { /** Underlying fixed-term protocol shape. */ model: 'lista' | 'midnight' | 'term' | 'exactly' | 'teller' | 'termmax'; /** * Single fixed calendar maturity (unix secs). Undefined for rolling-duration * menus (Lista) and multi-maturity markets (Exactly — the maturity menu lives * on `params.market.terms`, keyed by `termId` = maturity timestamp). */ maturity?: number; /** Market-level fees. Empty for lenders without them (Lista); values are 0 when genuinely off. */ fees: { /** Continuous fee, %/yr — ongoing lender-side haircut (Midnight). */ continuousFeeApr?: number; /** Settlement fee as a fraction at the current time-to-maturity (Midnight). */ settlementFee?: number; /** * LATE-repayment penalty, %/yr, accruing per second on overdue debt after * maturity until repaid (Exactly `penaltyRate`). Absent for lenders whose * overdue handling is liquidation-only (Midnight/Term). */ latePenaltyApr?: number; /** * UPFRONT origination fee as a percent of the borrowed principal, charged * once at borrow time (Teller: market fee + protocol fee). Not an APR. */ originationFeePercent?: number; }; /** Early-repayment policy. */ earlyRepay: FixedTermEarlyRepay; /** Who offers the term (Lista broker vs Midnight order book). */ provider?: FixedTermProvider; /** * Origination window, for `provider.kind: 'auction'` markets only (Term * Finance). Absent for lenders whose terms are continuously available — a * missing `auction` means "no window applies", NOT "closed". * * Where a FILL-NOW surface also exists (Term Terminal 1 limit orders), * `canBorrow`/`canLend` reflect ALL entry paths — they can be true while * `status` is `closed`. `fillNow` below carries the instant-path detail. */ auction?: FixedTermAuction; /** * Instant (limit-order) origination liquidity, where the lender has one * (Term Finance Terminal 1). Unlike the auction, these rates are obtainable * at fill time: a taker settles a maker's standing order at the order's own * rate. Absent = the lender has no fill-now surface or none is live. */ fillNow?: FixedTermFillNow; } /** * Fill-now (limit-order) origination summary for a fixed-term market. Rates * are best-executable percents in the lender's own day-count convention; * liquidity is loan-token assets (human-scaled). The per-level book lives on * `params.market.book` — this is the CTA-gating summary. */ interface FixedTermFillNow { /** A NEW borrow can be filled instantly against standing lend orders. */ canBorrow: boolean; /** A NEW lend can be filled instantly against standing borrow orders. */ canLend: boolean; /** Best instantly-executable borrow APR, percent. */ borrowAprPct?: number; /** Best instantly-executable lend APR, percent. */ lendAprPct?: number; /** Instantly-borrowable depth, loan-token assets. */ borrowLiquidity?: number; /** Instantly-lendable depth, loan-token assets. */ lendLiquidity?: number; } /** * A single fixed-term loan, attached to its own entry in the positions array. * * Named for Lista (the first producer) but SHARED by every fixed-term lender * that emits per-loan rows — Lista, Exactly, TermMax, Teller. Fields are * therefore mostly optional and several are lender-specific; see * [FIXED_TERM_REPAY_TERMS.md](../../../../FIXED_TERM_REPAY_TERMS.md) for which * lender populates what and for the exact repay economics behind each number. * * `loanId` is the WRITE TARGET and its meaning differs per lender (Lista posId / * Exactly maturity-as-string / TermMax gtId / Teller bidId) — check the lender * before using it. `termId` is the RATE-MENU id and is NOT interchangeable with * it (Exactly is the only lender where the two coincide, both = maturity). */ interface ListaTermLoan { /** loanId — the repay target for the LISTA_BROKER_REPAY composer op. For fixed loans this is the * posId; for the dynamic (flexible) loan it is the dynamic sentinel (type(uint128).max). * Other lenders reuse the slot: Exactly = String(maturity), TermMax = gtId, Teller = bidId. */ loanId: string; /** true for the flexible (dynamic / variable-rate) loan; fixed loans omit it */ isDynamic?: boolean; /** best-effort term product id (matched from duration vs the current menu); may be undefined */ termId?: number; /** outstanding debt in loan-token units. Lista: principal + accrued interest. * Static-face-value lenders (Exactly / TermMax / Term): the EXIT-NOW cost — * for Exactly that is discounted early and penalty-inflated when overdue, so * compare against `faceValue` rather than assuming it is the face. */ debt: string; /** locked annualised borrow rate in PERCENT (e.g. 3.857 = 3.857% APR) */ apr?: number; /** unix maturity timestamp */ maturity?: number; termDays?: number; /** outstanding accrued interest in loan-token units */ accruedInterest?: string; /** early-repayment penalty (loan-token units) to close the loan now; 0 once matured. * Lista only — the OPPOSITE sign to Exactly's `earlyRepayDiscount` below. */ earlyRepayPenalty?: string; isMatured?: boolean; /** amount owed AT maturity (principal + fee). Static — no accrual index; it * grows only via a late penalty where the protocol has one. */ faceValue?: string; /** Exactly: rebate for repaying BEFORE maturity (`faceValue − debt`). Exactly * never charges an early-repay fee, but this is 0 when the fixed pool has no * unassigned earnings left, so it is not a guaranteed saving. */ earlyRepayDiscount?: string; /** Exactly: penalty accrued so far past maturity (`debt − faceValue`). */ latePenalty?: string; /** Exactly: further penalty per additional day overdue — LINEAR on face, not * compounding. */ latePenaltyPerDay?: string; /** annualized late-penalty rate in PERCENT (Exactly `penaltyRate`; ~164 %/yr). * A mutable market parameter, snapshotted per fetch. */ latePenaltyApr?: number; /** seconds past maturity; 0 until overdue */ secondsLate?: number; } interface MorphoLendingPositions extends BaseLendingPositions { isWhitelisted?: boolean; /** * Lista fixed-term loan detail. A brokered market emits, under the same accountId, one extra * positions[] entry per fixed-term loan carrying this `term` (with termId + term data); its * debt sits in `debtStable` and `loanId` is the repay target. The aggregate loan position * (no `term`) carries the rollup `debtStable` total for health. */ term?: ListaTermLoan; } interface AaveV3UserReserveResponse { chainId: string; lendingPositions: { [marketUid: string]: BaseLendingPositions; }; userEMode: number; rewards?: any; account: string; } interface MorphoUserReserveResponse { chainId: string; lendingPositions: { [marketUid: string]: MorphoLendingPositions; }; id: string; rewards?: any; account: string; } interface CompoundV3UserReserveResponse { lendingPositions: { [marketUid: string]: { deposits: string; debt: string; debtStable: string; depositsUSD: number; debtUSD: number; debtStableUSD: number; collateralEnabled: boolean; isAllowed: boolean; }; }; chainId: string; baseAsset: string; rewards: UserRewardEntry; account: string; } interface AaveV3Public extends LenderPublicBase { lastUpdateTimestamp?: number; collateralActive?: boolean; hasStable?: boolean; isActive?: boolean; isFrozen?: boolean; eMode?: EModeData; debtCeiling: number; supplyCap: number; borrowCap: number; config: { [modeId: string]: LenderConfigData; }; } /** * Interface shared by all lenders */ interface LenderPublicBase extends LenderYields$1, LenderTotalAmounts { borrowingEnabled: boolean; depositsEnabled: boolean; /** * Max fraction of a borrower's debt repayable in one liquidation (0..1). * Protocol-level constant for most lenders (Aave `0.5`, Compound V2 * `closeFactorMantissa`); `1` (full liquidation) for isolated / * credit-account protocols. Defaults to `1` when not applicable. */ closeFactor: number; /** * Liquidation target health factor (e.g. 1.05) for protocols that liquidate * to a target HF rather than by a fixed close factor (Aave V4, spoke-level). * `undefined` where the protocol has no such parameter. */ targetHealthFactor?: number; config: LenderConfigMap; } interface LenderYields$1 extends BaseYields$1 { intrinsicYield: number; rewards?: RewardsList$1; } interface LenderTotalAmounts { totalDebt: number; totalDebtStable: number; totalDeposits: number; totalLiquidity: number; /** USD values */ totalDebtUSD: number; totalDebtStableUSD: number; totalDepositsUSD: number; totalLiquidityUSD: number; } interface LenderConfigMap { [modeId: string]: LenderConfigData; } /** * The Mode-specific configuration for a lender asset */ interface LenderConfigData { label?: string; category: number; borrowCollateralFactor: number; collateralFactor: number; borrowFactor: number; /** * Liquidation penalty for this mode, as a fraction of repaid debt the * liquidator receives on top of par (e.g. `0.05` = 5% bonus). Mode-specific * where the protocol supports it (Aave e-modes, Dolomite categories, Euler * vaults); `0` when not liquidatable in this mode or unsupported. */ liquidationPenalty: number; /** * Max fraction of debt repayable in a single liquidation (0..1). Mirrors the * pool-level {@link LenderPublicBase.closeFactor}; identical across every mode * of a market (no supported protocol varies close factor by e-mode). */ closeFactor: number; /** * Liquidation target health factor (e.g. 1.05) for protocols that liquidate * to a target HF rather than by a fixed close factor (Aave V4, spoke-level). * Mirrors {@link LenderPublicBase.targetHealthFactor}; `undefined` otherwise. */ targetHealthFactor?: number; collateralDisabled?: boolean; debtDisabled?: boolean; /** * Borrower-chosen open-time parameter this config's numbers depend on. * Mirrors the API-side `ConfigEntry.openParameter`; see * POSITION_PARAMETERS_PLAN.md. */ openParameter?: OpenParameter; } interface ModeBase { category: number; label: string; } interface EModeData extends ModeBase { borrowCollateralFactor: number; collateralFactor: number; priceSource: string; } interface AaveV2Public extends LenderPublicBase { lastUpdateTimestamp?: number; decimals?: number; reserveFactor?: SerializedBigNumber; collateralActive?: boolean; hasStable?: boolean; isActive?: boolean; isFrozen?: boolean; } interface CompoundV3Public extends LenderPublicBase { supplyCap: number; collateralActive: boolean; utilization: number; } interface UserApr { apr: number; borrowApr: number; depositApr: number; } interface InitUserReserveResponse { chainId: string; lendingPositions: { [posId: string]: { [marketUid: string]: { deposits: string; debt: string; depositsUSD: number; debtUSD: number; debtStable: '0'; debtStableUSD: 0; collateralEnabled: boolean; isAllowed: boolean; }; }; }; /** Mode per position ID, separated from positions for clean array serialization */ modes: { [posId: string]: any; }; account: string; } interface InitPublic extends LenderPublicBase { collateralActive?: boolean; debtCeiling: number; supplyCap: number; borrowCap: number; } declare function getMorphoTypeMarketConverter(lender: string, chainId: string, prices: { [a: string]: number; }, additionalYields: AdditionalYields, tokenList?: GenericTokenList, marketsOverride?: string[]): [(data: any[]) => any | undefined, number]; declare const getLenderPublicData: (chainId: string, lenders: string[], prices: { [asset: string]: number; }, additionalYields: AdditionalYields, multicallRetry: MulticallRetryFunction, tokenList?: () => Promise) => Promise<{ [lender: string]: any; }>; declare const getLenderPublicDataViaApi: (chainId: string, lenders: string[], prices: { [asset: string]: number; }, additionalYields: AdditionalYields, tokenList?: () => Promise, includeUnlisted?: boolean) => Promise<{ [lender: string]: any; }>; /** * Returns true when the lender should ONLY use the API path (no on-chain * fallback). Currently Morpho-type only — the Morpho GraphQL indexer has * been reliable on every chain it supports, so there's no benefit to * double-fetching. * * Exported so the config-consistency test can assert the invariant that makes * the Morpho list safe: a chain routed to the on-chain path MUST have a * `MORPHO_LENS` entry, otherwise `buildMorphoCall` produces a call with an * undefined address and the chain silently yields no markets at all. */ declare function lenderApiOnly(lender: string, chainId: string): boolean; declare const getLenderPublicDataAll: (chainId: string, lenders: string[], prices: { [asset: string]: number; }, additionalYields: AdditionalYields, multicallRetry: MulticallRetryFunction, tokenList?: () => Promise, /** * Omit to take the per-chain default from `morphoIncludesUnlisted` — the * SAME resolver `fetchOraclePrices` uses, so the market roster and the price * roster cannot disagree. Pass a boolean only to override for one call. */ includeUnlistedMorphoMarkets?: boolean) => Promise<{ [lender: string]: any; }>; interface PreparedCall { address: string; functionName: string; params: any[]; abi: any[]; } interface RawRpcCall { jsonrpc: '2.0'; id: number; method: 'eth_call'; params: [ { to: string; data: string; }, string ]; } interface RawRpcBatch { batchIndex: number; calls: RawRpcCall[]; callMetadata: PreparedCall[]; } declare const multicall3Abi: readonly [{ readonly type: "function"; readonly name: "aggregate3"; readonly stateMutability: "payable"; readonly inputs: readonly [{ readonly name: "calls"; readonly type: "tuple[]"; readonly components: readonly [{ readonly name: "target"; readonly type: "address"; }, { readonly name: "allowFailure"; readonly type: "bool"; }, { readonly name: "callData"; readonly type: "bytes"; }]; }]; readonly outputs: readonly [{ readonly name: "returnData"; readonly type: "tuple[]"; readonly components: readonly [{ readonly name: "success"; readonly type: "bool"; }, { readonly name: "returnData"; readonly type: "bytes"; }]; }]; }]; interface MulticallRpcBatch { batchIndex: number; call: RawRpcCall; callMetadata: PreparedCall[]; } declare function createRawRpcCalls(preparedCalls: PreparedCall[], batchSize?: number, blockTag?: string): RawRpcBatch[]; /** * Creates a single multicall3 aggregate3 RPC call that batches all prepared calls * This reduces multiple RPC calls to a single call, avoiding rate limiting */ declare function createMulticallRpcCall(preparedCalls: PreparedCall[], multicallAddress: string, batchSize?: number, blockTag?: string, allowFailure?: boolean): MulticallRpcBatch[]; type Call = GeneralCall; /** * Sentinel written into the result array for a call that returned NO data — * a revert, an RPC error, or a whole `aggregate3` chunk that was rejected * (rate limit, subrequest cap, dropped connection: viem marks every call in a * rejected chunk as `status: 'failure'`). * * It is NOT a zero value. Parsers must skip these slots — coercing one to `0` * turns a failed read into a phantom "no balance" position and, worse, makes a * real deposit or debt silently disappear from a user's portfolio. */ declare const MULTICALL_FAILURE = "0x"; /** True when a multicall slot holds no usable data (see {@link MULTICALL_FAILURE}). */ declare const isFailedCall: (value: unknown) => boolean; /** Reported for each endpoint that failed to serve a batch. */ interface EndpointFailure { chainId: string; /** Endpoint URL, or `rpc#` when the transport does not expose one. */ url: string; rpcId: number; /** `transport` — the request itself died. `slots` — it answered with nothing but failures. */ kind: 'transport' | 'slots'; } interface MulticallEndpointOptions { /** * Endpoint URLs already attempted for this call set. Failover consults it so * a retry lands on an endpoint that has NOT already failed — see * {@link resolveEndpoint}. */ tried?: Set; /** Invoked for every endpoint that fails, so callers can demote it. */ onEndpointFailure?: (info: EndpointFailure) => void; } declare function prepareMulticallInputs(abi: any[], calls: Call[]): PreparedCall[]; interface PreparedUserDataRpcCalls { batches: MulticallRpcBatch[]; preparedCalls: PreparedCall[]; queries: LenderUserQuery[]; rpcCalls: RawRpcCall[]; } interface ChainQuery { chainId: string; providerOptions: ProviderOptions; /** query details - if not provided, it will be populated with context vars */ queries?: LenderUserQuery[]; } interface ProviderOptions { getEvmClient?: GetEvmClientFunction; allowFailure?: boolean; batchSize?: number; retries?: number; logs?: boolean; } interface BaseLendingPosition { deposits: string; debt: string; debtStable: string; /** * Raw par magnitude of the debt (Dolomite only): the index-scaled principal, * stable as interest accrues. Used for dust-free native repay-all via the * router's `depositParPayable`. */ debtPar?: string; debtShares?: string; depositShares?: string; depositsUSD: number; debtUSD: number; debtStableUSD: number; collateralEnabled: boolean; claimableRewards?: number; } interface BalanceData { rewards?: any; borrowDiscountedCollateral: number; borrowDiscountedCollateralAllActive: number; collateral: number; collateralAllActive: number; deposits: number; debt: number; adjustedDebt: number; nav: number; deposits24h: number; debt24h: number; nav24h: number; } interface AprData { apr: number; borrowApr: number; depositApr: number; rewards: any; rewardApr: number; rewardDepositApr: number; rewardBorrowApr: number; intrinsicApr: number; intrinsicDepositApr: number; intrinsicBorrowApr: number; } /** Sinlge market config */ interface UserConfig { selectedMode: string; id: string; /** if defined and false, the user cannot interact * with the market unless whitelisted */ isWhitelisted?: boolean; } interface BasicReserveResponse { chainId: string; account: string; /** Single asset positions within a lender */ lendingPositions: { [id: string]: { [marketUid: string]: BaseLendingPosition; }; }; /** Totals for account in lender */ balanceData: { [id: string]: BalanceData; }; /** Apr summary for lender in account */ aprData: { [id: string]: AprData; }; /** Configs per sub account - determines e.g. E-Modes */ userConfigs: { [id: string]: UserConfig; }; /** Auxiliary rewards data */ rewards?: any; } type MarketConfigEntry = { category: string; label: string; borrowCollateralFactor: number | null; collateralFactor: number | null; borrowFactor: number | null; /** Liquidation penalty for this mode as a fraction of repaid debt (e.g. 0.05 = 5%). */ liquidationPenalty: number | null; /** Max fraction of debt repayable per liquidation (0..1); mirrors the pool-level value. */ closeFactor: number | null; /** Liquidation target health factor (Aave V4); null/absent where unsupported. */ targetHealthFactor?: number | null; debtDisabled: boolean; collateralDisabled: boolean; /** * Declares this market PARAMETERIZED: the position's `modes[posId]` slot * carries a borrower-chosen VALUE (LlamaLend's band count), not a config * key. Consumers must resolve configs through `resolveModeConfig`, never a * bare `configs[mode]` — see that function for what the bare lookup broke. * Shape mirrors `OpenParameter` in `apiReturnType.ts`. */ openParameter?: { kind: string; dimension: string; domain: { min: number; max: number; } | { values: number[]; }; default: number; immutableAfterOpen: boolean; adjustCooldownSeconds?: number; curve?: { [value: string]: number; }; /** The value IS the moved dimension — no curve needed (Twyne). */ identityMapping?: boolean; }; }; type MarketConfigs = Record; type MarketFlags = { isActive: boolean | null; isFrozen: boolean | null; hasStable: boolean | null; borrowingEnabled: boolean | null; collateralActive: boolean | null; }; type AssetInfo = { chainId: string | null; address: string | null; symbol: string | null; name: string | null; decimals: number | null; logoURI: string | null; assetGroup: string | null; currencyId: string | null; props: unknown | null; }; type PriceInfo = { priceUsd: number | null; priceTs: any | null; priceUsd24h: number | null; priceTs24h: any | null; priceChange24h: number | null; }; type OraclePrice = { oraclePrice: number | null; oraclePriceUsd: number | null; }; type SumerMarketMeta = { groupId: number; isCToken: boolean; intraCRate: number; intraMintRate: number; intraSuRate: number; interCRate: number; interSuRate: number; }; type CompoundV2Metadata = { cToken: string; exchangeRate: string | undefined; cTokenDecimals: number | undefined; sumer?: SumerMarketMeta; }; type AaveMetadata = { aToken: string; vToken: string | undefined; sToken: string | undefined; }; type InitMetadata = { poolId: string; }; type EulerV2Metadata = { vault: string; dToken: string | undefined; oracle: string | undefined; interestRateModel: string | undefined; unitOfAccount: string | undefined; governorAdmin: string | undefined; }; type SiloV2Metadata = { silo: string; counterpartySilo: string; siloConfig: string; oracle: string; irm: string; shareTokens: { collateral: string; protected: string; debt: string; }; fees: { dao: string; deployer: string; liquidation: string; flashloan: string; }; }; type ProtocolParams = { metadata: CompoundV2Metadata; } | { metadata: AaveMetadata; } | { metadata: InitMetadata; } | { metadata: EulerV2Metadata; } | { metadata: SiloV2Metadata; }; type LenderYieldComplete = { depositRate: number | null; stableBorrowRate: number | null; variableBorrowRate: number | null; intrinsicYield: number | null; /** * Gearbox-style per-collateral quota rate (percent APR) — a borrow-side * carrying cost charged on quoted collateral. Already netted into * `intrinsicYield` on the collateral market; surfaced here for attribution. * Do NOT subtract again. Absent/0 for lenders without quotas. */ quotaRate?: number | null; underlying: string; configs: MarketConfigs | null; flags: MarketFlags | null; rewards: RewardsList$1 | null; asset: AssetInfo; price: PriceInfo; oraclePrice?: OraclePrice; borrowLiquidity: number | null; withdrawLiquidity: number | null; depositable: number | null; /** Max fraction of debt repayable per liquidation (0..1); pool-level constant. */ closeFactor?: number | null; /** Liquidation target health factor (Aave V4); null/absent where unsupported. */ targetHealthFactor?: number | null; params?: ProtocolParams; }; type LenderCrossPoolMeta = Record; type LenderToLenderCrossPoolMeta = Record; interface UserDataForSubAccount { accountId: string; health: number | null; /** Total USD borrowable while maintaining health >= 1 */ borrowCapacityUSD: number; balanceData: BalanceData; aprData: AprData; positions: BaseLendingPositions[]; userConfig: UserConfig; } type LenderInfo = { lenderKey: string; name: string; logoUri: string | null; }; /** Map of lender key → LenderInfo, typically scoped to one chain. */ type LenderInfoMap = Record; type UserData = { lender: string; chainId: string; account: string; data: UserDataForSubAccount[]; /** * Set when some of the lender's on-chain reads failed (revert, RPC error, * rejected multicall chunk) and the entry was built from what did come back. * Balances may therefore be understated — treat as "at least this much", * not as the user's full position. */ incomplete?: boolean; /** * Set when this entry did NOT come from the current read: the live read failed * and a previously COMPLETE snapshot was served in its place. The position was * accurate as of `staleAgeMs` ago; it is not a partial read (those are * `incomplete`) and it is never served for a lender that read successfully. */ stale?: boolean; /** Age of the served snapshot in ms. Only set alongside `stale`. */ staleAgeMs?: number; }; /** * How a lender's slice reacts to a failed read. * * Note what this is NOT keyed on: "is the position cross-margin". EVERY position * with debt is corrupted by a lost read — an isolated Morpho market computes its * health from a collateral read and a debt read, so losing either fabricates the * same nonsense a lost Aave reserve does. The question here is narrower and * purely mechanical: **what is the smallest thing we can void?** A failed slot * carries no market label, so the only unit we can void is the lender key. * * - `strict` — void the whole slice on any lost read. Correct when the slice * resolves to ONE risk computation, which is true both for a single-market * lender and for a multi-market lender that is nevertheless cross-margin * (Exactly scores every market under one per-chain Auditor). Nothing smaller * can be voided, and publishing the remainder would publish a fiction. * - `lenient` — publish, flag `incomplete`, and let per-record invariant * validation catch the corrupted one. Correct ONLY when the slice fans out to * many INDEPENDENT isolated positions, where voiding the key would discard * hundreds of intact markets to hide one — a cure worse than the disease. * * So leniency requires BOTH properties: many independent positions AND no shared * risk computation across them. */ type ReadFailurePolicy = 'strict' | 'lenient'; declare const getReadFailurePolicy: (lender: string) => ReadFailurePolicy; /** Why a lender's slice did not convert cleanly. */ type IncompleteReason = /** Every read in the slice failed. */ 'all-reads-failed' /** Reads failed on a strict lender — the whole risk set was voided. */ | 'partial-read-cross-margin' /** Reads failed on a lenient lender — surviving markets were published. */ | 'partial-read' /** The converter threw. */ | 'converter-error' /** The converted entry asserted something that cannot be true. */ | 'invariant-violation'; /** Reported per lender whose multicall slice did not convert cleanly. */ interface IncompleteLenderRead { lender: string; /** Number of slots in the lender's slice that returned no data. */ failedCalls: number; /** * Subset of `failedCalls` that could plausibly succeed on a re-read — i.e. * excluding calls known to have reverted. Zero means re-fetching this lender * is pointless: the markets in question always revert for this account. * Equals `failedCalls` when no `permanentFailures` set was supplied. */ retryableFailedCalls: number; /** Size of the lender's slice. */ totalCalls: number; /** True when nothing was published for this lender. */ dropped: boolean; /** What went wrong. */ reason: IncompleteReason; /** Extra context for logs (converter message, violation details). */ detail?: string; } interface ConvertLenderUserDataOptions { /** Invoked once per lender that did not convert cleanly — for logging / surfacing. */ onIncomplete?: (info: IncompleteLenderRead) => void; /** * Indices (into `rawResults`) of calls that failed deterministically, as * collected by `getLenderUserDataResult`. Used only to compute * `retryableFailedCalls`. */ permanentFailures?: Set; } /** * Converts the raw results into the desired format * * Slots that hold the multicall failure sentinel are NOT data: a failed read * says nothing about the user's position. Coercing one to zero is how a * rate-limited RPC ends up rendering phantom $0 rows and understated balances, * so failures are handled explicitly, in three gates: * * 1. **Failure policy** (see {@link ReadFailurePolicy}). Every read failing * drops the lender under either policy. Beyond that, a `strict` * (cross-margin) lender drops on ANY failure because its aggregates are only * meaningful over the complete set, while a `lenient` (multi-market) lender * publishes the markets that did read and is flagged `incomplete`. * 2. **Converter errors** are reported rather than swallowed — a throwing * converter used to leave a lender silently absent, indistinguishable from a * user with no position there. * 3. **Invariant validation** (see `validate.ts`) rejects sub-accounts that * cannot be true regardless of how the reads went — a `NaN` anywhere in the * aggregates, or (alongside failed reads) debt with no collateral behind it. * * Anything published after that is either complete or explicitly marked as not. * * @param chainId - The chain ID * @param queriesRaw - The queries to fetch data for * @param rawResults - The raw results from the multicall * @param lenderState - The state of the lender * @param options - Optional reporting hooks * @returns The converted data */ declare const convertLenderUserDataResult: (chainId: string, queriesRaw: LenderUserQuery[], rawResults: any[], lenderState: LenderToLenderCrossPoolMeta, options?: ConvertLenderUserDataOptions) => { [lender: string]: UserData; }; /** * Why this exists SEPARATELY from the failure sentinels * ----------------------------------------------------- * The sentinel path (`isFailedCall`) catches reads that announced themselves as * failures. This catches the ones that did not: a decodable-but-wrong response, * a market whose metadata went missing so its price resolved to `undefined`, a * converter that divided by a zero it should never have seen. Those produce the * SAME user-visible artefact as a dropped read — a debt with no collateral, a * `NaN` health factor — while every slot reports success. * * So this is the last gate before a position is published: it asserts what must * be true of any real lending position, independent of how the data was * obtained. */ /** One failed assertion about a sub-account's published shape. */ interface InvariantViolation { /** Sub-account this fired on (`accountId`). */ accountId: string; /** Machine-readable check name. */ code: 'non-finite' | 'debt-without-collateral' | 'invalid-mode'; /** Human-readable detail for logs. */ detail: string; /** * `true` when the violation is only conclusive because the read was also * known-incomplete (see {@link validateUserData}). */ requiresFailedReads: boolean; } interface ValidationResult { /** Sub-accounts that passed. Empty means the whole entry must be dropped. */ kept: UserDataForSubAccount[]; /** Every violation found, including ones on kept sub-accounts. */ violations: InvariantViolation[]; /** Sub-account ids dropped as corrupt. */ dropped: string[]; } /** * Validates a converted entry and drops the sub-accounts that cannot be true. * * `hadFailedReads` gates the checks whose violation is ambiguous on its own: * with a known-incomplete read, `debt-without-collateral` is the signature of a * dropped collateral slot and the sub-account is corrupt; with a clean read it * is a genuine (if grim) position and is kept. Unconditional checks — anything * non-finite — fire either way, because no read produces those legitimately. */ declare function validateUserData(userData: UserData, hadFailedReads: boolean): ValidationResult; interface ExposureInfo { asset: GenericCurrency; collateralFactor: number; } type PoolWithMeta = PoolData & { chainId: string; lender: string; utilitzation: number; apr: number; exposure: ExposureInfo[]; /** current price (if found) */ price?: number; /** past price (if found) */ histPrice?: number; }; type PriceMap = Record; /** * Flattens LenderData and enriches each pool with exposure information. */ declare const generateLendingPools: (lenderData: LenderData, prices: PriceMap, histPrices: PriceMap) => PoolWithMeta[]; /** * Rebuilds the original nested LenderData structure from a flattened array of PoolWithMeta. * * - Groups by chainId -> lender -> poolId * - Removes flattened-only fields: chainId, lender, exposure, price, histPrice, utilitzation */ declare function unflattenLenderData(pools: PoolWithMeta[]): LenderData; /** * Builds the multicall calls for the given queries and returns the raw results * @param chainId - The chain ID * @param queriesRaw - The queries to fetch data for * @param getEvmClient - Injected function to get EVM client * @param allowFailure - multicall can fail in single call, default is true * @param batchSize - multicall batch size, default is 4096 * @param logs - show multicall error logs, default is false * @param concurrency - number of distinct RPC endpoints to shard batches * across in parallel; <= 1 keeps the legacy single-endpoint path * @param permanentFailures - optional collector filled with the indices of * calls that failed DETERMINISTICALLY (revert / no code / unknown selector) * rather than because of the RPC. Pass it to * {@link convertLenderUserDataResult} so a caller can tell "this market * always reverts" from "this read was lost" and only re-fetch the latter. * @param onEndpointFailure - optional hook invoked for every RPC endpoint that * fails to serve a batch. Only the caller knows where to persist that (KV, * metrics), and without it every request rediscovers the same bad endpoint. * @returns The raw results from the multicall, "0x" for failures */ declare const getLenderUserDataResult: (chainId: string, queriesRaw: LenderUserQuery[], getEvmClient: GetEvmClientFunction, allowFailure?: boolean, batchSize?: number, retries?: number, logs?: boolean, concurrency?: number, permanentFailures?: Set, onEndpointFailure?: (info: EndpointFailure) => void) => Promise; /** * Prepares the RPC calls for fetching user data without executing them * Uses multicall3 aggregate3 to batch all calls into a single RPC call * This reduces multiple RPC calls to one, avoiding rate limiting * @param chainId - The chain ID * @param queriesRaw - The queries to fetch data for * @param batchSize - Multicall batch size, default is 4096 * @param blockTag - Block tag for the RPC calls, default is 'latest' * @param allowFailure - Allow individual calls to fail within multicall, default is true * @returns The prepared RPC batches and metadata needed for parsing */ declare const prepareLenderUserDataRpcCalls: (chainId: string, queriesRaw: LenderUserQuery[], batchSize?: number, blockTag?: string, allowFailure?: boolean) => Promise; /** * Fetch structured lender balance set for addresses * @param account general account as override in queries * @param chainQueries chain quieres -can only be {chainId} array, account required then * @param lenderState lender state data for rates * @returns lender user data map */ declare function getLenderUserDataMulti(account: string | undefined, chainQueries: ChainQuery[], lenderState: LenderData | PoolWithMeta[]): Promise<{ [chainId: string]: { [lender: string]: UserData; }; }>; interface PermissionParams { chainId: string; account: string; subAccount?: string | null; lenders: Lender[]; tokenAddressesByLender: Record; spender: string; aaveV4PmCheck?: { spoke: string; gateway: string; }; /** Silo: withdraw from protected (non-borrowable) collateral share token */ siloIsProtected?: boolean; /** * Gearbox V3: BotListV3 contract address. Resolve it PER FACADE * (`creditFacade.botList()`), not from the per-chain metadata constant — * V3.0 and V3.1 markets coexist on a chain and use different BotLists, and * `gearbox-resolvers.json` publishes only the V3.1 address. */ gearboxBotList?: string; /** * Gearbox facade `version()`. >= 310 selects the V3.1 * `botPermissions(bot, creditAccount)` read; anything else keeps the V3.0 * `(bot, creditManager, borrower)` form. Both generations are live. */ gearboxBotListVersion?: bigint; } interface PermissionMeta { chainId: string; lenders: Lender[]; metadata: { lender: Lender; tokens: string[]; }[]; } interface TokenApprovalParams { chainId: string; tokenAddresses: string[]; account: string; spenders: string[]; tokenListForChain?: { list?: any; }; } interface TokenApprovalMeta { tokenAddresses: string[]; spenders: string[]; tokenListForChain?: { list?: any; }; } /** * Caller-supplied validation call slotted into the merged multicall as a * fourth segment between the permission and balance calls. Results land * verbatim on `MergedUserData.extras` in input order — consumers decode * them with their own ABI knowledge. Used today for Fluid * `VaultFactory.ownerOf(nftId)` pre-flight ownership checks. */ interface ExtraValidationCall { call: Call; abi: any; } interface PreparedMergedRpcCalls { batches: MulticallRpcBatch[]; preparedCalls: PreparedCall[]; rpcCalls: RawRpcCall[]; /** Number of token approval calls — first split point */ tokenApprovalCallCount: number; /** Number of lender permission calls — second split point */ permissionCallCount: number; /** Number of caller-supplied extra validation calls — third split point */ extraCallCount: number; tokenApprovalMeta: TokenApprovalMeta; permissionMeta: PermissionMeta; balanceQueries: LenderUserQuery[]; } interface PreparedMergedMulticallParams { /** Flat calls array: [...tokenApprovalCalls, ...permissionCalls, ...extraCalls, ...balanceCalls] */ calls: Call[]; /** Per-call ABI array, same length as calls */ abis: any[]; /** Number of token approval calls — first split point */ tokenApprovalCallCount: number; /** Number of lender permission calls — second split point */ permissionCallCount: number; /** Number of caller-supplied extra validation calls — third split point */ extraCallCount: number; tokenApprovalMeta: TokenApprovalMeta; permissionMeta: PermissionMeta; balanceQueries: LenderUserQuery[]; } interface MergedUserData { tokenApprovals: Record; permissions: Record; balances: { [lender: string]: UserData; }; /** Raw decoded results for each caller-supplied extra call, in input order. */ extras: any[]; } /** * Prepares a single set of multicall RPC calls that fetches * token approval data, permission/delegation data, AND user balance data * in one round-trip. */ declare function prepareMergedRpcCalls(chainId: string, balanceQueries: LenderUserQuery[], permissionParams: PermissionParams, tokenApprovalParams?: TokenApprovalParams, batchSize?: number, blockTag?: string, allowFailure?: boolean, extraCalls?: ExtraValidationCall[]): Promise; /** * Prepares merged token approval + permission + balance calls in the format * expected by `multicallRetry`: flat calls[] and per-call abis[]. */ declare function prepareMergedMulticallParams(chainId: string, balanceQueries: LenderUserQuery[], permissionParams: PermissionParams, tokenApprovalParams?: TokenApprovalParams, extraCalls?: ExtraValidationCall[]): Promise; /** * Parses the raw multicall results produced by `prepareMergedRpcCalls` * or `prepareMergedMulticallParams` into separate token approval, * permission, and balance data. */ declare function parseMergedResult(chainId: string, rawResults: any[], prepared: Pick, lenderState: LenderToLenderCrossPoolMeta): MergedUserData; /** * Fetches merged token approval + permission + balance data * in a single multicallRetry call. */ declare function getMergedUserData(chainId: string, balanceQueries: LenderUserQuery[], permissionParams: PermissionParams, lenderState: LenderToLenderCrossPoolMeta, multicallRetry: MulticallRetryFunction, batchSize?: number, maxRetries?: number, tokenApprovalParams?: TokenApprovalParams): Promise; /** * Checks whether a lender-level delegation/approval is needed for * a given token and amount. * * Returns `true` if approval IS needed, `false` if already approved. */ declare function needsLenderApproval(params: { lender: string; lenderDebitData: LenderDebitData | undefined; tokenAddress: string; amount: bigint; chainId: string; cToken?: string; aaveV4Spoke?: string; /** Silo: check the protected share token instead of collateral */ isProtected?: boolean; /** * This is a BORROW delegation check — resolve against the entry the borrow * grant actually lives on (Silo: debt share token; Aave: variable/stable * debt token's `borrowAllowance`; Venus: the comptroller's `updateDelegate` * boolean) instead of the withdraw-side key. Without it a borrow check * reads a withdraw allowance that is 0 for a borrow-only grant and the * permission is re-emitted forever. */ isBorrow?: boolean; /** Aave borrow checks: which debt token to resolve (default VARIABLE). */ mode?: LendingMode$1; }): boolean; /** * Checks whether an ERC20 or Permit2 token approval is needed for * a given spender and amount. * * Returns `true` if approval IS needed, `false` if already approved. */ declare function needsTokenApproval(params: { debitData: DebitData | undefined; spender: string; amount: bigint; chainId: string; usePermit2?: boolean; }): boolean; declare function getBalanceForMarketUid(lender: string, marketUid: string, balances: { [lender: string]: UserData; }, subAccount?: string): BaseLendingPositions | undefined; interface BalanceInfo { index: number; supplyShares: bigint; borrowShares: bigint; supplyAssets: bigint; borrowAssets: bigint; collateral: bigint; } /** * Decode: * [ uint16 count (2 bytes) ] * then for each of `count` records: * uint16 index (2 bytes), * uint256 supplyShares (32 bytes), * uint128 borrowShares (16 bytes), * uint256 supplyAssets (32 bytes), * uint256 borrowAssets (32 bytes), * uint128 collateral (16 bytes) */ declare function decodePackedMorphoUserDataset(hex: string): BalanceInfo[]; interface ListaBalanceInfo { index: number; supplyShares: bigint; borrowShares: bigint; supplyAssets: bigint; borrowAssets: bigint; collateral: bigint; } interface ListaUserData { whitelistFlags: boolean[]; balances: ListaBalanceInfo[]; } /** * Decode Lista user data with leading whitelist flags. * * @param hex ABI-encoded hex string * @param marketsCount marketsIds.length (needed to slice flags) */ declare function decodePackedListaUserDataset(hex: string, marketsCount: number): ListaUserData; declare const MORPHO_LENS: { [c: string]: string; }; declare const buildMorphoTypeUserCallWithLens: (chainId: string, account: string, lender: string, marketsToQuery: string[], getClient?: (chainId: string, rpcId?: number) => any) => Call[] | Promise; /** * Back-compat alias. New callers should use `RelayProxyConfig` directly from * `@1delta/proxy-fetch`. */ type MorphoSubgraphProxyConfig = RelayProxyConfig; interface MorphoUserMarketBalance { /** 0x-prefixed lowercase 66-char market key */ marketId: string; /** Raw supply shares as decimal string */ supplyShares: string; /** Raw borrow shares as decimal string */ borrowShares: string; /** Raw collateral amount as decimal string */ collateral: string; } declare function hasMorphoUserSubgraph(chainId: string): boolean; declare function hasMorphoUserApi(chainId: string): boolean; declare function hasMorphoPositionIndex(chainId: string): boolean; /** * Fetches per-market balance data (supply shares, borrow shares, collateral) * for all markets where the account has active positions. Queries the Goldsky * subgraph or the Morpho Blue API depending on the chain. * * Returns undefined when the chain has no index OR the request fails (timeout, * non-2xx, etc.) — caller should fall back to querying all markets on-chain. * Returns an empty array when the account has no positions on that chain. * * Concurrent callers for the same (chainId, account) share a single upstream * request, and results are memoized for a short window (see `CACHE_TTL_MS`). */ declare function fetchMorphoUserBalances(chainId: string, account: string, proxyConfig?: RelayProxyConfig): Promise; /** * Convenience wrapper — returns only the set of market IDs where the account * has active positions. Use `fetchMorphoUserBalances` when you also need the * shares/collateral amounts. */ declare function fetchMorphoUserPositionMarkets(chainId: string, account: string, proxyConfig?: RelayProxyConfig): Promise | undefined>; /** * Summary-level balance data. * Same shape as BalanceData but without discounted/adjusted fields * that are only meaningful at the sub-account risk level. */ interface SummaryBalanceData { deposits: number; debt: number; collateral: number; collateralAllActive: number; nav: number; deposits24h: number; debt24h: number; nav24h: number; rewards?: any; } /** * Summary-level APR data (same shape as AprData). */ interface SummaryAprData { apr: number; depositApr: number; borrowApr: number; rewardApr: number; rewardDepositApr: number; rewardBorrowApr: number; intrinsicApr: number; intrinsicDepositApr: number; intrinsicBorrowApr: number; rewards: any; } /** * Summary for a sub-account within a lender */ interface SubAccountSummary { accountId: string; health: number | null; balanceData: BalanceData; aprData: AprData; userConfig: UserConfig; positionCount: number; } /** * Summary for a single lender/market */ interface LenderSummary { lender: string; chainId: string; balanceData: SummaryBalanceData; aprData: SummaryAprData; /** Leverage ratio (deposits / (deposits - debt)) */ leverage: number; /** Sub-account info */ subAccounts: SubAccountSummary[]; } /** * Summary for a single chain */ interface ChainSummary { chainId: string; totalDepositsUSD: number; totalDebtUSD: number; netWorth: number; lenderCount: number; } /** * Lightweight totals-only summary (no per-lender breakdown or positions) */ interface PortfolioTotals { balanceData: SummaryBalanceData; aprData: SummaryAprData; leverage: number; activeLenders: number; activeChains: number; } /** * Overall portfolio summary across all lenders and chains */ interface PortfolioSummary extends PortfolioTotals { /** Per-lender summaries (array, sorted by netWorth descending) */ lenders: LenderSummary[]; /** Per-chain totals */ chains: ChainSummary[]; } /** * Fused lender entry: UserData positions + aggregated LenderSummary metrics. * Used in the API response to avoid separate data + summary lookups. */ interface LenderDataEntry extends Omit { account: string; lenderInfo?: LenderInfo; data: UserDataForSubAccount[]; /** * Set when some of this lender's on-chain reads could not be completed. The * positions listed are real but the set is a LOWER BOUND — anything derived * from the whole picture (NAV, net APR, health factor) is unreliable and must * not be rendered as fact. */ incomplete?: boolean; /** * Set when this entry was served from the last COMPLETE snapshot because the * live read failed. Internally consistent — unlike `incomplete` — but as of * `staleAgeMs` ago rather than now. */ stale?: boolean; /** Age of the served snapshot in ms. Only set alongside `stale`. */ staleAgeMs?: number; } /** * Input type for buildSummaries - user data result from convertLenderUserDataResult */ type UserDataResult = { [chainId: string]: { [lender: string]: UserData; }; }; /** * Calculates weighted average * @param items Array of { value: number, weight: number } * @returns Weighted average, or 0 if total weight is 0 */ declare function calculateWeightedAverage(items: { value: number; weight: number; }[]): number; /** * Calculates net APR for a sub-account based on deposits and debt */ declare function calculateNetApr(aprData: AprData, balanceData: BalanceData): number; /** * Calculates leverage ratio * @returns leverage = deposits / netWorth, or 0 if netWorth <= 0 */ declare function calculateLeverage(deposits: number, netWorth: number): number; /** * Calculates overall net APR from totals */ declare function calculateOverallNetApr(totalDepositsUSD: number, totalDebtUSD: number, avgDepositApr: number, avgBorrowApr: number): number; /** * Builds only portfolio totals - lightweight version without per-lender breakdown * Skips position extraction and lender summary objects for better performance * * @param userDataResult - The result from convertLenderUserDataResult or getLenderUserDataMulti * @returns PortfolioTotals with aggregated totals and weighted APRs * * @example * ```typescript * const userData = await getLenderUserDataMulti(account, chainQueries, lenderState) * const totals = buildPortfolioTotals(userData) * * console.log(`Net worth: $${totals.balanceData.nav.toFixed(2)}`) * console.log(`Net APR: ${totals.aprData.apr.toFixed(2)}%`) * ``` */ declare function buildPortfolioTotals(userDataResult: UserDataResult): PortfolioTotals; /** * Builds portfolio summaries from user data results * * @param userDataResult - The result from convertLenderUserDataResult or getLenderUserDataMulti * @returns PortfolioSummary with aggregated data across all lenders and chains * * @example * ```typescript * const userData = await getLenderUserDataMulti(account, chainQueries, lenderState) * const summary = buildSummaries(userData) * * console.log(`Total net worth: $${summary.balanceData.nav.toFixed(2)}`) * console.log(`Overall leverage: ${summary.leverage.toFixed(2)}x`) * console.log(`Net APR: ${summary.aprData.apr.toFixed(2)}%`) * ``` */ declare function buildSummaries(userDataResult: UserDataResult): PortfolioSummary; /** * Filters portfolio summary to only include lenders with non-zero positions */ declare function filterActiveLenders(summary: PortfolioSummary): PortfolioSummary; /** * Fuses UserData entries with their LenderSummary counterparts into * a flat array of LenderDataEntry objects, sorted by nav descending. */ declare function fuseLenderData(userDataResult: UserDataResult, summary: PortfolioSummary): LenderDataEntry[]; interface BaseYields { variableBorrowRate: number; stableBorrowRate: number; depositRate: number; } interface RewardEntry extends BaseYields { asset: string; } type RewardsList = RewardEntry[]; /** * One aggregated level of an order-book side, provider-agnostic (rate + size). * Emitted best-first (best executable offer first). Used by fixed-rate * order-book lenders (Morpho Midnight, Term Finance) to carry a bounded slice of * the live book so consumers can filter later (rate-at-size, dust removal, …). */ interface PublicBookLevel { /** annualised rate at this level, PERCENT. */ apr: number; /** aggregate size at this level, loan-token base units (raw string). */ units: string; /** aggregate size at this level, loan-token assets (human number). */ assets: number; } /** * Bounded top-of-book slice (best-first, capped per side). `bids` = the BORROW * side (take to borrow), `asks` = the LEND side (take to lend). Either side may * be empty (e.g. a one-sided secondary market). Present only on order-book * fixed-rate markets. */ interface MarketBook { bids: PublicBookLevel[]; asks: PublicBookLevel[]; } /** * One entry in a fixed-term rate menu. Lives on `params.market.terms` for * single-borrowable-asset markets, and on `data[*].terms` for cross-margin * multi-asset lenders (Exactly), where each asset has its own fixed pools. * * `termId` semantics are LENDER-SPECIFIC — Exactly/TermMax = the unix maturity, * Teller = duration in seconds, Lista = the broker product id, Midnight/Term = * `0` placeholder. See FIXED_TERM_REPAY_TERMS.md. */ interface MarketTermEntry { termId: number; durationSecs: number; durationDays: number; /** annualised borrow rate in PERCENT (e.g. 3.857 = 3.857% APR) */ apr: number; /** annualised fixed LEND rate in PERCENT at this maturity (Exactly only) */ depositApr?: number; /** borrowable liquidity at this maturity, loan-token human units (Exactly only) */ available?: number; } interface MorphoMarket { /** the 1delta lender enum */ lender: string; collateralDecimals?: number; loanDecimals?: number; /** the market hash */ id: string; /** market params */ lltv: string; oracle: string; irm: string; collateralAddress: string; loanAddress: string; /** Lista extensions */ minLoan?: string; broker?: string; loanProvider?: string; collateralProvider?: string; hasWhitelist?: boolean; /** false when the market has been removed from the Morpho whitelist */ isListed?: boolean; /** protocol fee */ fee?: string; /** IRM rate at target utilization */ rateAtTarget?: string; /** IRM rate cap */ rateCap?: string; /** IRM rate floor */ rateFloor?: string; /** Fixed-term rate menu — available term products (Lista brokered markets, * Term/Midnight single-maturity markets). * * MARKET-LEVEL menu, valid only when the lender key has ONE borrowable asset * (every isolated-market fixed-term lender). CROSS-MARGIN multi-asset * lenders — Exactly — carry a menu PER ASSET on `data[*].terms` instead, * since each asset has its own fixed pools. * * A market-level card still has to be ATTRIBUTED to a row, and matching * `loanAddress` against each row's asset is wrong the moment the loan token * is also a collateral leg (Morpho Midnight permits it). So Midnight ALSO * puts the card on its loan row (`data[].terms`); prefer the row's * own card wherever one is present and treat this as the fallback. */ terms?: MarketTermEntry[]; /** * Canonical cross-protocol fixed-term descriptor (Lista brokered + Morpho * Midnight). Present on fixed-rate/fixed-maturity markets only. See * {@link FixedTermInfo}. */ fixedTerm?: FixedTermInfo; /** * Bounded top-of-book slice (best-first, capped per side) for order-book * fixed-rate markets (Midnight, Term). Baseline for downstream filtering; the * aggregate best rate + full depth stay on the `data[*]` entries. Absent on * non-order-book markets. */ book?: MarketBook; } interface MorphoGeneralPublicResponse { data: { [tokenSymbol: string]: { marketUid: string; name?: string; poolId: string; underlying: string; asset: GenericCurrency; /** Supply/borrow accumulator (`assets / shares`) — see `MarketAccumulator`. */ accumulator?: MarketAccumulator; totalDeposits: number; totalDebtStable: 0; totalDebt: number; totalLiquidity: number; borrowLiquidity: number; totalLiquidityUSD: number; borrowLiquidityUSD: number; totalDepositsUSD: number; totalDebtStableUSD: 0; totalDebtUSD: number; /** Borrow utilization (0..1). */ utilization: number; depositRate: number; variableBorrowRate: number; intrinsicYield: number; /** fixed-term (broker) borrow rate in percent; 0 for non-brokered Morpho-type markets */ stableBorrowRate: number; /** * How the ongoing borrow rate (`variableBorrowRate`) is set, so integrators * can label/treat it correctly instead of assuming a pool APR. Absent ⇒ * `'variable'` (the pool-lender default). * - `'variable'` — utilization-curve pool rate (Aave, Compound, Morpho…). * - `'userSet'` — borrower picks the per-position rate (Liquity family). * - `'fixedTerm'` — fixed rate per maturity (Midnight, Term, Exactly, Lista); * see `params.market.fixedTerm` / `terms`. * - `'zeroInterest'` — NO ongoing rate at all (River/Satoshi). The borrow * cost is the one-off `originationFee`, not an APR. */ rateModel?: 'variable' | 'userSet' | 'fixedTerm' | 'zeroInterest' | 'dbr' | 'protocolSet'; /** * One-off fee charged ONCE at borrow time, as a PERCENT of the amount * borrowed (e.g. `0.5` = 0.5%). Front-loaded cost that is NOT an APR and * must never be added into `variableBorrowRate`: River mint fee, Liquity * upfront fee. Absent / `0` ⇒ no origination fee. For an open position it * is already realized into the debt; annualize it over the holding period * only for an effective-cost-since-open view. */ originationFee?: number; /** * PER-ASSET fixed-term rate menu, for CROSS-MARGIN multi-asset fixed-term * lenders (Exactly): one lender key covers every asset, and each asset has * its own fixed pools, so the menu cannot live on `params.market`. * Isolated-market fixed-term lenders (Term, Lista broker, TermMax, * Teller) keep using `params.market.terms` — read that as the fallback * when this is absent. Morpho Midnight emits BOTH: the card sits on its * loan row as well, so a consumer never has to decide which row of the * market is the borrow side (a loan token can also be a collateral leg * there, and a `loanAddress` match then picks two rows). * * Invariant, whichever level it is read from: a rate card describes a * BORROW side. A row with `borrowingEnabled: false` never carries one. */ terms?: MarketTermEntry[]; /** * PER-ASSET fixed-term descriptor, same rationale as `terms` above * (Exactly). Falls back to `params.market.fixedTerm` when absent. */ fixedTerm?: FixedTermInfo; rewards?: RewardsList; decimals: number; config: { [0]: { category: number; borrowCollateralFactor: number; collateralFactor: number; borrowFactor: 1; liquidationPenalty: number; closeFactor: number; collateralDisabled: boolean; debtDisabled: boolean; }; }; closeFactor: number; collateralActive: boolean; borrowingEnabled: boolean; depositsEnabled: boolean; hasStable: boolean; /** Lista fixed-term broker markets: variable (flexible) borrowing is not available — borrow * is fixed-term only (via the broker). Supply yield stays variable. False/absent otherwise. */ variableBorrowDisabled?: boolean; isActive: true; isFrozen: false; }; }; /** morpho market info */ params: { market: MorphoMarket; }; chainId: string; } declare function buildMorphoTypeCall(chainId: string, lender: string, marketsOVerride?: string[]): { address: string; name: string; params: (string | string[])[]; }[]; type Market = { loanToken: string; collateralToken: string; oracle: string; irm: string; lltv: bigint; price: bigint; loanTokenPrice?: bigint; collateralTokenPrice?: bigint; rateAtTarget: bigint; totalSupplyAssets: bigint; totalSupplyShares: bigint; totalBorrowAssets: bigint; totalBorrowShares: bigint; lastUpdate: bigint; fee: bigint; }; type ListaMarket = { loanToken: string; collateralToken: string; oracle: string; irm: string; lltv: bigint; price: bigint; loanTokenPrice?: bigint; collateralTokenPrice?: bigint; minLoan?: bigint; rateCap?: bigint; rateFloor?: bigint; hasWhitelist?: boolean; loanProvider?: string; collateralProvider?: string; broker?: string; rateAtTarget: bigint; totalSupplyAssets: bigint; totalSupplyShares: bigint; totalBorrowAssets: bigint; totalBorrowShares: bigint; lastUpdate: bigint; fee: bigint; }; /** * Decode packed markets bytes into typed objects. * Auto-detects format based on data length and decodes accordingly. * Integers are parsed as big-endian BigInt. Addresses are 0x-prefixed lowercase hex. */ declare function decodeMarkets(input: string | Uint8Array): (Market | ListaMarket)[]; declare function decodeListaMarkets(bytes: Uint8Array): ListaMarket[]; declare function normalizeToBytes(input: string | Uint8Array): Uint8Array; interface PostTradeMetrics { pre: { healthFactor: number; borrowCapacity: number; }; post: { healthFactor: number; borrowCapacity: number; balanceData: BalanceData; aprData?: AprData; }; } declare const EMPTY_BALANCE: BalanceData; declare function getHealthFactor(collateral: number, adjustedDebt: number): number; declare function getBorrowCapacity(bd: BalanceData): number; declare function getAssetConfig(config: LenderConfigMap, modeId: string): LenderConfigData; declare function computePostTradeMetrics(balanceData: BalanceData, postTrade: BalanceData): PostTradeMetrics; /** * A single Sumer market position with all data needed * to rebuild waterfall accumulators after a trade delta. * * Callers build this from: * - `payload.lendingPositions[marketUid]` → depositsUSD, debtUSD, debtStableUSD, collateralEnabled * - `lenderData[marketUid].params.metadata.sumer` → SumerMarketMeta * - `lenderData[marketUid].flags?.collateralActive` → collateralActive */ type SumerPositionInput = { marketUid: string; depositsUSD: number; debtUSD: number; debtStableUSD: number; collateralEnabled: boolean; sumerMeta: SumerMarketMeta; collateralActive: boolean; }; type GroupAccumulator = Record; /** * Unified deposit operation for all lending protocols. * * Routes to the appropriate implementation based on the lender type: * - Sumer: Uses waterfall absorption model * - Standard: Uses standard collateral factor model (Aave, Compound, Venus, etc.) * * @param lender - The lender protocol identifier (e.g., 'AAVE_V3', 'SUMER') * @param amount - The token amount to deposit * @param price - The USD price of the token * @param balanceData - Current balance state * @param config - (Standard only) Lender configuration map * @param modeId - (Standard only) Mode/category ID * @param targetMarketUid - (Sumer only) Target market UID * @param positions - (Sumer only) Array of all Sumer positions * @param createNewSubAccount - Whether to create a new sub-account * @param apr - Optional current APR data * @param yieldParams - Optional yield parameters for APR calculation */ declare function computeDepositDelta(lender: string, amount: number, price: number, balanceData: BalanceData, config?: LenderConfigMap, modeId?: string, createNewSubAccount?: boolean, apr?: AprData, yieldParams?: LenderYields$1, targetMarketUid?: string, positions?: SumerPositionInput[]): PostTradeMetrics; /** * Unified withdraw operation for all lending protocols. * * Routes to the appropriate implementation based on the lender type: * - Sumer: Uses waterfall absorption model * - Standard: Uses standard collateral factor model (Aave, Compound, Venus, etc.) * * @param lender - The lender protocol identifier (e.g., 'AAVE_V3', 'SUMER') * @param amount - The token amount to withdraw * @param price - The USD price of the token * @param balanceData - Current balance state * @param config - (Standard only) Lender configuration map * @param modeId - (Standard only) Mode/category ID * @param targetMarketUid - (Sumer only) Target market UID * @param positions - (Sumer only) Array of all Sumer positions * @param apr - Optional current APR data * @param yieldParams - Optional yield parameters for APR calculation */ declare function computeWithdrawDelta(lender: string, amount: number, price: number, balanceData: BalanceData, config?: LenderConfigMap, modeId?: string, apr?: AprData, yieldParams?: LenderYields$1, targetMarketUid?: string, positions?: SumerPositionInput[]): PostTradeMetrics; /** * Unified borrow operation for all lending protocols. * * Routes to the appropriate implementation based on the lender type: * - Sumer: Uses waterfall absorption model * - Standard: Uses standard collateral factor model (Aave, Compound, Venus, etc.) * * @param lender - The lender protocol identifier (e.g., 'AAVE_V3', 'SUMER') * @param amount - The token amount to borrow * @param price - The USD price of the token * @param balanceData - Current balance state * @param config - (Standard only) Lender configuration map * @param modeId - (Standard only) Mode/category ID * @param targetMarketUid - (Sumer only) Target market UID * @param positions - (Sumer only) Array of all Sumer positions * @param apr - Optional current APR data * @param yieldParams - Optional yield parameters for APR calculation * @param irMode - Interest rate mode (1=stable, 2=variable) */ declare function computeBorrowDelta(lender: string, amount: number, price: number, balanceData: BalanceData, config?: LenderConfigMap, modeId?: string, apr?: AprData, yieldParams?: LenderYields$1, irMode?: number, targetMarketUid?: string, positions?: SumerPositionInput[]): PostTradeMetrics; /** * Unified repay operation for all lending protocols. * * Routes to the appropriate implementation based on the lender type: * - Sumer: Uses waterfall absorption model * - Standard: Uses standard collateral factor model (Aave, Compound, Venus, etc.) * * @param lender - The lender protocol identifier (e.g., 'AAVE_V3', 'SUMER') * @param amount - The token amount to repay * @param price - The USD price of the token * @param balanceData - Current balance state * @param config - (Standard only) Lender configuration map * @param modeId - (Standard only) Mode/category ID * @param targetMarketUid - (Sumer only) Target market UID * @param positions - (Sumer only) Array of all Sumer positions * @param apr - Optional current APR data * @param yieldParams - Optional yield parameters for APR calculation * @param irMode - Interest rate mode (1=stable, 2=variable) */ declare function computeRepayDelta(lender: string, amount: number, price: number, balanceData: BalanceData, config?: LenderConfigMap, modeId?: string, apr?: AprData, yieldParams?: LenderYields$1, irMode?: number, targetMarketUid?: string, positions?: SumerPositionInput[]): PostTradeMetrics; /** * Compute post-trade balance metrics for a Sumer deposit operation. * * Unlike the standard deposit, Sumer must re-run the waterfall absorption * across ALL positions to determine the new effective collateral. */ declare function computeSumerDepositDelta(amount: number, price: number, targetMarketUid: string, balanceData: BalanceData, positions: SumerPositionInput[], createNewSubAccount?: boolean, apr?: AprData, yieldParams?: LenderYields$1): PostTradeMetrics; /** * Lending interest-rate mode (variable vs stable). */ declare enum LendingMode { NONE = 0, STABLE = 1, VARIABLE = 2 } /** * Per-asset yield parameters from the lender. * Uses `intrinsicYield` (e.g. staking/LST yield). */ interface LenderYields extends BaseYields$1 { intrinsicYield: number; rewards?: RewardsList$1; } interface LoopPostTradeMetrics { pre: { healthFactor: number; borrowCapacity: number; }; post: { healthFactor: number; borrowCapacity: number; balanceData: BalanceData; aprData: AprData; }; } /** * Compute post-trade balance & APR metrics for closing a leveraged position. * * Withdraws `dollarIn` of collateral and repays `dollarOut` of debt. */ declare function computeCloseTradeDeltas(dollarIn: number, dollarOut: number, targetMode: LendingMode, yieldParamsIn: LenderYields, yieldParamsOut: LenderYields, balance: BalanceData, apr: AprData, bfOut: number, ltvIn: number, collateralLtvIn: number): LoopPostTradeMetrics; /** * Compute post-trade balance & APR metrics for a collateral swap. * * Withdraws `dollarIn` of one collateral asset and deposits `dollarOut` of another. */ declare function computeCollateralSwapDeltas(dollarIn: number, dollarOut: number, yieldParamsIn: LenderYields, yieldParamsOut: LenderYields, balance: BalanceData, apr: AprData, ltvIn: number, collateralLtvIn: number, ltvOut: number, collateralLtvOut: number): LoopPostTradeMetrics; /** * Compute post-trade balance & APR metrics for a debt swap. * * Borrows `dollarIn` of a new debt asset and repays `dollarOut` of the existing debt. */ declare function computeDebtSwapDeltas(dollarIn: number, dollarOut: number, sourceMode: LendingMode, targetMode: LendingMode, yieldParamsIn: LenderYields, yieldParamsOut: LenderYields, balance: BalanceData, apr: AprData, bfInFactor: number, bfOutFactor: number): LoopPostTradeMetrics; /** * Compute post-trade balance & APR metrics for opening a leveraged position. * * Borrows `dollarIn` of the source asset and deposits `dollarOut` of the target asset. */ declare function computeOpenTradeDeltas(dollarIn: number, dollarOut: number, sourceMode: LendingMode, yieldParamsIn: LenderYields, yieldParamsOut: LenderYields, balance: BalanceData, apr: AprData, bfIn: number, collateralLtvOut: number, borrowLtvOut: number, useAllActive?: boolean): LoopPostTradeMetrics; /** * Compute post-trade balance & APR metrics for a zap (single-sided leverage entry). * * Similar to open but does not update the `*AllActive` collateral fields. */ declare function computeZapTradeDeltas(dollarIn: number, dollarOut: number, sourceMode: LendingMode, yieldParamsIn: LenderYields, yieldParamsOut: LenderYields, balance: BalanceData, apr: AprData, bfIn: number, collateralLtvOut: number, borrowLtvOut: number): LoopPostTradeMetrics; /** * Thresholds for maximum parameter calculations. * * SAFE_HF is the minimum health factor we allow after the trade — * going below this puts the account dangerously close to liquidation. */ declare enum MaxParamThresholds { MIN_HF = 1, SAME_OPEN_HF = 1.1, SAFE_HF = 1.01 } /** * Compute the maximum dollar amount a user can borrow-swap-deposit * when opening a leveraged position, without dropping the health * factor below `SAFE_HF`. * * For a "same-asset" loop (borrow and deposit the same token) we * use the stricter `SAME_OPEN_HF` threshold because the position * is more sensitive to rate changes. * * @param borrowDiscountedCollateral Current risk-adjusted collateral (for borrow capacity) * @param collateral Current collateral value (for health factor) * @param debt Current total debt * @param cfOut Collateral factor of the deposited (out) asset * @param bfIn Borrow factor of the borrowed (in) asset * @param sameAsset Whether in and out assets are the same token * @returns Maximum additional dollar amount that can be borrowed */ declare function getMaxAmountOpen(borrowDiscountedCollateral: number, collateral: number, debt: number, cfOut: number, bfIn: number, sameAsset?: boolean): number; /** * Compute the maximum dollar amount for a collateral swap without * dropping the health factor below `SAFE_HF`. * * A collateral swap withdraws $X of asset A and deposits $X of asset B. * The borrow-discounted collateral changes by `(ltvOut - ltvIn) * X`. * * If the target asset has a higher LTV than the source, the swap always * improves health → returns Infinity (capped by position size upstream). * * @param borrowDiscountedCollateral Current risk-adjusted collateral * @param adjustedDebt Current risk-adjusted debt * @param ltvIn borrowCollateralFactor of the withdrawn (source) asset * @param ltvOut borrowCollateralFactor of the deposited (target) asset * @returns Maximum dollar amount that can be swapped */ declare function getMaxAmountCollateralSwap(borrowDiscountedCollateral: number, adjustedDebt: number, ltvIn: number, ltvOut: number): number; /** * Compute the maximum dollar amount for a debt swap without * dropping the health factor below `SAFE_HF`. * * A debt swap repays $X of asset A (source) and borrows $X of asset B (target). * The adjusted debt changes by `(bfOut - bfIn) * X` where: * bfIn = borrowFactor of the source (repaid) debt * bfOut = borrowFactor of the target (new) debt * * If the target asset has a lower or equal borrowFactor, the swap always * improves health → returns Infinity (capped by position size upstream). * * @param borrowDiscountedCollateral Current risk-adjusted collateral * @param adjustedDebt Current risk-adjusted debt * @param bfIn borrowFactor of the source debt (being repaid) * @param bfOut borrowFactor of the target debt (being borrowed) * @returns Maximum dollar amount that can be swapped */ declare function getMaxAmountDebtSwap(borrowDiscountedCollateral: number, adjustedDebt: number, bfIn: number, bfOut: number): number; /** * Compute the maximum dollar amount for closing (deleveraging) a position * without dropping the health factor below `SAFE_HF`. * * A close operation withdraws $X of collateral and repays $X of debt. * BDC decreases by `ltvIn * X` * adjustedDebt decreases by `bfOut * X` * * If `ltvIn <= SAFE_HF * bfOut`, the close always improves health * → returns Infinity (capped by position size upstream). * * @param borrowDiscountedCollateral Current risk-adjusted collateral * @param adjustedDebt Current risk-adjusted debt * @param ltvIn borrowCollateralFactor of the collateral being withdrawn * @param bfOut borrowFactor of the debt being repaid * @returns Maximum dollar amount that can be closed */ declare function getMaxAmountClose(borrowDiscountedCollateral: number, adjustedDebt: number, ltvIn: number, bfOut: number): number; declare const positivePart: (n: number) => number; declare function nanTo(possiblyNaN: number, replacement?: number): number; /** Collect all unique keys from two maps */ declare function keysFromMaps(a: Record | undefined, b: Record | undefined): string[]; /** No-op result when dollar amounts are zero */ declare function noOpResult(balance: BalanceData, apr: AprData): LoopPostTradeMetrics; /** Build the final post-trade metrics from updated balance and apr */ declare function buildLoopResult(balance: BalanceData, newBalance: BalanceData, apr: AprData, newApr: AprData, useAllActive?: boolean): LoopPostTradeMetrics; /** * The token a reward is paid in. * * ABSENT on a points program — that absence IS the signal, mirroring * `RewardTerm.asset` in ../terms/types.ts, and it is what lets a consumer * refuse to fold an unpriceable program into a headline APR. */ interface RewardTokenRef { /** Lowercased contract address. */ address: string; chainId?: string; symbol?: string; decimals?: number; /** Icon URL from the source, where it supplies one (Merkl does). */ logoURI?: string; /** USD price the SOURCE used to derive the APR — not our oracle. */ priceUsd?: number; } /** * WHO is paying, as opposed to how it is claimed. * * The pre-existing `LenderAssetReward.distribution` conflates the two: it reads * `'merkle'` for every Merkl campaign regardless of protocol, so three * unrelated programs render one indistinguishable chip. `id` is a stable slug * (`merkl:aave`, `dtrinity:rebate`) safe to key on; `label` is the human string; * `link` is the exact deep link to the program, not a protocol homepage. */ interface RewardSourceRef { /** Stable slug — `:`. Safe to switch on. */ id: string; /** Human label for display, e.g. `Merkl · Aave`. */ label: string; /** Deep link to THIS program. */ link?: string; /** Platform hosting the program: `merkl`, `protocol`, … */ platform?: string; /** Per-platform identifiers, verbatim, for support + deduplication. */ refs?: Record; } /** * One reward program on one side of one market. * * This is the typed replacement for the untyped `additional*Data` bags, whose * shape differed per fetcher (Merkl emitted `{token, tokenAddress, * dailyUsdValue}`, the on-chain readers `{tokenAddress, emissionPerSecond, * distributionEnd}`) and which therefore could not be rendered generically. * Every field a UI needs to explain a reward without a second lookup lives * here: the APR, the token, the source, and the end date. */ interface RewardStream { side: 'deposit' | 'borrow'; /** Nominal APR in percent, on this side, from this program alone. */ apr: number; /** `points` ⇒ not priceable; MUST be excluded from any headline APR. */ kind: 'token' | 'points'; /** Absent ⇔ `kind === 'points'`. */ token?: RewardTokenRef; source: RewardSourceRef; /** How it is realized — decides whether the APR is actually bankable. */ claim: 'accrual' | 'merkl' | 'manual'; /** Unix seconds. An APR with two weeks left is not an APR. */ endsAt?: number; /** Unix seconds, where the source publishes it. */ startsAt?: number; /** Program-wide payout rate in USD/day, as the source reports it. */ dailyRewardsUsd?: number; } interface LenderAssetReward { /** * Legacy source/mechanism tag (`merkle`, `onchain-incentives`, `native`). * Kept because the recorder's `market_rewards.source` column is derived from * it and is part of that table's primary key. Prefer `streams[].source.id`, * which distinguishes programs this cannot. */ distribution: string; /** Summed deposit-side APR across every stream. */ deposit: number; /** Summed borrow-side APR across every stream. */ borrow: number; /** * @deprecated Untyped per-source bag, superseded by `streams`. Still * populated verbatim so the yield-tracer recorder keeps working unchanged * while it migrates; remove once nothing reads it. */ additionalDepositData: any; /** @deprecated See `additionalDepositData`. */ additionalBorrowData: any; /** @deprecated First program's link only. Prefer `streams[].source.link`. */ link?: string; /** * Every reward program backing `deposit` / `borrow`, fully described. A * multi-token campaign produces one stream PER TOKEN, so nothing collapses * onto a first entry the way `additional*Data[0]` did. */ streams?: RewardStream[]; } interface LenderRewards { [chainId: number]: { [lender: string]: { [asset: string]: LenderAssetReward; }; }; } /** * Rewards keyed by the RESERVE TOKEN the campaign pays on — the aToken for a * supply campaign, the variable debt token for a borrow one. * * Why this exists: a protocol's Merkl campaigns cover every deployment under * one `mainProtocolId` ("aave" spans V3 mainnet, Horizon, Prime, Ether.fi and * every V4 hub/spoke), and nothing in the opportunity payload names the * deployment reliably — `depositUrl`'s `marketName` is wrong on at least one * live campaign ("Borrow USDC on Aave" claims `proto_mainnet` while paying on * `variableDebtHorRwaUSDC`, i.e. Horizon). Attributing by (lender, underlying) * therefore collapsed every deployment's campaign onto the base lender key and * SUMMED them: Aave V3 USDC read 5.75% borrow (1.75 + 2 + 2 across three * deployments) and V3 USDG read 12% (two V4 hubs), while Horizon/Prime/V4 * markets read 0%. * * The reserve token is unambiguous — it exists in exactly one market of exactly * one deployment. Consumers hold the other half of the join already: Aave-type * market nodes carry `params.metadata.{aToken, vToken}`, so matching on those * lands each campaign on exactly one marketUid, and cross-deployment summing * becomes impossible by construction. */ interface RewardsByReserveToken { /** chainId → lowercased reserve-token address → reward */ [chainId: string]: { [reserveToken: string]: LenderAssetReward; }; } interface YieldDataWithTimestamp { intrinsicYields: { [asset: string]: number | undefined; }; lenderRewards: LenderRewards; /** * Reserve-token-keyed rewards for protocols whose campaigns cannot be * attributed to a deployment from the campaign payload alone (Aave). * Those protocols are absent from `lenderRewards` — better no reward than * one filed against the wrong market. */ rewardsByReserveToken: RewardsByReserveToken; } interface LenderRewardsByMarketUid { [marketUid: string]: LenderAssetReward; } interface YieldDataByMarketUid { intrinsicYields: { [asset: string]: number | undefined; }; /** * Flattened to marketUid via `createMarketUid(chain, lender, asset)`. * * CAVEAT: that construction assumes a market's uid ends in its underlying * address, which is not universal — Aave V4 uids end in a numeric RESERVE ID * (`AAVE_V4_94E7A5DC…:1:3` is WBTC), so a reward flattened this way can never * match one. Prefer `rewardsByLenderAsset` when the consumer has the market * nodes to hand; this stays for callers that only have uids. */ lenderRewards: LenderRewardsByMarketUid; /** * The same lender-keyed rewards UNflattened — (chain → lender → underlying). * Consumers holding market nodes should join on this: it needs no assumption * about how a lender composes its marketUid. */ rewardsByLenderAsset: LenderRewards; /** See {@link RewardsByReserveToken} — joined by the consumer against each * market node's `params.metadata.{aToken, vToken}`. */ rewardsByReserveToken: RewardsByReserveToken; } declare const fetchGeneralYields: () => Promise; declare const fetchGeneralYieldsByMarketUid: () => Promise; /** Convert a Midnight tick into a WAD zero-coupon price (rounded to the step). */ declare function tickToPrice(tick: bigint): bigint; /** APR as a plain fraction (e.g. `0.0512` for 5.12%). Convenience for display. */ declare function tickToAprNumber(tick: bigint, timeToMaturity: bigint): number; /** One aggregated price level of a Midnight book side (raw bigints, loan-token units). */ interface MidnightBookLevel { /** Midnight tick (price point). */ tick: bigint; /** Total credit/debt units available at this level. */ units: bigint; /** Total loan-token assets implied at this level. */ assets: bigint; } /** * Top-of-book snapshot for a single Midnight market, already reduced to the * best executable rate per side plus the aggregate depth. * * Side semantics (Midnight): `bids` are maker BUY offers (makers lending) — a * taker consumes them to BORROW. `asks` are maker SELL offers (makers * borrowing) — a taker consumes them to LEND. So: * - best BORROW rate = min period-rate over bids (cheapest to borrow) * - best SUPPLY yield = max period-rate over asks (highest to lend) * Selection is order-agnostic (we scan every level), so it does not depend on * the API returning levels best-first. */ interface MidnightBookTop { /** Tick of the best (lowest-rate) borrow offer, or undefined if the bid side is empty. */ bestBorrowTick?: bigint; /** Tick of the best (highest-yield) supply offer, or undefined if the ask side is empty. */ bestSupplyTick?: bigint; /** Aggregate borrowable units/assets (bid side). */ borrowDepthUnits: bigint; borrowDepthAssets: bigint; /** Aggregate lendable units/assets (ask side). */ supplyDepthUnits: bigint; supplyDepthAssets: bigint; } /** * Full order-book ladder for a market — every executable price level per side, * pre-sorted BEST-FIRST (unlike {@link MidnightBookTop}, which collapses to the * single best level + aggregate depth). Empty levels are dropped. */ interface MidnightBook { /** Bid levels — maker BUYs / lends, a taker consumes them to BORROW. Best borrow first (tick ↓). */ bids: MidnightBookLevel[]; /** Ask levels — maker SELLs / borrows, a taker consumes them to LEND. Best supply first (tick ↑). */ asks: MidnightBookLevel[]; } /** Pluggable Midnight order-book source — hosted API today, self-indexed mempool later. */ interface MidnightBookSource { /** Best-rate + depth snapshot for a market, or null when unavailable. */ getBookTop(marketId: string): Promise; /** * Full best-first ladder for a market, or null when unavailable. Optional so * lightweight stubs need only implement `getBookTop`; the hosted API source * provides it for the live "all offers" endpoint. */ getBook?(marketId: string): Promise; /** * ONE fetch: the full-depth aggregate {@link MidnightBookTop} PLUS a bounded * best-first ladder slice (top `maxLevels` per side). Same `/books/{id}` call * as `getBookTop`/`getBook` — the public batch uses this so it gets the top * AND a chunk of the book without a second request. Optional (stubs may omit). */ getTopAndBook?(marketId: string, maxLevels?: number): Promise<{ top: MidnightBookTop; book: MidnightBook; } | null>; } /** Default hosted Midnight API (see @morpho-org/midnight-sdk MidnightApi). */ declare const DEFAULT_MIDNIGHT_API = "https://api.morpho.org/v0/midnight"; type FetchLike$2 = typeof fetch; /** * Hosted-API book source. Reads `GET {base}/books/{marketId}` and reduces the * `asks`/`bids` price levels to a {@link MidnightBookTop}. This is the swappable * seam: a self-indexed mempool source can implement the same interface later. */ declare class ApiBookSource implements MidnightBookSource { private readonly baseUrl; private readonly fetchImpl; constructor(baseUrl: string, fetchImpl?: FetchLike$2); getBookTop(marketId: string): Promise; /** * Full ladder for a market — every level per side, best-first (unlike * `getBookTop`, which collapses to the best level + aggregate depth). Same * `GET {base}/books/{marketId}` fetch; empty levels are dropped and each side * is sorted so the best executable offer is first (bids by tick ↓ = cheapest * borrow, asks by tick ↑ = highest lend yield). The caller derives per-level * APR (from tick + TTM) and applies count/size filters. */ getBook(marketId: string): Promise; /** * ONE fetch → the full-depth aggregate top + a bounded best-first ladder slice * (top `maxLevels` per side). Same `/books/{marketId}` call as `getBookTop`, so * the public batch captures a chunk of the book for free (no extra request). */ getTopAndBook(marketId: string, maxLevels?: number): Promise<{ top: MidnightBookTop; book: MidnightBook; } | null>; /** * Maker addresses per price level, keyed by tick string. `getBook` levels are * aggregated by tick and carry NO maker (a level can be several makers), so * this reads the individual offers from the `{side}/quote` endpoint (which the * book endpoint lacks) and groups their makers by tick. `assets` should be the * book's total depth so the quote returns EVERY offer (it returns none when the * target exceeds depth). Best-effort: `{}` on any failure. */ getOfferMakers(marketId: string, side: 'bids' | 'asks', assets: bigint): Promise>; } /** Build the default (hosted-API) book source for a chain. */ declare function createMidnightBookSource(chainId: string, fetchImpl?: FetchLike$2): MidnightBookSource; /** * Top-of-book snapshot for a single Term repo, already reduced to best * executable APR per side + aggregate depth. Unlike Midnight (tick math), the * Term subgraph is expected to return APRs directly (secondary-listing discount * rates and auction-clearing rates), so these are plain percents. * * Side semantics: SUPPLY = lend (buy repo tokens on the secondary book / submit * auction offers); BORROW = auction bids. */ interface TermBookTop { /** Best lend APR in percent (secondary listings / auction offers), if any. */ supplyAprPct?: number; /** Best borrow APR in percent (auction bids), if any. */ borrowAprPct?: number; /** Lendable depth in loan-token units (human, already decimal-scaled). */ supplyLiquidity: number; /** Borrowable depth in loan-token units (human, already decimal-scaled). */ borrowLiquidity: number; } /** One aggregated book level (already rate+size normalized). */ interface TermBookLevel { /** annualised rate at this level, percent. */ apr: number; /** aggregate size, loan-token base units (raw string). */ units: string; /** aggregate size, loan-token assets (human number). */ assets: number; } /** * Bounded book slice. `asks` = the secondary repo-token orders (RepoTokenLinkedList, * the continuous LEND book, analogous to Midnight asks); `bids` = the BORROW * side (typically empty — Term borrow origination is sealed-bid auction, not a * continuous book). */ interface TermBook { bids: TermBookLevel[]; asks: TermBookLevel[]; } /** One secondary-market repo-token listing (RepoTokenLinkedList), from the subgraph. */ interface TermListing { listingId: string; seller: string; repoToken: string; /** Listed amount in repo-token units (raw string). */ amount: string; /** Effective discount rate in percent, if the source provides it. */ discountRatePct?: number; } /** * One PRIMARY-auction submission by a user (the sealed-bid book that has no * instant-settle counterpart): `offer` = a lend commitment, `bid` = a borrow * commitment. Only surfaces AFTER the on-chain entity exists (i.e. locked) — * the pre-image price stays sealed (`revealed=false`) until the reveal window. * The `active` flag marks submissions whose auction is still running (awaiting * reveal/clearing, so still cancellable/actionable); once the auction clears, * `assignedAmount` is how much was filled into a position. */ interface TermAuctionOrder { /** Subgraph entity id (the offer/bid id). */ id: string; side: 'offer' | 'bid'; /** Auction (offer/bid locker round) this submission belongs to. */ auctionId: string; /** Submitter address (offeror for offers, bidder for bids). */ account: string; /** Submitted amount, purchase(loan)-token base units (raw string). */ amount: string; /** Same amount decimal-scaled to loan-token assets (human number). */ assets: number; /** Amount assigned when the auction cleared, base units (raw; 0 while pending). */ assignedAmount: string; /** Still locked in the auction locker. */ locked: boolean; /** True once the sealed price has been revealed (revealed price > 0). */ revealed: boolean; /** Revealed annualized rate (WAD, raw string) or null while still sealed. */ revealedPriceWad: string | null; /** Auction still open — not complete and not cancelled (actionable). */ active: boolean; auctionComplete: boolean; auctionCancelled: boolean; /** Reveal window opens (unix seconds). */ revealTime: number; /** Auction closes / clears (unix seconds). */ auctionEndTime: number; } /** A user's primary-auction submissions for one repo, split by side. */ interface TermAuctionOrders { offers: TermAuctionOrder[]; bids: TermAuctionOrder[]; } /** * Pluggable Term public-data source. The hosted subgraph is the source today; * an on-chain reader could implement the same interface later. All methods * return null/[] when no endpoint is configured (see `resolveTermApiBase`). */ interface TermBookSource { /** Best-APR + depth snapshot for a repo, or null when unavailable. */ getBookTop(config: TermMarketConfig): Promise; /** Active secondary listings for a repo, or null when unavailable. */ getListings?(config: TermMarketConfig): Promise; /** * A user's PRIMARY-auction submissions (offers + bids) for a repo — the * sealed-bid book that has no continuous/instant surface. Returns null when * no subgraph is configured. Prices stay sealed until reveal. */ getAuctionOrders?(config: TermMarketConfig, account: string): Promise; /** * ONE query → the aggregate top PLUS a bounded book slice (top `maxLevels` * per side). The public batch uses this so it captures a chunk of the order * book without a second request. Optional (stubs may omit). */ getTopAndBook?(config: TermMarketConfig, maxLevels?: number): Promise<{ top: TermBookTop; book: TermBook; /** Live/upcoming auction round; null when none is listed. */ auction: TermAuctionWindow | null; } | null>; } /** * The repo's CURRENT primary auction round, when one is listed. * * Term borrow origination is a periodic sealed-bid auction, not a continuous * book: outside the submission window there is nothing to bid on, so a repo * whose auction has cleared is lend-only (buy repo tokens on the secondary * book) until the next round is listed. Timestamps are raw so consumers can * derive a live countdown; `status` is a snapshot at fetch time. */ interface TermAuctionWindow { /** Auction round id (the TermAuction entity id). */ id: string; /** Submissions open (unix seconds). */ startTime: number; /** Submissions CLOSE and the sealed prices start revealing (unix seconds). */ revealTime: number; /** Auction clears (unix seconds). Equal to `revealTime` on current deployments. */ endTime: number; /** Minimum bid (borrow) size, loan-token base units (raw string; '0' when unset). */ minBidAmount: string; /** Minimum offer (lend) size, loan-token base units (raw string; '0' when unset). */ minOfferAmount: string; /** Highest accepted bid rate, WAD (raw string; '0' when unset). */ maxBidPriceWad: string; /** Highest accepted offer rate, WAD (raw string; '0' when unset). */ maxOfferPriceWad: string; } /** * One side of the Terminal 1 FILL-NOW book (limit orders on the intent * diamond), reduced to the best executable rate + depth + best-first levels. * Unlike the auction clearing rate this IS obtainable right now: a taker * settles against the maker's order at the order's own rate. */ interface TermFillNowSide { /** Best executable APR at this instant, percent (Term 360-day convention). */ aprPct: number; /** Aggregate fillable depth, loan-token base units (raw string). */ units: string; /** Same depth decimal-scaled to loan-token assets (human number). */ assets: number; /** Best-first per-order levels (real per-level rates). */ levels: TermBookLevel[]; } /** * Fill-now liquidity for one repo from the Terminal 1 order store, taker * perspective: `borrow` aggregates maker LEND orders (what our user can borrow * against, cheapest first), `lend` aggregates maker BORROW orders (what our * user can lend into, highest rate first). */ interface TermFillNow { borrow?: TermFillNowSide; lend?: TermFillNowSide; } /** A Term repo paired with its current top-of-book (null when the fetch failed). */ interface TermMarketRaw { config: TermMarketConfig; top: TermBookTop | null; /** Bounded book slice (top-N levels/side); null/absent when unavailable. */ book?: TermBook | null; /** * The live/upcoming auction round, or null when no round is currently listed * (the common case between auctions — the repo is then lend-only). */ auction?: TermAuctionWindow | null; /** * Terminal 1 fill-now order liquidity, or null when the order store is * unreachable / has no fillable orders for this repo. Independent of the * auction window — this is what makes a repo borrowable BETWEEN rounds. */ fillNow?: TermFillNow | null; } /** * Term Finance Terminal 1 order store — the FILL-NOW limit-order surface. * * Between sealed-bid auction rounds a Term repo used to be display-only on the * borrow side. Terminal 1 adds a maker/taker limit-order book settling into the * SAME repo markets: makers post EIP-712 (or on-chain presigned) lend/borrow * orders keyed by `repoServicer`, takers fill them on the Terminal 1 diamond * (`settleLimitLend` / `settleLimitBorrow`), and the resulting position is an * ordinary Term repo position (repo tokens / collateralized debt). * * Discovery is an open, unauthenticated REST store. Enforcement is on-chain, * so a stale store can only under-report — never mis-settle. See * TERM_TERMINAL1.md for the full surface. * * Side semantics (taker/our-user perspective): * - a maker LEND order = fill-now BORROW liquidity (taker borrows at its rate) * - a maker BORROW order = fill-now LEND liquidity (taker lends at its rate) */ declare const DEFAULT_TERM_ORDER_STORE = "https://api.global.termfinance.io/protocol"; /** Order-store base for a chain: override → config → public hosted store. */ declare function termOrderStoreBaseUrl(chainId: string): string; /** One order as served by `GET {base}/orders?chainId=` (fields we consume). */ interface TermStoreOrder { id: string; orderKind: 'lend' | 'borrow'; chainId: number; /** THE market join key — the repo's TermRepoServicer (NOT termRepoId). */ repoServicer: string; /** Order size, purchase-token base units (raw string). */ purchaseTokenAmount: string; /** * Fixed rate, 1e18-scaled FRACTION annualized on Term's 360-day year — the * same convention as auction clearing prices (`termOfferRateToAprPct`). */ offerRate: string; maker: string; /** Pinned counterparty; zero address = anyone may fill. */ taker: string; /** Unix seconds (stringified uint256; max-uint = good-til-cancelled). */ expiry: string; salt: string; sigType: number; sigData: string; isPreSigned: boolean; orderState: string; /** Unfilled remainder, purchase-token base units (raw string). */ remainingAmount: string; filledAmount: string; /** Maker's live spendable balance (lend orders; raw string). */ cachedAvailableBalance?: string; hasSufficientApproval?: boolean; /** True for auto-quoted Blue Sheets VAULT liquidity (not a human maker). */ isSynthetic?: boolean; /** Fee the order charges the taker (raw string; semantics per order kind). */ borrowFee?: string; feeRecipient?: string; repoToken?: string; } /** * A maker order is fillable by an arbitrary taker when it is live, open to * anyone, and (for lend orders) actually funded. The store pre-computes the * funding checks (`cachedAvailableBalance` / `hasSufficientApproval`); trust * them for DISPLAY — actions re-validate on-chain at settle time anyway. */ declare function fillableRemaining(order: TermStoreOrder, nowSec: number): bigint; /** * Fetch the full order store for a chain (ONE request) and group orders by * `repoServicer` (lowercased). Returns null on transport failure so callers * can distinguish "store down" from "no orders". * * `filter` (default `'fillable'`) keeps only orders an ARBITRARY taker can * fill right now — the book/rate view. `'all'` keeps taker-pinned, unfunded * and exhausted rows too: the view a MAKER needs of their own orders. */ declare function fetchTermStoreOrders(chainId: string, fetchImpl?: typeof fetch, filter?: 'fillable' | 'all'): Promise | null>; /** * Reduce one repo's store orders to the fill-now summary consumed by the * converter: best-executable APR per side + depth + best-first levels. */ declare function toTermFillNow(orders: TermStoreOrder[] | undefined, loanDecimals: number, nowSec?: number): TermFillNow | null; /** * Fetch the current top-of-book + a bounded book chunk for every configured * Term repo on a chain. * * Public data = static repo config (from the `termMarkets` registry) + live * secondary-listing / auction rates + depth (from the subgraph source). The * aggregate best rate + FULL depth live on `top`; `book` carries the best * `TERM_BOOK_LEVELS` open orders per side for downstream filtering. Both are * null when the fetch failed and no recent snapshot is cached, and when no data * endpoint is configured (rates fall back to 0). */ declare function fetchTermMarkets(chainId: string, source?: TermBookSource, fetchOrders?: (chainId: string) => Promise | null>): Promise; /** Synthesized per-market lender key, e.g. `TERM_FINANCE_`. */ declare function termLenderKey(termRepoId: string): string; /** * Map fetched Term repos into the shared `MorphoGeneralPublicResponse` shape * (identical to Midnight), keyed by the synthesized `TERM_FINANCE_` lender * key. One LOAN entry carries the fixed rates (`variableBorrowRate` = auction * borrow APR, `depositRate` = lend APR) + order-book depth as liquidity; one * COLLATERAL entry per leg carries the maintenance-ratio → collateralFactor + * liquidated-damages → penalty. `params.market.fixedTerm.model = 'term'`. */ declare function convertTermMarketsToResponse(raw: TermMarketRaw[], chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; type FetchLike$1 = typeof fetch; /** Resolve a chain's Term subgraph URL (override/config → per-chain default → ''). */ declare function termApiBaseUrl(chainId: string): string; /** * GraphQL subgraph source. `getBookTop` derives the fixed APR from the repo's * latest completed auction clearing price and open-order depth; `getListings` * returns the open secondary-market repo-token orders. */ declare class TermSubgraphSource implements TermBookSource { private readonly url; private readonly fetchImpl; constructor(url: string, fetchImpl?: FetchLike$1); private gql; getBookTop(config: TermMarketConfig): Promise; /** * ONE query → the aggregate top (best APR + FULL depth) PLUS a bounded book * slice (top `maxLevels` open orders per side) PLUS the repo's current * auction round. `asks` = orders selling repo tokens (the secondary LEND * book); `bids` = the rest (borrow side, usually empty — Term borrow is * sealed-bid auction, not a continuous book). Term secondary orders carry no * per-order rate, so every level shares the market's clearing APR; the levels * expose per-order SIZE for filtering. * * Two auction reads, deliberately distinct: * - `cleared` — the latest COMPLETE round, whose clearing price IS the * market's fixed APR (and stays the reference rate between auctions). * - `pending` — rounds not yet complete/cancelled. Only one of these is a * real, actionable round; the rest are abandoned listings the subgraph * never marked complete, filtered out below. */ getTopAndBook(config: TermMarketConfig, maxLevels?: number): Promise<{ top: TermBookTop; book: TermBook; auction: TermAuctionWindow | null; } | null>; getListings(config: TermMarketConfig): Promise; /** * A user's primary-auction offers + bids for a repo. Offers/bids link to an * `auction`, which links to a `term` (the repo) — so we filter by the * submitter AND the nested `auction_.term`. The revealed price is 0 (Bytes * `unrevealed*Price` still sealed) until the reveal window; `active` marks * submissions whose auction is neither complete nor cancelled. */ getAuctionOrders(config: TermMarketConfig, account: string): Promise; } /** Default Term public-data source for a chain (subgraph via resolved URL). */ declare function createTermBookSource(chainId: string, fetchImpl?: FetchLike$1): TermBookSource; /** * Convert a Terminal 1 order `offerRate` (1e18-scaled fraction, annualized on * Term's 360-day year) into the display APR percent. Deliberately the SAME * treatment as auction clearing prices (`rate / WAD * 100`, no 365/360 * adjustment) so fill-now and auction rates on one row stay comparable — * both carry Term's own day-count convention. */ declare function termOfferRateToAprPct(offerRate: string | undefined): number; /** * Decoded shapes of the Exactly `Previewer.exactly(account)` aggregate view. * Field names/order mirror the on-chain struct (verified IDENTICAL on Optimism * and Base). All rates are WAD-scaled ANNUALIZED fractions (1e18 = 100%/yr), * `usdPrice` is 1e18-scaled USD, token amounts are raw base units. */ interface ExactlyFixedPool { /** unix seconds */ maturity: bigint; /** total fixed borrows at this maturity (raw asset units) */ borrowed: bigint; /** total fixed deposits at this maturity (raw asset units) */ supplied: bigint; /** borrowable liquidity at this maturity incl. floating backup (raw) */ available: bigint; /** WAD fixed-pool utilization */ utilization: bigint; /** WAD annualized fixed DEPOSIT rate at the current pool state */ depositRate: bigint; /** WAD annualized fixed BORROW rate floor (rate for a minimal borrow) */ minBorrowRate: bigint; /** deposit size that would capture the pool's unassigned earnings (raw) */ optimalDeposit: bigint; } interface ExactlyFixedPosition { maturity: bigint; /** current exit value: withdraw-now (deposits) / repay-now (borrows), raw. * Includes the early-exit discount and, when overdue, the late penalty. */ previewValue: bigint; position: { principal: bigint; fee: bigint; }; } interface ExactlyMarketAccount { market: string; symbol: string; decimals: number; asset: string; assetName: string; assetSymbol: string; usdPrice: bigint; /** WAD per-second late-repayment penalty rate */ penaltyRate: bigint; /** WAD collateral/borrow adjust factor (multiplicative, like Dolomite premiums) */ adjustFactor: bigint; maxFuturePools: number; reserveFactor: bigint; fixedPools: readonly ExactlyFixedPool[]; /** WAD annualized floating borrow rate */ floatingBorrowRate: bigint; /** WAD floating utilization */ floatingUtilization: bigint; floatingAssets: bigint; floatingDebt: bigint; floatingBackupBorrowed: bigint; floatingAvailableAssets: bigint; totalFloatingBorrowAssets: bigint; totalFloatingDepositAssets: bigint; totalFloatingBorrowShares: bigint; totalFloatingDepositShares: bigint; isCollateral: boolean; maxBorrowAssets: bigint; floatingBorrowShares: bigint; floatingBorrowAssets: bigint; floatingDepositShares: bigint; floatingDepositAssets: bigint; fixedDepositPositions: readonly ExactlyFixedPosition[]; fixedBorrowPositions: readonly ExactlyFixedPosition[]; } /** Raw public-data batch: one Previewer pass + the Auditor liquidation bonus. */ interface ExactlyMarketsRaw { markets: ExactlyMarketAccount[]; /** WAD fractions: liquidator bonus + lenders share (null if the read failed) */ liquidationIncentive: { liquidator: bigint; lenders: bigint; } | null; } /** * Fetch all Exactly market data for a chain — FULLY ON-CHAIN (no API, no * indexer): a single `Previewer.exactly(address(0))` eth_call returns every * market with its fixed pools (maturities + rates + liquidity), floating side, * prices and risk params; a second call reads the Auditor liquidation bonus. * The account-scoped fields are zero for the zero address and ignored. * * Returns `{ markets: [], liquidationIncentive: null }` when the chain has no * Exactly config or the read fails (the converter then emits nothing). */ declare function fetchExactlyMarkets(chainId: string): Promise; /** * The ONE Exactly lender key per chain. * * Exactly is a CROSS-MARGIN protocol: a single per-chain `Auditor` (a * Compound-V2-shaped comptroller, NOT a Euler controller) holds one * `enterMarket` bitmap per account, every entered deposit backs debt in ANY * market simultaneously, and health is one global check. The per-asset `Market` * contracts exist because each is the ERC-4626 share token for its asset and * carries that asset's rates / fixed pools — exactly like cUSDC and cETH under * one Comptroller. They are NOT isolated markets. * * So Exactly is modeled like Compound V2: ONE lender key, one entry per asset. * (It was previously split into synthesized `EXACTLY_` keys — that * only ever existed because `terms[]` / `fixedTerm` lived on `params.market`, * which assumes one borrowable asset per key. Both now also exist per asset on * `data[*]`, so the split is gone along with the cross-margin collateral * mirroring, the double-count hazard and the optimistic per-key health it * forced. Resolve a Market contract from the ASSET via * `exactlyMarketByAsset(chainId, asset)` — or from the entry's `poolId`.) */ declare const EXACTLY_LENDER_KEY = "EXACTLY"; /** * Map the on-chain Previewer batch into the shared `MorphoGeneralPublicResponse` * shape, under the SINGLE cross-margin {@link EXACTLY_LENDER_KEY} — one entry * per ASSET (the Compound V2 shape), never one key per Market. * * Per asset entry: * - FLOATING rates (`depositRate` / `variableBorrowRate`) plus the best live * fixed borrow APR on `stableBorrowRate`; * - its OWN `terms[]` maturity menu (`termId` = the pool's maturity) and its * OWN `fixedTerm` descriptor pointing at that asset's Market — per-asset * because each asset has its own fixed pools; * - risk as `collateralFactor = adjustFactor` + `borrowFactor = 1/adjustFactor`, * which is the Auditor's own formula (their product = the pairwise LTV); * - `poolId` / `exactly.market` = the Market contract (the write target). * * Every asset is simultaneously borrowable AND collateral for every other, so * there are no sibling-collateral rows. `params.market` carries only the * pool-wide descriptor (Auditor as `id`, a market-level `fixedTerm` without a * provider address). See the wrapper README for the repay mechanics. */ declare function convertExactlyMarketsToResponse(raw: ExactlyMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Exactly rate/scale helpers. All on-chain rates are WAD (1e18) fractions. */ /** WAD annualized rate → percent (0.0547e18 → 5.47). */ declare function exactlyWadRateToPercent(rate: bigint | undefined): number; /** * WAD per-second late-repay `penaltyRate` → annualized percent. The penalty * accrues linearly per second on OVERDUE fixed debt (principal + fee) from * maturity until repaid, ON TOP of the owed amount. E.g. the live value * 52083333333/s ≈ 164%/yr ≈ 0.45%/day. */ declare function exactlyPenaltyRateToAprPercent(penaltyRatePerSecond: bigint | undefined): number; /** * Effective pairwise LTV between two Exactly markets. Exactly health is * Σ(collateral × adjustFactor_c) ≥ Σ(debt / adjustFactor_b), so the max * borrow of market B against collateral in market A is adjF_A × adjF_B — * MULTIPLICATIVE adjust factors (same convention as Dolomite premiums). */ declare function exactlyPairLtv(collateralAdjustFactor: bigint | undefined, borrowAdjustFactor: bigint | undefined): number; /** * Per-position fixed-term detail attached to the position row (raw strings). * * Carries the FULL exit economics so a repay/withdraw UI needs no second read: * `faceValue` is what is owed/paid at maturity, `previewValue` is what the exit * actually costs/pays RIGHT NOW, and exactly one of `earlyRepayDiscount` / * `earlyExitCost` / `latePenalty` explains the gap. See the "repay terms" * section of the Exactly README for the source-verified formulas. */ interface ExactlyUserFixedPosition { /** unix maturity */ maturity: number; /** 'deposit' | 'borrow' */ kind: 'deposit' | 'borrow'; /** face principal (raw asset units) */ principal: string; /** face fee locked at trade time (raw asset units) */ fee: string; /** face value at maturity = principal + fee. Static — Exactly fixed debt does * NOT accrue an index; it only grows via the late penalty below. */ faceValue: string; /** live exit value now: withdraw-now / repay-now incl. discount or overdue * penalty (raw asset units) — from the Previewer */ previewValue: string; /** true once maturity passed and the position is still open (borrows accrue * the per-second late penalty until repaid) */ overdue: boolean; /** seconds past maturity (0 until overdue) */ secondsLate: number; /** BORROW before maturity: face − repay-now, the REBATE for repaying early * (Exactly never charges an early-repay fee). Absent otherwise. */ earlyRepayDiscount?: string; /** DEPOSIT before maturity: face − payout-now, the HAIRCUT for exiting a * fixed deposit early (sold back at the current curve rate). Absent * otherwise. */ earlyExitCost?: string; /** BORROW past maturity: repay-now − face, penalty accrued SO FAR. Absent * otherwise. */ latePenalty?: string; /** BORROW: penalty this position accrues per further day overdue (raw units, * linear on face — not compounding). Present for borrows only. */ latePenaltyPerDay: string; /** market's linear late-penalty rate as an annualized percent (e.g. 164.24) */ latePenaltyApr: number; } /** * Raw shapes for the Flying Tulip public-data fetch (one LendingLens * multicall pass per chain; see FLYING_TULIP.md §4). */ /** One asset's live lens read, merged over its lender-metadata roster row. */ interface FlyingTulipAssetRaw { address: string; symbol: string; name: string; decimals: number; /** Live IRM from `assetCfg` — per (chain, asset class), redeploys move it. */ irm: string; /** Live maintenance-margin bps — charged on BOTH legs (cross-margin). */ mmBps: number; enabled: boolean; borrowable: boolean; isCollateral: boolean; supplyCap: bigint; borrowCap: bigint; depositPaused: boolean; withdrawPaused: boolean; borrowPaused: boolean; /** Roster flag: the protocol oracle reverts for this asset (FT itself). */ priceable: boolean; /** `priceAndDecimals` pxWad (1e18 USD), null when unpriceable/failed. */ priceWad: bigint | null; cash: bigint; borrows: bigint; reserves: bigint; utilWad: bigint; /** `irmSampleAPR(irm, [utilWad])[0]` — live borrow APR, 1e18 = 100%/100. */ borrowAprWad: bigint | null; } interface FlyingTulipMarketsRaw { /** Open gate (1e4): new borrows/withdrawals must leave HF ≥ this. */ hfSafeBps: number; /** Liquidation trigger (1e4): 12500 — HF < 1.25, never 1.0. */ hfTargetBps: number; /** Minimum account equity while any debt exists — PER CHAIN (wad string). */ minEquityUSDWad: string; positionsManager: string; oracleRouter: string; assets: FlyingTulipAssetRaw[]; } /** * Fetch all Flying Tulip market data for a chain — FULLY ON-CHAIN (there is * no API; `api.flyingtulip.com` is analytics). The roster comes from * lender-metadata's `AssetSet`-log discovery; every VALUE is read live from * the `LendingLens` (config is one ungated Safe call from moving, so the * roster snapshot is only the fallback for a failed item). * * Two multicall rounds, both `allowFailure` per item: * 1. per asset: `assetState` + `assetCfg` + caps + pauses + * `priceAndDecimals` — the LAST one deliberately per-asset, never the * batched `pricesUSD`, which reverts whole-batch on any unpriceable * asset (FT itself, on both chains); * 2. per asset: `irmSampleAPR(liveIrm, [liveUtil])` — the exact live borrow * APR from the deployed curve, no ported model to drift. */ declare function fetchFlyingTulipMarkets(chainId: string): Promise; /** * The ONE Flying Tulip lender key per chain. * * Flying Tulip is a CROSS-MARGIN protocol: a single per-chain * `PositionsManager` holds every asset, health is one global check * (`HF = equity / Σ bothLegs × mmBps`), and there is no per-market key space * to fan out. So it is modeled exactly like Exactly post-collapse: ONE lender * key, one entry per asset. */ declare const FLYING_TULIP_LENDER_KEY = "FLYING_TULIP"; /** * Map the lens batch into the shared `MorphoGeneralPublicResponse` shape, * under the SINGLE cross-margin {@link FLYING_TULIP_LENDER_KEY} — one entry * per ASSET. * * Risk factors (FLYING_TULIP.md §2.1): `maint` charges mmBps on BOTH legs, so * pair LTV is multiplicative like Exactly's Auditor — but the open gate and * the liquidation trigger use DIFFERENT HF thresholds (1.50 / 1.25), and our * `config[0]` carries a single `borrowFactor`. The build uses `hfSafe` for * `borrowFactor`, which makes the OPEN gate exact * (`borrowCollateralFactor_i × borrowFactor_j` = the protocol's max LTV to * the basis point) and leaves the liquidation threshold conservative by * ~0.35 pp on stables — deliberate: quotes never revert, warnings come early. * * borrowCollateralFactor_i = 1 − hfSafe·mm_i (open) * collateralFactor_i = 1 − hfTgt ·mm_i (liquidation) * borrowFactor_j = 1 / (1 + hfSafe·mm_j) * * `depositRate = variableBorrowRate × utilization`, with NO reserve factor. * * This CORRECTS an earlier reading (FLYING_TULIP.md §3.1) that supplier yield * is only an epoch-indexed FT emission and that any IRM-derived supply APR is * therefore wrong. The FT emission is real and is still not modelled here — it * needs an external FT price and belongs in the rewards pipeline — but it is * ON TOP of in-kind interest, not instead of it. Flying Tulip's own UI computes * the supply leg exactly as borrow × utilization, verified against the live * Sonic book on 6/6 assets (2026-08-26): * * USDC 94.26 % util → 10.7609 % borrow → 10.1434 % (their UI: 10.14 %) * ftUSD 87.96 % → 6.3527 % → 5.5879 % (5.58 %) * wS 42.57 % → 2.1453 % → 0.9132 % (0.91 %) * USSD 14.38 % → 3.1921 % → 0.4591 % (0.45 %) * WBTC 1.74 % → 1.1337 % → 0.0197 % (0.01 %, they truncate) * WETH 1.67 % → 1.1283 % → 0.0188 % (0.01 %) * * Two things this pins down. There is **no reserve factor** today — the naive * `borrow × util` matches to four decimals, so do not introduce a `(1 − rf)` * term on the assumption that every pool has one; `reserves` on `astate` is an * accrued balance, not a rate. And their column is LABELLED "Deposit APY" but * carries the APR (10.14 %, not the 10.67 % that compounds to) — so emit it as * nominal APR per the package convention and do not compound it. * * Use the lens's own `utilWad` as the denominator. `borrows / totalSupplied` * agrees to four decimals on every asset, but `utilWad` is the figure the * protocol itself feeds to the IRM, so the two legs cannot drift apart. */ declare function convertFlyingTulipMarketsToResponse(raw: FlyingTulipMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** * Raw per-branch on-chain snapshot of a Liquity-family deployment. All bigints * are raw WAD/base units straight from the multicall; `null` marks a failed * (allowFailure) read — the converter degrades gracefully per field. */ interface LiquityBranchRaw { branch: LiquityBranchConfig; /** TroveManager.getEntireBranchDebt() — total branch debt incl. accrued interest */ entireDebt: bigint | null; /** TroveManager.getEntireBranchColl() — total branch collateral */ entireColl: bigint | null; /** TroveManager.shutdownTime() — 0 while the branch is live */ shutdownTime: bigint | null; /** ActivePool.aggRecordedDebt() — recorded aggregate debt (interest accrual base) */ aggRecordedDebt: bigint | null; /** ActivePool.aggWeightedDebtSum() — Σ debt·rate (1e36 scale): drives avg rate + SP APR */ aggWeightedDebtSum: bigint | null; /** StabilityPool.getTotalBoldDeposits() — the branch "earn" pool size */ spDeposits: bigint | null; /** * PriceFeed.lastGoodPrice() — USD scaled by `branch.priceDecimals` (1e18 on * canonical 18-dec collateral; fork-dependent otherwise). Always read it * through `liquityCollateralPrice`, never a bare `/1e18`. */ collPrice: bigint | null; /** SortedTroves.getSize() — live (non-zombie) trove count */ troveCount: bigint | null; } /** Raw public-data batch for ONE deployment (lender) on one chain. */ interface LiquityMarketsRaw { /** The deployment's bare lender key, e.g. `LIQUITY_V2` — keys the config row. */ lender: string; config: LiquityConfigChain | undefined; branches: LiquityBranchRaw[]; } /** * Fetch all branch data of ONE Liquity-family deployment — FULLY ON-CHAIN via * one retrying multicall (no API/indexer; the optional api.liquity.org stats * endpoint is display enrichment only and is NOT fetched here, so a dead API * can never break the data path). Branch addresses + fork deviation params * come from lender-metadata (`liquityConfig`/`liquityMarkets`, keyed * lender → chain). `multicallRetryUniversal` rotates RPCs on failure. * * Price: `lastGoodPrice` (view) — updated on every borrower op, so fresh on * any active branch. (`fetchPrice` would be exact but returns a tuple and is * nonpayable; revisit if quiet-fork staleness ever matters.) */ declare function fetchLiquityMarkets(lender: string, chainId: string): Promise; /** * Synthesized per-branch lender key, e.g. `LIQUITY_V2_1_1` (= mainnet wstETH * branch). The CHAIN ID is part of the key (Fluid convention, * `FLUID__`) so keys stay GLOBALLY unique for multi-chain * deployments (Ebisu spans Ethereum + Plasma) — labels, yield-tracer rows and * anything else keyed by lender_key alone would collide otherwise. */ declare function liquityLenderKey(lender: string, chainId: string | number, collIndex: number): string; /** * Recover `{ lender, chainId, collIndex }` from a per-branch key (or * undefined for a bare deployment key / non-family key). Fork brands share no * prefix, so this matches against the family list. */ declare function liquityKeyParts(key: string): { lender: string; chainId: string; collIndex: number; } | undefined; /** * Map one deployment's on-chain branch batch into the shared * `MorphoGeneralPublicResponse` shape (identical to Midnight/Term/Exactly), * keyed by `__` — one key per collateral branch. * * Per branch: * - the COLLATERAL entry (branch coll token): totals = branch collateral; * LTV = 1/MCR (a trove is liquidatable below ICR = MCR), liquidation * penalty = the SP-offset penalty; borrowing disabled; * - the LOAN entry (the deployment's stable token): `totalDebt` = branch * debt (incl. accrued interest), `totalDeposits` = the branch Stability * Pool ("earn" side), `depositRate` = SP APR (spYieldSplit × Σ debt·rate / * SP size), `variableBorrowRate` = branch average user-set rate. The * ACTUAL borrow rate is per-trove user-set — bounds + averages live in * `params.market` (`minAnnualInterestRate` / `maxAnnualInterestRate` / * `avgBorrowRate`, WAD strings). BOLD-side deposits do NOT collateralize. * - `borrowLiquidity` = mintable headroom: collateral value / MCR − debt, * additionally capped by the branch debt cap on forks that have one. * * All fork deviations ride in from metadata via `raw.config` / `raw.branch` — * nothing here is deployment-specific. */ declare function convertLiquityMarketsToResponse(raw: LiquityMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Per-trove detail attached to the debt position row (raw strings). */ interface LiquityTroveInfo { /** decimal uint256 troveId — the sub-account id */ troveId: string; collIndex: number; /** user-set annual interest rate, WAD string (batch rate when delegated) */ annualInterestRate: string; /** unix seconds of the last rate change (premature-adjust fee window) */ lastInterestRateAdjTime: number; /** redeemed below min debt — adjust via adjustZombieTrove */ zombie: boolean; /** pending redistribution gains (from liquidations shared to the branch) */ redistCollGain: string; redistBoldDebtGain: string; /** interest + batch-management fee accrued into entireDebt */ accruedInterest: string; accruedBatchManagementFee: string; } /** Stability-pool position detail attached to the `sp` sub-account rows. */ interface LiquitySpInfo { /** compounded stable deposit (raw) */ deposit: string; /** claimable stable yield incl. pending aggregate interest (raw) */ yieldGain: string; /** claimable liquidation collateral gain (raw coll units) */ collGain: string; /** collateral stashed from prior doClaim=false ops (raw coll units) */ stashedColl: string; /** post-liquidation collateral surplus claimable via claimCollateral (raw) */ collSurplus: string; } interface LiquityDiscoveredTrove { /** decimal uint256 troveId */ troveId: string; zombie: boolean; } interface LiquityDiscovery { /** Discovered troves per branch, index-aligned with `liquityBranchesByChain`. */ perBranch: LiquityDiscoveredTrove[][]; at: number; } declare const getCachedLiquityTroves: (chainId: string, lender: string, account: string) => LiquityDiscovery | undefined; /** Candidate trove ids for one user: direct + per-zapper salted, as decimal strings. */ declare function liquityCandidateTroveIds(account: string, zappers: string[]): string[]; /** * Async build: runs the discovery status multicall itself, caches the found * per-branch trove ids, then returns the data-phase call set executed by the * shared sharded multicall (against the merged Liquity ABI from `getAbi`): * * per branch (config order): * troves × [TroveManager.getLatestTroveData(id), TroveNFT.ownerOf(id)] * + StabilityPool.[getCompoundedBoldDeposit, getDepositorYieldGainWithPending, * getDepositorCollGain, stashedColl](account) * + CollSurplusPool.getCollateral(account) (when configured) */ declare const buildLiquityUserCall: (chainId: string, lender: string, account: string) => Promise; /** * Raw per-TroveManager on-chain snapshot of a River deployment. All bigints * are raw units straight from the multicall; `null` marks a failed * (allowFailure) read — the converter degrades gracefully per field. */ interface RiverMarketRaw { market: RiverMarketConfig; /** TroveManager.getEntireSystemDebt() — total market debt incl. redistribution */ entireDebt: bigint | null; /** TroveManager.getEntireSystemColl() */ entireColl: bigint | null; /** TroveManager.getBorrowingRateWithDecay() — the CURRENT one-off mint-fee rate (WAD) */ mintFeeRate: bigint | null; /** TroveManager.interestRate() — ongoing annual borrow rate (WAD; 0 = no interest, Prisma model) */ interestRate: bigint | null; /** TroveManager.fetchPrice() — oracle price, 1e18 USD (nonpayable, simulated) */ price: bigint | null; /** TroveManager.getTroveOwnersCount() */ troveCount: bigint | null; } /** Raw public-data batch for ONE River deployment (lender) on one chain. */ interface RiverMarketsRaw { /** The deployment's bare lender key, `RIVER`. */ lender: string; config: RiverConfigChain | undefined; chainData: RiverChainData | undefined; /** Single per-chain StabilityPool size (diamond facet). */ spDeposits: bigint | null; markets: RiverMarketRaw[]; } /** * Fetch all market data of ONE River deployment — FULLY ON-CHAIN via one * retrying multicall (no API/indexer). TroveManager list + owner-mutable * params come from lender-metadata (`riverConfig`/`riverMarkets`, keyed * lender → chain); this fetch reads the LIVE totals + current mint-fee rate + * oracle price. `fetchPrice` is nonpayable but simulates fine under the * multicall eth_call (same trick as Liquity). */ declare function fetchRiverMarkets(lender: string, chainId: string): Promise; /** * Synthesized per-TroveManager lender key, e.g. `RIVER_8453_2` (= Base cbBTC * market). The CHAIN ID is part of the key (Fluid convention) so keys stay * GLOBALLY unique — River deploys the same factory indexes on BNB, Base and * Hemi with different collaterals. */ declare function riverLenderKey(lender: string, chainId: string | number, index: number): string; /** Recover `{ lender, chainId, index }` from a per-market key (or undefined). */ declare function riverKeyParts(key: string): { lender: string; chainId: string; index: number; } | undefined; /** * Map one River deployment's on-chain batch into the shared * `MorphoGeneralPublicResponse` shape, keyed by `RIVER__` — one key * per TroveManager (collateral market). * * Per market: * - COLLATERAL entry: totals = market collateral; LTV = 1/MCR; the * "liquidation penalty" is approximated as MCR − 1 (V1-style liquidation * hands the SP the WHOLE trove collateral, so the borrower's max loss at * liquidation ≈ the ICR buffer — there is no fixed penalty parameter); * - LOAN entry (satUSD): `totalDebt` = market debt; the interest rate is * PROTOCOL-SET (currently 0% → `variableBorrowRate` 0); the one-off * decaying-baseRate MINT FEE is NOT an APR and therefore lives in * `params.market.river.mintFeeRate` (WAD string), not in the rate fields; * `borrowLiquidity` = min(coll·price/MCR − debt, maxSystemDebt − debt). * - The single per-chain StabilityPool ("earn") is attached to market * index 0 ONLY (its loan-row `totalDeposits`); other markets report 0 so * chain aggregates do not double-count. SP yield is 0 while the protocol * rate is 0% (SP earns liquidation gains + OSHI emissions only). */ declare function convertRiverMarketsToResponse(raw: RiverMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Per-market position detail attached to the debt row (raw strings). */ interface RiverPositionInfo { /** TroveManager (market) index. */ index: number; /** Post-liquidation/redemption collateral surplus claimable via claimCollateral. */ collateralSurplus: string; } /** * Per-market snapshot of a FiRM market. Numbers are HUMAN units (the * Inverse API serves human numbers; the on-chain fallback normalizes to * match). `null` marks a value the active source could not provide — * the converter degrades gracefully per field. */ interface InverseMarketRaw { market: InverseMarketConfig; /** Market.totalDebt — DOLA units (human). */ totalDebt: number | null; /** DOLA sitting in the Market = instant borrowable ceiling (human). */ dolaLiquidity: number | null; /** min(dolaLiquidity, dailyLimit − dailyBorrows) — API only. */ leftToBorrow: number | null; /** Collateral price in USD (pessimistic-oracle based). */ price: number | null; /** Live borrowPaused (falls back to the metadata snapshot). */ borrowPaused: boolean | null; /** Borrows already taken today against `dailyLimit` — API only. */ dailyBorrows: number | null; /** * `Market.replenishmentIncentiveBps` (1000 = 10%) — the replenisher * bot's cut of a force-replenish, per market. It is carved OUT of the * `replenishmentPriceBps` cost and paid in DOLA from the market's own * liquidity; the borrower's debt grows by the FULL cost either way, so * this is a protocol/bot split, not an extra borrower charge. */ replenishmentIncentiveBps: number | null; } /** Raw public-data batch for the FiRM deployment (one chain). */ interface InverseMarketsRaw { /** The bare lender key, `INVERSE`. */ lender: string; config: InverseConfigChain | undefined; chainData: InverseChainData | undefined; /** * DBR price in DOLA — THE fixed borrow APR as a decimal (0.041 = * 4.1%). API-first, metadata snapshot as fallback, `null` if neither * resolves. */ dbrPriceDola: number | null; /** Force-replenish penalty APR in bps (54.75% = 5475) — static read. */ replenishmentPriceBps: number | null; markets: InverseMarketRaw[]; /** Which source filled the market rows. */ source: 'api' | 'chain' | 'none'; } declare function fetchInverseMarkets(lender: string, chainId: string): Promise; /** * Synthesized per-market lender key, e.g. * `INVERSE_63DF5E23DB45A2066508318F172BA45B9CD37035` (= the WETH * market). Address-suffixed (Teller/Exactly convention) — FiRM is * Ethereum-only so the chain id is not part of the key. */ declare function inverseLenderKey(lender: string, market: string): string; /** Recover `{ lender, market }` from a per-market key (or undefined). */ declare function inverseKeyParts(key: string): { lender: string; market: string; } | undefined; /** * Map the FiRM batch into the shared `MorphoGeneralPublicResponse` * shape, keyed by `INVERSE_` — one key per Market * (collateral). * * Per market: * - COLLATERAL entry: deposit-only; LTV = `collateralFactorBps`; * liquidation penalty = `liquidationIncentiveBps`; the close factor * is `liquidationFactorBps` (a liquidation may only close that * share of the position). * - LOAN entry (DOLA): `totalDebt` = market debt; the borrow rate is * the DBR price (FIXED APR — interest is prepaid in DBR, not * accrued on principal, `rateModel: 'dbr'`); `borrowLiquidity` = * `leftToBorrow` (API: min(dailyLimit headroom, DOLA in market)) or * `dolaLiquidity` in the on-chain fallback. There is NO supply side * — `totalDeposits` on the loan row is always 0 (DOLA is Fed-minted * into markets, not user-deposited). When the DBR price is unknown * the loan row is served un-borrowable rather than at 0% — see * `rateKnown` below. * * The full FiRM descriptor (minDebt, dailyLimit, escrow implementation, * DBR addresses, replenishment penalty) rides in * `params.market.inverse` for the calldata builders + worker-api * resolvers. */ declare function convertInverseMarketsToResponse(raw: InverseMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** * One Fraxlend pair as read from the chain. * * A pair is an ISOLATED 2-asset market and a single contract: `asset` is the * supply + borrow leg (the pair itself is that leg's ERC-4626 vault) and * `collateral` is deposit-only. There is no supply side on the collateral leg * and no borrow side on the collateral leg — that asymmetry is the whole shape. */ interface FraxlendPairRaw { /** The `FraxlendPair` contract — the market id. */ pair: string; /** The pair's own fToken symbol, e.g. `ffrxUSD(sfrxETH)-58`. */ symbol: string; /** fToken share decimals. */ decimals: number; asset: string; assetSymbol: string; assetName: string; assetDecimals: number; collateral: string; collateralSymbol: string; collateralName: string; collateralDecimals: number; /** RAW `maxLTV`, scaled by `ltvPrecision` (1e5) — 75 % reads `75000`. */ maxLtv: bigint; ltvPrecision: bigint; exchangePrecision: bigint; /** Liquidation fees, scaled by `liqPrecision`. */ cleanLiquidationFee: bigint; dirtyLiquidationFee: bigint; protocolLiquidationFee: bigint; liqPrecision: bigint; depositLimit: bigint; borrowLimit: bigint; totalAssetAmount: bigint; totalAssetShares: bigint; totalBorrowAmount: bigint; totalBorrowShares: bigint; totalCollateral: bigint; oracle: string; /** The two-sided band. BOTH are COLLATERAL-PER-ASSET, i.e. INVERTED, and * they must not be collapsed into one number: the protocol values * collateral with the HIGH rate for a borrow and the LOW rate for a * liquidation check. Equal on pairs whose oracle is a deterministic * ERC-4626 share price. */ lowExchangeRate: bigint; highExchangeRate: bigint; /** Unix seconds of the last oracle refresh. Can be DAYS old — Fraxlend runs * at ~2.4 tx/day protocol-wide and the rate only updates on interaction. */ exchangeRateLastTimestamp: bigint; maxOracleDeviation: number; /** Per-second, 1e18-scaled. */ ratePerSec: bigint; /** The STATEFUL term of the V3 IRM: the rate at 100 % utilization, which * decays toward the current utilization with a 2-day half-life. Any offline * reproduction of the curve needs this AND a delta-time. */ fullUtilizationRate: bigint; /** Protocol cut of borrower interest, `feeToProtocolRate / 1e5`. */ feeToProtocolRate: number; rateLastTimestamp: bigint; /** The `VariableInterestRateV3` contract. Its `getNewRate` is a plain view, * so exact rate-at-depth needs no modelling. */ rateContract: string; isRepayPaused: boolean; isWithdrawPaused: boolean; isLiquidatePaused: boolean; isInterestPaused: boolean; /** * `swappers(cfg.leverageSwapper)` — read LIVE, per pair, every refresh. * * THE gate for native looping. Owner-mutable in both directions and set per * pair, so it can never be cached across a roster or inferred from config. * `false` here means `leveragedPosition` reverts `BadSwapper()`. * * It is ANDed with the swapper's OWN global kill switch (`shutoff()`, read * once per roster) before it is published, because the two failures are * indistinguishable to a caller: while the lever is shut off every loop * reverts `ShutOff()` on every pair, and nothing on the pair says so. */ leverageSwapperApproved: boolean; /** Echo of which swapper was probed, so a consumer can encode the loop * without re-reading config. Undefined when none is configured. */ leverageSwapper?: string; } interface FraxlendPairsRaw { lender: string; config?: FraxlendConfigChain; pairs: FraxlendPairRaw[]; } declare function fetchFraxlendPairs(lender: string, chainId: string): Promise; /** * Synthesized per-pair lender key, e.g. `FRAXLEND_1_AB3CB84C…`. The chain id * rides in the key (the Fluid / River / Resupply / Curvance convention) even * though Fraxlend is Ethereum-only today. */ declare function fraxlendLenderKey(lender: string, chainId: string | number, pair: string): string; /** Recover `{ lender, chainId, pair }` from a per-pair key. */ declare function fraxlendKeyParts(key: string): { lender: string; chainId: string; pair: string; } | undefined; /** * How many ASSET units one COLLATERAL unit is worth, per the pair's own oracle. * * **`exchangeRate` is COLLATERAL-PER-ASSET — it is INVERTED relative to every * other lender we carry**, and it is NOT decimal-normalised. Fraxlend's own LTV * math is * `ltv = borrowAmount * exchangeRate * LTV_PRECISION * / (collateralAmount * EXCHANGE_PRECISION)` * which is only dimensionless if `exchangeRate` carries units of * collateral-per-asset in RAW BASE UNITS. So inverting it needs the decimal * correction too: * * assetPerCollateral = (EXCHANGE_PRECISION / exchangeRate) * * 10^(collateralDecimals - assetDecimals) * * Verified on-chain 2026-08-11 across three decimal shapes: frxUSD/sfrxETH * (18/18) -> 2162.2, frxUSD/WBTC (18/**8**) -> 60,386.5, frxUSD/sfrxUSD * (18/18) -> 1.2009. The WBTC pair is the one that catches a missing decimal * term — without it the price comes out 1e10 too large. The sfrxUSD figure was * independently confirmed against a simulated swap through the pair's approved * leverage swapper (1e18 frxUSD -> 0.8327e18 sfrxUSD = 1/1.2009). * * `which`: the protocol is deliberately two-sided. Use the HIGH rate to value * collateral for a BORROW (conservative: collateral looks cheaper) and the LOW * rate for a liquidation check. They are equal on pairs whose oracle is a * deterministic ERC-4626 share price. */ declare function fraxlendAssetPerCollateral(p: FraxlendPairRaw, which?: 'low' | 'high'): number; /** * Map one Fraxlend deployment's on-chain batch into the shared * `MorphoGeneralPublicResponse` shape, keyed `FRAXLEND__`. * * Modelling decisions worth knowing: * * - **A pair publishes TWO rows and they are ASYMMETRIC.** The `asset` leg is * supply + borrow (the pair itself is that leg's ERC-4626 vault). The * `collateral` leg is deposit-only: no lender side, no borrow side, 0 % * supply rate. That is not a gap in the data — Fraxlend genuinely pays * nothing on posted collateral, and its return is the underlying's own * intrinsic yield, which the yields layer attaches separately. Publishing a * supply rate there would double-count. * - **`maxLTV` is scaled by `LTV_PRECISION = 1e5`**, not WAD and not bps. * - **The LTV belongs to the COLLATERAL row.** Fraxlend has exactly one * collateral and one debt, so the pair-level `maxLTV` IS that row's * borrow-collateral factor; the asset row is never collateral. * - **Liquidation threshold == maxLTV.** Fraxlend has a single ratio: the * same `maxLTV` gates both opening a borrow and being liquidated * (`_isSolvent` uses it verbatim). There is no separate LT, so publishing * one would invent a safety buffer that does not exist. * - **Prices are derived from the pair's own oracle where possible.** The * shared price map covers the asset leg (frxUSD / FRAX / crvUSD / DOLA are * all well-priced), and the collateral leg is then priced RELATIVELY via * `fraxlendAssetPerCollateral`. That is strictly better than looking the * collateral up independently: it is the same number the protocol enforces * limits with, so health factors we publish agree with the chain. We fall * back to the price map only if the oracle read is unusable. * - **Caps are `type(uint256).max` on every live pair.** A pair is frozen by * setting them to 0, since v3.1.0 has NO deposit/borrow pause flag — the * four flags it does have cover repay / withdraw / liquidate / interest. * - **`isFrozen` means "cannot be entered", not "dead".** Exits stay open by * design so users can close; the Lista lesson is that a market in run-off * must keep its UI. */ declare function convertFraxlendPairsToResponse(raw: FraxlendPairsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** * The immutable half of a Resupply pair. Cached across refreshes because none * of it can change: `collateral` and `underlying` are set in the pair's * constructor and there is no setter (`setConvexPool` only moves where the * SHARES are staked, never what the collateral token is). */ interface ResupplyPairIdentity { pair: string; /** e.g. `Resupply Pair (CurveLend: crvUSD/sfrxUSD) - 1`. */ name: string; /** The ERC-4626 share of the WRAPPED market — the accounting unit. */ collateral: string; /** What the user actually deposits and withdraws: crvUSD or frxUSD. */ underlying: string; collateralDecimals: number; underlyingDecimals: number; /** * The asset the WRAPPED market lends against (sfrxUSD, WBTC, …), read from * the collateral vault itself. Immutable, so it is cached with the identity. * * This is the per-market image source: every CurveLend pair's own two rows * are crvUSD/reUSD, so nothing else distinguishes them visually. */ wrappedCollateralToken?: string; /** Which family answered — `collateral_token()` vs `collateralContract()`. */ wrappedFamily?: 'curvelend' | 'fraxlend'; } /** * One Resupply pair after the state batch. Raw bigints; `null` = failed * allowFailure read. */ interface ResupplyPairRaw { identity: ResupplyPairIdentity; /** 1e5-scaled (95000 = 95%). */ maxLTV: bigint | null; /** Debt ceiling. ZERO ⇒ paused/retired — this is the liveness signal. */ borrowLimit: bigint | null; /** 1e5-scaled penalty on top of the debt at liquidation. */ liquidationFee: bigint | null; /** 1e5-scaled fee added to minted debt (0 on every live pair). */ mintFee: bigint | null; /** Hard per-position floor (1,000 reUSD). */ minimumBorrowAmount: bigint | null; /** Face reUSD debt, interest previewed. */ totalBorrowAmount: bigint | null; totalBorrowShares: bigint | null; /** Collateral SHARES held by the pair. */ totalCollateral: bigint | null; /** 1e18-scaled per SECOND, from `currentRateInfo` (last checkpoint). */ ratePerSec: bigint | null; /** Same, but recomputed live by the Utilities lens — preferred. */ liveRatePerSec: bigint | null; /** The WRAPPED market's supply rate per second, 1e18-scaled. */ underlyingSupplyRatePerSec: bigint | null; /** `convertToAssets(1e18)` on the collateral vault — UNDERLYING per share. * ~1e15 for Curve Lend vaults, ~1e18 for Fraxlend pairs. */ collateralPrice: bigint | null; /** Cached `1e36 / collateralPrice` from the pair (stale between writes). */ exchangeRate: bigint | null; /** Convex pool id the collateral is staked into. 0 = not staked, no rewards. */ convexPid: bigint | null; /** This pair's WEIGHT in the RSUP emission stream (not a token balance). */ rsupWeight: bigint | null; /** Convex reward streams on the staked collateral: reward wei per second per * 1e18 of staked SHARES, aggregated by token (a pool can list the same * token twice). */ collateralRewards: { token: string; ratePerSecPerShare: bigint; }[]; } /** * Chain-level RSUP emission state — one read for the whole roster. * * `pairEmissions` stakes governance WEIGHT, not tokens: `totalWeight` is the * sum over all pairs and each pair's slice is its `rsupWeight`. A pair's RSUP * per second is `rewardRate x rsupWeight / totalWeight`. */ interface ResupplyRsupEmissions { /** The RSUP token. */ govToken: string; /** RSUP wei per second across ALL pairs. */ rewardRate: bigint; /** Sum of every pair's weight. */ totalWeight: bigint; /** Emissions stop here; past it the stream pays nothing. */ periodFinish: bigint; } interface ResupplyMarketsRaw { lender: string; config?: ResupplyConfigChain; pairs: ResupplyPairRaw[]; /** Absent when the stream has ended or could not be read. */ rsup?: ResupplyRsupEmissions; } /** * Fetch every Resupply market on a chain — FULLY ON-CHAIN, no API and no * published market roster. * * Discovery is `ResupplyRegistry.getAllPairAddresses()`. That is deliberate: * governance adds pairs (7 appeared between the docs' published list and * 2026-08) and retires them by zeroing `borrowLimit`, so a static file would * both miss new markets and advertise frozen ones as borrowable. The registry * is permissionless to read and is the same source the protocol's own * periphery uses. * * Three rounds cold, two warm: * 1. registry → pair addresses (intersected with `pairAllowlist` if set); * 2. identity (`name`/`collateral`/`underlying` + both tokens' decimals) for * pairs not already cached — immutable, so this is once per pair ever; * 3. state: terms, accounting, rates and the collateral vault's share price. * * The rate is read from the `Utilities` lens rather than `currentRateInfo`, * which is only a checkpoint from the last write — on a quiet pair that can be * hours stale, and the off-peg amplifier moves with the reUSD price. The * checkpoint is kept as a fallback. */ declare function fetchResupplyMarkets(lender: string, chainId: string): Promise; /** * The external lending market a Resupply pair wraps. * * Every Resupply pair's collateral IS another lender's supply position, so a * position here carries that market's risk on top of Resupply's own. This * resolves the link where we can: the collateral vault is matched against the * LlamaLend roster by ADDRESS (LlamaLend indexes markets by Controller, so the * lookup goes through `llamaLendMarketByVault`). * * Verified 2026-08-04: 16 of the 21 registered pairs match a LlamaLend market * exactly, and the generation agrees independently — Resupply's own * `CurveLend:` names map to LlamaLend `version: 1` and `CurveLendV2:` to * `version: 2`. The other 5 are Fraxlend pairs, which we do not integrate as a * lender, so they resolve to `provider: 'fraxlend'` with no market key. * * The roster is an ENRICHMENT, not the source of the identity: the wrapped * collateral is read from the vault itself (`collateral_token()` on a Curve * Lend vault, `collateralContract()` on a Fraxlend pair — the same probe * Resupply's own `Utilities` uses to tell the families apart), so all 21 pairs * carry one whether or not LlamaLend metadata is published. */ interface ResupplyWrappedMarket { /** Which protocol the collateral position lives in. */ provider: 'llamalend' | 'fraxlend' | 'unknown'; /** The ERC-4626 the pair custodies — always known (it IS the collateral). */ vault: string; /** `LLAMALEND_` when we integrate that market, else absent. * This is the key to look the wrapped market up in our own data. */ lender?: string; /** LlamaLend Controller (the borrow surface of the wrapped market). */ controller?: string; /** LlamaLend LLAMMA. Carried because a leverage route through Resupply must * never touch it — the wrapped Controller asserts its band state. */ amm?: string; /** 1 = `oneway`, 2 = `oneway-v2`. Matches Resupply's CurveLend/CurveLendV2. */ version?: 1 | 2; /** What the wrapped market lends against, e.g. `sfrxUSD`. */ collateralSymbol?: string; /** * The wrapped market's collateral TOKEN. * * This is the per-market image source. Every CurveLend pair looks identical * on our two rows — both are crvUSD/reUSD — so the only thing that visually * distinguishes `crvUSD/sfrxUSD` from `crvUSD/WBTC` is the asset the WRAPPED * market lends against, which is not one of our rows. Consumers resolve the * token icon from this address; the brand icon (`lenderIcon`) stays the * fallback and is deliberately still one image for all 21 pairs. */ collateralToken?: string; collateralDecimals?: number; } /** * Human label for a pair, from its on-chain `name()`. * * The pair deployer emits `Resupply Pair (CurveLend: crvUSD/sfrxUSD) - 1`: * the useful part is inside the parentheses — it names the WRAPPED market, * which is the only thing distinguishing one Resupply pair from another. The * `- N` suffix is a redeploy counter (there are two `crvUSD/sDOLA` pairs), so * it is kept only when it is not `- 1`. * * Falls back to the raw name rather than inventing one: a pair whose name * stops matching this shape should read oddly, not silently lose its identity. */ declare function resupplyMarketLabel(rawName: string): string; /** * Synthesized per-pair lender key, e.g. `RESUPPLY_1_C5184CCC…`. The chain id * rides in the key (Fluid/River/Frankencoin convention) even though Resupply * is Ethereum-only today. */ declare function resupplyLenderKey(lender: string, chainId: string | number, pair: string): string; /** Recover `{ lender, chainId, pair }` from a per-pair key. */ declare function resupplyKeyParts(key: string): { lender: string; chainId: string; pair: string; } | undefined; /** * Map one Resupply deployment's on-chain batch into the shared * `MorphoGeneralPublicResponse` shape, keyed `RESUPPLY__`. * * Four modelling decisions worth knowing (all from RESUPPLY.md): * * - **The collateral we publish is the UNDERLYING (crvUSD / frxUSD), not the * ERC-4626 share.** The share is an internal accounting unit that no token * list carries and no price feed covers, and both user-facing entry points * (`addCollateral` / `removeCollateral`) are denominated in the underlying. * Share amounts are converted with the vault's own * `convertToAssets(1e18)` — the exact number Resupply's oracle uses. NB * that price is ~**1e15** for Curve Lend vaults, so the share count is * ~1000x the underlying; a bare 1e18 divide is wrong by three orders of * magnitude. * - **The collateral carries the wrapped market's yield.** A Resupply deposit * is a Curve Lend / Fraxlend supply position, so `getUnderlyingSupplyRate` * is published as the collateral row's `intrinsicYield`. Without it the * position looks like it pays nothing, when in fact the whole product is * the spread between that and the ~half-of-it borrow rate. * - **`borrowLimit == 0` means PAUSED.** `pause()` zeroes it and there is no * `isPaused`; 9 of 21 pairs sat at zero at integration. Such a pair is * reported frozen and non-borrowable, but deposits/withdrawals stay open so * users can exit. * - **There is no supply side** — reUSD is minted — so `totalDeposits` on the * loan row is 0 and `depositRate` is 0. The earn leg is sreUSD, which * belongs to the savings provider. */ declare function convertResupplyMarketsToResponse(raw: ResupplyMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Per-position Resupply detail attached to the debt row (raw strings). */ interface ResupplyPositionInfo { /** Internal borrow shares. NOT an amount — see `debt` for reUSD units. */ borrowShares: string; /** Collateral in VAULT SHARES (the pair's own accounting unit). */ collateralShares: string; /** `convertToAssets(1e18)` on the collateral vault at read time — the * factor that turned those shares into the reported underlying. ~1e15 for * Curve Lend vaults. */ collateralSharePrice: string; /** The pair contract — the market id and every write target. */ pair: string; /** The collateral vault (Curve Lend / Fraxlend 4626 share token). */ collateralVault: string; } /** Test seam — drops the roster + discovery caches. */ declare function __resetResupplyUserCaches(): void; /** Off-chain `Market.predictEscrow(user)`. */ declare function predictInverseEscrow(market: Address, escrowImplementation: Address, user: Address): Address; /** Per-position FiRM detail attached to the debt row (raw strings). */ interface InversePositionInfo { /** Pessimistic-oracle borrow ceiling for the CURRENT collateral (DOLA raw). */ creditLimit: string; /** Max collateral withdrawable right now (collateral raw). */ withdrawalLimit: string; /** DBR wallet balance (raw) — the prepaid-interest runway. */ dbrBalance: string; /** DBR deficit (raw). > 0 ⇒ force-replenishable at 54.75% APR AND * withdrawals are FROZEN until the user buys DBR. */ dbrDeficit: string; /** DBR signed balance (raw, may be negative). */ dbrSignedBalance: string; /** `DBR.debts(user)` (raw DOLA) — debt across ALL FiRM markets, which is * what burns DBR at 1 per DOLA-year. Chain-wide, repeated on every row. */ dbrTotalDebt: string; /** Seconds until `dbrBalance` is exhausted at that burn ('0' if no debt). */ dbrRunwaySeconds: string; /** Unix seconds of the projected depletion — past that point anyone can * force-replenish the account, charging the replenished DBR to its DOLA * debt at `replenishmentPriceBps`, and withdrawals freeze. Absent when * there is no debt. */ dbrDepletionTimestamp?: string; } /** * Snapshot of the CoolerLtvOracle's origination-LTV drip. Values are raw * on-chain words (wads / unix seconds) — the converter stringifies them for * the descriptor so future OLTV stays computable in closed form downstream. */ interface CoolerDripRaw { /** OLTV at `startTime` (debt-token wad per gOHM). */ startingValue: bigint; /** Unix seconds the current segment started. */ startTime: bigint; /** OLTV the segment interpolates towards (wad). */ targetValue: bigint; /** Unix seconds the target is reached. */ targetTime: bigint; /** Wad-per-second slope of the interpolation. */ slope: bigint; } /** * Raw public-data batch for Olympus Cooler V2 (one chain, ONE market ever: * gOHM → the live debt token on the monolithic MonoCooler). 100 % on-chain — * there is no API anywhere in the path. Every MonoCooler amount is WAD * regardless of the debt token's own decimals. `null` marks a value the * multicall could not provide — the converter degrades gracefully per field. */ interface CoolerMarketsRaw { /** The bare lender key, `COOLER` (also the market key). */ lender: string; config: CoolerConfigChain | undefined; /** MonoCooler.totalCollateral() — gOHM, wad. NEVER read `balanceOf`: the * collateral sits in the DLGTE module + per-delegate escrows. */ totalCollateral: bigint | null; /** MonoCooler.totalDebt() — debt token, wad. */ totalDebt: bigint | null; /** Per-year continuously-compounded rate, wad (ln(1.005) ≈ 0.5 % eff.). */ interestRateWad: bigint | null; /** loanToValues().maxOriginationLtv — a PRICE: debt wad per gOHM token. */ oltvPrice: bigint | null; /** loanToValues().liquidationLtv — OLTV × (1 + premiumBps/1e4), wad. */ lltvPrice: bigint | null; /** minDebtRequired() — wad. Gates borrows AND partial repays. */ minDebtRequired: bigint | null; borrowsPaused: boolean | null; liquidationsPaused: boolean | null; /** debtToken() read LIVE — governance-swappable, prefer over the seed. */ debtToken: string | null; /** collateralToken() read live (gOHM). */ collateralToken: string | null; /** CoolerLtvOracle.originationLtvData() — the whole drip schedule. */ drip: CoolerDripRaw | null; /** CoolerLtvOracle.liquidationLtvPremiumBps() (100 = LLTV = OLTV × 1.01). */ liquidationLtvPremiumBps: number | null; /** * sUSDS.maxWithdraw(treasury) — the treasury's instant borrow headroom in * debt-token units (18 dec). This is the ONLY borrow cap: V2 is funded * just-in-time from TRSRY and there is no debt ceiling. */ susdsHeadroom: bigint | null; /** Which source filled the batch. */ source: 'chain' | 'none'; } declare function fetchCoolerMarkets(lender: string, chainId: string): Promise; /** * Map the Cooler batch into the shared `MorphoGeneralPublicResponse` shape, * keyed by the bare `COOLER` key — ONE market ever (gOHM → the live debt * token), so the lender key IS the market key and there is no fan-out. * * - COLLATERAL entry (gOHM): deposit-only; `totalDeposits` = * `totalCollateral()` (never `balanceOf` — collateral sits in the DLGTE * module and per-delegate escrows). The protocol's LTV is a PRICE * (debt-per-gOHM wad), so the FRACTIONAL factors here are display-only, * derived against the external gOHM USD price: CF = oltvPrice / collUsd, * LT = lltvPrice / collUsd. Liquidation math must stay price-free — the * raw price-LTVs ride in the `cooler` descriptor as the protocol truth. * - LOAN entry (live `debtToken()`): `totalDebt` = book debt; * `borrowLiquidity` = the treasury's sUSDS headroom (0 when borrows are * paused); the borrow rate is the protocol-set `interestRateWad` * annualised nominally (`rateModel: 'protocolSet'`, no utilization * curve). There is NO supply side — debt is minted from the treasury. */ declare function convertCoolerMarketsToResponse(raw: CoolerMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Per-position Cooler detail attached to the debt row (raw wad strings). */ interface CoolerPositionInfo { /** Contract-computed health vs LLTV (1e18 = at the threshold). This is the * AUTHORITATIVE liquidation signal — there is NO price trigger, so never * derive one from a market price (the Frankencoin lesson). */ healthFactor: string; /** Current LTV as a PRICE: debt wad per gOHM token (not a ratio). */ currentLtv: string; /** gOHM delegated out via the DLGTE module (wad). Withdrawing delegated * collateral reverts without undelegation requests first. */ totalDelegated: string; /** Max debt at the current OLTV for the current collateral (wad). */ maxOriginationDebtAmount: string; /** Debt at which the position becomes liquidatable (LLTV, wad). */ liquidationDebtAmount: string; } /** * Per-market snapshot of a LlamaLend market. Amounts are HUMAN units (the * Curve API serves human numbers; the on-chain fallback normalizes to match). * Rates are DECIMALS (0.0391 = 3.91% APR), nominal — never the compounded * `borrowApy` the API also carries. * * `null` marks a value the active source could not provide; the converter * degrades per field rather than dropping the market. */ interface LlamaLendMarketRaw { market: LlamaLendMarketConfig; /** Controller.total_debt — borrowed-token units (human). */ totalDebt: number | null; /** Vault.totalAssets — borrowed-token units (human). */ totalSupplied: number | null; /** * Borrowable right now. v2: `Controller.available_balance()`. v1: * `borrowedToken.balanceOf(controller)`. This is also the withdrawal * ceiling for lenders — the vault cannot pay out what is lent. */ availableToBorrow: number | null; /** Nominal borrow APR as a decimal. */ borrowApr: number | null; /** Nominal lend APR as a decimal. */ lendApr: number | null; /** Collateral price in borrowed-token terms, from the AMM's EMA oracle. */ collateralPrice: number | null; /** USD price of the collateral, API only (the chain read has no USD leg). */ collateralPriceUsd: number | null; /** USD price of the borrowed token, API only. */ borrowedPriceUsd: number | null; /** * Effective collateral factor at the market's `defaultBands`, derived from * `max_borrowable(1 unit, N)` divided by the oracle price. * * There is NO market-constant LTV in LlamaLend — the number moves with the * band count. Measured on sreUSD/crvUSD: 0.991 at N=4 down to 0.886 at * N=50. Reporting the N=4 maximum would flatter every risk comparison * against Aave/Morpho, so the default N is what gets reported and the rest * of the curve travels alongside in `bandLtv`. */ collateralFactor: number | null; /** `{ [N]: collateralFactor }` — the trade-off curve for the UI and sizer. */ bandLtv: { [bands: string]: number; } | null; /** * v2 borrow cap in borrowed-token units (human). `0` DISABLES borrowing — * a fresh v2 market looks live but is not. `null` on v1 (uncapped). */ borrowCap: number | null; /** Whether new borrows are possible at all right now. */ borrowingEnabled: boolean; /** Vault.maxDeposit — `0` disables deposits (v2 `max_supply`). */ maxDeposit: number | null; /** Number of open loans, for the liquidations surface. */ nLoans: number | null; /** * Soft-liquidation state of the market as a whole: the AMM's active band. * Not a per-user value, but it tells the UI whether the market is currently * converting anyone's collateral. */ activeBand: number | null; /** * Assets per 1e18 vault shares — the multiplier that turns a lender's share * balance into an amount of the borrowed token. * * Read once per market rather than per user. It is NOT ~1.0: `DEAD_SHARES` * puts LlamaLend vault shares roughly 1000x the asset scale, so it reads * around 1e-3. A consumer that treats a share balance as an amount overstates * a lender's position by three orders of magnitude. */ pricePerShare: number | null; } /** Raw public-data batch for one LlamaLend chain (both generations together). */ interface LlamaLendMarketsRaw { /** The bare lender key, `LLAMALEND`. */ lender: string; config: LlamaLendConfigChain | undefined; chainData: LlamaLendChainData | undefined; markets: LlamaLendMarketRaw[]; /** Which source filled the market rows. */ source: 'api' | 'chain' | 'none'; } declare function fetchLlamaLendMarkets(lender: string, chainId: string): Promise; /** * Synthesized per-market lender key, e.g. * `LLAMALEND_4F79FE450A2BAF833E8F50340BD230F5A3ECAFE9` (= the sreUSD/crvUSD * market). Keyed by the CONTROLLER, which is what every write and every user * read targets — the vault is a lookup off it. Address-suffixed * (Teller/Exactly/Inverse convention); the chain id is not part of the key * because chain scoping happens at the marketUid level. * * Both generations share this key space on purpose: to a user they are one * protocol, and the Curve API returns them in one list. The `version` field on * the market row is what encoders branch on. */ declare function llamaLendLenderKey(lender: string, controller: string): string; /** Recover `{ lender, controller }` from a per-market key (or undefined). */ declare function llamaLendKeyParts(key: string): { lender: string; controller: string; } | undefined; declare function convertLlamaLendMarketsToResponse(raw: LlamaLendMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** * Per-position LlamaLend detail attached to the debt row (raw strings unless * noted). * * The two fields a consumer must not ignore: * * - `softLiquidating` / `priceUpper` / `priceLower` — a LlamaLend position * does not have a liquidation price. It has a band RANGE, and it starts * converting collateral into the borrowed token as soon as the oracle * enters that range. Rendering a single number here misrepresents the * protocol. * - `bandCollateralInBorrowed` — the borrowed-token leg the LLAMMA has * already produced from the user's collateral. It sits inside the position, * offsets debt, and is NOT a wallet balance. */ interface LlamaLendPositionInfo { /** Signed health, WAD. `< 0` ⇒ hard-liquidatable. */ health: string; /** Upper bound of the soft-liquidation band range, WAD. */ priceUpper: string; /** Lower bound of the soft-liquidation band range, WAD. */ priceLower: string; /** Band indices `[n1, n2]` the collateral currently occupies. */ bands: [number, number] | undefined; /** Band count `N` chosen at loan creation — IMMUTABLE for the loan's life. */ bandCount: number; /** * Borrowed-token amount held inside the user's bands (raw). Non-zero means * the position IS or HAS BEEN in soft liquidation. */ bandCollateralInBorrowed: string; /** * The user's SUPPLY position on this market, in the borrowed token — vault * shares plus gauge-staked shares, converted to assets. * * Separate from the row's `deposits`, which sums this with * `bandCollateralInBorrowed`. Only this part earns the vault's lend APR. */ lendAssets: string; /** Raw lend SHARES (vault + gauge). ~1000x the asset scale — never an amount. */ lendShares: string; /** True when some or all of the lend shares are staked in the market's gauge. */ lendStaked: boolean; /** True when the LLAMMA currently holds a borrowed-token leg for this user. */ softLiquidating: boolean; /** * Whether the probed spender holds the Controller's boolean grant for this * market. FAILS CLOSED — older-blueprint controllers have no `approval` * method, the call fails, and this reads `false`, which is correct. */ delegated: boolean; /** Whether the market's Controller supports delegation at all. */ supportsDelegation: boolean; /** Market generation — the leverage/encoder ABIs differ. */ version: 1 | 2; } /** * One dss market (= one collateral ilk) after the on-chain batch. * Raw bigints; `null` = failed allowFailure read. Maker fixed-point: * wad 1e18 / ray 1e27 / rad 1e45. * * The types are still spelled `Usdd*` upstream in data-sdk (USDD was the first * dss fork we integrated); they are brand-agnostic dss shapes and serve Sky * verbatim. */ interface DssMarketRaw { market: UsddMarketConfig; /** Vat.ilks — total normalised debt (wad). */ Art: bigint | null; /** Vat.ilks — debt accumulator (ray); debt = Art × rate (rad). */ rate: bigint | null; /** Vat.ilks — liquidation-adjusted price (ray): price / (par × mat). */ spot: bigint | null; /** Vat.ilks — ilk debt ceiling (rad). */ line: bigint | null; /** Vat.ilks — per-urn debt floor (rad). */ dust: bigint | null; /** Jug.ilks — per-second stability fee (ray). */ duty: bigint | null; /** Spot.ilks — liquidation ratio (ray). */ mat: bigint | null; /** gem.balanceOf(gemJoin) — total collateral custodied by the adapter * (locked ink + unswept gem), gem-native decimals. `null` when the roster * row carries no usable gem-join address. */ joinBalance: bigint | null; } interface DssMarketsRaw { lender: string; config?: UsddConfigChain; chainData?: UsddChainData; markets: DssMarketRaw[]; } /** @deprecated brand-specific alias — use `DssMarketRaw`. */ type UsddMarketRaw = DssMarketRaw; /** @deprecated brand-specific alias — use `DssMarketsRaw`. */ type UsddMarketsRaw = DssMarketsRaw; /** Ilk string → bytes32 (`'WBTC-A'` → right-padded hex). */ declare const dssIlkBytes32: (ilk: string) => `0x${string}`; /** * Fetch all market data of ONE dss (MakerDAO-shaped) deployment — FULLY * ON-CHAIN via one retrying multicall. Serves every dss brand (`SKY`, `USDD`, * any future fork): the deployment addresses and the ilk roster come from * lender-metadata through the brand-agnostic `dssConfigFor`/`dssChainData` * resolvers, and this fetch reads the LIVE Vat/Jug/Spot params per ilk plus * the gem-join balance (total custodied collateral — the Vat keeps no per-ilk * ink total). * * An empty roster (the standing USDD state on both EVM chains — `cdpi() = 0`, * no ilk filed, see USDD.md) returns zero markets without issuing a * multicall, so the code path stays live and metadata alone activates a * deployment. */ declare function fetchDssMarkets(lender: string, chainId: string): Promise; /** @deprecated brand-specific alias — use `fetchDssMarkets`. */ declare const fetchUsddMarkets: typeof fetchDssMarkets; /** @deprecated brand-specific alias — use `dssIlkBytes32`. */ declare const usddIlkBytes32: (ilk: string) => `0x${string}`; /** * Synthesized per-ilk lender key, e.g. `SKY_1_ETH_A` / `USDD_1_WBTC_A`. The * CHAIN ID is part of the key (Fluid/River convention) because two dss * deployments — even of the same brand — are INDEPENDENT Maker stacks that * could file the same ilk string. * * **`_` IS THE ONLY SEPARATOR — the ilk's own `-` is re-spelled to `_`.** * Maker ilks are the first market suffixes in the codebase that contain a * hyphen (`ETH-A`, `PSM-USDT-A`), and a key mixing both separators cannot * survive a round-trip through any case- or slug-mapping layer: a consumer * that lower-cases on `_` and restores on `-` cannot tell which dashes were * structure and which were payload. That is not hypothetical — it silently * resolved `sky-1-wbtc-a` to the wrong lender in the allocator UI. Keys are * therefore hyphen-free, and the real ilk is recovered by `dssKeyParts`. * * Safe because a Maker ilk never contains `_` (the on-chain convention is * `-`), making `-` ⇄ `_` injective over the roster; the metadata * generators reject an ilk carrying `_` so that stays true. */ declare function dssLenderKey(lender: string, chainId: string | number, ilk: string): string; /** Ilk → key segment: `WBTC-A` → `WBTC_A`. */ declare const ilkToKeySegment: (ilk: string) => string; /** Key segment → ilk: `WBTC_A` → `WBTC-A`. Inverse of the above. */ declare const keySegmentToIlk: (seg: string) => string; /** * Recover `{ lender, chainId, ilk }` from a per-market key (or undefined), * with the ilk in its true on-chain spelling (`WBTC_A` → `WBTC-A`). * * **Tolerant on input, canonical on output.** The canonical key is * hyphen-free (see `dssLenderKey`), but this also accepts the legacy * hyphenated form `SKY_1_WBTC-A` and any mixture, because those keys were * already emitted into caller databases and bookmarks. Both spellings map to * the same ilk, so a stale link keeps resolving instead of falling through to * "unknown lender". The leading `\d+_` disambiguates from the bare * `SKY` / `USDD` key. */ declare function dssKeyParts(key: string): { lender: string; chainId: string; ilk: string; } | undefined; /** * Map one dss deployment's on-chain batch into the shared * `MorphoGeneralPublicResponse` shape, keyed `__` — one * key per collateral ilk. Brand-agnostic: `SKY` (the original MakerDAO) and * `USDD` (its fork) differ only in the metadata rows fed in here. * * Per market: * - COLLATERAL entry: totals = the gem-join balance (the Vat keeps no * per-ilk ink total; the adapter custodies locked + unswept gems); * LTV = 1/mat; liquidation penalty = chop − 1 (Dog.chop, wad). * - LOAN entry (DAI / USDD): `totalDebt` = Art × rate (rad → human); * `variableBorrowRate` = the stability fee as a nominal APR percent — * `(duty − RAY)/RAY × YEAR_SECONDS × 100`, the same annualisation as the * Pot's dsr in the savings fetcher (never `^ seconds − 1`, which is the * APY); `borrowLiquidity` = ceiling headroom `(line − Art × rate)/1e45`. * There is NO protocol supply side (the stable is Vat-minted) — the earn * side is the savings token (sDAI / sUSDD), carried by the savings * provider, so `totalDeposits` on the loan row is 0 and `depositRate` 0. * - Collateral price: Vat.spot × mat (both ray) recovers the par-adjusted * OSM price without reading the pip (whitelisted `peek` would revert); * shared price map as fallback. */ declare function convertDssMarketsToResponse(raw: DssMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** @deprecated brand-specific alias — use `convertDssMarketsToResponse`. */ declare const convertUsddMarketsToResponse: typeof convertDssMarketsToResponse; /** @deprecated brand-specific alias — use `dssLenderKey`. */ declare const usddLenderKey: typeof dssLenderKey; /** @deprecated brand-specific alias — use `dssKeyParts`. */ declare const usddKeyParts: typeof dssKeyParts; /** Per-CDP position detail attached to the debt row (raw strings). */ interface DssPositionInfo { /** DssCdpManager id — the sub-account id and every write op's target. */ cdpId: string; /** Urn handle in the Vat. */ urn: string; ilk: string; } /** * One Frankencoin market (= one ORIGINAL position) after the on-chain batch. * Raw bigints; `null` = failed allowFailure read. */ interface FrankencoinMarketRaw { market: FrankencoinMarketConfig; /** Owner-declared liquidation price, 36-dec scaled vs collateral decimals. */ price: bigint | null; /** FACE debt on the original itself (clones carry their own). */ minted: bigint | null; /** ZCHF the original + its clones may still draw — market borrow capacity. */ availableForClones: bigint | null; /** Collateral custodied by the ORIGINAL position contract. */ collateralBalance: bigint | null; /** Hub lead rate + risk premium, ppm. */ annualInterestPPM: bigint | null; /** Upfront fee for minting now (pro-rata to expiry), ppm. */ currentFeePPM: bigint | null; /** Withheld into the equity reserve at mint, ppm. */ reserveContribution: bigint | null; /** Non-zero while a Dutch-auction challenge is running. */ challengedAmount: bigint | null; expiration: bigint | null; isClosed: boolean | null; } interface FrankencoinMarketsRaw { lender: string; config?: FrankencoinConfigChain; chainData?: FrankencoinChainData; markets: FrankencoinMarketRaw[]; } /** * Fetch all market data of ONE Frankencoin deployment — FULLY ON-CHAIN via * one retrying multicall over the curated ORIGINAL-position roster from * lender-metadata (`frankencoinConfig` / `frankencoinMarkets`, generated by * its `update:frankencoin` job, which filters to V2 + open + a priceable * collateral allowlist). * * Only originals are read here: they carry the terms AND the market-level * borrow capacity (`availableForClones`). Clones are USER positions and are * resolved per-account in the user-data path. * * NB `price` is the owner-DECLARED liquidation price, not an oracle quote — * see the converter for how that is surfaced. */ declare function fetchFrankencoinMarkets(lender: string, chainId: string): Promise; /** * Synthesized per-market lender key, e.g. * `FRANKENCOIN_1_5F2C10F7…` — one per ORIGINAL position. The chain id is part * of the key (Fluid/River convention) even though Frankencoin is * Ethereum-only today, so an L2 hub would not collide. */ declare function frankencoinLenderKey(lender: string, chainId: string | number, position: string): string; /** Recover `{ lender, chainId, position }` from a per-market key. */ declare function frankencoinKeyParts(key: string): { lender: string; chainId: string; position: string; } | undefined; /** * Map one Frankencoin deployment's on-chain batch into the shared * `MorphoGeneralPublicResponse` shape, keyed `FRANKENCOIN__`. * * Three modelling decisions worth knowing (all from FRANKENCOIN.md): * * - **The liquidation price is owner-DECLARED, not an oracle.** `price` is * 36-dec scaled against the collateral's decimals and is policed by a * Dutch-auction challenge game. We surface it as the LIQUIDATION price * (it defines `collateralFactor = 1`, i.e. minting is allowed up to * `coll × price`) but value collateral with OUR price feeds. The * divergence between the two is the risk signal, and it is carried * verbatim in `params.market.frankencoin.declaredPrice` so a consumer can * compute it. Never present the resulting health as a protocol invariant. * - **Debt ≠ proceeds.** `minted` is FACE debt including the withheld * `reserveContribution` (10–40 %) plus the upfront pro-rata fee. Both * ppm figures ride along in the descriptor so a quote layer can convert. * - **Positions expire.** `expiration` is surfaced and an expired market is * reported halted (its collateral is subject to forced sale). * * Per market: a COLLATERAL entry (the original's own collateral) and a LOAN * entry (ZCHF). There is no protocol supply side — ZCHF is minted, and the * earn leg is the separate savings module carried by the savings provider — * so `totalDeposits` on the loan row is 0. */ declare function convertFrankencoinMarketsToResponse(raw: FrankencoinMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Per-position detail attached to the debt row (raw strings). */ interface FrankencoinPositionInfo { /** The position contract — the sub-account id and every write op's target. */ position: string; /** The ORIGINAL this position was cloned from (its market). */ original: string; /** Owner-declared liquidation price (raw, 36-dec scaled). */ declaredPrice: string; /** Unix seconds; a position past this is subject to forced sale. */ expiration: string; /** Non-zero while a Dutch-auction challenge is running against it. */ challengedAmount: string; /** ppm withheld into the equity reserve — needed to quote a close, since * repaying burns with reserve credit and costs LESS than `minted`. */ reserveContributionPPM: string; } /** * Raw on-chain read for ONE Teller `LenderCommitmentGroup` pool. All amounts are * raw token base units; `minRateBps` is the pool's min borrow APR in BASIS * POINTS (10000 = 100%). Failed reads (allowFailure multicall) arrive as null. */ interface TellerPoolRaw { /** The curated pool config (addresses + token metadata) from data-sdk. */ config: TellerPoolConfig; /** getPrincipalAmountAvailableToBorrow — the live liquidity gate (raw). */ available: bigint | null; /** totalPrincipalTokensCommitted — total supplied principal (raw). */ committed: bigint | null; /** getMinInterestRate(0) — min borrow APR at current utilization (bps). */ minRateBps: number | null; /** getCollateralRequiredForPrincipalAmount(1 principal token) — raw collateral units. */ collateralPerPrincipal: bigint | null; /** getMaxLoanDuration() (seconds) — on-chain, authoritative over config. */ maxLoanDuration: number | null; /** totalAssets() — ERC-4626 CURRENT principal TVL (V2/V3); null on V1 pools * (fall back to `committed`). This is the correct "deposits" metric — * `committed` is a cumulative lifetime counter. */ totalAssets: bigint | null; /** getMarketId() — the MarketRegistry market this pool lends into. */ marketId: bigint | null; /** Market requires borrower attestation (whitelist) to borrow. Undefined when * the MarketRegistry read was unavailable. */ requiresBorrowerAttestation?: boolean; /** MarketRegistry.isMarketOpen(marketId). Undefined when the read failed. */ marketOpen?: boolean; /** Upfront market fee (bps) charged on the borrow (getMarketplaceFee). */ marketFeeBps?: number; /** Global TellerV2 protocol fee (bps), added to the market fee. */ protocolFeeBps?: number; /** Grace window (seconds) after a payment is due before DEFAULT + * full-collateral liquidation (getPaymentDefaultDuration). */ paymentDefaultDuration?: number; } /** Raw public-data batch: one multicall over the chain's curated pool list. */ interface TellerMarketsRaw { chainId: string; pools: TellerPoolRaw[]; } /** * Fetch all Teller pool data for a chain — FULLY ON-CHAIN (no API/indexer): * 1. one retrying multicall reads each curated `LenderCommitmentGroup` pool's * live state (borrowable liquidity, committed principal, min borrow rate, * collateral requirement, max duration, marketId); * 2. a second multicall reads each UNIQUE market's `MarketRegistry` * attestation requirement + open flag — so we can surface which pools gate * borrowers (a permissionless integration must know). * * The pool list comes from lender-metadata (`tellerPools`). Returns an empty * batch when the chain has no Teller pools or the read fails (the converter then * emits nothing). */ declare function fetchTellerMarkets(chainId: string): Promise; /** Synthesized per-pool lender key, e.g. `TELLER_`. */ declare function tellerLenderKey(pool: string): string; /** Recover the pool address from a `TELLER_` lender key (or undefined). */ declare function tellerPoolFromLenderKey(lender: string): string | undefined; /** * Map the on-chain Teller pool batch into the shared `MorphoGeneralPublicResponse` * shape (identical to Exactly/Midnight/Term/River), keyed by * `TELLER_` — one key per `LenderCommitmentGroup` pool. * * Per pool (a fixed principal↔collateral pair): * - a LOAN entry on the principal token: `borrowLiquidity` = * `getPrincipalAmountAvailableToBorrow` (the liquidity gate); the fixed borrow * APR sits on `stableBorrowRate` (fixed-rate convention, like Exactly/Term/ * Lista — `variableBorrowRate` stays 0, Teller has no floating leg); * - a COLLATERAL entry on the collateral token with the implied origination LTV * and `liquidationPenalty: 0` (Teller liquidation is TIME-based — a missed * payment past the market window seizes collateral, there is no price-based * penalty parameter); * - `params.market.fixedTerm` = `{ model: 'teller', earlyRepay: 'none' (full * early repay is pro-rata, penalty-free), provider: { kind: 'pool' } }` — a * rolling duration (borrower picks ≤ `maxLoanDuration`), so no market-level * calendar maturity. */ declare function convertTellerMarketsToResponse(raw: TellerMarketsRaw, chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** * Teller rate/scale helpers. Teller expresses APR / interest rates as a `uint16` * in BASIS POINTS where 10000 = 100% (NumbersLib `PCT_100 = 1e4`), so the * interest on a loan is `principal * apr * duration / (365d * 10000)`. */ /** uint16 basis points → percent (1000 bps → 10). */ declare function tellerBpsToPercent(bps: number | null | undefined): number; /** * Implied max origination LTV of a pool: value of 1 principal token divided by * the value of the collateral the pool requires to back it. Note Teller has NO * price liquidation — this is the collateral requirement at ORIGINATION, not an * ongoing liquidation threshold. Returns 0 when prices or the collateral read * are unavailable (informational only). */ declare function tellerImpliedLtv(collateralPerPrincipal: bigint | null | undefined, collateralDecimals: number, principalPrice: number, collateralPrice: number): number; /** * User-data call build for Teller. Borrower loans ("bids") are keyed by a * uint256 bidId and a user can hold MANY, so this is a DISCOVERY-first build * (async, same pattern as the Liquity family): * * 1. `TellerV2.getBorrowerActiveLoanIds(account)` → the active bidIds; * 2. `TellerV2.getLoanLender(bidId)` per id → attribute each bid to a curated * pool (the pool is the lender-of-record after `acceptFundsForAcceptBid`); * 3. data phase per kept bid: `bids`, `calculateAmountOwed(now)`, * `CollateralManager.getCollateralAmount`, `isLoanDefaulted`. * * The discovered layout is stashed in a short-lived module cache so the parser * (a separate phase fed only the multicall results) can slice it — same trick * as Liquity / the Lista broker caches. */ declare const TELLER_CALLS_PER_BID = 4; interface TellerDiscoveredBid { /** decimal uint256 bidId */ bidId: string; /** the curated pool this bid was funded by */ pool: TellerPoolConfig; } interface TellerDiscovery { bids: TellerDiscoveredBid[]; at: number; } declare const getCachedTellerBids: (chainId: string, account: string) => TellerDiscovery | undefined; declare const buildTellerUserCall: (chainId: string, _lender: string, account: string) => Promise; /** * TermMax public-data types. * * ─── SIDE SEMANTICS, READ THIS FIRST ─────────────────────────────────────── * TermMax names its two curves from the MAKER's perspective, and they cross * over relative to what a taker (our user) is doing: * * maker's `lendCurveCuts` → consumed by a taker who BORROWS * maker's `borrowCurveCuts` → consumed by a taker who LENDS * * `ITermMaxOrder.apr()` inherits that naming, so its `lendApr` is our user's * BORROW rate and its `borrowApr` is our user's LEND rate — and the API's * `priceInfo.term.lcft` / `bcft` follow the same convention. * * Every field in this file is named for the TAKER action. The crossing happens * exactly once, at the adapter boundary in `apiClient.ts` / `onchain.ts`, and * nowhere else. If a value ever produces lend > borrow on the same order, the * mapping has been applied twice. * ─────────────────────────────────────────────────────────────────────────── */ /** * One segment of an order's piecewise pricing curve (TermMax `CurveCut`). * * `xtReserve` is the LEFT EDGE of the segment's validity interval, not a * quantity. Within a segment the swap solves a constant product over * `(xtReserve + offset, liqSquare_eff / (xtReserve + offset))` where * `liqSquare_eff = liqSquare · daysToMaturity · nif / (365 · 1e8)`. * * This is the TermMax analogue of a Midnight book LEVEL, but continuous rather * than discrete — which is why it cannot reuse `MidnightBookLevel`. */ interface TermMaxCurveSegment { xtReserve: bigint; liqSquare: bigint; /** Signed — shifts the virtual reserve. */ offset: bigint; } /** Per-order fee ratios, 1e8-scaled (`0.02e8` = 2%). Fees apply to the INTEREST, not principal. */ interface TermMaxFeeConfig { lendTakerFeeRatio: bigint; lendMakerFeeRatio: bigint; borrowTakerFeeRatio: bigint; borrowMakerFeeRatio: bigint; mintGtFeeRatio: bigint; mintGtFeeRef: bigint; } /** * Live state of one maker order, already mapped to taker-side semantics. * * An order can source liquidity beyond its own token balance (from an ERC-4626 * `pool`, or by minting fresh FT against the maker's own `gtId`), so * balance-derived depth UNDERSTATES what is actually executable. Prefer the * capacity fields on {@link TermMaxBookTop}, or quote. */ interface TermMaxOrderState { orderAddress: string; marketAddress: string; /** The maker; `makerIsVault` when it is a curated ERC-4626 vault. */ makerAddress?: string; makerIsVault?: boolean; /** THE pricing state in V2 (V1 used the real XT balance). */ virtualXtReserve: bigint; ftReserve: bigint; xtReserve: bigint; maxXtReserve: bigint; /** Maker's own GT, used to mint FT on demand. 0 = none (it is a sentinel). */ gtId: bigint; /** Curve a TAKER LENDS against (TermMax's `borrowCurveCuts`). */ takerLendCuts: TermMaxCurveSegment[]; /** Curve a TAKER BORROWS against (TermMax's `lendCurveCuts`). */ takerBorrowCuts: TermMaxCurveSegment[]; feeConfig?: TermMaxFeeConfig; /** Optional ERC-4626 base-yield sink; absent/zero when unset. */ pool?: string; /** Executable size caps in USD, as reported upstream. */ lendCapacityUsd: number; borrowCapacityUsd: number; /** Executable size caps in debt-token units (human, not raw). */ lendCapacityAmount: number; borrowCapacityAmount: number; /** Fee-free mid APRs as fractions (0.055 = 5.5%), taker-side. Display only. */ takerLendApr?: number; takerBorrowApr?: number; } /** * Best executable rate per side plus aggregate depth for one market — the * direct analogue of `MidnightBookTop`, collapsed across every order. * * "Best" is order-agnostic (we scan all orders): best LEND = highest taker-lend * APR, best BORROW = lowest taker-borrow APR. */ interface TermMaxBookTop { /** Highest taker LEND APR available, as a fraction. Undefined when no order quotes the side. */ bestLendApr?: number; /** Lowest taker BORROW APR available, as a fraction. */ bestBorrowApr?: number; /** Aggregate executable depth, USD. */ lendDepthUsd: number; borrowDepthUsd: number; /** Aggregate executable depth in debt-token units (human). */ lendDepthAmount: number; borrowDepthAmount: number; } /** * A TermMax market, i.e. one (debtToken, collateral, maturity) tuple. * * Discovered DYNAMICALLY — never read from a static registry. Matured markets * disappear from upstream entirely rather than lingering with a flag, and ~15% * of the book can roll on a single maturity date. */ interface TermMaxMarketConfig { /** Market contract — also the per-market lender-key body. */ market: string; /** * The router THIS market's API row points at — the **V1** router on every * live market (even `v2_01` ones), verified on-chain 2026-07-31. This is the * working `borrowTokenFromCollateral` surface: the V1 form takes the order * list directly, while the V2 router's form needs a whitelisted * `TermMaxSwapAdapter` that does not exist on Ethereum (and BNB/Arbitrum * have no V2 router at all). See TERMMAX.md → "The two routers". */ routerAddr?: string; /** FT: the zero-coupon bond ERC-20. THE LEND POSITION. */ ft: string; /** XT: the complement (`FT + XT = 1` debt token). */ xt: string; /** GT: the ERC-721 loan. THE BORROW POSITION (sub-accounts). */ gt: string; /** Debt / loan token. */ debtToken: string; debtDecimals: number; collateral: string; collateralDecimals: number; /** * False when decimals could not be resolved from the payload and the 18-dec * default was used. Exists because reading the wrong `assetConfigs` field * names once made EVERY 6-dec stablecoin market silently 10^12 out; a * consumer that cares about exactness should treat `false` as suspect. */ debtDecimalsResolved?: boolean; collateralDecimalsResolved?: boolean; /** Display symbol, e.g. `USDC/PT-sUSDE-13AUG2026@16AUG2026`. */ symbol?: string; /** Unix seconds. */ maturity: number; /** 1e8-scaled, as strings (as upstream reports them). */ maxLtv: string; liquidationLtv: string; /** false ⇒ NO liquidation at all, only post-maturity physical delivery. */ liquidatable: boolean; /** Post-maturity liquidation window, seconds (7200 on every live market). */ liquidationWindowSeconds?: number; /** Contract `getVersion()`: `v2` = "2.0.0", `v2_01` = "2.0.1". Both are V2. */ version?: string; /** Market-level fee config (1e8-scaled). */ feeConfig?: TermMaxFeeConfig; /** Oracle the protocol itself prices LTV/liquidation against. */ oracle?: string; isMatured?: boolean; isEnabled?: boolean; } /** A market paired with its live book state. `top` is null when the fetch failed. */ interface TermMaxMarketRaw { config: TermMaxMarketConfig; top: TermMaxBookTop | null; /** Per-order state, best-first by taker rate. Empty when the market has no live orders. */ orders: TermMaxOrderState[]; } /** * Pluggable TermMax data source — the hosted API today, a self-hosted indexer * or a pure on-chain reader later. Mirrors `MidnightBookSource`. */ interface TermMaxDataSource { /** * Every live market on a chain with its orders, or null when unavailable. * One upstream call per chain on the happy path. */ getChainMarkets(chainId: string): Promise; } /** * Resolve a TermMax market from the discovery cache, or undefined when it was * never fetched / has gone stale. Callers that need a guaranteed answer should * `await fetchTermMaxMarkets(chainId)` first (or read the market on-chain). */ declare function getCachedTermMaxMarket(chainId: string | number, market: string): TermMaxMarketConfig | undefined; /** All markets cached for a chain (may be empty before the first fetch). */ declare function getCachedTermMaxMarkets(chainId: string | number): TermMaxMarketConfig[]; /** * Fetch every live TermMax market on a chain, with its order book collapsed to * a best-rate + depth snapshot. * * Returns `[]` (not an error) when the chain has no TermMax deployment * configured, so callers can fan out across chains unconditionally. * * Markets are DISCOVERED, never read from a static list: matured markets vanish * from upstream entirely rather than lingering with a flag, and ~15% of the * book can roll on a single maturity date. */ declare function fetchTermMaxMarkets(chainId: string, source?: TermMaxDataSource, options?: { includeMatured?: boolean; }): Promise; /** * Map fetched TermMax markets into the shared `MorphoGeneralPublicResponse` * shape, keyed by the synthesized `TERMMAX_` lender key. * * Each TermMax market is ONE (debt, collateral, maturity) tuple, so it emits * exactly two entries — unlike Midnight, which has several collateral legs per * market: * - LOAN entry: `depositRate` = best taker LEND APR, `variableBorrowRate` = * best taker BORROW APR (already crossed from TermMax's maker-side names in * `apiClient.parseOrder`), plus order-book depth as the liquidity proxy. * - COLLATERAL entry: maxLtv → collateralFactor, liquidationLtv → * borrowCollateralFactor, fixed 10% liquidation penalty. * * Rates are DISPLAY values from the fee-free mid quote. Anything that actually * executes must price off a live quote at build time. */ declare function convertTermMaxMarketsToResponse(raw: TermMaxMarketRaw[], chainId: string, prices?: { [asset: string]: number; }, _additionalYields?: AdditionalYields, tokens?: GenericTokenList): { [m: string]: MorphoGeneralPublicResponse; }; /** Hosted TermMax data API (Swagger at `/api-docs`, spec at `/api-docs-json`). */ declare const DEFAULT_TERMMAX_API = "https://api.termmax.ts.finance"; type FetchLike = typeof fetch; /** Resolve a chain's TermMax API base (config override → hosted default). */ declare function termMaxApiBase(chainId: string): string; /** * Hosted-API data source. ONE call per chain — `GET /market/data?chainId=` * returns global config, asset configs, markets, order configs and live order * state together (~240KB on Ethereum). * * This is the swappable seam: a self-hosted indexer or a pure on-chain reader * can implement {@link TermMaxDataSource} later without touching callers. * * Reliability posture: the endpoint is an undocumented app backend (no * versioning or rate-limit statement, `/admin/*` routes on the same host), so * treat it exactly like the Morpho GraphQL API — primary, but the caller keeps * a last-known-good snapshot (see fetchPublic.ts). */ declare class TermMaxApiSource implements TermMaxDataSource { private readonly baseUrl; private readonly fetchImpl; constructor(baseUrl: string, fetchImpl?: FetchLike); getChainMarkets(chainId: string): Promise; } /** Default data source for a chain (hosted API, resolved per chain). */ declare function createTermMaxDataSource(chainId: string, fetchImpl?: FetchLike): TermMaxDataSource; /** TermMax ratio base: `0.01e8` = 1%. */ declare const DECIMAL_BASE = 100000000n; /** * Days to maturity, **ceilinged to whole days** — exactly as * `TermMaxOrderV2._daysToMaturity` does it: * * `(maturity - now + SECONDS_IN_DAY - 1) / SECONDS_IN_DAY` * * Reproduce the ceiling or an off-chain quote drifts by up to a full day of * interest against the contract. Returns 0 at/after maturity. */ declare function daysToMaturity(maturity: number, nowSec: number): bigint; /** * Marginal (fee-free) APR implied by a curve at a given reserve, 1e8-scaled — * the same formula `TermMaxOrderV2.apr()` uses: * * `apr = vFt · 1e8 · 365 / (vXt · daysToMaturity)` * * Returns 0n for an empty curve or a matured market. This is a MID PRICE at the * current reserve: it ignores both the taker fee and trade size, so it is for * display and sorting only — never quote against it. */ declare function curveApr(cuts: TermMaxCurveSegment[], virtualXtReserve: bigint, days: bigint): bigint; /** 1e8-scaled ratio → plain fraction (`5_500_000n` → `0.055`). */ declare function ratioToNumber(v: bigint): number; /** 1e8-scaled ratio → percent (`5_500_000n` → `5.5`). */ declare function ratioToPercent(v: bigint): number; /** * Marginal APR as a fraction for one side, taker-side by construction. * * Pass `takerLendCuts` for the lend rate and `takerBorrowCuts` for the borrow * rate — the maker/taker crossing has already been applied when those fields * were built (see types.ts). */ declare function curveAprNumber(cuts: TermMaxCurveSegment[], virtualXtReserve: bigint, days: bigint): number; /** * Net-interest factor for a taker LEND (`1e8 - lendTakerFeeRatio`). * Guarded so a malformed fee config cannot produce a non-positive factor. */ declare function lendNif(fee?: TermMaxFeeConfig): bigint; /** Net-interest factor for a taker BORROW (`1e8 + borrowTakerFeeRatio`). */ declare function borrowNif(fee?: TermMaxFeeConfig): bigint; /** * The GT-mint fee ratio at a given time to maturity, 1e8-scaled — mirrors * `TermMaxMarketV2.mintGtFeeRatio()`: * * `days · feeRatio · feeRef / (365·1e8 + feeRef·days)` * * This is a genuine percentage OF PRINCIPAL charged once at borrow time * (`issueFee = debt · ratio / 1e8`), so it maps onto the cross-protocol * `FixedTermInfo.fees.originationFeePercent`. It is NOT the raw * `feeConfig.mintGtFeeRatio` — prefer reading the contract when you can. */ declare function mintGtFeeRatio(fee: TermMaxFeeConfig | undefined, days: bigint): bigint; /** * TermMax liquidation penalty as a fraction of the repaid debt. * * Protocol constants, not per-market: the liquidator is paid 5% and the * protocol reserve takes 5%, so the borrower loses 10% of the liquidated debt * value. (Loans over $10k can only be liquidated 50% at a time; that is a size * cap, not a penalty, and is modeled as `closeFactor`.) */ declare const TERMMAX_LIQUIDATION_PENALTY = 0.1; declare const TERMMAX_LIQUIDATOR_BONUS = 0.05; /** Partial-liquidation threshold, USD: above it a single call can take at most 50%. */ declare const TERMMAX_PARTIAL_LIQUIDATION_THRESHOLD_USD = 10000; declare const TERMMAX_PARTIAL_CLOSE_FACTOR = 0.5; /** Post-maturity liquidation window before physical delivery (`Constants.LIQUIDATION_WINDOW`). */ declare const TERMMAX_LIQUIDATION_WINDOW_SECS = 7200; /** * Parse a 1e8-scaled LTV string into a plain fraction (`87500000` → `0.875`). * Returns 0 for missing/garbage input rather than NaN, so a bad config row * degrades to "no collateral value" instead of poisoning the optimizer. */ declare function parseTermMaxLtv(v: string | number | bigint | undefined): number; /** * TermMax user data is a DISCOVERY-first, async build — the Teller/Liquity * shape, not the Midnight one. * * Midnight can build synchronously because its positions are keyed * `(marketId, user)`: one fixed slot per user per market, with the market list * coming from a static registry. TermMax has neither property: * * 1. the market list is DYNAMIC (markets churn on every maturity roll and * matured ones vanish upstream), so it must be resolved at call time; and * 2. borrow positions are GT ERC-721 sub-accounts — a user can hold N per * market and the ids are not derivable. * * But it is cheaper than Teller: `TermMaxViewer.getPositionDetails(markets[], * owner)` returns FT/XT/collateral balances AND every GT with its debt and * collateral in ONE call, so discovery and data collapse into a single * multicall entry — no two-phase discovery round-trip. The viewer `try`-guards * each position internally, so one bad loan cannot poison the batch. * * Health is NOT read per GT (`getLiquidationInfo`): it is computed downstream * from LTVs + oracle prices in `createMultiAccountTypeUserState`, exactly as * Midnight does. That keeps this to one call. */ /** * Markets per `getPositionDetails` call. * * Measured against the live Ethereum viewer: 200 markets answer cleanly (44.9 KB * of return data), 706 in one array REVERTS on gas. Matured markets are not the * problem — a market matured 2025-04-02 reads fine on its own — the ARRAY SIZE is. * 180 leaves headroom under the observed ceiling. */ declare const TERMMAX_MARKETS_PER_CALL = 180; interface TermMaxDiscovery { /** Markets passed to the viewer, IN ORDER — the parser slices results by index. */ markets: TermMaxMarketConfig[]; /** How many `getPositionDetails` calls the roster was split across. */ chunks: number; at: number; } /** * The market layout used for the last build on this (chain, account). * * The parser runs as a separate phase, fed only the multicall results, so it * needs the layout the builder chose — the same trick the Teller / Liquity / * Lista-broker caches use. */ declare const getCachedTermMaxDiscovery: (chainId: string, account: string) => TermMaxDiscovery | undefined; /** * Build the user-data call for every live TermMax market on a chain. * * Returns `[]` when the chain has no TermMax deployment, no viewer configured, * or no live markets — callers can fan out unconditionally. */ declare const buildTermMaxUserCall: (chainId: string, _lender: string, account: string) => Promise; type PendleAssetTypes = 'PT' | 'YT' | 'SY' | 'PENDLE_LP'; /** * Main function to fetch Pendle prices */ declare function fetchPendlePrices(lists?: { [chainId: string]: { list: TokenList; }; }, assetType?: PendleAssetTypes[]): Promise<{ [address: string]: number; }>; /** * Oracle price data for a single asset */ interface OraclePriceEntry { /** Asset address (lowercase) */ asset: string; /** Raw oracle price (e.g., for Morpho this is debt/collateral rate) */ price: number; /** USD price of the asset */ priceUSD: number; /** Unique market identifier (lender:chainId:refAddress) */ marketUid: string; /** Override lender key for output grouping (used by grouped fetchers like Morpho/Lista) */ targetLender?: string; /** Oracle feed description (e.g. "ETH / USD", "Constant price feed") */ description?: string; /** True when this asset IS the base asset and has a constant/identity oracle price (e.g. loan asset = 1.0) */ staticBase?: boolean; /** Address of the base asset when the oracle quotes in a non-USD unit (e.g. WETH, WKLAY) */ baseAsset?: string; /** * Config-scoped oracle identifier (e.g. spoke address for Aave V4). * * When set, this price applies only within the scope of this config. * The same underlying asset may have different prices from different * config scopes (e.g., two Aave V4 spokes using different price feeds * for the same token). * * `undefined` means the price is config-agnostic (all other lenders). */ configId?: string; /** * Unix seconds of the feed's last update, when the oracle exposes one * (Chainlink-style `latestRoundData().updatedAt`). * * Set directly by fetchers that read round data (Compound V3) and * back-filled for AAVE-family entries by the source probe in * `feedStaleness.ts`. `undefined` means the oracle has no readable * timestamp (e.g. Compound V2 `getUnderlyingPrice`, Morpho `price()`) * — NOT that the feed is fresh. Staleness for those oracles can only be * caught by the cross-source outlier guard in `selectAssetGroupPrices`. */ updatedAt?: number; /** Feed address the `updatedAt` was read from (diagnostics only) */ feedSource?: string; /** * How this USD price was obtained. The single highest-signal ranking input: * a derived price inherits the error of whatever it was derived from and * can never be better than it, so it must lose to a direct oracle read. * * Set by the fetcher (or defaulted per fetcher group in * `fetchOraclePrices`); resolved with `resolveDerivation` so legacy or * third-party entries still classify sensibly from `baseAsset`/`staticBase`. */ derivation?: PriceDerivation; } /** * Quality class of a USD price, in descending order of trust. * * - `direct` — the oracle returns USD (or the asset IS the base and prices * itself at 1). No dependency on any other price. * - `quoted` — a real feed for this asset denominated in another asset * (e.g. `cbBTC / ETH`), multiplied by that asset's USD price. One * dependency, but the asset itself has a dedicated feed. * - `derived` — no feed for this asset at all: a market ratio * (Morpho `price()`, Fluid/Silo/Teller/Gearbox exchange ratios) * multiplied by another asset's USD. Error compounds and the result is * only as good as the leg it hangs off. */ type PriceDerivation = 'direct' | 'quoted' | 'derived'; /** * Structured oracle price data * chainId -> lender -> array of price entries */ type StructuredOraclePrices = { [chainId: string]: { [lender: string]: OraclePriceEntry[]; }; }; /** * Flat USD price map for lookups */ type USDPriceMap = { [assetKey: string]: number; }; /** * Token list entry (matches @1delta/data-sdk TokenListEntry) */ interface TokenListEntry { chainId: string; name: string; address: string; symbol: string; decimals: number; logoURI?: string; assetGroup?: string; props?: { [key: string]: any; }; } /** * Token list map (address -> entry) */ type TokenListMap = { [address: string]: TokenListEntry; }; /** * Custom market override entry for Morpho-style markets */ interface MorphoMarketOverride { oracle: string; loanAsset: string; collateralAsset: string; loanAssetDecimals?: number; collateralAssetDecimals?: number; /** Optional custom market ID (defaults to oracle address without 0x, uppercased) */ marketId?: string; } /** * Custom market override entry for Lista-style markets */ interface ListaMarketOverride { oracle: string; loanAsset: string; collateralAsset: string; /** Optional custom market ID (defaults to oracle address without 0x, uppercased) */ marketId?: string; } /** * Controls which lender + chain combination wins when multiple oracles * report a USD price for the same assetGroup. * * Entries are processed in priority order (index 0 = highest). * The first write for a given assetGroup key wins; later (lower-priority) * entries only fill gaps. */ interface FlattenPriorityConfig { /** * Lender prefixes in descending priority. * A lender matches if it starts with one of these prefixes. * Lenders not listed here fall into a middle tier. */ lenderPriority: string[]; /** * Chain IDs in descending priority (e.g. ["1"] = mainnet first). * Chains not listed here are sorted after the listed ones. */ chainPriority: string[]; /** * Lender prefixes that are always lowest priority (e.g. Morpho, Lista). * These are appended last so they only fill in assets not covered above. */ lowPriorityLenders: string[]; /** * Lenders whose oracle data is known to be invalid/broken on specific chains. * Map of chainId -> lender prefixes to skip entirely. */ excludedLenders: { [chainId: string]: string[]; }; /** * Per-chain lender priority overrides. * Map of chainId -> lender prefixes in descending priority. * If a chain is listed here, these take precedence over the global lenderPriority. */ lenderPriorityPerChain: { [chainId: string]: string[]; }; /** * Per-chain low-priority lender overrides. * Map of chainId -> lender prefixes that are always lowest priority on that chain. */ lowPriorityLendersPerChain: { [chainId: string]: string[]; }; /** * Per-chain excluded lender overrides for specific markets. * Map of chainId -> market UIDs to skip entirely. */ excludedLendersPerChain: { [chainId: string]: string[]; }; /** * Entries whose `updatedAt` is older than this many seconds are demoted * below every fresh candidate (they still fill gaps when nothing else * prices the asset). Default `DEFAULT_STALE_REJECT_SECONDS` (48h). * * Deliberately far above the reporting threshold in `fetchOraclePrices`: * the slowest standard Chainlink heartbeat is 24h, so a healthy stablecoin * feed is routinely several hours old and must NOT be demoted. * Set to 0 to disable staleness demotion. */ staleRejectSeconds?: number; /** * Cross-source sanity guard. When several independent protocol families * agree on a price, candidates that disagree by more than * `rejectFactor` are dropped before priority is applied — this is the only * defense against oracles that expose no timestamp (Compound V2 forks, * Morpho `price()`), where a dead feed is indistinguishable from a live * one on-chain. Set to `false` to disable. */ outlierGuard?: OutlierGuardConfig | false; } /** * Cross-source outlier rejection settings. * * Deliberately conservative: it only fires when a large majority of * INDEPENDENT protocol families agree, and only drops candidates that are * off by a factor, not a percentage. A minority of broken feeds must never * be able to reject a correct price. */ interface OutlierGuardConfig { /** * Minimum number of distinct protocol families that must agree (within * `agreementTolerance` of the reference price) before any candidate is * dropped. Below this the group is left untouched. */ minAgreeingFamilies: number; /** Relative tolerance for counting a family as "agreeing" (0.05 = 5%) */ agreementTolerance: number; /** * Multiplicative deviation from the reference price above which a * candidate is rejected (2 = more than 2x or less than half). */ rejectFactor: number; } /** TVL map keyed by lender -> total liquidity USD (legacy shape) */ type TvlMap = { [lender: string]: number; }; /** * Market depth behind a price, keyed by `marketUid` * (`LENDER:chainId:asset`, see `createMarketUid`) — the same key the lending * pipeline already uses, so producers can join the two without a new * identifier. * * Accepts lender-level keys too, so the legacy `TvlMap` keeps working: the * per-market key is tried first and the lender key is the fallback. */ type DepthMap = { [marketUidOrLender: string]: number; }; /** * Why a given asset group ended up with the price it did. Emitted per group * via `SelectPricesOptions.onSelection` — the flat price map alone makes * "where did this number come from" unanswerable. */ interface PriceSelection { assetGroup: string; priceUSD: number; chainId: string; lender: string; /** Aggregator address, or `lender|asset` when the oracle exposes none */ feedKey: string; derivation: PriceDerivation; /** Candidates considered for this group */ candidates: number; /** How many the outlier guard dropped */ rejected: number; } /** * Per-tracker diagnostics (one entry per fetcher/lender per chain) */ interface TrackerDiagnostic { /** Lender identifier */ lender: string; /** Number of multicall slots for this tracker */ callCount: number; /** Number of slots that returned '0x' (failed on-chain) */ failedCalls: number; /** Number of parsed price entries produced */ parsedEntries: number; /** If the parser threw, the error message */ parseError?: string; } /** * Per-chain diagnostics */ interface ChainDiagnostic { /** Chain ID */ chainId: string; /** Total multicall slots executed */ totalCalls: number; /** Total '0x' failures across all trackers */ totalFailedCalls: number; /** Total parsed price entries */ totalParsedEntries: number; /** Wall-clock time for this chain in ms */ durationMs: number; /** Per-tracker breakdown */ trackers: TrackerDiagnostic[]; /** Fetchers whose getCalls() threw (fetcher name -> error message) */ getCallsErrors: { [fetcher: string]: string; }; /** If the entire chain failed (e.g. all RPCs down), the error message */ chainError?: string; /** Morpho markets that could not derive a USD price */ unresolvedMorphoMarkets: string[]; /** Chainlink feeds with stale prices (updatedAt older than threshold) */ staleFeeds: StaleFeedEntry[]; } /** * Entry for a stale Chainlink price feed */ interface StaleFeedEntry { /** Asset address */ asset: string; /** Lender using this feed */ lender: string; /** Feed oracle address */ oracle: string; /** Seconds since the feed was last updated */ staleSeconds: number; /** Feed description when the aggregator exposes one (e.g. "EURC / USD") */ description?: string; } /** * Top-level diagnostics report returned alongside oracle prices */ interface OracleDiagnostics { /** Per-chain diagnostics */ chains: ChainDiagnostic[]; /** Total wall-clock time for all chains in ms */ totalDurationMs: number; /** Chains that failed entirely */ failedChains: string[]; } /** * Combined result from fetchOraclePrices */ interface OraclePricesResult { /** Structured oracle prices (partial if some chains failed) */ prices: StructuredOraclePrices; /** Diagnostics report */ diagnostics: OracleDiagnostics; } /** * Market overrides for extending Morpho markets beyond JSON data */ type MorphoMarketOverrides = { [chainId: string]: MorphoMarketOverride[]; }; /** * Market overrides for extending Lista markets beyond JSON data */ type ListaMarketOverrides = { [chainId: string]: ListaMarketOverride[]; }; /** * Curve LlamaLend oracle fetcher — DERIVED (Pass 2). * * Each market's price feed is `price_oracle()` on its own LLAMMA. Three * properties of that read drive every decision in this file: * * 1. **It is always WAD**, regardless of either token's decimals. Verified * on-chain across the decimal spread: the 8-decimal WBTC / crvUSD market * returns `65042815002675129318680` (= 65,042.82) and the 18-decimal * sfrxUSD market returns `1205489834170667241` (= 1.2055). So this fetcher * divides by 1e18 and NEVER consults token decimals — unlike Morpho, whose * oracles scale by `10^(36 + loanDec - collDec)`. * 2. **It is denominated in the BORROWED token**, not USD. Hence Pass 2 with * `updatePrices=false`: `collateralUSD = ratio × borrowedUSD`, where the * borrowed token's direct USD price came from Pass 1. This is the * Morpho/Midnight/Teller shape, so the derivation class is `'derived'`. * It matters for real markets, not just in theory — 8 of ~99 markets * borrow something other than crvUSD (CRV, WETH, tBTC, ynETH, USDC, * wstETH), where treating the ratio as USD would be badly wrong. * 3. **It lives on the AMM, and only on the AMM.** A Curve * `price_oracle_contract` is a different contract exposing `price()`, and * the LLAMMA does NOT implement `price()`. That distinction is the whole * reason this fetcher exists: LlamaLend markets used to fall through a * catch-all `else` into the MORPHO override bucket, which called `price()` * on the LLAMMA — reverting, mapping to '0x', and dropping every market * silently. Had the address been a `price_oracle_contract` instead, the * call would have SUCCEEDED and been rescaled by 1e36, i.e. ~1e18 off. * * Markets are sourced exclusively from overrides (the database), like Morpho * and Lista. There is no on-chain market enumeration to fall back on. */ /** * One LlamaLend market, as supplied by the caller's database. */ interface LlamaLendMarketOverride { /** * The market's LLAMMA. `price_oracle()` is read from here — NOT from the * generic `oracle` column, which is written as the AMM but would silently * become unreadable if a `price_oracle_contract` were ever stored there. */ amm: string; /** Borrowed token — the oracle's unit of account. */ loanAsset: string; collateralAsset: string; /** Present for symmetry with the other override types; NOT used for scaling. */ loanAssetDecimals?: number; /** Present for symmetry with the other override types; NOT used for scaling. */ collateralAssetDecimals?: number; /** * Controller address, 0x-stripped and uppercased — the suffix of the * per-market lender key the lending converter emits. */ marketId: string; } type LlamaLendMarketOverrides = { [chainId: string]: LlamaLendMarketOverride[]; }; /** * Token list type expected by this function * Token decimals are read from list[chainId].list[address].decimals */ type TokenListInput = { [chainId: string]: TokenListMap; }; /** * Options for fetchOraclePrices with market overrides */ interface FetchOraclePricesOptions { /** Optional RPC URL overrides per chain */ rpcOverrides?: { [chainId: string]: string[]; }; /** Token lists with decimals and asset groups for price resolution */ lists?: TokenListInput; /** Number of retries for RPC calls */ retries?: number; /** Optional batch size for multicall, per chain */ batchSize?: { [chainId: string]: number; }; /** Whether to allow individual call failures (default: true) */ allowFailure?: boolean; /** Optional base USD prices to use for deriving other prices */ basePrices?: USDPriceMap; /** Custom Morpho markets to add beyond JSON data */ morphoMarketOverrides?: MorphoMarketOverrides; /** Custom Lista markets to add beyond JSON data */ listaMarketOverrides?: ListaMarketOverrides; /** * Chainlink staleness threshold in seconds (default: 3600 = 1 hour). * Feeds with updatedAt older than this are flagged in diagnostics. * Set to 0 to disable staleness checking (also skips the AAVE feed probe). * * This is a REPORTING threshold only — it is intentionally tighter than the * `staleRejectSeconds` used by `selectAssetGroupPrices` to demote a price, * because a healthy 24h-heartbeat stablecoin feed is routinely hours old. */ stalenessThresholdSeconds?: number; /** * Resolve AAVE-family feed sources and read their `updatedAt` (default true * when staleness checking is on). Costs two extra multicalls per chain, run * concurrently with the price groups. Without it, chains whose only price * source is an AAVE fork — Avalanche has no Compound V3 deployment — get no * staleness coverage at all. */ probeFeedStaleness?: boolean; /** * Only run these fetcher groups. Useful for debugging individual protocols. * Values: 'aave', 'compoundV2', 'compoundV3', 'lista', 'llamalend', * 'eulerV2', 'aaveV4', 'morpho', 'midnight', 'exactly', 'term', 'liquity', * 'river', 'teller', 'siloV2', 'siloV3', 'fluid', 'curvance', 'resupply'. * If omitted, all fetchers run. */ onlyFetchers?: string[]; } /** * Fetches oracle prices from AAVE, Morpho, Lista, Compound V2/V3, and Euler V2 protocols. * Returns structured data and a diagnostics report. * * Architecture: * - Each fetcher group (AAVE, CompoundV2, CompoundV3, Lista, Euler, Morpho) * runs as an independent multicall in parallel — heavy protocols don't block others * - Parsing happens afterward in dependency order (AAVE sources → others → Morpho) * - Chain-level isolation via Promise.allSettled * - Fetcher/parser-level isolation via try-catch */ declare function fetchOraclePrices(chainIds: string[], rpcOverrides?: { [chainId: string]: string[]; }, lists?: TokenListInput, retries?: number, batchSize?: { [chainId: string]: number; } | undefined, allowFailure?: boolean, basePrices?: USDPriceMap, morphoMarketOverrides?: MorphoMarketOverrides, listaMarketOverrides?: ListaMarketOverrides, stalenessThresholdSeconds?: number, onlyFetchers?: string[], probeFeedStaleness?: boolean, /** * Curve LlamaLend markets. Appended LAST rather than slotted next to the * other two override params on purpose — ~50 call sites already pass all 12 * positional arguments, and inserting here would silently shift * `onlyFetchers` / `probeFeedStaleness` in every one of them. */ llamaLendMarketOverrides?: LlamaLendMarketOverrides, /** * Omit to take the per-chain default from `morphoIncludesUnlisted` — the * SAME resolver `getLenderPublicDataAll` uses, so the price roster and the * market roster cannot disagree. Resolved PER CHAIN below, which a single * boolean could not do: this function takes `chainIds[]` while the market * fetch is one chain at a time. Pass a boolean only to override every chain * in this call. Appended last for the same reason * `llamaLendMarketOverrides` was. */ includeUnlistedMorphoMarkets?: boolean): Promise; /** * Self-calibrating per-feed quality stats. * * Everything here is learned from data the price pipeline already fetches — * nobody ranks a lender by hand. Two observations per cycle per feed: * * 1. **Its own update cadence.** Chainlink heartbeats range from ~27s to 24h, * and we cannot read a feed's configured heartbeat on-chain. Recording the * gaps between distinct `updatedAt` values learns it, so "stale" becomes * "far past what THIS feed normally does" instead of one global constant * that is wrong for almost every feed. * * 2. **Its agreement with the rest of the market.** A feed that keeps * disagreeing with every other source for the same asset is broken, * whether or not it still updates. This is the only signal that catches a * frozen Compound-V2-style oracle, which exposes no timestamp at all. * * The stats are a plain JSON blob: this module computes, the caller persists * (worker KV, a DB row, a file — margin-fetcher stays storage-agnostic). * * SAFETY: reliability may only ever DEMOTE a candidate. A cold start, a lost * blob or a corrupted stat therefore degrades to the structural ranking * rather than promoting a bad price. */ /** Stats for one feed, keyed `|`. */ interface FeedStat { /** Chain the feed lives on */ chainId: string; /** * Feed identity: the aggregator address when the oracle exposes one, * otherwise `lender|asset` (Compound V2 forks, Morpho `price()` — no * readable feed address, but still a stable identity to score). */ feedKey: string; /** Most recent `updatedAt` seen (unix seconds); 0 when the feed has none */ lastUpdatedAt: number; /** Wall-clock (unix seconds) of the last observation */ lastObservedAt: number; /** Number of distinct updates observed */ updates: number; /** Rolling estimate of this feed's typical update interval, seconds */ intervalEwmaSeconds: number; /** Largest interval observed between updates, seconds */ intervalMaxSeconds: number; /** Rolling |price/consensus - 1| across cycles; undefined until measurable */ deviationEwma?: number; /** Number of cycles where a consensus existed to compare against */ deviationSamples: number; } type FeedStatsMap = { [key: string]: FeedStat; }; type FreshnessClass = 'fresh' | 'stale' | 'unknown'; type ReliabilityClass = 'good' | 'degraded' | 'bad' | 'unknown'; /** * Stable identity of the source behind an entry. * * Prefers the aggregator address so the many protocols reading the same * Chainlink feed collapse to one source — on Avalanche, Aave V3, Aave V2, * Granary, Nereus and Benqi all read the same AVAX/USD aggregator, and * counting them as five independent opinions is exactly the mistake that * makes consensus checks worthless. * * When no aggregator is readable (Compound V2 forks, Morpho `price()`), the * fallback is keyed on the protocol FAMILY, not the raw lender key: Morpho's * per-market keys are one protocol's opinion repeated, not N sources. */ declare function feedKeyOf(entry: OraclePriceEntry, lender: string): string; declare function feedStatKey(chainId: string, feedKey: string): string; /** * One observation of a feed within a cycle. * `consensusDeviation` is `|price/consensus - 1|`, omitted when the asset * group had no independent consensus to compare against. */ interface FeedObservation { chainId: string; feedKey: string; updatedAt?: number; consensusDeviation?: number; } /** * Fold a cycle's observations into the stats blob. Pure and idempotent for * repeated `updatedAt` values, so re-running a cycle cannot corrupt the * learned cadence. */ declare function updateFeedStats(prev: FeedStatsMap, observations: FeedObservation[], nowSeconds?: number): FeedStatsMap; /** * Is this feed past its own normal cadence? * * Falls back to `defaultStaleSeconds` until the feed has enough observed * updates to trust its learned interval. */ declare function classifyFreshness(updatedAt: number | undefined, stat: FeedStat | undefined, defaultStaleSeconds: number, nowSeconds?: number): FreshnessClass; /** How far this feed habitually sits from the rest of the market. */ declare function classifyReliability(stat: FeedStat | undefined): ReliabilityClass; /** * Collect this cycle's observations straight from the structured prices. * * A feed is only scored on agreement when the rest of the group forms a * STRONG consensus — the same standard the selection guard applies. Learning * "this feed disagrees" from two other sources would repeat the mistake the * guard exists to avoid: for a thinly-sourced yield-bearing token, the * majority is often the one that is wrong, and a feed must never accumulate * a bad reputation for being right about something obscure. * * The reference excludes the feed being scored, so no feed is ever measured * against itself, and a protocol emitting one entry per market votes once. */ declare function collectFeedObservations(structuredPrices: StructuredOraclePrices, lists?: { [chainId: string]: TokenListMap; }, guard?: OutlierGuardConfig): FeedObservation[]; /** * Drop stats for feeds not seen in a long time so the blob cannot grow * without bound as markets come and go. */ declare function pruneFeedStats(stats: FeedStatsMap, maxAgeSeconds?: number, nowSeconds?: number): FeedStatsMap; /** * Cross-source consensus: the one signal that works on oracles exposing no * timestamp at all (Compound V2 forks return a bare `getUnderlyingPrice`, * Morpho a bare `price()`), where a dead feed is indistinguishable from a * live one on-chain. * * Shared by the selection guard and by the feed-reliability stats so BOTH * apply the same standard of proof. Learning "this feed disagrees" from a * two-source group would be the same mistake as rejecting on it. */ declare const DEFAULT_OUTLIER_GUARD: OutlierGuardConfig; /** Anything carrying a price and the identity of the source behind it. */ interface FeedPriced { priceUSD: number; feedKey: string; } /** * The reference price a group agrees on, or `undefined` when the group has * no broad agreement. * * The reference is the median of PER-FEED medians, so the five protocols * reading one Chainlink aggregator count once, not five times, and a * protocol emitting a price per market cannot outvote everyone else. * * Requiring a real cluster is what stops a minority of broken feeds from * overruling a correct price: where two dead feeds quote a `1e-8` sentinel * and only AAVE quotes the truth, no cluster forms and the caller is told * there is no consensus — rather than being handed `1e-8` as one. */ declare function consensusReference(candidates: FeedPriced[], guard?: OutlierGuardConfig): number | undefined; /** * Drop candidates that contradict a strong cross-feed consensus. * * Rejects on a FACTOR (default >2x off), never on a percentage, so ordinary * oracle disagreement is untouched — and never empties a group. * * Exported so tests can assert against the real implementation instead of a * copy that can drift from it. */ declare function rejectOutliers(candidates: T[], guard?: OutlierGuardConfig): T[]; /** * Feeds older than this are demoted below every fresh candidate, when the * feed has no learned cadence of its own yet. * * 48h = 2x the slowest standard Chainlink heartbeat (24h). A healthy * stablecoin feed on a 24h heartbeat is routinely 10-20h old, so anything * tighter would demote perfectly good prices. Once `feedStats` has watched a * feed for a few updates, its own cadence replaces this constant. */ declare const DEFAULT_STALE_REJECT_SECONDS = 172800; /** Options for the evidence-driven tiers. */ interface SelectPricesOptions { /** * Market depth behind each price, keyed by `marketUid` * (`LENDER:chainId:asset`) or, for the legacy shape, by lender. * Orders the long tail of lenders that no priority list mentions — without * it, ties there fall through to the deterministic identity tiebreak. */ depth?: DepthMap | TvlMap; /** Learned per-feed cadence + agreement stats from `feedStats.ts` */ feedStats?: FeedStatsMap; /** Wall clock override (unix seconds), for deterministic tests */ nowSeconds?: number; /** * Receives the winning candidate per asset group. Provenance is otherwise * unrecoverable from the flat map — "why is AVAX $23.78" should not * require re-deriving the whole pipeline. */ onSelection?: (selection: PriceSelection) => void; } /** * Flattens structured oracle prices to a simple USD price map. * * Ranking, in order: * 1. freshness — past this feed's own learned cadence loses * 2. reliability — a feed that habitually disagrees with the market loses * 3. derivation — a direct oracle read beats a price derived from one * 4. lender — operator preference from the priority config * 5. depth — deeper market wins (also picks an asset's home chain) * 6. chain — configured chain preference * 7. identity — deterministic, never input order * * Before ranking, candidates contradicting a strong cross-feed consensus are * dropped by the outlier guard. */ declare function selectAssetGroupPrices(structuredPrices: StructuredOraclePrices, lists?: TokenListInput, depthOrTvl?: DepthMap | TvlMap, priorityCfg?: FlattenPriorityConfig, options?: SelectPricesOptions): USDPriceMap; /** * Resolve an entry's derivation class. * * Precedence: * 1. What the fetcher put on the entry (most specific — e.g. Euler knows * per-vault whether its unit of account is a fiat code or a token). * 2. The fetcher group's declared default. `derived` wins over the * structural guess below: Fluid/Silo collateral legs carry a `baseAsset` * exactly like a real quoted feed, but there is no feed for the asset — * only a market ratio. * 3. The `baseAsset`/`staticBase` convention every fetcher already follows: * a non-base asset carrying a `baseAsset` is quoted in it; anything else * is a direct USD read. * * Step 3 is what keeps third-party or older entries (which predate the * `derivation` field) from silently ranking as `direct`. */ declare function resolveDerivation(entry: OraclePriceEntry, groupDefault?: PriceDerivation): PriceDerivation; /** * Collapse a market-keyed lender key to its protocol family: * `MORPHO_BLUE_` / `AAVE_V4_` / `SILO_V2_` -> `MORPHO_BLUE`, * `AAVE_V4`, `SILO_V2`. * * Used wherever INDEPENDENT sources are counted. One protocol emitting a * price per market must not look like agreement between many protocols — * that is the difference between "four oracles agree" and "one oracle, read * four times". * * Suffixes that are not pure hex (`COMPOUND_V3_USDC`, `AAVE_V3_ETHER_FI`) * are distinct deployments and keep their key. * * Lives in its own module so `feedStats` and `selectAssetGroupPrices` can * both use it without an import cycle. */ declare function lenderFamily(lender: string): string; /** * Feed timestamps for AAVE-family oracles. * * `AaveOracle.getAssetPrice()` returns a bare uint — no timestamp — so the * only way to tell a live feed from a dead one is to resolve the per-asset * source (`getSourceOfAsset`) and read `latestRoundData().updatedAt` on it. * That matters because AAVE forks are the dominant (often only) price source * on several chains: Avalanche has no Compound V3 deployment at all, so * before this probe existed NOTHING on that chain was ever checked for * staleness. * * Two multicalls, both `allowFailure` and both best-effort: a chain where the * probe fails just yields no timestamps, never a missing price. Roughly half * of AAVE's sources are price-cap adapters that do not implement * `latestRoundData` — those legitimately return no timestamp. */ /** asset (lowercase) -> feed timestamp info */ type AssetFeedInfo = { source: string; updatedAt: number; description?: string; }; /** lender -> asset (lowercase) -> feed info */ type FeedTimestampMap = { [lender: string]: { [asset: string]: AssetFeedInfo; }; }; interface ProbeOptions { rpcOverrides?: { [chainId: string]: string[]; }; batchSize?: number; retries?: number; } /** * Resolve `updatedAt` for every AAVE-family (lender, asset) pair on a chain. * * Returns an empty map — never throws — when the chain has no AAVE-family * lender or when the RPC calls fail. */ declare function probeAaveFeedTimestamps(chainId: string, options?: ProbeOptions): Promise; /** * API key for `api.mysticfinance.xyz`, set by the host process. * * The `morphoCache` endpoint used to be open; as of 2026-09 it answers every * request — including one with no `chainId` — with * `401 {"message":"API key required. Send it as the \`x-api-key\` header."}`. * * **Nothing sets this today and no key exists in either repo**, so the Mystic * remote is inert in production; `mysticPricesAvailable` therefore routes the * Mystic chains (Flare 14, Plume 98866, Citrea 4114) to the ON-CHAIN Morpho * oracle multicall, which is their source of record. That path is complete on * its own — verified 2026-09-07 with no key: 9/9 published Flare markets and * 9/11 Plume markets priced, all carrying the real oracle ratio Mystic never * returns. Two things must hold for it to stay that way, and both are * off-by-default failure modes rather than errors: * * 1. `lender-metadata` must keep DISCOVERING markets on these chains, and * today it CANNOT. Its `update:onchain-markets` event scan no longer * credits `hasMysticApi()` (fixed 2026-09-07 — it had been excluding these * chains on the strength of an indexer that answers 401), but the scan * itself does not run here either: Plume's drpc endpoint refuses * `eth_getLogs` outright, Citrea serves no usable range, and Flare caps a * range at 30 blocks over a 69M-block history so the budgeted scan returns * ZERO markets while reporting success. The roster is therefore frozen at * what Mystic last served (9 Flare / 14 Plume / 1 Citrea) and a market * created after that is invisible. Discovery needs a key or a paid * archival RPC; it is append-only, so nothing is ever deleted by this. * 2. Some other lender on the chain must publish a USD price for the loan * asset, since Morpho entries are derived (Kinetic/Enosys on Flare, * Avalon/LayerBank on Plume). Citrea has no other lender at all. * * A setter exists so worker/browser hosts — which have no `process.env` — can * supply the key, matching `setResupplyPairRoster` / `setCurvanceAssetRoster`. * When nothing has been set, `MYSTIC_API_KEY` is read from the environment so * Node hosts (the yield-tracer crons) need no wiring of their own. */ /** Publish the key. Pass undefined/empty to fall back to `MYSTIC_API_KEY`. */ declare function setMysticApiKey(key: string | undefined): void; /** Creates a unique identifier * Default format: `${lender}:${chainId}:${underlying.toLowerCase()}` * Compound V2 format: `${lender}:${chainId}:${cToken.toLowerCase()}` * Init format: `${lender}:${chainId}:${poolId.toLowerCase()}` * * The ref is lower-cased on EVM chains ONLY, where hex is case-insensitive and * a canonical form is what makes look-ups match. It is preserved verbatim * everywhere else: base58 is case-SIGNIFICANT, so lower-casing a Solana * pubkey does not canonicalize it, it corrupts it. * * Every one of the 1,100+ EVM chains takes the first branch, so every uid this * has ever produced is byte-identical — no served data, KV entry or * client-held uid changes. * * @throws if any argument is missing */ declare function createMarketUid(chainId: string, lender: string, refAddress: string): string; declare const getLendersForChain: (c: string) => string[]; /** Filter lenders by protocol list */ declare const filterLendersByProtocol: (allLenders: string[], protocolList?: string[]) => string[]; /** All aave protocols per chain */ declare const getAavesForChain: () => { [c: string]: string[]; }; declare const getLenderAssets: (chainId: string | number | undefined, lendingProtocol?: string) => string[]; declare enum FlashLoanIds { MORPHO = 0, BALANCER_V2 = 1, AAVE_V3 = 2, AAVE_V2 = 3, SINGLETON = "SINGLETON" } interface FlashLoanLiquidityForAsset { id: number; type: FlashLoanIds; name: string; source: string; fee: string; availableRaw: string; /** * `availableRaw` scaled by `decimals`. ABSENT when the asset's decimals * could not be resolved — neither from the token list nor from an on-chain * `decimals()` — because a guessed scale silently misreports liquidity by * orders of magnitude. `availableRaw` is always exact; prefer it. */ available?: number; /** ABSENT when unresolvable — see `available`. */ decimals?: number; } type FlashLiquiditiesOnChain = { [asset: string]: FlashLoanLiquidityForAsset[]; }; declare function fetchFlashLiquidityForChain(chain: string, multicallRetry: MulticallRetryFunction, list?: TokenList): Promise; /** * Add prices to get dollar flash liquidties * Ideal for de-based flash loans */ declare function attachPricesToFlashLiquidity(chainId: string, liq: FlashLiquiditiesOnChain, prices: { [k: string]: number; }, list?: TokenList): { [asset: string]: FlashLoanLiquidityForAsset[]; }; /** * A single underlying market a curated/meta vault allocates deposits into, * with the vault's current size in that market. Lets a depositor see what * their funds are actually exposed to (collateral type, concentration, * per-market rate) rather than just the blended vault APR. * * Shared across vault providers that expose an allocation breakdown * (Morpho/Moolah markets, Silo markets). `marketId` is provider-specific: * a 0x64 market key for Morpho/Moolah, a 0x40 market (silo) address for * Silo. */ interface VaultMarketExposure { /** Lowercased market identifier (provider-specific format). */ marketId: string; /** * The `${lender}:${chainId}:${underlying}` market uid — the same key the * lending/markets API uses for this market's supply (loan) side, so an * exposure can be joined to the full market record. Present only on real * market entries (absent on the synthetic `idle` entry). */ marketUid?: string; /** Lowercased collateral token address backing this market. */ collateralAddress: string; /** Hydrated collateral metadata from the token list, if available. */ collateral?: GenericCurrency; /** Vault assets allocated to this market, human-formatted. */ assets: number; /** Allocated assets in USD (uses the vault underlying price). */ assetsUsd: number; /** Share of the vault's total assets, in percent (0..100). */ weightPct: number; /** This market's supply APR in percent, net of the market protocol fee. */ supplyApr: number; /** * True for the synthetic "idle" entry representing uninvested vault assets * (deposits not currently deployed into any market). Idle funds earn * nothing, so `supplyApr` is `0`, `collateral`/`collateralAddress` are * empty, and `marketId` is the sentinel `'idle'`. With the idle entry * present, allocation-based breakdowns (Lista, Silo) sum to ~100%. * Absent/false on real market entries. Not emitted by Fluid (its * breakdown is over borrows, not vault allocation). */ idle?: boolean; } /** Sentinel `marketId` for the synthetic idle exposure entry. */ declare const IDLE_MARKET_ID = "idle"; /** * Builds the multicall descriptor for Fluid fToken public data. * * Returns a single call to `LendingResolver.getFTokensEntireData()` — the * resolver hands back every fToken on the chain in one shot. No per-fToken * enumeration needed. * * Resolver address is constant across chains but sourced via the SDK so * future chain-specific deployments can diverge without code changes. */ declare const buildFluidFTokensCall: (chainId: string) => { address: string; name: string; params: never[]; }[]; /** * Parsed Gearbox V3 `PoolV3` entry — the passive-lender / ERC-4626 side. * * `PoolV3` contracts are standalone ERC-4626 vaults; the pool contract * itself is the Diesel share token. Not a lending market (no collateral, * no per-user borrow, no liquidation) — just a deposit → earn shape, * funded by draw-downs from attached `CreditManagerV3` suites. * * Modeled separately from the per-CreditManager credit-side markets * (`src/lending/public-data/gearbox/`), which are isolated and each get * their own `GEARBOX_V3_` lender key. */ interface GearboxV3Pool extends VaultClassificationFields { /** Pool (Diesel share) contract address, lowercased. */ address: string; /** Lowercased underlying ERC20 address. WETH is normalized to * `zeroAddress` on Ethereum mainnet so downstream code only sees one * native-ETH shape — matches the convention in the rest of the stack. */ underlying: string; /** Diesel share symbol, e.g. `dUSDCV3`, `dWETHV3`. */ symbol: string; /** Diesel share name as returned by `name()`, e.g. `Gearbox USDC V3`. * Raw — use `displayName` for UI. */ name: string; /** Cross-provider UI label — `${curatorName ?? 'Gearbox'} ${asset.symbol}` * (e.g. `cp0x USDC`, `Re7 WETH`). Always non-empty. */ displayName: string; /** Pool / Diesel decimals (ERC-4626 keeps share and asset decimals * aligned). */ decimals: number; /** Total underlying assets held by the pool, raw integer as string. */ totalAssets: string; /** Total Diesel shares minted, raw integer as string. */ totalSupply: string; /** Raw `convertToAssets(10**shareDecimals)` — 1 share → X underlying, * asset-scaled. Derived from `totalAssets / totalSupply`. */ convertToAssets: string; /** Total underlying currently lent out by all attached Credit Managers. */ totalBorrowed: string; /** Underlying freely withdrawable by lenders (totalAssets − totalBorrowed). */ availableLiquidity: string; /** `expectedLiquidity` on-chain — totalAssets accrual-projected. */ expectedLiquidity: string; /** Diesel-per-underlying rate, raw integer — `1 underlying → N shares`. */ dieselRate: string; /** Withdraw-fee in basis points (e.g. `10` = 0.10 %). */ withdrawFeeBps: number; /** MarketConfigurator address (lowercased) that deployed this pool's * market. Each configurator is operated by a known curator (cp0x, Re7, * Chaos Labs, …); maps 1:1 to `curatorName` via * `gearboxMarketConfigurators(chainId)` in `@1delta/data-sdk`. */ curator?: string; /** Human-readable curator label for UI (e.g. `cp0x`, `Re7`, * `Chaos Labs`). Resolved from the configurator address via the * `gearboxResolvers.chains[chainId].marketConfigurators` registry. */ curatorName?: string; /** Base supply APR in percent (e.g. `3.41` = 3.41 %). Pool-level rate, * same value every attached CM sees. */ supplyRate: number; /** Base borrow APR in percent. */ baseInterestRate: number; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10^decimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; /** Currently withdrawable underlying, raw integer as string. Mirrors * `availableLiquidity` (= expectedLiquidity − totalBorrowed); exposed * under the cross-vault `liquidity*` names for parity with Morpho / * Fluid / Silo. */ liquidity: string; /** Human-formatted immediate withdrawable liquidity. */ liquidityFormatted: number; /** Human-formatted immediate withdrawable liquidity in USD. */ liquidityUsd: number; } /** Full parsed payload: per-underlying map for easy UI lookup. */ type GearboxV3Pools = { /** Keyed by lowercased underlying address (`zeroAddress` for native ETH). */ [underlying: string]: GearboxV3Pool; }; /** Curator metadata as surfaced by the Morpho API. */ interface VaultCuratorMeta { /** Curator slug, e.g. `gauntlet`, `kpk`, `steakhouse`. */ id: string; /** Display name, e.g. `Gauntlet`, `KPK`, `Steakhouse Financial`. */ name: string; /** Logo URL (typically a CDN-hosted SVG). */ image?: string; /** Whether Morpho has verified this curator. */ verified?: boolean; } /** * Parsed MetaMorpho vault entry. * * MetaMorpho vaults are standalone ERC-4626 yield vaults that allocate * their deposits across one or more Morpho Blue markets (per-vault * strategy configured by a curator). They are NOT a lending market — no * collateral, no borrow, no liquidation — just a deposit → earn shape, * modeled separately from the per-market borrow side in * `src/lending/public-data/morpho/`. * * Multiple vaults typically exist per underlying (different curators / * risk profiles), so the top-level map is keyed by vault address rather * than underlying — differs from Fluid/Gearbox where one vault per * underlying is the norm. */ interface MorphoVault extends VaultClassificationFields { /** MetaMorpho vault (share token) contract address, lowercased. */ address: string; /** Lowercased underlying ERC20 address. */ underlying: string; /** Vault share-token symbol, e.g. `bbUSDC`, `steakUSDC`. */ symbol: string; /** Vault share-token name as returned by `name()`, e.g. * `Morpho USDC Steakhouse`. Raw — may be inconsistently formatted * across vaults; use `displayName` for UI. */ name: string; /** Cross-provider UI label — `${curatorName ?? 'Morpho'} ${asset.symbol}` * (e.g. `Steakhouse USDC`, `Gauntlet WETH`). Always non-empty. */ displayName: string; /** Share and underlying decimals (ERC-4626 keeps them aligned). */ decimals: number; /** Total underlying assets held by the vault, raw integer as string. */ totalAssets: string; /** Total shares minted, raw integer as string. */ totalSupply: string; /** Raw `convertToAssets(10**shareDecimals)` — 1 share → X underlying, * asset-scaled. Derived from `totalAssets / totalSupply`. */ convertToAssets: string; /** Supply APR in percent, net of vault fee (e.g. `4.12` = 4.12 %). */ supplyRate: number; /** Extra rewards APR in percent, on top of `supplyRate`. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor actually earns. */ depositRate: number; /** Performance fee in percent (e.g. `5.0` = 5 %). */ fee: number; /** Timelock for vault config changes, in seconds. */ timelock: number; /** Whether the vault is listed by the Morpho frontend. Goldsky-sourced * chains default to `true` (listing is Morpho-API-only). */ whitelisted: boolean; /** Owner address, lowercased — may be absent if not set. */ owner?: string; /** Curator address, lowercased — may be absent if not set. */ curator?: string; /** Guardian address, lowercased — may be absent if not set. */ guardian?: string; /** Human-readable curator label for UI (e.g. `Steakhouse`, `Gauntlet`). * Populated from the Morpho API's `state.curators[].name`; undefined on * Goldsky-sourced chains where the subgraph carries only the address. */ curatorName?: string; /** Curator logo URL from the Morpho API (CDN-hosted SVG). Undefined on * Goldsky-sourced chains. */ curatorImage?: string; /** Full curator list — set when a vault has co-curators. First entry * matches `curatorName` / `curatorImage`. Undefined on Goldsky-sourced * chains. */ curators?: VaultCuratorMeta[]; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied / returned. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10^decimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; /** Underlying currently withdrawable *right now*, raw integer as string. * Caps immediate withdrawals — the rest of `totalAssets` is locked in * markets up to their utilisation. A vault's `publicAllocatorConfig` * (when present) can expand this at tx time by reallocating between * markets before the exit. Not a withdrawal timelock — MetaMorpho has * no per-deposit timelock; the `timelock` field is governance-only. */ liquidity: string; /** Human-formatted immediate withdrawable liquidity. */ liquidityFormatted: number; /** Human-formatted immediate withdrawable liquidity in USD. */ liquidityUsd: number; /** Per-market allocation breakdown — which Morpho/Moolah markets the * vault lends into and how much, ordered by weight descending. Populated * by the on-chain Lista (Moolah) path; undefined on paths that don't * surface allocations (API/subgraph/standard on-chain). */ exposures?: VaultMarketExposure[]; } /** * Full parsed payload: per-vault-address map. * * Morpho has many vaults per underlying (one per curator strategy), so * keying by vault address keeps every entry distinct. Callers that want a * per-underlying view can group by `underlying` downstream. */ type MorphoVaults = { /** Keyed by lowercased vault address. */ [vaultAddress: string]: MorphoVault; }; /** * Parsed Silo vault entry. * * Silo vaults ("soToken" / curated vaults) are standalone ERC-4626 yield * vaults that allocate deposits across one or more Silo v2/v3 lending * silos. They are NOT a lending market — no collateral, no borrow, no * liquidation — just a deposit → earn shape, modeled separately from the * per-silo borrow side in `src/lending/public-data/silo-v{2,3}/`. * * The same `api-v3.silo.finance` endpoint serves vaults for both protocol * versions; `protocolVersion` distinguishes them. Multiple vaults may * share an underlying (different curators / strategies), so the map is * keyed by vault address like `MorphoVaults`. */ interface SiloVault extends VaultClassificationFields { /** Vault (share-token) contract address, lowercased. */ address: string; /** Lowercased underlying ERC20 address. */ underlying: string; /** Vault share-token symbol, e.g. `soUSDC`, `soETH`. */ symbol: string; /** Vault share-token name as returned by `name()`. Raw — use * `displayName` for UI. */ name: string; /** Cross-provider UI label — `${curatorName ?? 'Silo'} ${asset.symbol}` * (e.g. `Turtle USDC`). `curatorName` is name-derived (heuristic) — the * indexer exposes only the curator ADDRESS — so this falls back to * `Silo ` when the vault name has no curator prefix. Always * non-empty. */ displayName: string; /** Share and underlying decimals (ERC-4626 keeps them aligned). */ decimals: number; /** Silo protocol version this vault allocates into. */ protocolVersion: 'v2' | 'v3' | string; /** Parent `lendingProtocol.id` from the Silo indexer, lowercased. */ protocolId: string; /** Total underlying assets held by the vault, raw integer as string. * The Silo indexer returns decimal-formatted values; we re-encode to * raw wei for parity with `MorphoVault` / `FluidFToken` / `GearboxV3Pool`. */ totalAssets: string; /** Total shares minted, raw integer as string. */ totalSupply: string; /** Raw `convertToAssets(10**shareDecimals)` — 1 share → X underlying, * asset-scaled. Derived from `totalAssets / totalSupply`. */ convertToAssets: string; /** BASE supply APR in percent, net of the performance fee (Silo's * `userApr`). Interest only — incentives are a separate leg, see * `rewardsRate`. Verified live across the whole book: * `userApr === apr × (1 − performanceFee)` on 16/16 vaults, so this field * carries no rewards. (It was documented as rewards-inclusive for months; * it never was.) */ supplyRate: number; /** Gross pre-fee APR in percent (mirrors Silo's `apr`). Present for * vaults with a non-zero performance fee where `supplyRate < grossRate`. */ grossRate: number; /** Incentive APR in percent — the sum over `rewards` of every LIVE program * whose reward token we could price. `0` means either no live campaign or * none we could value; `rewardsIncomplete` distinguishes the two. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor actually earns. */ depositRate: number; /** Live incentive programs paying this vault's depositors, with * provenance. Absent when none are running. */ rewards?: SiloVaultReward[]; /** `true` when at least one live program could NOT be priced, so * `rewardsRate` is a FLOOR rather than the whole incentive yield. Absent * when every live program was valued (including when there are none). */ rewardsIncomplete?: boolean; /** Performance fee in percent (e.g. `15.0` = 15 %). */ fee: number; /** Timelock for vault CONFIG changes, in seconds — a depositor's notice * period before the curator can change the deal. This is NOT a withdrawal * cooldown: Silo vaults are plain ERC-4626 and a holder's own exit is * never delayed by it. Do not sum or merge the two. */ timelock: number; /** Owner address, lowercased. */ owner?: string; /** Curator address, lowercased — may be absent if not set. */ curator?: string; /** Guardian address, lowercased — may be absent if not set. */ guardian?: string; /** Allocator addresses, lowercased. An allocator reallocates the vault * between silos WITHOUT the timelock, so this is the curation power that * can change a depositor's exposure in the next block. Absent when none * are set. */ allocators?: string[]; /** Fee recipient, lowercased — may be absent. */ feeRecipient?: string; /** Human-readable curator label for UI, derived from the vault's own name * (`curatorNameFromVaultName`) — the Silo indexer exposes only * `curatorId`, an address. Undefined when the name carries no curator * prefix, in which case `displayName` falls back to `Silo `. */ curatorName?: string; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied / returned. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10^decimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD (authoritative value from the * indexer when present). */ totalAssetsUsd: number; /** Currently withdrawable underlying, raw integer as string — reconstructed * as `idle + Σ min(allocation, market.liquidity)` across the vault's silos * and clamped to `totalAssets`. */ liquidity: string; /** Human-formatted immediate withdrawable liquidity. */ liquidityFormatted: number; /** Human-formatted immediate withdrawable liquidity in USD. */ liquidityUsd: number; /** Set to `'instant-capped'` ONLY when no allocation row resolved, so * `liquidity` fell back to `totalAssets` and does not prove a same-block * exit. Left undefined on a proven figure, where the term-sheet builder * decides `instant` vs `instant-capped` from the liquidity itself. */ withdrawalMode?: 'instant' | 'instant-capped'; /** The vault's assets that are currently lent out, raw integer as string — * the allocation-weighted `Σ allocation × marketUtilization`, with idle * counted at zero. Paired with `expectedLiquidity` as the utilization * numerator/denominator. Absent when no allocation resolved. * * Distinct from `liquidity` on purpose: liquidity is capped per market at * that market's cash, utilization is pro-rata. A small position in a deep, * heavily-borrowed market is fully withdrawable AND almost fully lent. */ totalBorrowed?: string; /** Utilization denominator — the vault's total assets, raw integer as * string. Absent when `totalBorrowed` is. */ expectedLiquidity?: string; /** Per-silo allocation breakdown — which Silo markets the vault lends * into and how much, ordered by weight descending. Collateral is the * market's paired (`otherMarket`) input token. Undefined when the * indexer returns no allocation rows. */ exposures?: VaultMarketExposure[]; } /** * One live incentive program paying a Silo vault's depositors, with enough * provenance to say what is being paid, in what, and until when. * * Sourced from the indexer's `incentivesPrograms` root, joined on * `shareTokenId === vault.address`. Only programs that are still emitting * (`emissionPerSecond > 0` and `distributionEnd` in the future) are carried — * an expired campaign is not a yield. */ interface SiloVaultReward { /** Indexer program id, e.g. `-ARB_soETH`. */ programId: string; /** Program label from the indexer. Often just the reward token address. */ name?: string; /** Reward token address, lowercased. */ tokenAddress: string; tokenSymbol?: string; tokenDecimals: number; /** Raw reward-token wei emitted per second, as a string. */ emissionPerSecond: string; /** Unix seconds at which emission stops. */ endsAt: number; /** * This program's APR in percent, computed as * `emissionPerSecond × secondsPerYear × tokenPrice / vaultTvlUsd`. * Absent when the reward token has no price in the supplied map — the * program is real and is reported, but its value is unknown and it is NOT * counted into `SiloVault.rewardsRate`. */ apr?: number; /** * The indexer's own `apr` field, carried VERBATIM and deliberately unused. * * Its unit is unverified: every program in the Silo book is currently * expired or emitting zero, so there is no live figure to reconcile * against, and the two plausible readings — percent (like `vault.apr`) or * fraction (like `market.utilization`) — differ by 100×. Reconcile this * against `apr` on the first live program and then delete the field. * Never render it or sum it. */ indexerApr?: number; } /** Full parsed payload: per-vault-address map. */ type SiloVaults = { /** Keyed by lowercased vault address. */ [vaultAddress: string]: SiloVault; }; /** * Parsed Euler Earn vault entry. * * Euler Earn is the curated-allocator side of Euler V2: standalone ERC-4626 * vaults that route deposits into one or more Euler V2 EVaults via a * curator-defined strategy. They are NOT a lending market — no collateral, * no borrow, no liquidation — just a deposit → earn shape, modeled * separately from the borrow side in `src/lending/public-data/euler/`. * * Multiple earn vaults typically exist per underlying (different curators / * risk profiles), so the top-level map is keyed by vault address rather * than underlying — same convention as `MorphoVaults` and `SiloVaults`. */ interface EulerEarnVault extends VaultClassificationFields { /** Earn vault (share-token) contract address, lowercased. */ address: string; /** Lowercased underlying ERC-20 address. */ underlying: string; /** Vault share-token symbol, e.g. `eUSDC-1`. */ symbol: string; /** Vault share-token name. */ name: string; /** Share-token decimals. Euler Earn meta-vaults can apply a decimals * offset (e.g. 18-dec shares over 6-dec USDC), so this is NOT always * equal to `assetDecimals`. */ decimals: number; /** Underlying asset decimals — frequently differs from `decimals` when * the earn vault uses a decimals offset. Load-bearing for share→asset * formatting (carried through to `VaultLookupEntry`). */ assetDecimals: number; /** Total underlying assets held by the vault, raw integer as string. */ totalAssets: string; /** Total shares minted, raw integer as string. */ totalSupply: string; /** Raw `convertToAssets(10**shareDecimals)` — 1 share → X underlying, * asset-scaled. Derived from `totalAssets / totalSupply`. */ convertToAssets: string; /** Supply APR in percent, net of performance fee. */ supplyRate: number; /** Extra rewards APR in percent, on top of `supplyRate`. * Goldsky doesn't surface rewards directly — defaults to 0. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor actually earns. */ depositRate: number; /** Performance fee in percent (e.g. `5.0` = 5 %). */ fee: number; /** Governance timelock in seconds — the delay before owner/curator config * changes take effect (Euler Earn `governance.timelock`). Governance-only, * NOT a per-deposit withdrawal lock. Mirrors `MorphoVault.timelock`. */ timelock?: number; /** Whether Euler's Data API lists the vault on the Euler frontend * (`visibility.status` of `visible` or `warning`). Vaults failing Euler's * automated config vetting are delisted to `hidden` / `pending_review` * and carry `false` — we still fetch them (the API's `visibility` filter * defaults to listed-only, which emptied this provider overnight in Aug * 2026), so downstream must flag rather than assume listed. Subgraph- * sourced vaults default to `true` (visibility is Data-API-only). * Mirrors `MorphoVault.whitelisted`. */ whitelisted: boolean; /** Raw Euler visibility status (`visible` | `warning` | `hidden` | * `pending_review`), when the Data API supplied one. */ visibilityStatus?: string; /** Owner address, lowercased — may be absent if not set. */ owner?: string; /** Curator / fee-recipient address, lowercased. */ curator?: string; /** Human-readable curator label for UI, derived best-effort from the * (curator-branded) vault name — the subgraph exposes only the address. * Mirrors `MorphoVault.curatorName` so consumers can write generic UI. */ curatorName?: string; /** Guardian address, lowercased — may be absent. */ guardian?: string; /** Fee recipient, lowercased — may be absent. */ feeRecipient?: string; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10^assetDecimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; /** Currently withdrawable underlying, raw integer as string. Computed * as `idle_cash + Σ min(allocated_i, evk_cash_i)` across the Earn * vault's strategies — accounts for per-EVK utilization caps. Falls * back to `totalAssets` (optimistic ceiling) when the EVK index is * unavailable. Never exceeds `totalAssets`. */ liquidity: string; /** Human-formatted immediate withdrawable liquidity. */ liquidityFormatted: number; /** Human-formatted immediate withdrawable liquidity in USD. */ liquidityUsd: number; } /** Full parsed payload: per-vault-address map. */ type EulerEarnVaults = { /** Keyed by lowercased vault address. */ [vaultAddress: string]: EulerEarnVault; }; /** * Parsed TermMax curated vault entry. * * TermMax vaults are the CONTINUOUS earn side of a fixed-maturity protocol. * The per-market lend position (buying FT) expires; a vault holds a rolling * book of orders across many maturities and rolls them for the depositor, so * from the outside it behaves like an ordinary ERC-4626 yield vault. Same * relationship Fluid has between its per-vault borrow markets and its fTokens. * * Multiple vaults exist per underlying with different curators (Keyrock, MEV * Capital, Origami and TermMax itself all curate), so the map is keyed by * VAULT ADDRESS — * the `MorphoVaults` / `SiloVaults` / `EulerEarnVaults` convention, not the * key-by-underlying convention Fluid and Gearbox use. * * IMPORTANT — vault assets and lender-side depth are the SAME capital. A * vault's deposits are what appear as lend-side order depth in the TermMax * lender data, so summing "TermMax lender TVL + TermMax vault TVL" double * counts. */ interface TermMaxVault extends VaultClassificationFields { /** Vault (share-token) contract address, lowercased. */ address: string; /** Lowercased underlying ERC-20 address (the markets' debt token). */ underlying: string; /** Vault share-token symbol, e.g. `TMKR-RLUSD`. */ symbol: string; /** Vault share-token name, e.g. `Coinshift rlUSD vault`. */ name: string; /** Share-token decimals. */ decimals: number; /** Underlying asset decimals — read on-chain, may differ from `decimals`. */ assetDecimals: number; /** Total underlying assets held, raw integer as string. */ totalAssets: string; /** Total shares minted, raw integer as string. */ totalSupply: string; /** Raw `convertToAssets(10**shareDecimals)` — 1 share → X underlying. */ convertToAssets: string; /** * Supply APR in percent, **net of the performance fee**. * * TermMax's own formula (`OrderManagerV2`): * `annualizedInterest · (1e8 − performanceFeeRate) / accretingPrincipal` * `annualizedInterest` is GROSS — the fee is taken out of it on accrual * (`_accretingPrincipal += interest − performanceFeeToCurator`), so it must * be netted here to match the README's "net of performance fee" convention. * * Served directly by the API. The on-chain fallback derives it, because * `apr()` AND `accretingPrincipal()` both revert on the deployed 2.0.0 * vaults — it substitutes `totalAssets` as the denominator, which is larger * and so understates rather than overstates. The derivation was validated * against the API on both funded Ethereum vaults: 2.579% and 1.449%, * matching to 3 decimals. */ supplyRate: number; /** Extra rewards APR (TMX emissions). 0 on the on-chain fallback, which * cannot see them. */ rewardsRate: number; /** `supplyRate + rewardsRate` — what a depositor actually earns. */ depositRate: number; /** Performance fee in percent (e.g. `10` = 10%). */ fee: number; /** Governance timelock in seconds for curator/guardian config changes. */ timelock?: number; /** Curator address, lowercased. */ curator?: string; /** * Human-readable curator label. * * ONLY the API carries this — the on-chain surface exposes an address and * nothing else. Do NOT derive it from `name`: the vault literally called * "Coinshift rlUSD vault" is curated by **Keyrock**. */ curatorName?: string; /** Guardian address, lowercased. */ guardian?: string; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** `totalAssets / 10^assetDecimals`. */ totalAssetsFormatted: number; /** `totalAssetsFormatted * priceUsd`. */ totalAssetsUsd: number; /** * Immediately withdrawable underlying, raw integer as string. * * A TermMax vault's capital is committed to maker orders until each order's * maturity, so only part is instantly exitable — the rest needs the curator * to unwind or a maturity to roll. Sourced from the API's `redeemableAmt` * (falling back to `idleFunds`), or the vault's own underlying balance * on-chain. Deliberately NOT `totalAssets`. */ liquidity: string; liquidityFormatted: number; liquidityUsd: number; /** Contract `getVersion()` / API `version`, e.g. `"2.0.0"` / `"v2"`. */ version?: string; /** Vault is paused — deposits blocked, existing funds still visible. */ isPaused?: boolean; /** Deposit cap in underlying base units (API `capacity`). */ supplyCap?: string; /** * The vault's ERC-4626 base-yield pool for idle funds (v2_01 "composable * base yield"). Idle capital earns here instead of sitting dead. */ basePool?: string; } /** Full parsed payload: per-vault-address map. */ type TermMaxVaults = { /** Keyed by lowercased vault address. */ [vaultAddress: string]: TermMaxVault; }; /** * How you actually get out — as a LIST, not a label. * * `withdrawalMode` names the SHAPE of an exit, and for half our modes that * shape is two different things at once. `fee-or-queued` means "pay a fee and * leave now, OR wait and leave cheaply", and the two legs disagree on every * number a holder cares about: the fee, the wait, the minimum size, and how * much can go through at all. Collapsing that into one mode string plus one * `withdrawalCooldownSeconds` plus one `withdrawFeeBps` forces every consumer * to re-derive the split — and they get it wrong in opposite directions, * because the fee belongs to one leg and the cooldown to the other. * * It is not a Treehouse problem. `fee-or-queued` is shared by Puffer pufETH, * b14g dualCORE, PrimeStaking psXDC, Native wNLP and the Treehouse tAssets; * `instant-or-queued` by rETH, weETH, beHYPE and more. So the routes are * derived generically from what a row already publishes, and a provider that * knows better — because it reads its own exit modules — overrides them with * live values. * * The invariant: **every row publishes at least one route**, and the union of * the routes is the whole truth about leaving. */ /** What settling on this route costs you in TIME. */ type VaultExitRouteKind = /** Same block. May still be capped by `capacity`. */ 'instant' /** Request now, claim later — `waitSeconds` if the protocol pins one. */ | 'queued' /** No redemption: you sell the instrument to somebody. */ | 'market'; /** * One way out of a position. Amounts are RAW integer strings in the * **underlying** asset's units, matching `liquidity` / `totalAssets` on the * row; the `*Formatted` twins are the same numbers scaled by its decimals. */ interface VaultExitRoute { /** Stable slug, unique within the row. Consumers may key on it. */ id: string; kind: VaultExitRouteKind; /** Short human label — `Instant`, `7-day queue`, `Sell on the market`. */ label: string; settlement: 'sync' | 'async' | 'market'; /** * Cost of taking THIS route, in basis points of the payout. `0` is a real * answer meaning free; `undefined` means the protocol publishes no fee for * it (a market route's cost is price impact, not a fee). */ feeBps?: number; /** * `true` when this leg definitely charges a fee but the row does not * publish its size — the mode itself guarantees the fee exists * (`fee-or-queued` means the instant leg is the paying one), while the * number is only known to providers that read their own dials. * * Without it an absent `feeBps` renders as "leave instantly", which reads * as FREE — the exact failure the term-sheet rules call out for `fees: []`. */ feeUnknown?: boolean; /** Seconds between requesting and being able to claim. Absent when the wait * is a queue with no pinned duration (Lido's validator exit). */ waitSeconds?: number; /** * Smallest size this route accepts, RAW underlying. The field that decides * whether a cheap leg is reachable at all: Treehouse's 5 bps queue has a * 50 wstETH floor, so almost every holder can only take its 0.5 % instant * leg. Absent ⇒ no minimum. */ minAmount?: string; minAmountFormatted?: number; /** * Most that can settle through this route RIGHT NOW, RAW underlying. * Absent ⇒ unbounded (a queue is not capped by inventory). A present `'0'` * means the route exists but cannot serve anything this block. */ capacity?: string; capacityFormatted?: number; capacityUsd?: number; /** `true` when the entrypoint pays `msg.sender` only, i.e. it cannot be * composed to credit somebody else. */ selfOnly?: boolean; /** One sentence, when the route needs one to be honest. */ description?: string; } /** * Validator / delegation dataset for LST deposits. * * A few LSTs require (or allow) the depositor to pick the validator, * validator-group, node, or pool the staked asset is delegated to — unlike * the pooled majority (Lido, Rocket Pool, EtherFi, …) where the protocol * decides. This module is the single source of truth for: * * 1. **the descriptor** ({@link LstDelegation}) — *whether* a selection is * needed, the deposit option key to send the choice back as, whether a * safe default exists, and where the list comes from; and * 2. **the live set** ({@link LstValidator}) — the selectable validators with * status + capacity, normalised across protocols. * * The descriptor mirrors `acceptedInputs` (the pay-asset accept-set): a * machine-readable answer to "do I have to pick a validator, and from where". * Both are attached to each LST by the vault fetcher (`fetchLstShareTokens`). */ type LstDelegationKind = 'validator' | 'validatorGroup' | 'node' | 'pool' | 'vault'; /** Descriptor of an LST's validator-selection requirement. */ interface LstDelegation { /** The integrator MUST supply a choice — no safe auto/default. (Core, * Solv.) When `false`, omitting the choice still produces a working tx * via `default` or server-side auto-resolution (Celo, Lair, TruFin). */ required: boolean; /** What kind of target is being chosen. */ kind: LstDelegationKind; /** The deposit option query param to send the chosen `id` back as * (e.g. `validator`, `validatorGroup`, `node`, `poolId`, `vault`). */ optionKey: string; /** A working default `id` when `required` is false; `'auto'` when the * server resolves one (Celo); `null` when the protocol picks internally * or a value is mandatory. */ default?: string | null; /** `endpoint` ⇒ the live set is on-chain and returned in `validators`; * `offchain` ⇒ the value comes from the protocol's docs/API (Solv * poolId), so `validators` is empty. */ source: 'endpoint' | 'offchain'; } /** A normalised, selectable delegation target. `id` is the opaque value to * pass back as the deposit option named by {@link LstDelegation.optionKey}. */ interface LstValidator { /** Value to pass back to the deposit endpoint (validator / group / node * address, or pool id). */ id: string; /** Operational status. Only `active` targets are `selectable`. */ status: 'active' | 'inactive' | 'jailed' | 'full'; /** Passes the protocol's eligibility (healthy + not blocked + has room). */ selectable: boolean; /** Our default pick (best capacity / health) — lets a UI preselect. */ recommended: boolean; /** Human label, when the target has one (e.g. the Solv pool's pay currency * `"WBTC"`). Absent for bare validator addresses. */ name?: string; /** For `kind:'pool'` (Solv): the pay-currency address this pool subscribes * with — the deposit's `payAsset` must match it. */ currency?: string; /** Remaining stake the target can absorb, raw underlying as string. * Populated where the protocol exposes it (Celo `getReceivableVotesForGroup`). */ receivableVotes?: string; } /** The delegation descriptor for an LST, or `undefined` when it is pooled * (no validator selection — the vast majority). */ declare const getLstDelegation: (chainId: string, shareToken: string) => LstDelegation | undefined; /** * Fetch the live, selectable validator set for an LST, normalised across * protocols. Returns `[]` for pooled LSTs, off-chain selections (Solv), or * when the on-chain read fails (best-effort, never throws). */ declare const getLstValidators: (chainId: string, shareToken: string) => Promise; /** * Withdrawal exit mode for an LST. Drives downstream UX: queued LSTs * need a withdrawal-request flow, instant ones can burn-to-asset right * away, off-chain ones must be routed through the issuer or a DEX. * * See LST_WITHDRAWAL_QUEUES.md for the per-LST mechanism details. */ type LstWithdrawalMode = 'queued' | 'fixed-cooldown' | 'instant-or-queued' | 'fee-or-queued' | 'off-chain' | 'dex-only'; /** * Parsed LST share-token entry. * * LSTs are protocol-issued staking-share tokens (Lido stETH, Rocket * Pool rETH, EtherFi weETH, …). Some are real ERC-4626 vaults * (pufETH), most use a custom share/exchange-rate interface. * * Modeled to mirror the existing vault provider shapes (`MorphoVault`, * `FluidFToken`, `EulerEarnVault`) so the cross-provider lookup picks * them up without branching. The required cross-provider fields * (`address`, `underlying`, `symbol`, `name`, `decimals`, `totalAssets`, * `totalSupply`) match `buildVaultLookup`'s structural constraint — * extending this type while dropping one of them will break the * compile. */ interface LstShareToken extends VaultClassificationFields { /** Share-token contract address, lowercased. */ address: string; /** Underlying asset address, lowercased. `zeroAddress` for LSTs whose * underlying is native ETH (most ETH LSTs). For pufETH the * underlying is wstETH. */ underlying: string; /** Share-token symbol, e.g. `wstETH`, `weETH`, `rETH`. */ symbol: string; /** Share-token name as returned by `name()`, or a falsy-safe fallback * composed from the brand + symbol. */ name: string; /** Cross-provider UI label — `${brand} ${symbol}` (e.g. `Lido wstETH`, * `EtherFi weETH`). Always non-empty. */ displayName: string; /** Brand label — `Lido`, `EtherFi`, `Rocket Pool`, … */ brand: string; /** Alias for `brand` to match `curatorName` on the other providers' * vault types — keeps cross-provider UI code uniform. */ curatorName: string; /** Share decimals. */ decimals: number; /** Underlying asset decimals. Equals `decimals` for the vast majority * of LSTs (share and underlying share a decimal count); diverges for * e.g. 18-dec SolvBTC over 8-dec WBTC, or 8-dec bridged pumpBTC over * 18-dec SolvBTC.CORE. Load-bearing for share→asset formatting and the * cross-provider `sharePrice` (parity with Lagoon/Yearn). */ assetDecimals: number; /** Total underlying held, raw integer in **underlying** units as string. * Computed as `totalSupply * exchangeRate / 1e18` (rescaled to the * underlying decimals) when the share token does not expose a direct * `totalAssets()`. */ totalAssets: string; /** Total shares minted, raw integer in **share** units as string. */ totalSupply: string; /** Raw `convertToShares(10**assetDecimals)` — 1 underlying → X shares, * in raw share units. */ convertToShares: string; /** Raw `convertToAssets(10**decimals)` — 1 share → X underlying, in raw * underlying units. */ convertToAssets: string; /** 1e18-scaled exchange rate: value of 1 whole share in whole underlying * (decimal-agnostic). The canonical pricing primitive — * `convertToAssets`/`convertToShares` are derived from it together with * the share/underlying decimals. */ exchangeRate: string; /** Supply APR in percent (e.g. `3.12` = 3.12 %). Sourced from the * matching intrinsic-yield fetcher. */ supplyRate: number; /** Extra rewards APR in percent — usually 0 for LSTs; populated only * when the protocol layers a separate reward stream on top. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor actually * earns. */ depositRate: number; /** True if the share token is a real ERC-4626. Drives whether the * calldata builder uses `deposit(uint256,address)` or a * protocol-specific entry. */ isErc4626: boolean; /** True if the share token rebases (e.g. stETH, eETH). Most LSTs * covered here are non-rebasing wrappers (wstETH, weETH). */ isRebasing: boolean; /** False when the share token cannot be minted permissionlessly * on-chain — e.g. cbETH (Coinbase off-chain mint). */ isMintable: boolean; /** True if the mint entry accepts native ETH (`payable`). */ isNativeUnderlying: boolean; /** Mint entry contract address, lowercased. Undefined when * `isMintable === false`. */ mintContract?: string; /** Input asset for the mint call. `native` for `payable` entries, * otherwise a lowercased ERC-20 address. Undefined when not * mintable. The primary input; see `acceptedInputs` for the full set. */ mintInputAsset?: 'native' | string; /** * Full set of pay assets the mint accepts, with each path's shape and any * required options — the machine-readable answer to "what can I pay with". * Sourced from calldata-sdk's `LST_INPUT_CAPABILITIES`; falls back to a * single entry derived from `mintInputAsset` for kinds with no alternatives. * Empty when `isMintable === false`. Use this to drive the deposit-action * request (`payAsset` + the per-path `needs` options). */ acceptedInputs: LstAcceptedInput[]; /** Validator-selection descriptor — present only for LSTs that require or * allow the depositor to pick a validator / group / node / pool the stake * is delegated to (Core, Celo, Solv, …). Absent for pooled LSTs (the * majority). Mirrors `acceptedInputs` — the machine-readable "do I have to * choose a validator, and from where". See `validators`. */ delegation?: LstDelegation; /** The live, selectable validator set for `delegation.optionKey`, when the * descriptor's `source` is `endpoint`. Each `id` is the value to pass back * as the deposit option. Empty/absent for pooled or off-chain-selected * (Solv poolId) LSTs. */ validators?: LstValidator[]; /** Withdrawal mechanism — see `LstWithdrawalMode`. */ withdrawalMode: LstWithdrawalMode; /** Fixed cooldown in seconds for `fixed-cooldown` mode. Undefined * otherwise. Queue-finalization protocols (Lido, EtherFi, …) have * no fixed delay — their wait depends on the validator-exit queue. */ withdrawalCooldownSeconds?: number; /** * Fee on the INSTANT exit leg, in basis points — the mirror of * `SavingsVault.withdrawFeeBps`, which the LST rows were missing entirely. * It belongs to the fee-paying leg only: on `fee-or-queued` the queued leg * settles at par and does not charge it. Absent when the protocol * publishes none. */ withdrawFeeBps?: number; /** * **Every way out, enumerated** — see `VaultExitRoute`. * * `withdrawalMode` names the shape; for `fee-or-queued` and * `instant-or-queued` that shape is two routes with different fees, waits, * minimums and capacities, and a consumer cannot recover the split from the * mode string plus one cooldown plus one fee. This is that split, always * populated (derived generically from the row, overridden with live values * by providers that read their own exit modules). */ exitRoutes: VaultExitRoute[]; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10**decimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; /** Instantly-withdrawable underlying right now, raw integer as string — * **always populated** (parity with the 4626 vault providers). It is the * amount a holder of all shares could redeem on-chain this block: * - a protocol redemption buffer where one is exposed (Rocket Pool * `RocketDepositPool.getBalance()`, Beets stS `totalPool()`); * - the vault's idle underlying for instant-capable ERC-4626 LSTs; * - `0` for queue / fixed-cooldown / dex-only / off-chain LSTs, which * have no same-block on-chain redemption (the wait *is* the exit). * Like the vault `liquidity`, it's a right-now estimate, not a guarantee. */ liquidity: string; /** Human-formatted instant-exit liquidity (`liquidity / 10**decimals`). */ liquidityFormatted: number; /** Human-formatted instant-exit liquidity in USD. */ liquidityUsd: number; } /** * Full parsed payload: per-share-token-address map. * * Keyed by lowercased share-token address (parity with Morpho / Silo / * Euler-Earn). LSTs do not naturally key by underlying — many LSTs * share `zeroAddress` (native ETH) as their underlying, so the * underlying-keyed shape used by Fluid/Gearbox would collide. */ type LstShareTokens = { [shareAddress: string]: LstShareToken; }; /** * Normalised shape for a single pending withdrawal request, uniform * across all LST protocols regardless of underlying mechanism * (fixed-cooldown vs queue-finalization vs ERC-7540). * * Consumers (UI, worker-api) treat the list as a single bucket; * protocol-specific fields like `queuePosition` are populated when * available and ignored otherwise. * * See [../LST_WITHDRAWAL_QUEUES.md](../LST_WITHDRAWAL_QUEUES.md) for * the per-protocol enumeration ABI and the * [GAPS.md](../../GAPS.md) section A for the design intent. */ interface LstWithdrawalRequest { /** LST share-token address the request was made against. */ lst: string; /** Brand label — `Lido`, `EtherFi`, `Renzo`, … */ brand: string; /** Symbol — `wstETH`, `weETH`, `ezETH`, … */ symbol: string; /** Protocol-native request identifier (NFT tokenId, queue index, * ERC-7540 requestId, …). Encoded as a string for cross-protocol * uniformity. */ requestId: string; /** Raw amount the request will return on claim, in the token that * escrow actually pays out. Wei-like integer string. Some protocols * only know this at claim time (queue-finalization with a floating * finalization rate) — those surface the **expected** amount at * request time. * * Strata is the one entry where the denomination is not simply the * vault's underlying, and it is easy to get wrong: the escrow is * **keyed** by the collateral token (`finalize(sUSDe, user)`) but the * amount it records is whatever that leg settles in — the tranche's * `asset()` (USDe) on the UnstakeCooldown, which books Ethena's * unstake output, and collateral-token shares on the ERC20Cooldown. * We do not normalize between them; read `withdrawQueue` to tell the * legs apart. Fork-verified for the UnstakeCooldown leg 2026-08-04 * (10,000 USDe in → 9,997.5 USDe out at a 2.49 bps exit fee, with * zero sUSDe paid); the ERC20Cooldown denomination is read off the * strategy source, which escrows `sUSDe.previewWithdraw(baseAssets)` * shares. */ amountUnderlying: string; /** Raw share amount of the request, for protocols whose claim call * takes shares (ERC-7540 `redeem`, sUSD3's plain 4626 `redeem`). * Passed back verbatim into the claim builder. */ shares?: string; /** The escrow contract this request actually lives on, when the * protocol runs more than one and the registry's default is not * necessarily the right claim target. Strata gives each market both * an `UnstakeCooldown` (base-asset leg) and an `ERC20Cooldown` * (collateral-token leg) — a claim built against the wrong one is a * no-op — so the reader reports which. Passed back verbatim into * the claim builder. */ withdrawQueue?: string; /** The token the escrow books this request under, when the claim * call takes it as an argument (Strata's * `finalize(claimToken, user)`). Passed back verbatim. */ claimToken?: string; /** Status discriminator. */ status: LstWithdrawalStatus; /** Unix seconds when the request becomes claimable. Set for * fixed-cooldown protocols where the deadline is deterministic. * Unset for queue-finalization protocols (use `queuePosition` + * `etaSeconds` instead) and ERC-7540 (use `status` directly). */ readyAt?: number; /** Unix seconds when the claim window closes. Currently only used * by sAVAX (15-day cooldown + 2-day claim window). After this * timestamp the request transitions to `expired` and the unlock * is forfeited unless admin recovery is triggered. */ expiresAt?: number; /** Position in the protocol's queue, relative to the finalization * frontier. Set for queue-finalization protocols. Positive values * mean "still pending"; zero or negative means "finalized". */ queuePosition?: number; /** Off-chain estimate of seconds-to-claim. Only set when the * orchestrator received a finalization-rate sample (TODO: not * yet wired). Frontier-based protocols can show this when * available. */ etaSeconds?: number; } /** Status discriminator for a withdrawal request. */ type LstWithdrawalStatus = /** Submitted; not yet claimable. */ 'pending' /** Ready to claim now. */ | 'claimable' /** Already claimed (typically pruned from the list — kept for * protocols that surface historical entries via the same getter). */ | 'claimed' /** Window expired without being claimed. Only meaningful for * sAVAX-style protocols with a finite claim window. */ | 'expired'; /** Withdrawal-reader implementation kind — drives which enumeration * function the user is queried against. */ type LstWithdrawalReaderKind = 'lidoQueue' | 'etherfiNft' | 'yieldNestNft' | 'staderEthxQueue' | 'staderMaticXQueue' | 'renzoQueue' | 'kelpQueue' | 'benqiSavaxQueue' | 'beetsStSQueue' | 'hyperbeatBeHype7540' | 'erc7540' | 'binanceWbethQueue' | 'ethenaCooldown' | 'susd3Cooldown' | 'strataCooldown' | 'swellNft' | 'stakeWiseSubgraph' | 'mantleCallerSuppliedIds' | 'pufferCallerSuppliedIds' | 'trufinCallerSuppliedIds' | 'lairCallerSuppliedIds' | 'stceloAccountQueue' | 'kinetiqQueue' | 'beHypeQueue' | 'valantisBurnQueue' | 'listaQueue' | 'iberaQueue' | 'primeStakingQueue' | 'berapawRedeemQueue' | 'treehouseRedemptionQueue' | 'eventsOnly' | 'noQueue' | 'unverified'; /** Map keyed by lowercased LST share-token address. The orchestrator * fetches all LSTs on a chain in parallel and returns this map * (possibly with empty arrays for LSTs the user has no requests * against). */ type LstWithdrawalRequestsByLst = { [lstAddress: string]: LstWithdrawalRequest[]; }; /** * Static registry entry for an LST's withdrawal-request enumeration * surface. * * Each entry pins: the LST share-token address, the brand/symbol * (for output labelling), the reader kind, and any auxiliary contract * addresses the reader needs (the withdrawal-manager NFT/queue * contract, Polygon stake manager for MaticX, …). * * Mirrors [LST_WITHDRAWAL_QUEUES.md](../LST_WITHDRAWAL_QUEUES.md); * read that for per-protocol semantics. */ interface LstWithdrawalRegistryEntry { /** LST share-token address (matches `address` in * `../registry.ts → LST_REGISTRY`). Lowercased. */ lst: string; /** Brand label. */ brand: string; /** Symbol — used to label the per-asset request list in UI. */ symbol: string; /** Reader kind. */ reader: LstWithdrawalReaderKind; /** Optional withdrawal contract — separate from the LST share token * for most protocols. Lowercased. * - Lido: WithdrawalQueueERC721 * - EtherFi: WithdrawRequestNFT * - YieldNest: WithdrawalQueueManager * - Stader ETHx: UserWithdrawalManager * - Stader MaticX: the home contract itself * - Renzo: WithdrawQueue * - BENQI sAVAX: the sAVAX contract itself * - Beets stS: the SonicStaking proxy itself * - Hyperbeat hbHYPE: the share token itself (ERC-7540 surface) */ withdrawalContract?: string; /** Polygon IStakeManager — only for `staderMaticXQueue`. The * finalization check requires `epoch()` + `withdrawalDelay()`. */ polygonStakeManager?: string; /** Second escrow contract probed with the same reader — only for * `strataCooldown`, where a market runs both an `UnstakeCooldown` * (base-asset leg, in `withdrawalContract`) and an `ERC20Cooldown` * (collateral-token leg). Lowercased. */ secondaryWithdrawalContract?: string; /** The token an escrow's requests are booked under — only for * `strataCooldown` (`balanceOf(escrowToken, user)` / * `finalize(escrowToken, user)`). The market's staked collateral * (sUSDe, sNUSD, mHYPER, …), NOT the tranche's `asset()`. * Lowercased. */ escrowToken?: string; } /** Returns the withdrawal-registry entries for a chain, or `[]`. */ declare const getLstWithdrawalRegistry: (chainId: string, extraEntries?: LstWithdrawalRegistryEntry[]) => LstWithdrawalRegistryEntry[]; /** Composite withdrawal id for protocols where the natural unit is a * `(validator, nonce)` pair (e.g. TruFin TruMATIC), in addition to * the standard `bigint` id used by everything else. */ type LstWithdrawalKnownId = bigint | { validator: string; nonce: bigint; }; /** Per-LST list of caller-known withdrawal IDs, keyed by lowercased * LST share-token address. Caller (frontend / worker-api) captures * these from the withdraw tx receipt at request time and passes the * accumulated list back on subsequent reads. * * See `INDEXING_STRATEGIES.md` — "Caller-supplied IDs" section — for * why this is the universal log-free fallback. */ type LstWithdrawalKnownIds = { [lstAddress: string]: LstWithdrawalKnownId[]; }; /** Optional knobs for the orchestrator. */ interface LstWithdrawalFetchOptions { /** Per-LST caller-supplied IDs. Required for protocols whose * readers are `callerSupplied`-shaped (Mantle, Puffer, TruFin) — * those readers return `[]` when no IDs are supplied. Ignored by * protocols with on-chain enumeration. Keyed by lowercased LST * share-token address. */ knownIds?: LstWithdrawalKnownIds; /** Caller-supplied registry entries appended to the static registry. * Lets the orchestrator read withdrawal requests for vaults not in * the hardcoded LST allowlist — e.g. an arbitrary ERC-7540 vault * surfaced by the public-data lookup. Each entry needs at least * `{ lst, brand, symbol, reader }`; the `erc7540` reader needs only * the address. De-duped against the static registry (static wins). */ extraEntries?: LstWithdrawalRegistryEntry[]; } /** * Fetch a user's pending LST withdrawal requests across all LSTs on * a chain. * * One reader-`fetch` invocation per registered LST, all running in * parallel via `Promise.all`. Per-reader failures (RPC error, * malformed response, unsupported protocol) are logged and produce * an empty array — they don't sink the whole call. * * Returns a map keyed by lowercased LST share-token address. * Empty-array entries are kept so callers can render "no pending * requests" UX uniformly. * * For protocols with no user-keyed on-chain storage and no public * subgraph (Mantle mETH, Puffer pufETH, TruFin TruMATIC), pass * `options.knownIds[lstAddress]` — the list of withdrawal IDs the * caller captured at request time. See `INDEXING_STRATEGIES.md` * "Caller-supplied IDs" section. * * Note: the orchestrator runs N parallel multicalls (one per LST, * possibly with internal sub-stages). Per-chain consolidation is not * yet implemented — see GAPS.md section F for the optimization * trade-off. */ declare const getLstWithdrawalRequests: (user: string, chainId: string, multicallRetry: MulticallRetryFunction, options?: LstWithdrawalFetchOptions) => Promise; /** * Alias for {@link getLstWithdrawalRequests}. The orchestrator is no * longer LST-specific — with `options.extraEntries` it reads withdrawal * requests for any vault (e.g. arbitrary ERC-7540) — so this name reads * better at non-LST call sites. Same signature, same behaviour. */ declare const getVaultWithdrawalRequests: (user: string, chainId: string, multicallRetry: MulticallRetryFunction, options?: LstWithdrawalFetchOptions) => Promise; interface CoreValidators { /** Currently-elected validators — the set earning rewards this round. * Prefer these for `options.validator` on stCORE / SCORE mints. */ validators: Address[]; /** All registered candidates (delegatable superset of `validators`). */ candidates: Address[]; } /** * Fetch the live Core validator + candidate operator addresses on-chain. * * Rotates through the configured Core RPCs (`maxRpcTries`) so a single * flaky endpoint doesn't fail the read. Throws only if every RPC fails. */ declare const getCoreValidators: (maxRpcTries?: number) => Promise; interface StCeloValidatorGroup { /** Validator-group address (checksummed) to pass as the deposit's * `options.validatorGroup`. */ group: Address; /** Receivable-vote capacity, raw CELO (wei) as string. The amount of new * stake this group can still absorb before it overflows. */ receivableVotes: string; } /** * Fetch the currently-depositable StakedCelo validator groups on-chain, * ranked by remaining capacity (descending). * * Rotates through the configured Celo RPCs (`maxRpcTries`) so one flaky * endpoint doesn't fail the read. Throws only if every RPC fails. */ declare const getStCeloValidatorGroups: (maxRpcTries?: number) => Promise; /** StakedCelo `Manager` on Celo mainnet (42220), lowercased. */ declare const STCELO_MANAGER_ADDRESS = "0x0239b96d10a434a56cc9e09383077a0490cf9398"; /** * Decide which validator group (if any) a stCELO deposit should set via * `changeStrategy` before `deposit()` — the value to pass as the builder's * `options.validatorGroup`. Resolution order: * * 1. An explicit `requestedGroup` is honoured verbatim (the zero address * means "use the default strategy" → returns `undefined`, no * `changeStrategy`). `changeStrategy` validates eligibility on-chain. * 2. Otherwise the caller's current strategy is read: if it is already a * specific group (`getAddressStrategy != 0`, which the Manager keeps * non-zero only while the group stays healthy), the plain `deposit()` * works and `undefined` is returned. * 3. If the caller is on the **default** strategy — which currently reverts * `Not validator group` on mainnet — the best-capacity eligible group is * returned so the deposit is routed somewhere valid. * * Best-effort: returns `undefined` if the chain reads fail, leaving the * deposit unchanged rather than throwing. */ declare const resolveStCeloDepositGroup: (user: Address, requestedGroup?: string, maxRpcTries?: number) => Promise
; /** * Term sheets — a structured, per-`marketUid` description of every lend and * borrow offer we serve. * * One shape for pool lenders, fixed-term lenders, CDPs and vaults, so an * integrator reads ONE object instead of ~15 protocol-specific fields plus a * per-lender mental model. See [TERM_SHEET_PLAN.md](../../../../TERM_SHEET_PLAN.md) * for the design rationale and the per-lender coverage matrix. * * ## Conventions that hold everywhere in this file * * - **Rates are nominal APR in PERCENT** (`3.85` = 3.85 %/yr), never a * fraction and never an APY. This is the package-wide convention. * - **Factors are fractions** (`0.85` = 85 % LTV) — matching `LenderConfigData`. * - **Durations are SECONDS**, timestamps are **unix seconds**. * - **Raw amounts are decimal strings** in base units; human amounts are * `number`. * - Every string union is deliberately OPEN (`| (string & {})`) so a new * lender can introduce a member without breaking a consumer's exhaustive * switch. Consumers MUST have a `default` branch and fall back to * `info.headline`. */ /** Nominal APR in percent (`3.85` = 3.85 %/yr). Never a fraction, never APY. */ type AprPercent = number; /** * Open-enum helper. `Open<'a' | 'b'>` keeps autocomplete for the known members * while still accepting any string, so adding a member later is an ADDITIVE * change rather than a breaking one. */ type Open = T | (string & {}); /** Compact token reference — enough to render without a second lookup. */ interface TermAssetRef { chainId: string; /** Lowercased contract address. */ address: string; symbol?: string; name?: string; decimals?: number; assetGroup?: string; logoURI?: string; } /** * Machine tags for filtering/faceting. DERIVED from the structured fields in * `tags.ts` — never hand-written per lender, so they cannot drift from the * numbers they summarize. */ type TermTag = Open<'fixed-rate' | 'variable-rate' | 'user-set-rate' | 'zero-interest' | 'prepaid-interest' | 'nav-accrual' | 'has-maturity' | 'perpetual' | 'rolling-duration' | 'static-debt' | 'accruing-debt' | 'time-liquidation' | 'price-liquidation' | 'redeemable' | 'no-liquidation' | 'full-collateral-seizure' | 'early-exit-free' | 'early-exit-penalty' | 'early-exit-discount' | 'exit-instant' | 'exit-capped' | 'exit-cooldown' /** * The DEBT cannot be repaid for a period after opening or topping up. * * Deliberately distinct from `exit-cooldown` (a supply-side withdrawal * delay): this one means the position cannot be closed, deleveraged or * migrated out — and, on the lender that has it, that an incoming * liquidation cannot be averted by repaying. */ | 'repay-locked' | 'exit-queued' | 'exit-market-sale' | 'exit-may-be-impossible' | 'permissioned' | 'capped' | 'cap-full' | 'first-loss' | 'socialized-loss' | 'physical-delivery' | 'undercollateralized' | 'nav-attested' | 'immutable' | 'no-timelock' | 'eoa-controlled' | 'points-rewards' /** * The headline yield is MOSTLY the asset's own (staking / RWA / savings) * yield, not interest this market pays. Set at >= 50 % of `aprTotal`. * * Worth a tag rather than only a detail row, because the two are not * interchangeable and the difference is invisible in a single percentage: a * market rate is paid by borrowers and moves with utilization, while * intrinsic yield you would earn holding the asset in your wallet — you are * taking this market's risk for the DIFFERENCE, not for the headline. * Decisive when looping: a loop only carries if the collateral out-earns the * debt, and intrinsic yield does not scale with leverage the way a lending * spread does. */ | 'intrinsic-yield' /** * This market pays NO interest of its own — the entire headline is intrinsic * yield and/or rewards. Curvance's collateral-only legs are the canonical * case (`debtCap == 0` ⇒ 0 % supply APR by construction), but any market * with no borrowers reads the same way. */ | 'no-market-interest' | 'oracle-flagged' | 'no-oracle'>; /** Human-facing copy for one side of a term sheet. */ interface TermInfo { /** * ≤ ~100 chars, templated from live numbers, ready to render. * `"Fixed 4.12 % until 3 Sep 2026 · repay any time at face value"`. * * ALWAYS populated — it is the graceful-degradation path for a consumer * that does not recognise a newer enum member. */ headline: string; /** 1–3 sentences. Invariant prose lives on the profile; this interpolates * the market's own values. */ description: string; /** Ready-to-display consequences, most important first. */ implications?: string[]; tags: TermTag[]; } type RateKind = Open< /** Utilization IRM (Aave, Compound, Morpho, Silo, Euler, Fluid…). */ 'variable-curve' /** Governance-set with no curve (USDD stability fee, Spark `vsr`). */ | 'variable-managed' /** Borrower picks the rate (Liquity family). */ | 'user-set' /** Locked for a maturity (Exactly, Midnight, Term, TermMax, Teller, Lista). */ | 'fixed-term' /** Locked, no maturity. Reserved — nothing uses it today. */ | 'fixed-open' /** No ongoing rate at all (River); cost is a one-off fee. */ | 'zero-interest' /** Interest prepaid in a separate token (Inverse DBR). */ | 'prepaid' /** Share price tracks an attested NAV (Re, Apyx, USPC). */ | 'nav-accrual' /** * There is no rate MECHANISM at all — the number is the realized result of a * trading or market-making book over a past window (GMX GM/GLV, HyperCore * vaults). * * Deliberately distinct from both `variable-*` (which implies something sets * the rate, and that it is a rate) and `none` (which means no yield). A * realized return is not a promise about the future in any degree, and the * principal that produced it can fall — see `principal.risks`. */ | 'realized' /** Pure collateral leg — no yield. */ | 'none'>; /** One reward program. Identity matters: points are not a bankable APR. */ interface RewardTerm { /** Absent for points programs — that absence IS the signal. */ asset?: TermAssetRef; kind: Open<'token' | 'points' | 'unknown'>; apr: AprPercent; side: 'supply' | 'borrow'; /** How it is realized — decides whether the APR is actually bankable. */ claim: Open<'accrual' | 'merkl' | 'manual' | 'none'>; /** Program end, where known. An APR with two weeks left is not an APR. */ endsAt?: number; /** * `true` ⇒ not priceable (points). MUST be shown separately and is * deliberately EXCLUDED from `RateTerms.aprTotal`. */ indicative?: boolean; } /** One entry in a fixed-term rate menu. Mirrors `MarketTermEntry`. */ interface RateMenuEntry { /** LENDER-SPECIFIC: Exactly/TermMax = unix maturity, Teller = duration * seconds, Lista = broker product id, Midnight/Term = `0`. */ termId: number; durationSecs: number; durationDays: number; /** Borrow APR at this term. */ apr: AprPercent; /** Lend APR at this term, where the lender quotes both sides. */ depositApr?: AprPercent; /** Borrowable liquidity at this term, human units. */ available?: number; } interface RateTerms { kind: RateKind; /** Base rate only — no rewards, no intrinsic yield. */ apr: AprPercent; components: { base: AprPercent; /** Priceable rewards only. */ rewards?: AprPercent; /** Underlying/LST yield the asset earns by itself. */ intrinsic?: AprPercent; }; /** `base + priceable rewards + intrinsic` — the headline number. */ aprTotal: AprPercent; /** Carried explicitly even though it has one value today: a silent * APR→APY change would be the classic undetectable break. */ basis: 'apr-nominal'; /** * WHAT PERIOD the number describes — orthogonal to {@link basis} (the unit) * and to `source` (where it came from). * * Lenders never need this: an IRM rate is the rate right now. Vaults do, and * the difference is not cosmetic — a vault reporting a 30-day TRAILING 14 % * and a market quoting a forward 14 % are different claims, and a leaderboard * that sorts them in one column silently ranks past performance against a * live offer. Every vault provider that measures rather than quotes carries * the window in its own dialect (`YearnVault.isForwardApr`, * `LagoonVault.aprWindow`, HyperCore's `apr`, any realized share-price APR); * this is the one field they normalize onto. * * Absent ⇒ `spot`, i.e. the ordinary "rate right now" reading. */ window?: { /** * - `spot` — the rate in force this block (an IRM, a governance-set rate). * - `forward` — a projection of what the position WILL earn (a PT's * implied APY, Yearn's forward APR). * - `trailing` — MEASURED over a past window and therefore not a promise. */ kind: Open<'spot' | 'forward' | 'trailing'>; /** Length of the measurement window, seconds. `trailing` only. */ secs?: number; /** The source's own label when it does not map to a duration * (`'inception'`, `'weekly'`). Rendered verbatim. */ label?: string; }; compounding: Open<'per-second' | 'per-block' | 'none' | 'unknown'>; /** * Seconds a FRESH deposit earns NOTHING before the rate starts applying. * * A warm-up, and it is a different fact from every other delay on a sheet. * `SupplyExitTerms.cooldownSecs` is how long your money is STUCK; * `GovernanceTerms.timelockSecs` is how long you have to react to someone * changing the deal. This is neither: the money is free to leave at any * moment, and the deal is not changing — you simply do not earn yet. Putting * it in either of the other two would describe a lock that does not exist. * * The reason it needs a field rather than a sentence: with it absent, a * headline reading "Variable 3.5 % · withdraw any time" is composed of two * true halves that together mislead, because a stay shorter than the warm-up * realises exactly ZERO. Frankencoin's savings module is the case that forced * it (`INTEREST_DELAY` = 3 days, and a top-up re-weights the whole position's * clock pro-rata rather than only the new money). * * Absent ⇒ the rate applies from the first block, which is the norm. */ warmupSecs?: number; source: Open<'utilization-curve' | 'orderbook' | 'auction' | 'governance' | 'borrower' | 'oracle' | 'api' | 'derived'>; /** Is the rate locked for the life of the position? */ isLocked: boolean; /** Protocol-enforced bounds (Liquity min/max, Morpho rateCap/rateFloor). */ minApr?: AprPercent; maxApr?: AprPercent; /** * `kind: 'user-set'` only — the contract a RATE SETTER needs. * * On the Liquity family the borrower picks their own rate at open, and the * choice is not cosmetic: it decides where you sit in the redemption queue, * so the cheapest rate is also the one that gets redeemed first. A borrow UI * that offers no input either cannot build the transaction or silently * accepts a default the user never saw. * * `default` is what the protocol applies when the caller omits a rate (the * branch average), so a UI can pre-fill it and stay consistent with what the * action would have done anyway. */ userSet?: { /** Is a rate part of the open call at all? */ required: boolean; /** Inclusive bounds, PERCENT. Outside these the open reverts. */ min?: AprPercent; max?: AprPercent; /** Applied when the caller omits one — the branch average. */ default?: AprPercent; /** Can it be changed after opening (Liquity `set-rate`)? */ adjustable: boolean; /** * Changing the rate re-charges the upfront fee — so it is not free, and a * UI that presents it as a slider should say so. */ adjustmentCostNote?: string; /** Seconds before the rate may be adjusted again without penalty. */ adjustmentCooldownSecs?: number; }; /** Per-program reward detail — token identity, claim path, end date. */ rewards?: RewardTerm[]; /** Rate menu when the market offers several terms at once. */ menu?: RateMenuEntry[]; /** Size the quote is valid for, when the rate is depth-dependent. */ quote?: { assets: number; basis: Open<'marginal' | 'average'>; }; lastChangedAt?: number; } interface MaturityTerms { kind: Open<'perpetual' | 'fixed-date' | 'rolling-duration'>; /** unix seconds; `kind: 'fixed-date'`. */ maturity?: number; /** ISO-8601 mirror so consumers need not re-format. */ maturityIso?: string; /** Snapshot — derive live from `maturity` for a countdown. */ secondsToMaturity?: number; /** `kind: 'rolling-duration'` (Teller, Lista broker). */ minDurationSecs?: number; maxDurationSecs?: number; /** * What happens at/after maturity if NOBODY acts. The field that most * surprises users — outcomes range from "interest simply stops" to * "liquidated within five minutes, losing all collateral". */ atMaturity?: Open<'stops-earning' | 'penalty-accrues' | 'liquidatable' | 'default-seizure' | 'physical-delivery' | 'refinanced' | 'auto-roll' | 'none'>; /** Grace window before `atMaturity` bites (Teller ~300 s, TermMax 7200 s). */ graceSecs?: number; } type FeeWhen = Open<'entry' | 'ongoing' | 'exit' | 'late' | 'liquidation' | 'performance'>; /** * One charge, in a shape general enough that a NEW fee is data rather than a * schema change. Replaces the current scatter: `originationFee`, * `withdrawFeeBps`, `rates.fee`, `fixedTerm.fees.*`, `river.mintFeeRate`, * `liquity.gasCompensation`. */ interface FeeTerm { /** Stable slug — the join key for UI copy and filtering. */ id: Open<'origination' | 'late-penalty' | 'early-repay-penalty' | 'early-repay-discount' | 'continuous' | 'settlement' | 'instant-exit' | 'performance' /** Charged on ASSETS and ongoing, not on yield — so it is owed in a flat * year, which a performance fee is not. */ | 'management' | 'reserve-factor' | 'gas-compensation' | 'redemption' | 'claim' | 'liquidation-bonus'>; /** Human label — lets an unknown `id` still render correctly. */ label: string; when: FeeWhen; unit: Open<'apr-percent' | 'percent' | 'bps' | 'absolute'>; basis: Open<'principal' | 'face-value' | 'yield' | 'collateral' | 'shares' | 'debt-repaid'>; /** * A NEGATIVE value is legal and means a REBATE (Exactly's early-repay * discount). Sign is load-bearing — never take an absolute value. */ value: number; payee?: Open<'protocol' | 'lenders' | 'liquidator' | 'curator' | 'gas-refund'>; /** Governance-mutable ⇒ this is a snapshot; re-verify before quoting. */ mutable?: boolean; /** * Protocol-enforced CEILING on `value`, same `unit`. Only meaningful * alongside `mutable: true`, and it is what makes that flag actionable: a * mutable fee with no stated bound reads as unlimited discretion, when the * contract may in fact refuse anything above a hard constant. * * Morpho Blue is the case — its market parameters cannot be changed at all, * but the owner may set a fee on interest up to a `MAX_FEE` of 25 %. "The fee * can change" and "the fee can change, but never above 25 %, and nothing else * about this market can change" are very different sentences, and only the * second one is true. * * Absent ⇒ no cap is known. NOT the same as "uncapped". */ cap?: number; /** Only resolvable at action time (Exactly discount, TermMax curve price). */ indicative?: boolean; /** Decaying/scheduled fees (Apyx: 3.40 % → 0 over 20 days). */ schedule?: { afterSecs: number; value: number; }[]; description?: string; } /** Superset of `SavingsWithdrawalMode` + `LstWithdrawalMode`, plus the two * lending-side exits neither covers. */ type SupplyExitMode = Open<'instant' | 'instant-capped' | 'instant-or-queued' | 'fee-or-queued' | 'fixed-cooldown' | 'queued' | 'request-based' /** Sell the instrument on a book (Term, TermMax, Midnight). */ | 'market-sale' /** No early exit at all. */ | 'at-maturity' | 'off-chain' | 'dex-only'>; /** * One concrete way out, as published by the vault row. Re-exported from the * vault layer so a term sheet and the `/v1/data/vaults` row cannot disagree * about what the legs are. */ type SupplyExitRoute = VaultExitRoute; interface SupplyExitTerms { mode: SupplyExitMode; /** * **Every leg of the exit, enumerated.** * * `mode` names the shape and the rest of this block describes it with ONE * number each — one `cooldownSecs`, one liquidity reading, one fee list. * That is a lie for the two-legged modes: `fee-or-queued` charges the fee on * the instant leg and applies the wait to the free one, so a reader taking * `cooldownSecs` and the exit fee together concludes it must pay 0.5 % AND * wait a week, which is true of no leg. `instant-or-queued` has the same * problem in the other direction. * * The routes are the per-leg truth: fee, wait, minimum size and capacity, * each attributed to the leg it actually belongs to. Ordered cheapest-first * where the legs are equally reachable — but read `minAmount`, because a * cheap leg with a floor above a holder's position is not an option they * have (Treehouse's 5 bps queue starts at 50 wstETH). * * Always at least one entry for a vault row; absent on lending markets, * whose exit is the pool's liquidity rather than a set of routes. */ routes?: SupplyExitRoute[]; /** * Coarse alias — identical semantics to * `VaultClassificationFields.redemptionType`, plus one member that field * cannot express. * * `'market'` ⇒ the instrument has NO redemption model at all: you leave by * selling it (a Pendle PT). Both `sync` and `async` are wrong there and both * mislead — `sync` invites the stuck-capital heuristic against what is really * pool depth, `async` implies a queue that does not exist — which is why * `classifyRedemption` returns `undefined` rather than picking one. The term * sheet answers positively instead of leaving a hole. */ settlement: Open<'sync' | 'async' | 'market'>; cooldownSecs?: number; /** Claim-window constraints (Apyx: blocked 3 d, free at 20 d). */ claimWindow?: { earliestSecs?: number; freeAfterSecs?: number; }; /** What can actually leave right now. */ liquidity?: { assets: number; assetsUsd?: number; ratio?: number; }; partialAllowed: boolean; /** * Does exiting early cost an UNKNOWN amount? * `none` par · `haircut-formula` deterministic discount · * `market-price` you sell into a book · `may-be-impossible` the book can be * empty. */ priceRisk: Open<'none' | 'haircut-formula' | 'market-price' | 'may-be-impossible'>; cancellable?: boolean; /** The `when: 'exit' | 'performance'` subset of the side's fees. */ fees: FeeTerm[]; } interface BorrowExitTerms { /** Three signs exist across our lenders: `discount` is a REBATE (Exactly). */ earlyRepay: Open<'free' | 'discount' | 'penalty' | 'market-price' | 'not-allowed'>; atMaturityCost: Open<'face' | 'accrued'>; lateBehaviour: Open<'penalty-accrues' | 'liquidatable' | 'default-seizure' | 'refinanced' | 'none'>; partialAllowed: boolean; /** Dust floor — Liquity `minDebt`, Morpho/Lista `minLoan`. Raw base units. */ minDebt?: string; /** Over-repay REVERTS (Midnight `uint128` underflow) — a real footgun. */ overRepayReverts?: boolean; /** * Seconds after OPENING (or topping up) during which the debt cannot be * repaid at all. * * The mirror of `SupplyExitTerms.cooldownSecs`, and deliberately a separate * field because it is a categorically worse term: a supply cooldown delays * your money, a REPAY cooldown means a position cannot be closed, * deleveraged or migrated out — and, on the one lender that has it, means an * incoming liquidation cannot be averted by repaying either. * * Only Curvance (`MIN_HOLD_PERIOD`, 1200 s) has one today, which is exactly * why it needs to be stated rather than assumed away: every other lender we * carry lets you exit at any block, and the generic builder's * `availability.canClose: true` encodes that assumption. */ cooldownSecs?: number; fees: FeeTerm[]; } /** One named penalty, for models where a single number is not enough. */ interface LiquidationPenaltyTerm { /** Stable slug: 'stability-pool' | 'redistribution' | 'hard' | 'protocol-fee'. */ id: Open; label: string; /** Fraction of the repaid debt (0.05 = 5 %). */ value: number; description?: string; } /** * Redemption — collateral taken from a HEALTHY position. * * This is the term users most often misread, because the effect ("your * collateral can be taken") sounds like a governance power or a liquidation * when on every lender we serve it is neither: it is a permissionless * arbitrage that defends the stablecoin's peg. * * **The per-lender answers are audited in CDP_REDEMPTION_TERMS.md.** Read it * before filling this block for a new CDP: the fields below vary INDEPENDENTLY * across protocols, and copying Liquity V2's answers — the best-documented CDP, * and therefore the one that gets copied — has already produced three wrong * sheets. Spelling out WHO can trigger * it, WHEN it pays them to, WHICH positions are hit and WHAT the borrower can * do about it turns an alarming sentence into an actionable one. */ interface RedemptionTerms { /** * WHO can trigger it — a statement about PERMISSION, not about frequency. * * - `permissionless-arbitrage` — any holder of the debt token can call it, * without a vote and without targeting anyone personally. * - `governance` / `protocol` — reserved; nothing uses these today. * * Read together with {@link driver}, which says when it actually PAYS. The two * are independent and conflating them overstates the risk: on every lender we * serve the call is open at any block, but under `below-peg` it is only * profitable while the stablecoin trades under target, so redemptions arrive * in bursts during depegs rather than continuously. */ trigger: Open<'permissionless-arbitrage' | 'governance' | 'protocol'>; /** * WHEN it becomes economically rational. `below-peg` means redemptions only * pay when the stablecoin trades under its target, which is exactly why they * exist — they push it back up. */ driver?: Open<'below-peg' | 'always'>; /** * WHICH positions are hit first. * - `lowest-rate-first` — Liquity V2 family: the cheapest borrowers are * redeemed first, so the rate you chose IS your queue position. * - `pro-rata` — Resupply: skimmed from EVERY borrower in the pair, so * there is no queue and nothing to out-run. * - `lowest-collateral-ratio` — Liquity V1 lineage. */ order: Open<'lowest-rate-first' | 'pro-rata' | 'lowest-collateral-ratio'>; /** * Does the borrower end up down in USD terms? On Liquity V2 in NORMAL * operation the redemption fee stays IN the trove as extra collateral, so the * borrower is roughly USD-neutral — what they lose is COLLATERAL EXPOSURE, * not value. Saying "you can lose your collateral" without this overstates it. * * **Do not copy that answer to another protocol without checking where the * fee goes.** It is a property of Liquity's specific mechanism, not of * redemptions generally: Resupply writes the collateral off across the pair * with half the fee going to the protocol and nothing credited back, and a * shut-down Liquity branch pays the redeemer a 2 % bonus out of the * borrower's collateral. ABSENT means we have not established it, which is * the honest state for anything but the two cases above. */ valueImpact?: Open<'usd-neutral' | 'loss'>; /** What the borrower can actually do. Absent ⇒ nothing. */ defence?: string; } interface LiquidationTerms { /** * HOW liquidation happens mechanically — distinct from WHAT triggers it. * * Without this, every market reads as the ordinary "a liquidator repays your * debt and seizes collateral plus a bonus" model, which is wrong for four of * the lenders we serve and materially misleading for one: * * - `soft-band` Collateral is converted GRADUALLY inside the market's * own AMM as the price enters a band range — with NO * penalty and REVERSIBLY (Curve LlamaLend). There is no * single liquidation price at all. * - `stability-pool` A pool absorbs the debt first and only falls back to * redistributing it across other borrowers (Liquity, * River) — the two paths carry DIFFERENT penalties. * - `auction` Price is discovered by a Dutch auction rather than an * oracle (Frankencoin challenges). * - `default-seizure` The whole escrow is forfeit on a missed payment * (Teller). * - `delivery` Unpaid collateral is delivered to lenders (TermMax). * - `repay-to-target-hf` * There is no close factor at all: the liquidator repays * however much it takes to restore the position to * {@link targetHealthFactor}, and the bonus SCALES with * how far under water it is (Aave V4). See * {@link healthFactorForMaxBonus}. */ model?: Open<'repay-seize' | 'soft-band' | 'stability-pool' | 'auction' | 'default-seizure' | 'delivery' | 'repay-to-target-hf' | 'none'>; /** Who ends up holding the seized collateral. */ absorber?: Open<'liquidator' | 'stability-pool' | 'other-borrowers' | 'lenders' | 'amm'>; /** * Can the position come BACK out of liquidation if the price recovers? * True only for `soft-band`: conversion is continuous and reverses, so * "being liquidated" is not terminal the way it is everywhere else. */ reversible?: boolean; /** * Named penalties when `penalty` alone cannot express the model — Liquity * charges a different rate depending on whether the Stability Pool absorbs * the debt or it is redistributed. `penalty` stays the headline (worst or * primary) so a naive consumer is still correct. */ penalties?: LiquidationPenaltyTerm[]; /** Window between becoming liquidatable and the terminal outcome (TermMax). */ windowSecs?: number; /** Only allowlisted keepers may liquidate. Absent/false ⇒ permissionless. */ permissioned?: boolean; /** Where a shortfall goes when the collateral does not cover the debt. */ badDebt?: Open<'socialized' | 'redistributed' | 'insurance-fund' | 'protocol-absorbed' | 'unknown'>; /** * `soft-band` only: the collateral factor is a FUNCTION of the band count * chosen at open (`{ [N]: ltv }` — 0.991 at N=4 vs 0.886 at N=50). `ltv` * reports the market's default N; a consumer quoting a different N must read * this curve instead. */ bandLtv?: Record; /** Band count `ltv` / `liquidationLtv` were computed at. */ defaultBands?: number; /** * The knob the BORROWER turns at open, when the factors above depend on one. * * Mirrors `ConfigEntry.openParameter` — it is the same descriptor, surfaced on * the term sheet because that is where a UI edits terms rather than reads * them. Describes the DOMAIN only; the value a given position chose lives in * the per-position `modes[posId]` slot. * * Prefer this over {@link bandLtv} to drive a CONTROL: the domain is always * known (`MIN_TICKS`/`MAX_TICKS`), whereas the curve is absent on any market * whose geometry could not be read. Where the curve IS present it now covers * the whole domain, so `bandLtv[N]` is the right source for the LTV that a * chosen `N` implies. See POSITION_PARAMETERS_PLAN.md. */ openParameter?: { kind: 'llamalend-bands' | 'interest-rate'; dimension: 'collateralFactor' | 'rate'; domain: { min: number; max: number; } | { values: number[]; }; default: number; immutableAfterOpen: boolean; adjustCooldownSeconds?: number; }; /** * Aave-style escalation: the close factor rises to 1 once health falls below * this. Without it, `closeFactor: 0.5` understates the worst case. */ /** * The health factor BELOW WHICH the position becomes liquidatable, when the * protocol's trigger is not the usual `HF < 1`. * * Added for Flying Tulip, whose `marginHfTargetBps = 12500` makes a position * liquidatable at **HF < 1.25** — `liquidateFlash` reverts above it. Every * consumer in this repo otherwise assumes `HF < 1 ⇒ liquidatable`, which * understates the danger zone by a quarter of a point of health on the side * that costs the borrower money. * * ABSENT means the ordinary `HF < 1`. Do not default it to 1 at the call * site — an explicit 1 and an absent field should read the same, and a * lender that has no health factor at all (Frankencoin's challenge game, * Teller's time-based default) must not acquire one by omission. */ liquidationHealthFactor?: number; fullCloseBelowHealthFactor?: number; /** * `repay-to-target-hf` only: the health factor at or below which the * liquidator's bonus reaches its maximum — i.e. the value {@link penalty} * actually describes. * * Load-bearing next to a scaling bonus, because the two fields say different * things: Aave V4's bonus grows from ~0 at HF 1 to `maxLiquidationBonus` * here, so publishing the max alone reads as a flat penalty every liquidation * charges, and publishing nothing reads as a market with no penalty at all. */ healthFactorForMaxBonus?: number; trigger: Open<'price' | 'time' | 'price-and-time' | 'redemption' | 'none'>; /** Max LTV at open. */ ltv?: number; /** Threshold at which liquidation becomes possible. */ liquidationLtv?: number; /** * Fraction of repaid debt paid to the liquidator on top of par. * * OPTIONAL, and the distinction is load-bearing: `0` means "the liquidator * gets no bonus", `undefined` means "we do not know it". The first cut * defaulted the unknown case to `0` and every market on `/lending/latest` * rendered a confident "Liquidator takes debt repaid + 0%" — the origin's * `market_config` had no penalty column at all, so NOTHING was known. A row * we cannot fill must be absent, not zero. */ penalty?: number; closeFactor: number; targetHealthFactor?: number; /** * `full-collateral` is the Teller case: the liquidator takes the ENTIRE * escrow, not a proportional slice — ~2× the debt at 50 % LTV. */ seizure: Open<'proportional' | 'full-collateral'>; /** * Collateral can be taken while the position is perfectly healthy. * See {@link RedemptionTerms} for what actually triggers it — the bare flag * says only THAT it can happen, never why, and "your collateral can be * taken" reads as a governance power or a bug unless the mechanism is * spelled out. */ redeemable?: boolean; /** How redemption works, when `redeemable`. */ redemption?: RedemptionTerms; gracePeriodSecs?: number; } interface CounterpartyTerms { kind: Open<'pool' | 'orderbook' | 'auction' | 'broker' | 'cdp' | 'p2p' | 'vault-strategy' | 'off-chain-credit'>; address?: string; /** The trust question, one field. */ solvency: Open<'overcollateralized' | 'tranched-senior' | 'tranched-junior' | 'undercollateralized' | 'nav-attested'>; socializedLoss?: boolean; curator?: string; } /** Origination window for auction-gated markets (Term Finance). */ interface AuctionWindow { status: Open<'upcoming' | 'open' | 'revealing' | 'closed'>; canBorrow: boolean; canLend: boolean; secondsUntilClose?: number; id?: string; startTime?: number; revealTime?: number; endTime?: number; minBorrowAmount?: string; minLendAmount?: string; } /** What must be granted BEFORE an action can even be built. */ type PermissionKind = Open<'token-approval' | 'lender-delegation' | 'manager-authorization' | 'eip712-permit' | 'nft-approval' | 'operator-set' /** Contract callers must be governance-approved (Inverse, Fraxlend). */ | 'caller-allowlist'>; interface AvailabilityTerms { /** Gate CTAs on THIS and nothing else — it already folds in caps, freezes, * auction windows and gating. */ canOpen: boolean; canClose: boolean; /** Machine-readable reason when `canOpen` is false. */ blockedBy?: Open<'frozen' | 'paused' | 'cap-full' | 'auction-closed' | 'no-liquidity' | 'not-whitelisted' | 'shutdown' | 'disabled'>; gating: Open<'permissionless' | 'whitelist' | 'attestation' | 'kyc' | 'allowlist-contract'>; /** Absent ⇒ no window applies. NOT the same as `closed`. */ window?: AuctionWindow; /** * Minimum position size to OPEN, in RAW base units of THIS side's asset. * * On the borrow side this is a minimum DEBT (Comet `baseBorrowMin`, Liquity * `minDebt`, Maker-fork `dust`). On the supply/collateral side it is a * minimum COLLATERAL — a genuinely different gate, and the only one some * lenders have: Frankencoin caps nothing on the debt but refuses a position * below `minimumCollateral`, and dropping under it closes the position * permanently. */ minSize?: string; cap?: string; /** 0..1 — how full the cap is. */ capUtilization?: number; requires?: PermissionKind[]; /** * Can a position be OPENED from this side alone, or only by supplying both * legs in one action? * * Absent / `'standalone'` ⇒ the ordinary pool case: a deposit on its own * creates a supply position, a borrow on its own draws against whatever * collateral the account already has. * * `'both-legs'` ⇒ the position does not exist until collateral AND debt are * committed together, so neither side can open it. This is the CDP shape and * it is enforced differently by each protocol — Liquity and its forks make it * structurally impossible (`openTrove` refuses anything under `minDebt`); * Frankencoin technically ACCEPTS `clone(…, initialMint = 0, …)` but the * result is a position that holds collateral, earns nothing and has no debt, * which is not a product. Either way the CTA belongs on the joint action * (`/v1/actions/lending/deposit-and-borrow`), not on this side. * * **This is about OPENING only.** Topping up or drawing further against an * EXISTING position is a normal standalone op on every one of these lenders, * which is exactly why `depositsEnabled` cannot carry this meaning — setting * it false would also block the top-up the protocol permits. * * The flag is genuinely discriminating, not a family label: Resupply is a CDP * too, and its `addCollateral(amount, borrower)` opens an unlevered * collateral-only position perfectly well, so it stays `'standalone'`. */ opensWith?: Open<'standalone' | 'both-legs'>; } interface PositionConstraints { /** Aave isolation mode: capped debt, no collateral mixing. */ isolation?: { enabled: boolean; debtCeiling?: string; ceilingUtilization?: number; }; /** Borrowing this asset forbids borrowing any other in the same account. */ siloedBorrowing?: boolean; crossMargin: boolean; /** * How a position is ADDRESSED. `loanId` already means five different things * across our lenders and `termId` six — making the model explicit is * cheaper than making every integrator rediscover it. */ positionModel: Open<'account' | 'sub-account' | 'nft' | 'cdp-id' | 'loan-id' | 'escrow'>; /** One line saying what the id in `loanId`/`posId` actually IS here. */ positionIdMeaning?: string; maxPositions?: number; /** * What this fetch actually SAW, as opposed to what the lender family * implies. Kept separate from `crossMargin` / `positionModel` on purpose: * those are family-level truths from the registry, these are per-fetch * observations, and collapsing the two would let a thin chain (a lender * listing one asset today) masquerade as a structural property. * * Useful precisely where they DISAGREE — e.g. Fluid is registered isolated * because its T1 vaults dominate, but its T2–T4 "smart" vaults genuinely * pool two collaterals; a `collateralAssetCount > 1` on a Fluid row is the * signal that this particular vault is one of them. */ observed?: { /** Distinct collateral assets this market actually accepts, this fetch. */ collateralAssetCount: number; /** Markets seen under this lender key on this chain, this fetch. */ marketCount: number; /** Does the lender key fan out to many markets (registry answer)? */ multiMarketKey: boolean; }; } type AdminKind = Open<'EOA' | 'SAFE' | 'TIMELOCK' | 'GOVERNOR' | 'GOVERNANCE' | 'CUSTOM' | 'UNKNOWN'>; type GovernancePower = Open<'pause-deposits' | 'pause-withdrawals' | 'pause-borrows' | 'freeze-market' | 'change-ltv' | 'change-rate' | 'change-fees' | 'set-caps' | 'add-collateral' | 'swap-oracle' | 'upgrade-implementation' | 'blacklist' | 'seize-funds' | 'reprice-pending-redemptions' /** Move funds between already-approved markets — on curated vaults this is * an allocator power and is NOT gated by the config timelock. */ | 'reallocate'>; interface GovernanceTerms { mutability: Open<'immutable' | 'governed' | 'unknown'>; /** The governance root, after hopping proxy admins / timelock admins. */ controller?: string; controllerKind?: AdminKind; safe?: { threshold: number; owners: number; }; /** * Enforced delay in SECONDS between a parameter change being queued and it * taking effect — the holder's NOTICE PERIOD. `0`, or any `controllerKind` * that is not `TIMELOCK`, means a parameter can change in the very next * block with no warning. * * **This is NOT a withdrawal lock.** `SupplyExitTerms.cooldownSecs` is how * long YOUR money is stuck; this is how long you have to react to someone * else changing the deal. Never merge or sum the two. */ timelockSecs?: number; timelockSource?: Open<'on-chain' | 'screened' | 'metadata'>; /** * The controller IS a timelock but its delay could not be read. Distinct * from `timelockSecs: undefined` on a non-timelock root, which genuinely * means "no notice period" — conflating the two would raise a false alarm * on the safest governance shape. */ timelockUnknown?: boolean; tier?: Open<'low' | 'medium' | 'high' | 'unknown'>; score?: number; powers?: GovernancePower[]; roles?: { owner?: string; curator?: string; guardian?: string; feeRecipient?: string; /** * Addresses that can REALLOCATE a curated vault between its markets. * * Load-bearing next to `timelockSecs`, and the reason the two must be * read together: on the MetaMorpho shape a timelock gates adding a market * or raising a cap, but moving money BETWEEN already-approved markets is * an allocator call that lands in the next block. So a vault can publish * a 24-hour notice period and still change what a depositor is exposed to * with no notice at all. A sheet showing the timelock alone overstates * how much warning the holder gets. */ allocators?: string[]; }; /** Governance screens refresh far slower than rates — own timestamp. */ asOfScreen?: number; } type OracleBand = Open<'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'>; interface OracleTerms { /** * `none` is MEANINGFUL, not missing data: Teller liquidates on TIME and has * no oracle and no health factor anywhere in its trigger. */ kind: Open<'price-feed' | 'nav-attested' | 'none'>; /** * THE oracle for this `marketUid` — SINGULAR, lowercased. Verified across * the full oracle classification: 8,993 marketUids, 0 with more than one * address. Singularity holds because `marketUid` granularity is already * per-asset; the array in the legacy `oracleInfo.feeds[]` is an artifact of * hanging off the lender/params level instead. * * For a composite/cross adapter this is the ADAPTER — the address whose * failure or replacement moves this market's price. */ address?: string; /** Underlying feeds when the adapter composes several and the classifier * decomposed them. `address` stays the single source of truth. */ components?: string[]; provider?: string; /** Decoded reported pair, e.g. `"ETH / USD"`. */ priceDescription?: string; /** What it SHOULD report, e.g. `"WETH / USD"`. */ intendedPair?: string; correctAsset?: boolean | null; correctNumeraire?: boolean | null; fixedRate?: boolean; score?: number; band?: OracleBand; flags?: string[]; /** Can the oracle be swapped/upgraded, and by whom. On an otherwise * IMMUTABLE market this is the ONLY mutable trust vector. */ mutability?: { mutable: boolean; kind: Open<'IMMUTABLE' | 'PROXY' | 'AUTHORITY' | 'UNKNOWN'>; controller?: string; controllerKind?: AdminKind; timelockSecs?: number; }; heartbeatSecs?: number; lastUpdateAt?: number; } interface AssetQuality { /** 1 (best) … 5 (worst). */ riskScore?: number; source?: Open<'whitelist' | 'default' | 'curated'>; /** On-chain USD liquidity available to absorb a liquidation — the number * that decides whether the LLTV is actually enforceable. */ liquidityUsd?: number; /** The TOKEN CONTRACT's own governance, distinct from the market's. An * upgradeable, pausable collateral is a supplier risk even on an * immutable market. */ governanceScore?: number; governanceLevel?: Open<'green' | 'amber' | 'red'>; upgradeable?: boolean; canPause?: boolean; adminKind?: AdminKind; } interface ExposureEntry { asset: TermAssetRef; /** That asset's OWN row in this lender — join key to its full term sheet. */ marketUid?: string; via: Open<'collateral' | 'vault-allocation' | 'strategy' | 'idle'>; assets?: number; assetsUsd?: number; /** 0..100. ABSENT when `weightBasis === 'unweighted'`. */ weightPct?: number; ltv?: number; liquidationLtv?: number; liquidationPenalty?: number; /** The collateral's OWN oracle. Your deposit's safety depends on the oracle * pricing SOMEONE ELSE'S collateral. */ oracle?: OracleTerms; quality?: AssetQuality; } interface ExposureTerms { count: number; /** * How `weightPct` was obtained, and therefore how much to trust it. * `unweighted` = POOLED lenders: Aave/Compound do not record which * collateral backs which borrow on-chain, so the list is the ACCEPTED SET, * not a measured split. Do not render a pie chart from it. */ weightBasis: Open<'debt' | 'allocation' | 'unweighted'>; worstRiskScore?: number; worstOracleBand?: OracleBand; /** Largest single exposure's `weightPct` — the concentration signal. Only * meaningful when `weightBasis !== 'unweighted'`. */ topWeightPct?: number; items: ExposureEntry[]; } interface UtilizationTerms { /** borrowed / supplied, 0..1 — the IRM input for this market. */ utilization: number; /** * The basis the ratio is computed over. NOT always this row: rates for * shared-liquidity protocols are set on a larger pool, and a simulation * must shift THAT, not the row totals. */ basis: Open<'market' | 'hub' | 'liquidity-layer' | 'pool'>; irmTotalDeposits?: number; irmTotalDebt?: number; /** Where the curve steepens — headroom before the rate jumps. */ targetUtilization?: number; kinkUtilization?: number; /** 0..1. `1` = cap full and the side is closed. */ supplyCapUtilization?: number; borrowCapUtilization?: number; /** Fluid: share of collateral locked below the withdrawal limit. */ lockupRatio?: number; } /** * A non-default risk category, expressed as a DELTA against the resolved * default. `config` is a MAP keyed by category (Aave e-modes, Dolomite * categories, Euler configs, Silo) — a single flat LTV silently reports the * default and hides the rest, which on an Aave ETH-correlated e-mode is the * difference between 80 % and 93 %. */ interface ModeVariant { /** The `config` map key. `'0'` is the default on every lender. */ modeId: string; label?: string; isDefault: boolean; entry?: Open<'automatic' | 'user-selected' | 'per-position'>; liquidation?: Partial; /** * Mode-scoped and usually RESTRICTED: an e-mode typically narrows the * accepted collateral to a correlated basket. Omitting it would make the * headline LTV look obtainable against collateral the mode forbids. */ acceptedCollateral?: ExposureTerms; rate?: Partial; availability?: Partial; } interface SupplyTermSheet { /** Is this position earning, or is it just collateral? */ role: Open<'yield' | 'collateral' | 'both'>; rate: RateTerms; maturity: MaturityTerms; exit: SupplyExitTerms; /** ALL fees on this side, including the exit subset. */ fees: FeeTerm[]; /** What secures the debt drawn against this deposit. */ backedBy?: ExposureTerms; modes?: ModeVariant[]; counterparty: CounterpartyTerms; availability: AvailabilityTerms; /** Is the supplied principal at risk beyond ordinary credit risk? */ principal: { protected: boolean; risks: Open<'bad-debt' | 'physical-delivery' | 'nav-drawdown' | 'first-loss' | 'depeg'>[]; }; info: TermInfo; /** Namespaced escape hatch — see the promotion rule in TERM_SHEET_PLAN §13.5. */ ext?: Record; } interface BorrowTermSheet { rate: RateTerms; maturity: MaturityTerms; /** * Does the amount owed GROW, or is it a static face value fixed at trade * time? The single biggest departure from variable-rate intuition — four of * six fixed-term lenders are static. */ debtShape: Open<'accruing' | 'static-face' | 'prepaid'>; exit: BorrowExitTerms; /** Fully resolved for the DEFAULT mode. `modes[]` carries the rest. */ liquidation: LiquidationTerms; /** What you may post, each with its own LTV, oracle and quality. */ acceptedCollateral?: ExposureTerms; modes?: ModeVariant[]; fees: FeeTerm[]; counterparty: CounterpartyTerms; availability: AvailabilityTerms; info: TermInfo; ext?: Record; } /** * Distinguishes "not applicable" from "not implemented yet" — the affordance * that lets phases ship incrementally without lying. A missing `oracle` must * never read as "this market has no oracle" when the truth is "we have not * classified it". */ interface CoverageInfo { /** Blocks genuinely computed for this market. */ present: string[]; /** Blocks that do NOT APPLY here — a positive fact. */ notApplicable?: Record; /** Blocks that WOULD apply but are not wired yet. */ pending?: Record; } /** Current schema version. Bumped ONLY for removals/semantic/unit changes; * new optional fields and new enum members are additive. */ declare const TERM_SHEET_SCHEMA_VERSION = 1; interface TermSheet { schemaVersion: number; /** unix seconds at fetch — everything here is a snapshot. */ asOf: number; /** `.@v`, e.g. `aave-v3.pool@v1`. Points at the prose * catalogue so the per-market payload stays small. */ profileId: string; /** The anchor this sheet describes. */ marketUid?: string; lender?: string; chainId?: string; /** * The market's own underlying asset. * * Load-bearing, not decoration: `availability.minSize`, `availability.cap` * and `exit.minDebt` are all RAW base units, and without decimals + symbol a * consumer cannot render any of them. The sheet described a market without * ever saying which asset it was about. */ asset?: TermAssetRef; supply?: SupplyTermSheet; borrow?: BorrowTermSheet; /** Shared — these describe the MARKET, not a side. */ governance?: GovernanceTerms; oracle?: OracleTerms; utilization?: UtilizationTerms; constraints?: PositionConstraints; coverage?: CoverageInfo; ext?: Record; } /** Compact form for list endpoints — `?terms=digest`. */ interface TermSheetDigest { schemaVersion: number; profileId: string; marketUid?: string; supply?: { rateKind: RateKind; aprTotal: AprPercent; maturityKind: MaturityTerms['kind']; maturity?: number; exitMode: SupplyExitMode; settlement: SupplyExitTerms['settlement']; canOpen: boolean; headline: string; tags: TermTag[]; backedBy?: Omit; }; borrow?: { rateKind: RateKind; apr: AprPercent; maturityKind: MaturityTerms['kind']; maturity?: number; debtShape: BorrowTermSheet['debtShape']; earlyRepay: BorrowExitTerms['earlyRepay']; liquidationTrigger: LiquidationTerms['trigger']; canOpen: boolean; headline: string; tags: TermTag[]; acceptedCollateral?: Omit; }; oracle?: Pick; governance?: Pick; utilization?: number; } /** A term profile — the invariant prose, one per lender family × variant. */ interface TermProfile { id: string; /** Display name, e.g. `Aave V3 pool market`. */ name: string; /** Which lender family this covers. */ family: string; supply?: { description: string; implications?: string[]; }; borrow?: { description: string; implications?: string[]; }; docsUrl?: string; } /** Deep-partial, for adapters that return only what they override. */ type DeepPartial = { [K in keyof T]?: T[K] extends (infer U)[] ? U[] : T[K] extends object | undefined ? DeepPartial> : T[K]; }; /** * Withdrawal mode for a savings vault. Drives UX: instant for plain * ERC-4626 wrappers, fixed-cooldown for Ethena-style sUSDe, queued for * Maple syrup pools, etc. * * Identical enumeration to `LstWithdrawalMode` from `vaults/lst/` — * kept separate so the two providers can evolve independently without * one provider's withdrawal taxonomy creep affecting the other. */ type SavingsWithdrawalMode = 'instant' | 'instant-capped' | 'fixed-cooldown' | 'queued' | 'request-based' | 'fee-or-queued'; /** * Parsed savings-vault entry. * * Models ERC-4626 yield wrappers — sUSDe, sUSDS, sDAI, stUSD, sUSDF, * siUSD, savUSD, syrupUSDC/USDT, wstUSR, yUSD, yoETH, … — under a * uniform shape. Structurally compatible with * [`buildVaultLookup`](../lookup.ts) (same load-bearing fields as * `LstShareToken`, `FluidFToken`, `EulerEarnVault`). * * Most entries are real ERC-4626 (asset-keyed share token with * `convertToAssets` / `totalAssets`). A few wrap a rebasing * underlying (wstUSR, wsrUSD); those are still 4626 on the wrapper * but their `asset()` returns a rebasing token, which downstream * `priceUsd` math should be aware of. */ interface SavingsVault extends VaultClassificationFields { /** Share-token contract address, lowercased. */ address: string; /** Underlying ERC-20 address, lowercased. */ underlying: string; /** Share-token symbol, e.g. `sUSDe`, `sUSDS`, `sDAI`. */ symbol: string; /** Share-token name as returned by `name()`, or a falsy-safe * fallback `${brand} ${symbol}`. */ name: string; /** Cross-provider UI label — `${brand} ${symbol}` * (e.g. `Ethena sUSDe`, `Sky sUSDS`). Always non-empty. */ displayName: string; /** Brand label — `Ethena`, `Sky`, `Maker`, `Angle`, `Falcon`, … */ brand: string; /** Curated user-facing explainer (from the registry): what the * underlying is, where the yield comes from, and the exit * mechanics / trust caveat when load-bearing. */ description: string; /** Alias for `brand` to match `curatorName` on the other providers' * vault types — keeps cross-provider UI code uniform. */ curatorName: string; /** Share decimals (most are 18; Maple syrup* are 6). */ decimals: number; /** Underlying asset decimals. Equals `decimals` for nearly every entry; * diverges for e.g. yUSD (18-dec shares over 6-dec USDC). Load-bearing * for share→asset formatting and the cross-provider `sharePrice` * (parity with the LST/Lagoon/Yearn providers). */ assetDecimals: number; /** Total underlying held, raw integer (wei-like) as string. From * `totalAssets()`. */ totalAssets: string; /** Total shares minted, raw integer as string. */ totalSupply: string; /** Raw `convertToShares(10**decimals)` — 1 underlying → X shares. */ convertToShares: string; /** Raw `convertToAssets(10**decimals)` — 1 share → X underlying. */ convertToAssets: string; /** 1e18-scaled exchange rate: 1 share → X underlying. Derived from * `convertToAssets(10**decimals)` regardless of share decimals. */ exchangeRate: string; /** Supply APR in percent (e.g. `7.42` = 7.42 %). Sourced from the * matching intrinsic-yield fetcher. */ supplyRate: number; /** Extra rewards APR in percent — usually 0; populated when the * protocol layers a separate reward stream on top. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor actually * earns. */ depositRate: number; /** * Incentive APR the vault's POSITION earns that a depositor does NOT, in * percent. Absent ⇒ not established; `0` ⇒ established and currently none. * * Deliberately outside `rewardsRate`/`depositRate`, which are what a * depositor actually earns — this is the opposite claim, and summing it in * would overstate every affected row. * * Venus's Liquidity Hub is the case it exists for. Its Core YieldGroup holds * vTokens, so XVS supply emissions accrue to that contract inside the * Comptroller — but nothing on the Hub, the YieldGroup or `AdapterCoreV1` * claims them, and nothing routes them into `totalAssets()`. After the * permissionless `claimVenus` the XVS sits on the YieldGroup and can only * leave through an ACM-gated `sweep`. The value is real, measurable, and * not the depositor's. * * Publishing it is a MONITOR, not a headline. Every Core market these Hubs * use read `venusSupplySpeeds == 0` at integration, so today this is `0` — * and the day Venus turns emissions back on, this is the only field on the * row that moves. */ strandedRewardsRate?: number; /** Whether the share token implements ERC-4626. True for every entry * except Native's wNLP, which is a bespoke wrapper (`asset()`, * `totalAssets()` and `convertToAssets()` all revert) — the * `convertTo*` / `exchangeRate` fields below are still populated for * it, derived from its own rate getter. Parity with * `LstShareToken.isErc4626`. */ isErc4626: boolean; /** Whether the share token itself rebases. False for nearly every * savings vault (they're the non-rebasing wrapper); rebasing * surfaces sit on the underlying (e.g. USDe inside sUSDe). */ isRebasing: boolean; /** Whether deposits are permissionless on-chain. False for entries * whose mint surface is allowlist-gated. */ isMintable: boolean; /** Mint entry — usually equals `address` (deposit on the share * token directly via ERC-4626 `deposit`/`mint`). May differ for * protocols with a dedicated minter / zap contract. */ mintContract?: string; /** * `true` ⇒ the protocol's mint AND redeem are permissioned (KYC / * allowlist), but the share token transfers freely and has real secondary * depth — so the route in and out is a TRADE, priced by the market rather * than by `exchangeRate` (which stays the NAV). Both legs need a slippage * bound. Absent ⇒ the ordinary case: `isMintable` alone describes entry. */ secondaryMarketOnly?: boolean; /** Withdrawal mechanism. */ withdrawalMode: SavingsWithdrawalMode; /** Waiting period in seconds before a requested redemption can be * claimed. For `fixed-cooldown` entries (Ethena, Avant) this is the * registry-pinned value. For `fee-or-queued` entries it is the * **live** on-chain queue window, which varies per asset (Native * runs 8 h on some pools and 3 days on most). */ withdrawalCooldownSeconds?: number; /** * Exit fee in basis points (`10` = 0.10 %) — same units and name as * `GearboxV3Pool.withdrawFeeBps`, so consumers read one field across * providers. * * **How it is charged** (Native): it is *not* a deposit fee, a * management fee, or a skim on yield — `exchangeRate` and `supplyRate` * are already net of everything Native takes on the way in. It is a * one-off haircut on the **instant** exit only, taken out of the * underlying paid to the receiver: * * received = shares × exchangeRate × (1 − withdrawFeeBps/10_000) * * so redeeming 9,867.98 wNLP-USDC worth 10,000 USDC returns 9,900 USDC * at 100 bps. The shares burn in full — the fee is deducted from the * payout, never charged as a separate transfer, so a caller does not * need to fund it or approve anything extra. * * The **queued** leg (`withdrawQueue`, after * `withdrawalCooldownSeconds`) pays out at par and does not touch this * field. Its cost is implicit instead: the payout is snapshotted when * the request is made, so yield accruing during the wait goes to the * protocol rather than the requester. * * `0` means the instant leg is free. Absent when the vault has no * instant leg at all. */ withdrawFeeBps?: number; /** * ENTRY fee in basis points — the mirror of `withdrawFeeBps`, same units and * same discipline: absent means NOT READ, `0` means read and currently free. * * Charged out of the deposit before shares are minted, so a depositor funds * `assets` and is credited on `assets × (1 − bps/10_000)`. `convertToShares` * on this row is already net of it wherever a reader publishes both, so a * consumer sizing an entry does not need to apply it twice. * * Populated only by readers whose protocol exposes the dial. Saturn's * `sUSDat` is the case: 0 at integration against a `MAX_DEPOSIT_FEE_BPS` of * 500, on a `DEFAULT_ADMIN_ROLE` setter, while its docs claim 10 bps. */ depositFeeBps?: number; /** * Seconds a FRESH deposit earns nothing before the rate applies. * * Distinct from every withdrawal delay on this type: the money is free to * leave the whole time, it simply does not earn yet. Frankencoin's savings * modules stamp a new account 3 days forward (`INTEREST_DELAY`), and a * top-up re-weights the whole position's clock pro-rata. Surfaces as * `termSheet.supply.rate.warmupSecs`. */ yieldWarmupSeconds?: number; /** `linear` when the accrual does not compound on its own; absent ⇒ * compounding, which is right for any growing share price. */ accrual?: 'linear' | 'compounding'; /** `false` when a deposit needs NO ERC-20 approval (Frankencoin's modules * are registered minters and already hold an implicit infinite allowance). */ needsDepositApproval?: boolean; /** * How a USER's position here is read. Absent ⇒ `balanceOf` on the share * token. `savings-account` marks an address that is NOT a token — the * Frankencoin module keeps an internal ledger and `balanceOf` reverts on it. */ balanceKind?: 'erc20' | 'savings-account'; /** Whether the instant leg is enabled at all — some assets are * queue-only. When `false`, `liquidity` is `0` regardless of the * protocol's inventory and `withdrawFeeBps` is unreachable. */ instantRedeemEnabled?: boolean; /** Contract the instant leg draws from — Native's per-chain * `CreditVault`. Its underlying balance is what `liquidity` * measures. */ inventoryContract?: string; /** Contract a delayed redemption is requested from and claimed * against, when it is not the share token itself. */ withdrawQueue?: string; /** * Every way out, one entry per route — see {@link VaultExitRoute}. * * The structured form of what `withdrawalMode` + `withdrawFeeBps` + * `withdrawalCooldownSeconds` + `liquidity` encode between them. It exists * because a two-legged exit is a CHOICE, and the fields a consumer needs to * make it (which leg charges what, which one it is big enough to use, what * the instant one can settle right now) are otherwise spread across four * places with no marker saying which belongs to which leg. */ exitRoutes: VaultExitRoute[]; /** * Underlying still **depositable this block**, raw integer string — * the mirror of `liquidity` on the entry side. * * Absent for every vault whose mint is uncapped (the normal case), so * `undefined` means "no known limit", **not** zero. `'0'` is a real * statement: the vault is full and a deposit reverts. * * Deliberately separate from `isMintable`, which is a *permission* * (allowlist / KYB gate). A vault can be freely mintable and still have * no room — Yield Basis's markets are permissionless yet sit at * 78–103 % of a protocol-wide cap, with one already over it. Only * readers whose protocol exposes a real cap populate this; there is no * `maxDeposit()` to read on those, which is why it is a reader concern * rather than a generic 4626 call. */ depositCapacity?: string; /** Human-formatted `depositCapacity`. Absent whenever that is. */ depositCapacityFormatted?: number; /** `depositCapacity` in USD. Absent when uncapped or unpriced. */ depositCapacityUsd?: number; /** * Signed basis-point gap between what a share **redeems for** and its * **fundamental** (oracle-anchored) value: `exchangeRate / * fundamental − 1`. Negative = redeeming below fundamental value. * * Populated only where the protocol publishes two per-share prices and * we quote the redeemable one. Yield Basis's TRD (Temporary Redemption * Discount) is the case: `preview_withdraw` prices a live Cryptoswap * unwind while `pricePerShare` reads the oracle, and the gap widens * during volatility before arbitrage closes it, typically within hours. * A few basis points either way is the normal state — it went positive * on three of four markets at integration. * * Absent for every single-priced vault, where `exchangeRate` is the * only per-share number there is. */ redemptionDiscountBps?: number; /** * Curated loss-waterfall / backing classification, straight from the * registry entry. Absent ⇒ not assessed (NOT "safe"). * * Surfaced on the row because the registry is not reachable from a consumer: * everything downstream — the earn projection, the recorder, the term sheet — * sees only this object, so a classification that stays in the registry is a * classification nobody can act on. */ solvency?: CounterpartyTerms['solvency']; /** * NAV feed address for share prices an operator PUBLISHES rather than the * contract computing them (Re, Apyx). Present exactly for the `nav-oracle` * reader's entries. * * This is the only signal that separates a governance-set savings rate from * an attested one, and without it on the row every NAV vault downstream read * as an ordinary managed rate — the distinction the whole `nav-attested` * trust class exists to draw. */ navOracle?: string; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10**decimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; /** Withdrawable underlying **right now**, raw integer string, per * `withdrawalMode`: * - `instant` — fully liquid (`= totalAssets`). * - `instant-capped` — settles in the same transaction, but only up * to a live inventory that is smaller than the vault: Spark * Savings V1 is capped by the PSM3 pocket's underlying balance * (6–29 % of TVL on the L2 deployments) and Spark Vaults V2 by the * vault's own idle balance (the rest is lent out through the Spark * Liquidity Layer). No fee and no cooldown on this leg — the * difference from `instant` is purely the size cap, and the * difference from `fee-or-queued` is that exceeding it costs * nothing extra, it simply cannot be done this block. * - `fee-or-queued` — the protocol's live instant-exit inventory, * clamped to `totalAssets`; `0` when the instant leg is disabled. * This is a **gross** figure: pulling it out instantly nets * `withdrawFeeBps` less (see that field). The queued leg is not * inventory-capped and pays at par, so `liquidity` is not a cap on * what the vault can ultimately return. * - `fixed-cooldown` / `queued` / `request-based` — `0`; these * require a waiting period. * A cooldown vault's underlying may still be redeemable after the * wait — this field is the *right now* figure, matching the * cross-provider `liquidity` semantic. */ liquidity: string; /** Human-formatted withdrawable liquidity. */ liquidityFormatted: number; /** Withdrawable liquidity in USD. */ liquidityUsd: number; /** * `liquidity / totalAssets`, clamped to `0…1` — the share of the vault a * holder could exit **this block**. `1 − instantLiquidityRatio` is the * share that must wait, so this is the vault's **lockup indicator**. * * Deliberately *not* called `utilization`. For a lending vault * utilization is `borrowed / supplied`, read from a debt accumulator; * none of these protocols expose one (Native's CreditVault and NTLP have * no debt getter at all, and the CreditVault commingles market-maker * collateral with pool inventory, so its balance can exceed the pool). * What this measures is exit **coverage**, which is the quantity that * actually predicts lockup — and unlike utilization it stays meaningful * for cooldown vaults that have no borrow side whatsoever. * * Reads per mode: * - `instant` → always `1` (fully liquid by construction). * - `fee-or-queued` → the live CreditVault coverage; the observed spread * across Native pools is the full `0…1` range, so it carries real * information (BNB `wNLP-T4B` sits near `0`, Ethereum `wNLP-USDC` at * `1`). Below `1` the remainder is not lost, just queued. * - `fixed-cooldown` / `queued` / `request-based` → always `0`; nothing * is redeemable without waiting. * * An empty vault reports `1` — there is nothing to be locked up. */ instantLiquidityRatio: number; } /** * Full parsed payload: per-share-token-address map. * * Keyed by lowercased share-token address (parity with Morpho / Silo * / Euler-Earn / LST). Many savings vaults share the same underlying * (sUSDS and sDAI both wrap stable-ish ERC-20s, syrupUSDC wraps USDC, * etc.) so per-share-address keying is the only sensible choice. */ type SavingsVaults = { [shareAddress: string]: SavingsVault; }; /** Returns the registry entries for a chain, or `[]` when unsupported. */ /** * How a user's position in `address` is read, from OUR registry. * * Deliberately a local lookup rather than a field on whatever payload built * the vault entry. Whether an address is an ERC-20 is a static protocol fact * we already know; sourcing it from the recorder origin — which is what * `/v1/data/vaults/user` does for the rest of a lookup entry — means the * answer silently becomes `undefined` for every origin-backed request, the * balance read falls back to `balanceOf`, and a Frankencoin savings position * reads as zero because `balanceOf` reverts on the module. * * Returns `undefined` for anything unregistered, which callers must treat as * the ordinary `balanceOf` shape. */ declare const savingsBalanceKind: (chainId: string, address: string) => "erc20" | "savings-account" | undefined; /** * Is this row one whose only route in and out — for a caller without the * protocol's KYC/allowlist — is a trade on the secondary market? * * The one source of truth for that question. `capabilities.ts` reads it to * publish `via: 'swap'` on both legs, and worker-api's vault routes read it to * build the trade instead of 4626 calldata the token does not implement. * Deriving it twice is how the advertisement and the route drift apart. */ declare const isSecondaryMarketOnly: (chainId: string, address: string) => boolean; /** What a secondary-market row trades as: the pair's default other side (its * registered underlying, i.e. what a mint would have taken) and the symbol, * so a refusal can name the token instead of a category. */ declare const secondaryMarketVault: (chainId: string, address: string) => { underlying: string; symbol: string; } | undefined; /** Every savings vault we register on a chain, addresses lowercased. */ declare const savingsAddresses: (chainId: string) => string[]; /** * Deposit availability mode reported by the Lagoon API (`state.syncMode`). * * Lagoon vaults are ERC-7540 async vaults, but expose an optional * synchronous deposit (`syncDeposit`) when the NAV is fresh * (`isTotalAssetsValid()`). `syncMode` summarises which entrypoints are * currently usable: * - `Sync` — only synchronous deposits accepted right now * - `Async` — only the `requestDeposit` queue is open * - `Both` — either path works * Redeems are always async (request → settle → claim). Kept as a string * union with a fallback so an unseen value doesn't break parsing. */ type LagoonSyncMode = 'Sync' | 'Async' | 'Both' | (string & {}); /** * Parsed Lagoon vault entry. * * Lagoon vaults are curator-run, ERC-7540 async vaults (DeFi / RWA * strategies) — NOT staking tokens. Unlike the ERC-4626 providers, the * share token decimals (18) frequently differ from the underlying asset * decimals (e.g. USDC 6, WBTC 8): the majority of Lagoon vaults are * 18-decimal shares over a non-18 asset. `assetDecimals` is therefore * load-bearing for any share→asset conversion and is carried through to * `VaultLookupEntry` so the user-balance endpoint formats assets * correctly. * * All public data (rate/NAV included) is sourced from the Lagoon public * GraphQL API (`https://api.lagoon.finance/query`) in one batched call * per chain — no multicall, no archival RPC. Mirrors the cross-provider * load-bearing fields (`address`, `underlying`, `symbol`, `name`, * `decimals`, `totalAssets`, `totalSupply`) so `buildVaultLookup` picks * it up without branching. */ interface LagoonVault extends VaultClassificationFields { /** Vault (share token) contract address, lowercased. */ address: string; /** Underlying ERC-20 address, lowercased. */ underlying: string; /** Share-token symbol, e.g. `tulipaUSDC`, `9SUSDC`. */ symbol: string; /** Share-token name as returned by the API / `name()`. */ name: string; /** Cross-provider UI label — `${curatorName ?? 'Lagoon'} ${symbol}`. */ displayName: string; /** Curator display name, when the API exposes one. */ curatorName?: string; /** Share-token decimals (18 for virtually every Lagoon vault). */ decimals: number; /** Underlying asset decimals — frequently differs from `decimals` * (USDC 6, WBTC 8). Required for correct share→asset math. */ assetDecimals: number; /** Total underlying held by the vault, raw integer as string * (asset-decimal scaled). From `state.totalAssets`. */ totalAssets: string; /** Total shares minted, raw integer as string (share-decimal scaled). */ totalSupply: string; /** Raw `pricePerShare` — 1 share → X underlying, asset-decimal scaled * (`convertToAssets(10**shareDecimals)`). */ pricePerShare: string; /** USD value of one share, when the API provides it. */ pricePerShareUsd?: number; /** Supply APR in percent (e.g. `10.37` = 10.37 %). Time-weighted net * APR from the Lagoon API — see `aprWindow`. */ supplyRate: number; /** Extra rewards APR in percent. 0 for v1 (incentives/airdrops not * yet decomposed from the headline rate). */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor earns. */ depositRate: number; /** Which API window `supplyRate` was taken from (`monthly` by * default, falling back through `weekly → yearly → inception`). */ aprWindow: 'weekly' | 'monthly' | 'yearly' | 'inception' | 'none'; /** All TWRR net APR windows from the API, percent. Null when the * window has insufficient history. */ apr: { weekly: number | null; monthly: number | null; yearly: number | null; inception: number | null; }; /** True when the vault only accepts async (`requestDeposit`) deposits * right now (`state.isAsyncOnly`). */ isAsyncOnly: boolean; /** Current deposit-path availability — see {@link LagoonSyncMode}. */ syncMode: LagoonSyncMode; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10**assetDecimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; } /** * Full parsed payload: per-vault-address map (parity with Morpho / Silo * / Euler-Earn / LST). Keyed by lowercased vault address. */ type LagoonVaults = { [vaultAddress: string]: LagoonVault; }; /** * One-shot fetcher for all visible Lagoon vaults on a chain. * * HTTP-only (Lagoon public GraphQL API) — no multicall executor needed, * mirroring the Morpho fetcher. Returns an empty map for chains without * a Lagoon deployment. * * @param chainId target chain * @param prices optional price map keyed by oracle key / underlying * @param tokenList optional token list for hydrating `asset` metadata * * @returns `LagoonVaults` — map keyed by lowercased vault address. */ declare const fetchLagoonVaults: (chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList) => Promise; /** * Lagoon public GraphQL API client. * * One batched query returns every visible Lagoon vault on a chain with * its full state (NAV, totalAssets/Supply, time-weighted APR windows, * deposit-mode flags). This is the data-of-record for Lagoon: pricing * and yield come straight from the API, so the public-data fetch needs * no multicall and no archival RPC (Lagoon's on-chain history is only * available via pruned RPCs / the subgraph — the API serves it * directly). * * Endpoint and chain coverage: * https://api.lagoon.finance/query (GraphQL, no auth) * Chains are gated by {@link LAGOON_CHAIN_IDS} so we don't fire a query * at chains where Lagoon has no factory. The API itself returns an empty * page for an unknown chain, but short-circuiting avoids the round-trip. */ declare const LAGOON_API_URL = "https://api.lagoon.finance/query"; /** * Chains with a Lagoon vault factory (per * docs.lagoon.finance/resources/networks-and-addresses). Numeric chain * ids as strings. Extend as Lagoon deploys to more chains — the parser * is chain-agnostic, this set only decides whether to issue the query. */ declare const LAGOON_CHAIN_IDS: Set; declare const hasLagoonVaults: (chainId: string) => boolean; /** Shape of a single `vaults.items[]` entry in the API response. */ interface LagoonApiVault { address: string | null; name: string | null; symbol: string | null; decimals: number | null; isVisible: boolean | null; asset: { address: string | null; symbol: string | null; decimals: number | null; } | null; /** Curator(s) running the vault; first entry is the primary. */ curators: ({ id: string | null; name: string | null; } | null)[] | null; state: { totalAssets: string | null; totalAssetsUsd: number | null; totalSupply: string | null; pricePerShare: string | null; pricePerShareUsd: number | null; isAsyncOnly: boolean | null; syncMode: string | null; weeklyApr: { twrrNetApr: number | null; } | null; monthlyApr: { twrrNetApr: number | null; } | null; yearlyApr: { twrrNetApr: number | null; } | null; inceptionApr: { twrrNetApr: number | null; } | null; } | null; } /** * Fetch every visible Lagoon vault on a chain, paginating until the API * returns a short page. Returns the raw API items; normalization to * `LagoonVault` happens in `fetchPublic`. */ declare function fetchLagoonApiVaults(chainId: string): Promise; /** * Parsed Aave Earn vault entry. * * Aave Earn ("stable") vaults are curator-run, standard ERC-4626 * yield-bearing wrappers over an Aave v3 supply position (the vault * supplies its assets into an Aave reserve and skims a performance fee * on the yield). Shares are minted 1:1 in decimals with the underlying, * so `decimals === assetDecimals` for every entry today — but we still * carry `assetDecimals` explicitly to stay uniform with the other * providers and survive a future divergent vault. * * Data provenance is hybrid: * - The Aave public GraphQL API (`https://api.v3.aave.com/graphql`) * supplies discovery, identity, the underlying reserve, `vaultApr` * and `totalAssets` (the reserve `balance`). * - A light on-chain multicall supplies `totalSupply`, the share * `decimals`, and `convertToAssets` (share price) — none of which the * API exposes on the generic `Vault` type. * * Mirrors the cross-provider load-bearing fields (`address`, * `underlying`, `symbol`, `name`, `decimals`, `totalAssets`, * `totalSupply`) so `buildVaultLookup` picks it up without branching. */ interface AaveEarnVault extends VaultClassificationFields { /** Vault (share token) contract address, lowercased. */ address: string; /** Underlying ERC-20 address, lowercased (the reserve's `underlyingToken`). */ underlying: string; /** Share-token symbol, e.g. `steakUSDC`. */ symbol: string; /** Share-token name as returned by the API (`shareName`). */ name: string; /** Cross-provider UI label — `${curatorName ?? 'Aave'} ${symbol}`. */ displayName: string; /** Curator display name, when resolvable from the vault owner. */ curatorName?: string; /** Vault owner (curator) address, lowercased. */ owner: string; /** Share-token decimals (equals `assetDecimals` for aToken vaults). */ decimals: number; /** Underlying asset decimals. */ assetDecimals: number; /** Total underlying held by the vault, raw integer as string * (asset-decimal scaled). From the reserve `balance` (`totalAssets`). */ totalAssets: string; /** Total shares minted, raw integer as string. From on-chain * `totalSupply()`. */ totalSupply: string; /** Raw `convertToAssets(10**shareDecimals)` — 1 share → X underlying, * asset-decimal scaled. From on-chain `convertToAssets`. Folded into the * uniform `sharePrice*` fields by `stampVaultClassification`. */ convertToAssets: string; /** Raw `convertToShares(10**underlyingDecimals)` — 1 underlying → X shares. */ convertToShares: string; /** Supply APR in percent (e.g. `4.25` = 4.25 %). Aave `vaultApr` * (already net of the vault fee). */ supplyRate: number; /** Extra rewards APR in percent. 0 for v1. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor earns. */ depositRate: number; /** Vault performance fee, fraction (e.g. `0.1` = 10 %). From `fee`. */ fee: number; /** True — Aave Earn vaults are standard synchronous ERC-4626 vaults. */ isErc4626: true; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit (API `usdPerToken`, else prices map). */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10**assetDecimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; } /** * Full parsed payload: per-vault-address map (parity with the other * providers). Keyed by lowercased vault address. */ type AaveEarnVaults = { [vaultAddress: string]: AaveEarnVault; }; /** * Parsed Upshift vault entry. * * Upshift vaults are curator-run ERC-4626-style yield vaults exposed via * a single public REST listing (`https://app.upshift.finance/api/proxy/ * vaults`). The deposit contract (`vaultAddress`) and the receipt/share * token a depositor actually holds (`address`) can differ on the newer * `evm-2` vaults, so we key the vault by the **receipt** token — that's * what shows up in a user's wallet and what the user-balance lookup * resolves against. * * Mirrors the cross-provider load-bearing fields (`address`, * `underlying`, `symbol`, `name`, `decimals`, `totalAssets`, * `totalSupply`) so `buildVaultLookup` picks it up without branching. * Share and underlying decimals can diverge, so `assetDecimals` is * carried through for correct share→asset math. */ interface UpshiftVault extends VaultClassificationFields { /** Receipt / share token address (held by depositors), lowercased. */ address: string; /** Deposit (vault) contract address, lowercased. Equals `address` on * older single-token vaults. */ vaultAddress: string; /** Underlying ERC-20 address (first accepted deposit asset), lowercased. */ underlying: string; /** * EVERY asset the vault accepts, lowercased — `underlying` is only the * first of them. * * The entry is genuinely multi-asset (`deposit(asset, amount, receiver)` * names the asset it pulls), and several vaults take four or five: Clearstar * Prism accepts USDC, USDT, USDS, deUSD and sdeUSD. Publishing only the * first made the other legs unreachable — a caller paying USDT was told the * vault takes USDC, which is true and not the whole truth. */ depositAssets: string[]; /** Receipt-token symbol, e.g. `earnAUSD`. */ symbol: string; /** Vault display name as returned by the API. */ name: string; /** * Curator display name (`strategists[0].name`) — the firm that actually runs * the strategy, e.g. `RockawayX`, `K3 Capital`, `Gamma Research`. * * Upshift is a PLATFORM: 13 distinct curators across the vaults we serve. * Absent this, every row rendered under the platform brand "Upshift", and * half the book carries a name that points at a third brand entirely (see * {@link UpshiftApiStrategist}). * * Deliberately NOT folded into a `displayName` composite the way Lagoon does * it — `${curator} ${symbol}` is precisely the shape that collapsed four * distinct Gauntlet USDC vaults into one label on the Morpho surface. The * vault's own `name` is already distinct; the curator renders as its own * field. */ curatorName?: string; /** Curator brand logo, when the API carries one. */ curatorLogoURI?: string; /** Curator-authored explainer, when present. */ description?: string; /** Contract generation (`evm-0` | `evm-1` | `evm-2`). */ version?: string; /** Receipt-token decimals. */ decimals: number; /** Underlying asset decimals — may differ from `decimals`. */ assetDecimals: number; /** Total underlying held by the vault, raw integer as string * (asset-decimal scaled). */ totalAssets: string; /** Total shares minted, raw integer as string (share-decimal scaled). */ totalSupply: string; /** Derived `pricePerShare` — 1 share → X underlying, asset-decimal * scaled (`totalAssets * 10**shareDecimals / totalSupply`). `'0'` for * an empty vault. */ pricePerShare: string; /** Base supply APY in percent (e.g. `10` = 10 %). From `apy.apy`. */ supplyRate: number; /** Incentive / campaign APR in percent, when the API reports one * (`apy.campaignApy`). Kept separate from the base rate. */ rewardsRate: number; /** `supplyRate + rewardsRate` — total APR a depositor earns. */ depositRate: number; /** All APY components reported by the API (percent), for transparency. */ apy: { base: number | null; campaign: number | null; points: number | null; underlying: number | null; }; /** * EFFECTIVE performance fee in percent — `0` when the curator has waived it. * Read by the term-sheet builder as `input.fee`. * * Ranges 0–20 % across the book (NEMO USDC Prime charges 20 % + 2 %). Until * this was wired, every Upshift row published an empty fee list, which reads * as "free". */ fee: number; /** * EFFECTIVE management fee in percent — `0` when waived. Charged on assets * and ongoing, unlike `fee`, which is charged on yield. */ managementFee: number; /** * The fee SCHEDULE behind the effective numbers above — what the curator may * charge, and whether a waiver is currently suppressing it. * * Kept because a waiver is revocable: `fee: 0` alongside * `feeDetail.performanceStanding: 10` is a materially different statement * from a vault that charges nothing by design, and five vaults we serve are * in exactly that state. */ feeDetail: { performanceStanding: number; managementStanding: number; performanceWaived: boolean; managementWaived: boolean; /** Bounds on the waiver, when the API states one. */ performanceWaivedUntilDate?: string; managementWaivedUntilDate?: string; }; /** * Redemption delay in seconds (`lagDuration`) — 0 to 30 days across the * book. Feeds `SupplyExitTerms.cooldownSecs`. */ withdrawalCooldownSeconds: number; /** * Exit mode, set ONLY where the vault proves something other than the * provider default. Absent ⇒ the `upshift` trait default (`request-based`) * applies, which is right for every vault without an instant leg. */ withdrawalMode?: 'request-based' | 'fee-or-queued'; /** Instant-leg spread in bps, when an instant redeem is configured. */ withdrawFeeBps?: number; /** True when an instant redeem path exists AND is not paused. */ instantRedeemEnabled?: boolean; /** Whether deposits are currently paused. */ isDepositPaused: boolean; /** Whether withdrawals are currently paused. */ isWithdrawalPaused: boolean; /** * The vault is in WIND-DOWN (`withdrawalOnly`) — it accepts no new capital * even though nothing is "paused". Surfaced as `isClosed` to the term-sheet * builder so availability answers `canDeposit: false` with a reason, rather * than presenting a closing vault as enterable. */ isClosed: boolean; /** * Room left for new deposits, RAW asset-scaled base units. * `undefined` ⇒ uncapped, `'0'` ⇒ full — the contract `depositCapacity` * already carries elsewhere. * * Derived from `depositCap` (the `evm-2` generation) falling back to * `maxSupply` (the older ones, where `depositCap` is null). `maxDepositAmount` * is deliberately NOT used: it is a per-deposit ceiling, not a vault total. */ depositCapacity?: string; /** Hydrated underlying metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10**assetDecimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD. */ totalAssetsUsd: number; } /** * Full parsed payload — per-vault map keyed by lowercased receipt-token * address (parity with the other vault providers). */ type UpshiftVaults = { [vaultAddress: string]: UpshiftVault; }; /** * One-shot fetcher for the Upshift vaults on a chain (see * {@link isServable} for which of them we serve). * * HTTP-only (Upshift public REST listing) — no multicall executor needed, * mirroring the Lagoon fetcher. The endpoint is global; we filter to the * requested chain. Returns an empty map for chains without an Upshift * deployment. * * @param chainId target chain * @param prices optional price map keyed by oracle key / underlying * @param tokenList optional token list for hydrating `asset` metadata * * @returns `UpshiftVaults` — map keyed by lowercased receipt-token address. */ declare const fetchUpshiftVaults: (chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList) => Promise; /** * Upshift public vault listing. * * A single un-paginated REST endpoint returns every Upshift vault across * all chains (EVM and non-EVM) in one response. We fetch it once and * filter to the requested EVM chain in `fetchPublic`. Pricing and yield * come straight from the API, so the public-data fetch needs no multicall * and no archival RPC. * * Endpoint (no auth): * https://app.upshift.finance/api/proxy/vaults */ declare const UPSHIFT_VAULTS_URL = "https://app.upshift.finance/api/proxy/vaults"; /** * EVM chains where Upshift has visible vaults. Numeric chain ids as * strings. The endpoint is global (returns every chain regardless), so * this set only short-circuits the round-trip on chains Upshift doesn't * cover. Extend as Upshift deploys to more chains — the parser is * chain-agnostic. */ declare const UPSHIFT_CHAIN_IDS: Set; declare const hasUpshiftVaults: (chainId: string) => boolean; /** A `depositAssets[]` entry — an accepted underlying token. */ interface UpshiftApiAsset { address: string | null; symbol: string | null; decimals: number | null; } /** An amount the API reports in both scales. `raw` is ASSET-decimal scaled. */ interface UpshiftApiAmount { normalized: string | null; raw: string | null; } /** * A `strategists[]` entry — **the curator**. * * Upshift is a curator PLATFORM, not a single operator: 26 visible vaults on * the chains we serve are run by 13 different firms. `name` is the only usable * handle — `address` is the literal string `"0x"` on every vault in the * listing, so there is nothing to key a curator registry on. * * Load-bearing because the vault NAME is not a substitute: half the book * (13 of 26 rows, $137.6M) is named after a brand that is NOT its curator — * "Tori Ecosystem Vault" is run by RockawayX, "Kelp Gain" by K3 Capital, * "Hyperbeat Ultra HYPE" by UltraYield. Dropping this field printed all of * them under the platform brand, which is the same misattribution that put a * SwissBorg vault under Gauntlet's name on the Morpho surface. */ interface UpshiftApiStrategist { name: string | null; logo: string | null; address: string | null; type: string | null; website_url: string | null; } /** * The `fees` block. * * **Read the waiver flags, never the bare rate.** Five of the vaults we serve * publish a non-zero `performance` alongside `isPerformanceWaived: true` — the * headline rate is the schedule the curator MAY charge, not what is charged * today. The `…WaivedUntilDate` / `…WaivedUntilTvl` companions bound the * waiver; both are null or 0 across the current book, so they are carried * through but not yet acted on. * * Both rates are PERCENT (`10` = 10 %), matching the package convention. */ interface UpshiftApiFees { performance: number | null; management: number | null; isPerformanceWaived: boolean | null; isManagementWaived: boolean | null; performanceFeeWaivedUntilDate: string | null; performanceFeeWaivedUntilTvl: number | null; managementFeeWaivedUntilDate: string | null; managementFeeWaivedUntilTvl: number | null; } /** * `instant_redeem_config` — the optional immediate exit, present on one vault * today (Upshift Clear RWA). Null on every other row, which is why the * provider default stays `request-based`. */ interface UpshiftApiInstantRedeem { subaccountAddress: string | null; outputAssetSymbol: string | null; redeemableAssets: ({ symbol: string | null; spreadBps: number | null; } | null)[] | null; isPaused: boolean | null; availableLiquidity: number | null; } /** Shape of a single `data[]` entry in the API response. */ interface UpshiftApiVault { chainId: number | null; address: string | null; name: string | null; decimals: number | null; status: string | null; isVisible: boolean | null; isDepositPaused: boolean | null; isWithdrawalPaused: boolean | null; apy: { apy: number | null; campaignApy: number | null; pointsApy: number | null; underlyingApy: number | null; } | null; depositAssets: (UpshiftApiAsset | null)[] | null; totalAssets: UpshiftApiAmount | null; totalSupply: UpshiftApiAmount | null; receipt: { symbol: string | null; address: string | null; decimals: number | null; } | null; latest_reported_tvl: number | null; /** The curator — see {@link UpshiftApiStrategist}. One entry per vault. */ strategists: (UpshiftApiStrategist | null)[] | null; /** Curator cut — see {@link UpshiftApiFees}. */ fees: UpshiftApiFees | null; /** * Redemption delay in SECONDS. Varies 0 → 2,592,000 (30 days, both NEMO * vaults) across the book, so a single provider-level exit mode describes * none of them: without this a 30-day lock and a same-day exit render * identically. */ lagDuration: number | null; /** * The vault is in WIND-DOWN — withdrawals only, deposits are pointless even * though `isDepositPaused` is false. True on Hyperbeat Ultra HYPE today. * Distinct from a pause, which is temporary by implication. */ withdrawalOnly: boolean | null; /** * Total deposit cap, asset-scaled. Present on the `evm-2` generation; the * older `evm-0`/`evm-1` vaults leave it null and cap via `maxSupply` * instead. */ depositCap: UpshiftApiAmount | null; /** Per-deposit maximum on the older generations. NOT a vault-level cap — * deliberately unused for capacity, which needs the total. */ maxDepositAmount: UpshiftApiAmount | null; /** Vault-level cap on the older generations; a `1e12` sentinel meaning * "uncapped" wherever `depositCap` carries the real number. */ maxSupply: UpshiftApiAmount | null; /** Optional immediate exit — see {@link UpshiftApiInstantRedeem}. */ instant_redeem_config: UpshiftApiInstantRedeem | null; /** Contract generation: `evm-0` | `evm-1` | `evm-2`. */ version: string | null; /** Curator-authored explainer. */ description: string | null; } /** * Fetch the global Upshift vault listing. Returns the raw `data[]` array; * chain filtering and normalization to `UpshiftVault` happen in * `fetchPublic`. */ declare function fetchUpshiftApiVaults(): Promise; /** * Yearn V3 vault kind, as reported by yDaemon. * - `Single Strategy` — a TokenizedStrategy (single allocation) * - `Multi Strategy` — a VaultV3 allocating across strategies * Kept as a string union with a fallback so an unseen value doesn't * break parsing. */ type YearnVaultKind = 'Single Strategy' | 'Multi Strategy' | (string & {}); /** * Parsed Yearn V3 vault entry. * * Yearn V3 vaults (VaultV3 + TokenizedStrategy) are standard ERC-4626 * yield vaults — `deposit`/`redeem`/`convertToAssets`, monotonic NAV. * Discovery + all public data (USD TVL, realized APR, price-per-share) * come from Yearn's yDaemon API in one batched call per chain — no * multicall, no archival RPC (parity with the Lagoon fetcher). * * Share decimals usually equal the underlying's, but yDaemon reports * both, so `assetDecimals` is carried through to `VaultLookupEntry` for * correct share→asset formatting. Mirrors the cross-provider * load-bearing fields (`address`, `underlying`, `symbol`, `name`, * `decimals`, `totalAssets`, `totalSupply`) so `buildVaultLookup` picks * it up without branching. */ interface YearnVault extends VaultClassificationFields { /** Vault (share token) contract address, lowercased. */ address: string; /** Underlying ERC-20 address, lowercased. */ underlying: string; /** Share-token symbol, e.g. `yvUSDC-1`, `yvWETH-2`. */ symbol: string; /** Share-token name as returned by the API / `name()`. */ name: string; /** Cross-provider UI label — `${curatorName ?? 'Yearn'} ${asset.symbol}`. */ displayName: string; /** Curator label — always `'Yearn'` (Yearn-curated product). */ curatorName: string; /** Share-token decimals. */ decimals: number; /** Underlying asset decimals — carried explicitly (usually equals * `decimals`, but kept for correct share→asset math). */ assetDecimals: number; /** Total underlying held, raw integer as string (asset-decimal scaled). * From `tvl.totalAssets`. */ totalAssets: string; /** Total shares minted, raw integer as string. Derived from * `totalAssets * 10**decimals / pricePerShare` (yDaemon doesn't * report supply directly). */ totalSupply: string; /** Raw `pricePerShare` — `convertToAssets(10**decimals)`, asset-scaled. */ pricePerShare: string; /** Supply APR in percent (e.g. `5.12` = 5.12 %). Yearn's realized net * APR (`apr.netAPR`), falling back to the forward APR for vaults with * no realized history yet. Net of the performance fee. */ supplyRate: number; /** Extra staking-rewards APR in percent (`apr.extra.stakingRewardsAPR`), * 0 when none. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor earns. */ depositRate: number; /** Performance fee in percent (e.g. `10` = 10 %). From * `apr.fees.performance`. */ fee: number; /** True when `supplyRate` came from the forward (expected) APR rather * than realized history — i.e. the vault is too new to have a TWRR. */ isForwardApr: boolean; /** Vault kind — see {@link YearnVaultKind}. */ kind: YearnVaultKind; /** yDaemon semver, e.g. `3.0.4`. */ version: string; /** Yearn product category, e.g. `Pendle`, `Curve`. Undefined when the * API doesn't classify it. */ category?: string; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit (`tvl.price`, or our price map). */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10**assetDecimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD (`tvl.tvl`, or formatted×price). */ totalAssetsUsd: number; /** Currently-withdrawable underlying, raw integer as string. Read * on-chain (best-effort): for single-strategy (TokenizedStrategy) * vaults from `availableWithdrawLimit`; for multi-strategy (VaultV3) * from `totalIdle + Σ strategy.maxWithdraw(vault)` over the default * queue. Clamped to `totalAssets`. **Absent** when the on-chain read * failed (no multicall, chain RPC down) — treat absent as "unknown", * not zero. */ liquidity?: string; /** Human-formatted withdrawable liquidity (`liquidity / 10**assetDecimals`). */ liquidityFormatted?: number; /** Withdrawable liquidity in USD (`liquidityFormatted × priceUsd`). */ liquidityUsd?: number; } /** * Full parsed payload: per-vault-address map (parity with Morpho / Silo * / Euler-Earn / Lagoon / LST). Keyed by lowercased vault address. */ type YearnVaults = { [vaultAddress: string]: YearnVault; }; /** * One-shot fetcher for all endorsed Yearn V3 vaults on a chain. * * Discovery is HTTP (yDaemon REST), then a best-effort on-chain multicall * adds withdrawable `liquidity` per vault (see {@link attachYearnLiquidity}). * Returns an empty map for chains without a Yearn V3 deployment. * * @param chainId target chain * @param multicallRetry provider-level multicall executor (used only for * the liquidity leg; discovery is HTTP) * @param prices optional price map keyed by oracle key / underlying * @param tokenList optional token list for hydrating `asset` metadata * * @returns `YearnVaults` — map keyed by lowercased vault address. */ declare const fetchYearnVaults: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList) => Promise; /** * Yearn V3 discovery via the public **yDaemon** API. * * yDaemon is Yearn's metadata service: one GET per chain returns every * vault it indexes — V3 *and* legacy V2 — already enriched with USD TVL, * realized APR, price-per-share and the underlying token. We fetch it * directly (no multicall, no archival RPC) and keep only the **V3** * vaults, mirroring the Lagoon fetcher's HTTP-only shape. * * Endpoint and chain coverage: * https://ydaemon.yearn.fi/{chainId}/vaults/all (REST, no auth) * Chains are gated by {@link YEARN_CHAIN_IDS} so we don't fire a request * at chains where Yearn has no V3 factory. yDaemon paginates with * `?limit=N&page=M` (1-indexed); we loop until a short page. * * V3 vs V2: every vault carries a `version`. V3 vaults are `3.x.y` * (`kind` is `Single Strategy` / `Multi Strategy`); V2/legacy are `0.x` * (`kind: Legacy`). We keep only `version` starting `3.` so this provider * stays a clean ERC-4626 (VaultV3 / TokenizedStrategy) surface. */ declare const YEARN_YDAEMON_BASE = "https://ydaemon.yearn.fi"; /** * Chains with endorsed Yearn **V3** vaults indexed by yDaemon (numeric * chain ids as strings). Verified against * `ydaemon.yearn.fi/{chainId}/vaults/all` — only chains that actually * return `version: 3.x` vaults are listed, so we don't waste a round-trip. * Extend as Yearn deploys V3 to more chains — the parser is * chain-agnostic, this set only decides whether to issue the request. */ declare const YEARN_CHAIN_IDS: Set; declare const hasYearnVaults: (chainId: string) => boolean; /** True when a yDaemon vault is a V3 (TokenizedStrategy / VaultV3) vault. */ declare const isYearnV3: (v: YDaemonVault) => boolean; /** Shape of a single yDaemon `/vaults/all` entry (fields we consume). */ interface YDaemonVault { address: string | null; /** Vault kind — `Single Strategy` | `Multi Strategy` (V3) | `Legacy` (V2). */ kind: string | null; symbol: string | null; name: string | null; /** Semver — `3.0.4`, `0.4.6`, … Drives the V3 filter. */ version: string | null; /** Yearn product category, e.g. `Pendle`, `Curve`, `Velodrome`. */ category: string | null; /** Share-token decimals. */ decimals: number | null; chainID: number | null; /** Curated/whitelisted by Yearn. We keep only endorsed V3 vaults. */ endorsed: boolean | null; /** Raw price-per-share — `convertToAssets(10**decimals)`, asset-scaled. */ pricePerShare: string | null; /** Underlying asset token. */ token: { address: string | null; symbol: string | null; name: string | null; decimals: number | null; } | null; tvl: { /** Total underlying held, raw integer string (asset-decimal scaled). */ totalAssets: string | null; /** TVL in USD (decimal number). */ tvl: number | null; /** USD price of one underlying unit. */ price: number | null; } | null; apr: { /** Realized net APR as a FRACTION (e.g. 0.0512 = 5.12 %). */ netAPR: number | null; fees: { /** Performance fee as a fraction (0.1 = 10 %). */ performance: number | null; management: number | null; } | null; extra: { /** Extra staking-rewards APR, fraction. */ stakingRewardsAPR: number | null; } | null; /** Forward-looking (expected) net APR, fraction. Fallback for new * vaults with no realized `netAPR` history yet. */ forwardAPR: { netAPR: number | null; } | null; } | null; } /** * Fetch every endorsed Yearn **V3** vault on a chain, paginating until * yDaemon returns a short page. Returns the raw yDaemon items already * filtered to V3 + endorsed and **deduped by address** (see * {@link PAGE_SIZE} — pages can overlap); normalization to `YearnVault` * happens in `fetchPublic`. */ declare function fetchYearnApiVaults(chainId: string): Promise; /** Hyperliquid HyperCore (L1) vault read types. */ /** Status of a HyperCore vault position w.r.t. its withdrawal lockup. */ type HypercoreLockStatus = 'locked' | 'withdrawable'; /** * A user's position in a single HyperCore vault, normalised from the * Hyperliquid info API (`userVaultEquities` + `vaultDetails`). * * HyperCore vaults are USDC-denominated and live on the Hyperliquid L1, * so amounts are decimal USD strings (not on-chain integers) and there * is no EVM share token. Deposits are subject to a lockup (typically * 4 days) before any withdrawal is allowed. */ interface HypercoreVaultPosition { /** HyperCore vault address (L1). Lowercased. */ vault: string; /** User's equity in the vault, decimal USD string (e.g. `"1234.56"`). */ equity: string; /** Unix seconds after which the user may withdraw, if known. */ lockupUntil?: number; /** Lock status derived from `lockupUntil` vs the provided `now`. */ lockStatus?: HypercoreLockStatus; /** Amount immediately withdrawable, decimal USD string, if known. */ maxWithdrawable?: string; /** User's PnL in the vault, decimal USD string, if known. */ pnl?: string; } interface HypercoreUserPositionsOptions { /** Override the info API base (defaults to mainnet). */ apiUrl?: string; /** Fetch per-vault `vaultDetails` for lockup/withdrawable (default true). */ withLockup?: boolean; /** `now` in unix seconds for deriving `lockStatus` (defaults to wall clock). */ nowSeconds?: number; } /** * Vault-level public data for a single HyperCore vault, normalised from * the `vaultDetails` info API. The listing counterpart to * {@link HypercoreVaultPosition} (which is per-user). * * HyperCore vaults are USDC-denominated perp/trading vaults — there's no * ERC-4626 share token, so TVL/PnL are decimal USD strings and the vault * is always `volatile` + `stable`-denominated. */ interface HypercoreVault extends VaultClassificationFields { /** Vault address (L1), lowercased. */ address: string; /** Display name. */ name: string; /** Vault leader / manager address, lowercased, if known. */ leader?: string; /** Total value locked in USDC, decimal string. */ tvlUsd: string; /** Annualised return as a fraction (e.g. `0.12` = 12%), when the API * reports one. */ apr?: number; /** Number of followers (depositors). */ followerCount?: number; /** Whether the vault is closed to new deposits. */ isClosed?: boolean; /** Underlying is always USDC for HyperCore vaults. */ underlyingSymbol: 'USDC'; /** Always volatile (perp/trading strategy). */ yieldProfile: 'volatile'; /** Always stable-denominated (USDC). */ denomination: 'stable'; } /** Vault-level listing keyed by lowercased address. */ type HypercoreVaults = { [address: string]: HypercoreVault; }; interface HypercoreVaultsFetchOptions { /** Override the info API base (defaults to mainnet). */ apiUrl?: string; /** Registry override — which vaults to list (defaults to the curated set). */ entries?: { address: string; name: string; }[]; } /** * Read a user's HyperCore vault positions from the Hyperliquid L1 info * API and normalise them to {@link HypercoreVaultPosition}. * * Two-stage: * 1. `userVaultEquities` → one call listing every vault the user holds * equity in (`{ vaultAddress, equity }`). * 2. (optional, default on) per-vault `vaultDetails` to read the * caller's `followers[]` entry for `lockupUntil` (ms → seconds), * `pnl`, and the vault's `maxWithdrawable`. Run in parallel. * * Off-chain and unproven on-chain — amounts are decimal USD strings as * returned by the L1. Per-vault `vaultDetails` failures degrade to a * position without lockup info rather than sinking the whole call. */ declare const getHypercoreUserPositions: (user: string, options?: HypercoreUserPositionsOptions) => Promise; /** * Fetch vault-level public data for the curated set of HyperCore vaults * via the Hyperliquid `vaultDetails` info API (read-only, no signing). * Returns a map keyed by lowercased address. Per-vault failures are * dropped rather than thrown. * * This is the listing counterpart to `getHypercoreUserPositions` — it * surfaces TVL / APR / leader for arbitrary HyperCore vaults, not just a * single user's positions. */ declare const fetchHypercoreVaults: (options?: HypercoreVaultsFetchOptions) => Promise; /** * Curated registry of Hyperliquid HyperCore vaults to surface as * public-data listings. * * HyperCore has no clean "list all vaults" info endpoint, so we pin a * curated set of addresses and read each via the `vaultDetails` info API * (see `fetchPublic.ts`). Seeded from the TradingStrategy dataset's * top HyperCore vaults by NAV — HLP (the protocol vault) plus the * largest leader/strategy vaults. Extend this list to track more. */ interface HypercoreVaultRegistryEntry { /** Vault address (L1), lowercased. */ address: string; /** Display name (fallback when `vaultDetails` omits it). */ name: string; } declare const HYPERCORE_VAULT_REGISTRY: HypercoreVaultRegistryEntry[]; declare const getHypercoreVaultRegistry: () => HypercoreVaultRegistryEntry[]; /** GMX V2 GM/GLV pool-token read types. */ /** * Which GMX product a listing represents. * - `gm` — a single GMX V2 market pool token (one index asset, backed by a * long + short token pair). * - `glv` — a GMX Liquidity Vault token: an auto-rebalancing meta-vault that * aggregates several GM markets sharing the same long/short pair. */ type GmxVaultKind = 'gm' | 'glv'; /** * Vault-level public data for a single GMX GM or GLV pool token, normalised * from the gmxinfra REST API (`/apy`, `/markets`, `/glvs`, `/tokens`). * * GM/GLV tokens are 18-decimal ERC-20s representing a share of a perp-DEX * liquidity pool. LPs earn trading + borrow fees but absorb trader PnL, so * NAV fluctuates and can draw down — hence always `volatile`. They are not * ERC-4626 and have no monotonic share/asset ratio, so (like HyperCore) they * are surfaced as a listing only, not folded into the generic vault lookup. */ interface GmxVault extends VaultClassificationFields { /** GM market token / GLV token address, lowercased. */ address: string; /** Display name from the API (e.g. `ETH/USD [WETH-USDC]`, `GLV [WBTC.b-USDC]`). */ name: string; /** GM market vs GLV vault. */ kind: GmxVaultKind; /** Share-token symbol — synthesised (`GM` / `GLV`); the API has no symbol. */ symbol: string; /** Share decimals. GM and GLV tokens are 18-decimal on GMX V2. */ decimals: number; /** Long-leg token address, lowercased. */ longToken: string; /** Short-leg token address (the quote leg, typically USDC), lowercased. */ shortToken: string; /** Index (priced) token address, lowercased. GM only — absent for GLV. */ indexToken?: string; /** Long-leg token symbol, when resolvable from the `/tokens` map. */ longSymbol?: string; /** Short-leg token symbol, when resolvable from the `/tokens` map. */ shortSymbol?: string; /** Total APY as a fraction (e.g. `0.16` = 16%) for the requested period. */ apy: number; /** Base (fee) APY as a fraction. */ baseApy: number; /** Bonus APR (incentives) as a fraction. */ bonusApr: number; /** Always volatile (perp-DEX liquidity provision). */ yieldProfile: 'volatile'; /** `stable` only when both legs are stablecoins; otherwise `volatile`. */ denomination: 'stable' | 'volatile'; /** USD price of one GM/GLV token, from `Reader.getMarketTokenPrice` / * `GlvReader.getGlvTokenPrice` (fed by `/prices/tickers`). Absent when * multicall pricing was unavailable (no `multicallRetry`, missing * oracle price, or a failed read). */ priceUsd?: number; /** Total pool value (TVL) in USD, from the same call. Absent under the * same conditions as {@link priceUsd}. */ tvlUsd?: number; /** * Withdrawal-side liquidity in USD — the most a holder of the entire vault * could withdraw right now: pool value minus the part reserved to back open * trader positions, capped at {@link tvlUsd}. For GLV this is an * approximation (the constituent markets' blended liquidity ratio applied to * the GLV's value). Absent when the reserve reads were unavailable. */ liquidityUsd?: number; /** * Remaining deposit capacity in USD — how much more liquidity the pool can * accept before GMX rejects the deposit. Per leg it's the unused room under * the absolute token cap (`MAX_POOL_AMOUNT`) and the USD cap * (`MAX_POOL_USD_FOR_DEPOSIT`), whichever binds first, summed across legs. * For GLV this also folds in the per-market GM-balance cap the GLV may hold * (`min(market room, GLV room)` across constituents) — an approximation * that ignores deposit price-impact. Absent when the cap reads were * unavailable or no cap is configured. */ depositCapacityUsd?: number; } /** Vault-level listing keyed by lowercased address. */ type GmxVaults = { [address: string]: GmxVault; }; interface GmxVaultsFetchOptions { /** APY averaging window passed to the API (default `90d`). */ period?: string; /** Override the gmxinfra API base for the chain (testing). */ apiUrl?: string; } /** * Per-chain minimum execution fees (wei) for the four GMX V2 GM/GLV * request types — what to pass as `executionFee` on the matching calldata * builder. Computed as `(baseGasLimit + opGasLimit * multiplier / 1e30) * * gasPrice`, mirroring the GMX SDK; GLV ops add `glvPerMarketGasLimit ×` * the chain's largest GLV market count. Gas price is volatile, so treat * these as fresh-at-fetch estimates (GMX refunds any overpayment). */ interface GmxExecutionFees { chainId: string; /** Gas price (wei) used for the estimate. */ gasPriceWei: string; deposit: string; withdrawal: string; glvDeposit: string; glvWithdrawal: string; } /** A user's GM/GLV token balance (raw ERC-20 units, 18-dec). */ interface GmxUserBalance { /** GM market token or GLV token address, lowercased. */ token: string; kind: GmxVaultKind; /** Raw integer balance string. */ balance: string; } /** * A pending (keeper-unexecuted) GM/GLV deposit request — the "ticket" * that mints GM/GLV once a keeper executes it. Cancellable until then via * the calldata-sdk `buildGmxCancel`. */ interface GmxPendingDeposit { /** DataStore request key (GM) — undefined for GLV deposits, which the * GlvReader returns without exposing the key. */ key?: string; kind: GmxVaultKind; account: string; receiver: string; /** GM market the request routes through. */ market: string; /** GLV token (GLV deposits only). */ glv?: string; initialLongToken: string; initialShortToken: string; initialLongTokenAmount: string; initialShortTokenAmount: string; /** Min GM out (GM) / min GLV out (GLV), raw. */ minOut: string; executionFee: string; /** Request creation time (unix seconds, as a string). */ updatedAtTime: string; } /** A pending GM/GLV withdrawal request (burn GM/GLV → long+short legs). */ interface GmxPendingWithdrawal { key?: string; kind: GmxVaultKind; account: string; receiver: string; market: string; glv?: string; /** GM/GLV token amount being redeemed, raw. */ tokenAmount: string; minLongTokenAmount: string; minShortTokenAmount: string; executionFee: string; updatedAtTime: string; } /** Combined GMX position view for one account on one chain. */ interface GmxUserPositions { chainId: string; account: string; /** Present only when the caller supplied tokens to balance-check. */ balances: GmxUserBalance[]; pendingDeposits: GmxPendingDeposit[]; pendingWithdrawals: GmxPendingWithdrawal[]; } interface GmxUserPositionsOptions { /** GM/GLV tokens to read `balanceOf` for (e.g. from the listing). When * omitted, `balances` is empty and only pending tickets are read. */ tokens?: { address: string; kind: GmxVaultKind; }[]; /** Max pending requests to enumerate per list (default 50). */ cap?: number; } /** * Fetch the GMX GM-market and GLV-vault listing for a chain from the * gmxinfra REST API. Returns a map keyed by lowercased token address. * * Each entry merges the `/apy` yield numbers with name + leg metadata from * `/markets` / `/glvs` and leg symbols from `/tokens`. Markets/GLVs that * report APY but lack metadata still surface (with a fallback name). GMX * vaults are always classified `volatile`; denomination follows the legs. * * Unsupported chains resolve to an empty map (not an error), so the universal * vault fetcher can request `gmx` on every chain without branching. * * When `multicallRetry` is supplied, each entry is additionally enriched with * `priceUsd` + `tvlUsd` via `Reader.getMarketTokenPrice` / * `GlvReader.getGlvTokenPrice` (fed by `/prices/tickers`). Pricing is * best-effort — a failure leaves the USD fields absent but never throws. */ declare const fetchGmxVaults: (chainId: string, multicallRetry?: MulticallRetryFunction, options?: GmxVaultsFetchOptions) => Promise; /** * Read a user's GMX position state on a chain: optional GM/GLV token * balances plus all pending (keeper-unexecuted) deposit/withdrawal * "tickets". * * GM tickets aren't exposed by a batch reader, so we enumerate the user's * request-key sets from the `DataStore` (`accountDeposit/WithdrawalListKey`) * and resolve each key via `Reader.getDeposit/getWithdrawal`. GLV tickets * have a convenience batch reader (`GlvReader.getAccountGlv*`), so those * are read directly. Balances are a plain `balanceOf` multicall. * * All sub-fetches run concurrently and degrade independently — a failing * reader yields an empty slice rather than sinking the whole call. Returns * empty arrays on unsupported chains. */ declare const getGmxUserPositions: (chainId: string, account: string, multicallRetry: MulticallRetryFunction, options?: GmxUserPositionsOptions) => Promise; interface TickerPrice { min: bigint; max: bigint; } type TickerPrices = Map; /** * Fetch the GMX oracle min/max prices from `/prices/tickers`. These are in * exactly the fixed-point form `getMarketTokenPrice` / `getGlvTokenPrice` * expect, so they can be fed straight in. Keyed by lowercased token address. */ declare const fetchGmxTickerPrices: (chainId: string, apiUrlOverride?: string) => Promise; interface GmxUsdValue { priceUsd: number; tvlUsd: number; /** Max withdrawable USD — what a holder of the whole vault could pull out * now: pool value minus the part reserved to back open trader positions, * capped at {@link tvlUsd}. The withdrawal-side liquidity. Absent when the * reserve reads failed; see {@link priceGmMarkets}. */ liquidityUsd?: number; /** Remaining deposit capacity in USD — how much more liquidity the market * can accept before hitting GMX's pool caps. Absent when the cap reads * failed or no cap is configured; see {@link priceGmMarkets}. */ depositCapacityUsd?: number; } interface GmMarketInput { marketToken: string; indexToken: string; longToken: string; shortToken: string; } /** * Value GM market tokens in USD via `Reader.getMarketTokenPrice`, fed the * index/long/short oracle prices from `/prices/tickers`. Markets missing an * oracle price are skipped. Returns a map (lowercased GM token → value). * Resolves to an empty map on any failure — pricing is best-effort. */ declare const priceGmMarkets: (chainId: string, multicallRetry: MulticallRetryFunction, markets: GmMarketInput[], prices: TickerPrices) => Promise>; /** * Value GLV tokens in USD. Two stages: `getGlvInfo` (each GLV → its * constituent GM markets), then `getGlvTokenPrice` with the per-market * index-price array assembled from `/prices/tickers`. Returns the value map, * the largest GLV market count (used to size the GLV execution-fee gas * estimate), and each GLV's constituent GM markets (lowercased) so callers * can approximate GLV withdrawable liquidity from the per-market figures. * Best-effort — empty on failure. */ declare const priceGlvVaults: (chainId: string, multicallRetry: MulticallRetryFunction, glvTokens: string[], prices: TickerPrices, /** market address (lowercased) → its index token address. From `/markets`. */ marketIndexToken: Map) => Promise<{ values: Map; maxMarketCount: number; /** GLV address (lowercased) → its constituent GM markets (lowercased). */ glvMarkets: Map; /** GLV address (lowercased) → its per-market balance caps + current holding, * for the GLV deposit-capacity approximation. */ glvCaps: Map; }>; /** Per-(glv, market) GLV deposit-cap state. Balances/amounts in GM-token * native units (18-dec); `maxBalUsd` in GMX 1e30 USD. A `0n` cap means that * cap is not enforced. */ interface GlvMarketCap { /** GM market token address, lowercased. */ market: string; maxBalUsd: bigint; maxBalAmount: bigint; /** GLV's current GM-token holding for this market. */ balanceAmount: bigint; } /** * Estimate per-chain minimum execution fees for the four GM/GLV request * types. Reads the DataStore gas-limit + multiplier config in one multicall * and multiplies by `gasPriceWei`. Returns `undefined` if the chain is * unsupported or the reads/gas-price fail. */ declare const fetchGmxExecutionFees: (chainId: string, multicallRetry: MulticallRetryFunction, gasPriceWei: bigint) => Promise; /** * Per-chain gmxinfra REST API host. GMX V2 is deployed on Arbitrum One and * Avalanche C-Chain; each has its own API host serving the same routes * (`/apy`, `/markets`, `/glvs`, `/tokens`). */ declare const GMX_API_HOSTS: { [chainId: string]: string; }; /** Chains for which a GMX vault listing is available. */ declare const GMX_SUPPORTED_CHAINS: string[]; /** Resolve the gmxinfra API base for a chain, or `undefined` if unsupported. */ declare const getGmxApiHost: (chainId: string) => string | undefined; /** * GMX V2 read-side contracts per chain (the data-side counterpart to the * calldata-sdk's `GMX_CONTRACTS`). `reader` and `glvReader` are the * SyntheticsReader / GlvReader view contracts; `dataStore` holds the * per-account request-key sets used for pending-ticket enumeration. * * Addresses pinned from the GMX frontend SDK config and verified to have * bytecode on-chain (both chains). */ interface GmxReadContracts { reader: Address; glvReader: Address; dataStore: Address; } declare const GMX_READ_CONTRACTS: { [chainId: string]: GmxReadContracts; }; declare const getGmxReadContracts: (chainId: string) => GmxReadContracts | undefined; /** Key of the set holding `account`'s pending GM deposit request keys. */ declare const accountDepositListKey: (account: Address) => Hex; /** Key of the set holding `account`'s pending GM withdrawal request keys. */ declare const accountWithdrawalListKey: (account: Address) => Hex; /** * A Pendle Principal Token, modelled as a fixed-rate earn product. * * ## Why this is a provider and not an ERC-4626 vault * * A PT is a zero-coupon bond, not a share. There is no `deposit`, no * `convertToAssets`, and no share price that accrues: you BUY the PT on * Pendle's AMM at a discount to face value, and at `expiry` it redeems 1:1 for * the underlying. The yield is the discount, fixed the moment you buy. * * Consequences that shape every field below, and that a consumer must not * paper over: * * - **No `totalAssets` / `totalSupply` / `sharePrice`.** Nothing here is a * share/asset ratio. Size is reported in USD only, exactly like the other * two non-4626 providers (GMX, HyperCore), and this provider is likewise * excluded from `buildVaultLookup`. * - **Entry and exit are SWAPS.** Both legs route through the Pendle * aggregator (already integrated as `TradeAggregator.Pendle`), so both need * a slippage tolerance and both are priced by pool depth — which is what * `liquidityUsd` reports. * - **The row is only valid until `expiry`.** After it, the fixed rate is * meaningless (the PT redeems at par, so the forward yield is zero) and the * product is gone. Expired markets are dropped by the fetcher; see * `isLiveMarket`. */ interface PendlePtMarket extends VaultClassificationFields { /** PT token address, lowercased. This is what a holder actually owns. */ address: string; /** The Pendle AMM market contract, lowercased. Where the swap routes. */ marketAddress: string; /** * Underlying/accounting asset address, lowercased — what the PT redeems for * at maturity, and the denomination of the fixed rate. */ underlying: string; /** YT address, lowercased. The complement; `PT + YT = SY`. */ ytAddress?: string; /** SY (Standardised Yield wrapper) address, lowercased. */ syAddress?: string; /** PT symbol, e.g. `PT-wstETH-30DEC2027`. */ symbol: string; /** Display name for the market. */ name: string; /** * PT decimals. * * **NOT reliably equal to the underlying's** — 9 of 57 live Ethereum markets * disagreed at integration (PT-mHyperBTC is 8 over an 18-decimal * mHyperBTC). Sourced from the token list, or read on-chain; a market whose * PT decimals cannot be established is DROPPED rather than defaulted, since * a wrong value mis-scales every amount silently. */ decimals: number; /** Underlying asset decimals. */ assetDecimals: number; /** Unix SECONDS. */ expiry: number; /** ISO-8601 mirror, straight from the API. */ expiryIso: string; /** Snapshot at fetch time — recompute from `expiry` for a live countdown. */ secondsToExpiry: number; /** `secondsToExpiry` in days, rounded to 2dp. Convenience for display. */ daysToExpiry: number; /** * The fixed yield to maturity, as a nominal APR percent. * * Converted from the API's `impliedApy`, which is a compounded APY fraction. * `impliedApyPercent` below carries Pendle's own figure unconverted, because * that is what pendle.finance displays and a user WILL compare the two. */ supplyRate: number; /** Always 0 — PENDLE emissions accrue to LPs, never to PT holders. */ rewardsRate: number; /** `supplyRate + rewardsRate`. The headline. */ depositRate: number; /** Pendle's `impliedApy` as a percent, uncompounded-out. Display parity. */ impliedApyPercent: number; /** * The SY's own floating yield (percent APR) — what a PT buyer GIVES UP. * Context for "is this fixed rate a good deal", never the row's own rate. */ underlyingApyPercent?: number; /** AMM swap fee as a fraction of the traded amount, e.g. `0.0005`. */ feeRate?: number; /** * Whole-market TVL in USD (`details.totalTvl`). * * USD-only by construction — there is no token-denominated "total assets" * for a PT. Mirrors GMX/HyperCore, which also report USD and set the raw * base-unit fields to null. */ totalAssetsUsd: number; /** * Same number as {@link totalAssetsUsd}. Present because the cross-source * sort field on `/v1/data/earn` is `formatted`, and for a USD-denominated * provider the USD figure IS the comparable magnitude — the convention GMX * and HyperCore already established in the recorder. */ totalAssetsFormatted: number; /** * AMM pool depth in USD (`details.liquidity`) — what can actually be traded * in or out right now. For a PT this is the real exit constraint: the * position is always sellable in principle and rarely sellable in size. */ liquidityUsd: number; /** Permissionless — anyone can buy a PT. Always true; kept explicit. */ isMintable: boolean; /** `market-sale`: exit is selling on Pendle's AMM at the prevailing price. */ withdrawalMode: 'market-sale'; /** Hydrated underlying metadata from the token list, if available. */ asset?: GenericCurrency; /** USD price of one underlying unit, when a price map was supplied. */ priceUsd?: number; /** USD price of one PT, from Pendle's own price feed when available. */ ptPriceUsd?: number; /** Pendle's category tags, e.g. `['eth','blue-chips','lido']`. */ categoryIds?: string[]; /** The protocol behind the underlying, per Pendle, e.g. `Lido`. */ protocol?: string; } /** Per-chain map, keyed by lowercased PT address (parity with the other providers). */ type PendlePtMarkets = { [ptAddress: string]: PendlePtMarket; }; interface FetchPendlePtOptions { /** * Include markets that have already matured. * * **Default `false`, and that default is the point of this provider.** An * expired PT redeems at par, so its forward yield is zero — but every rate * field the API and the token list carry still holds the last pre-expiry * value. Serving those rows would put a stale fixed APY at the top of an * APR-sorted earn list on a product that no longer exists. * * The escape hatch exists for the same reason yield-tracer's `/assets` * route has one: a user who HOLDS a matured PT still needs the row to * redeem it. Mirrors `?includeExpired=` there, including the name. */ includeExpired?: boolean; /** * Clock override, unix seconds. Tests only — production always judges * expiry against the real clock, never against a cached flag. */ nowSecs?: number; } /** * Fetch every LIVE Pendle PT market on a chain, modelled as fixed-rate earn * products. * * **HTTP-only in every normal case.** Token metadata resolves in three tiers — * the caller's token list, then Pendle's own global asset listing, then an * on-chain `decimals()` multicall — and each tier is consulted only for what * the previous one could not answer. A caller with a hydrated token list makes * exactly ONE request (the shared, cached markets listing); a caller with none * (the worker's `?source=live` path passes `{}`) makes two, and still never * touches an RPC. * * The multicall is a last resort on purpose. Reading `decimals()` for ~110 PTs * on Ethereum makes the entire chain's listing hostage to one multicall, and * viem's `allowFailure` returns `'0x'` per call instead of throwing when the * transport dies — so the failure mode is a silent, complete disappearance of * Pendle on the busiest chain rather than an error anyone would notice. * * @param chainId target chain * @param multicallRetry last-resort decimals gap-fill only; usually unused * @param prices price map keyed by oracle key / address * @param tokenList token list for PT + underlying metadata * @param options `includeExpired` and a test clock — see * {@link FetchPendlePtOptions} * * @returns map keyed by lowercased PT address; empty on chains without a * Pendle deployment. */ declare const fetchPendlePtMarkets: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, options?: FetchPendlePtOptions) => Promise; /** * Pendle V2 public market listing. * * ONE un-paginated endpoint returns every Pendle market on every chain, * including the per-market `details` block (implied APY, pool liquidity, TVL). * That is the whole data source for this provider — no multicall, no subgraph, * no API key. * * https://api-v2.pendle.finance/core/v1/markets/all * * There is also a per-chain `/v1/{chainId}/markets/active`, which pre-filters * to live markets. We deliberately do NOT use it: it costs one request per * chain for the same data, and — more importantly — expiry has to be judged * here anyway (see {@link isLiveMarket}), so taking the filter from the API * would just move the one rule this provider must never get wrong out of our * code and into someone else's. * * Docs: https://docs.pendle.finance/pendle-v2/introduction */ declare const PENDLE_MARKETS_URL = "https://api-v2.pendle.finance/core/v1/markets/all"; /** * Token metadata for every Pendle-known asset on every chain — decimals, * symbol, name, icon. One global call, same as the markets listing. * * It exists here because `/markets/all` publishes addresses but NO decimals, * and a PT's decimals cannot be inferred from its underlying's (they disagree * on ~16 % of live markets). The alternative — reading `decimals()` on-chain * for every PT — makes the whole provider depend on a ~110-call multicall on * Ethereum, and when that multicall fails viem returns `'0x'` per call rather * than throwing, so the entire chain's listing silently disappears. Observed, * not hypothesised. An HTTP source that fails loudly is the better dependency. */ declare const PENDLE_ASSETS_URL = "https://api-v2.pendle.finance/core/v1/assets/all"; /** * Chains with Pendle deployments. The endpoint is global (it returns every * chain regardless), so this set exists only to skip the round-trip on chains * Pendle does not cover — the parser is chain-agnostic and a new chain appears * as soon as it is added here. */ declare const PENDLE_CHAIN_IDS: Set; declare const hasPendleMarkets: (chainId: string) => boolean; /** * The `details` block. Every field is a FRACTION (`0.0721` = 7.21 %) except * the three USD amounts. * * **Only `impliedApy` describes what a PT holder earns.** The others are * different products' numbers sharing one object, and picking the wrong one is * the same class of error as TermMax's maker-side `apr()` naming: * * - `impliedApy` — the fixed yield locked in by buying PT and holding to * maturity. THE PT RATE. * - `underlyingApy` — the SY's own floating yield. What the YT side earns, * and what PT holders give up. * - `pendleApy` — PENDLE emissions paid to LPs. Not to PT holders. * - `aggregatedApy` / `maxBoostedApy` / `swapFeeApy` — LP-side returns. */ interface PendleApiMarketDetails { /** AMM pool depth, USD. What can actually be traded in or out. */ liquidity?: number | null; /** Whole-market TVL, USD (pool + SY backing). */ totalTvl?: number | null; tradingVolume?: number | null; /** Fixed yield to maturity, as a FRACTION. The PT rate. */ impliedApy?: number | null; /** The SY's floating yield, as a FRACTION. NOT the PT rate. */ underlyingApy?: number | null; /** PENDLE emissions to LPs, as a FRACTION. NOT the PT rate. */ pendleApy?: number | null; /** LP total, as a FRACTION. NOT the PT rate. */ aggregatedApy?: number | null; maxBoostedApy?: number | null; swapFeeApy?: number | null; /** AMM swap fee, as a FRACTION of the traded amount. */ feeRate?: number | null; yieldRange?: { min?: number | null; max?: number | null; } | null; } /** * One `markets[]` entry. * * **`pt` / `yt` / `sy` / `underlyingAsset` are `"-
"`**, not * bare addresses — see {@link splitChainScopedAddress}. `expiry` is ISO-8601, * not a unix stamp (the token list carries the unix form). */ interface PendleApiMarket { /** Underlying's display name, e.g. `wstETH` — NOT the PT symbol. */ name?: string | null; /** The AMM market contract, a bare address. */ address?: string | null; /** ISO-8601. */ expiry?: string | null; /** `"1-0xb253…"` */ pt?: string | null; yt?: string | null; sy?: string | null; underlyingAsset?: string | null; accountingAsset?: string | null; protocol?: string | null; icon?: string | null; details?: PendleApiMarketDetails | null; isNew?: boolean | null; isPrime?: boolean | null; categoryIds?: string[] | null; chainId?: number | null; } /** * Split Pendle's `"-
"` composite into its parts. * * Returns `undefined` for anything that is not that shape — a bare address * included. Pendle has never returned one, and silently accepting it would * mean guessing the chain, which is how a market gets attributed to the wrong * network. */ declare function splitChainScopedAddress(value: string | null | undefined): { chainId: string; address: string; } | undefined; /** Parse the ISO-8601 `expiry` to unix SECONDS. `undefined` when unparseable. */ declare function parseExpirySeconds(expiry: string | null | undefined): number | undefined; /** * Is this market still live? * * **Judged against the clock, every time — never off a cached flag.** The * token list carries a `props.pendle.expired` boolean that is only as fresh as * the last regeneration, and an expired PT that keeps showing a pre-expiry * fixed APY is precisely the bug yield-tracer migration 0088 had to clean up * after. A market with no parseable expiry is treated as NOT live: an * unbounded fixed-rate row is never the safe default. */ declare function isLiveMarket(market: PendleApiMarket, nowSecs?: number): boolean; /** One `assets[]` entry from {@link PENDLE_ASSETS_URL}. */ interface PendleApiAsset { chainId?: number | null; address?: string | null; symbol?: string | null; name?: string | null; decimals?: number | null; /** `['PT']`, `['YT']`, `['SY']`, … */ tags?: string[] | null; expiry?: string | null; proIcon?: string | null; } /** Drop both cached listings. Tests only. */ declare function clearPendleMarketsCache(): void; /** * Fetch the global Pendle market listing (all chains, live and expired). * * Chain filtering, expiry filtering and normalization happen in `fetchPublic`. */ declare function fetchPendleApiMarkets(): Promise; /** `"-"` — the key both Pendle listings use. */ declare const assetKey: (chainId: string | number, address: string) => string; /** * Fetch Pendle's global asset metadata, keyed by {@link assetKey}. * * Only called when something is missing from the caller's token list, so a * fully-hydrated caller pays nothing for it. */ declare function fetchPendleApiAssets(): Promise>; /** * A Spectra Principal Token, modelled as a fixed-rate earn product. * * ## Why this is a provider and not an ERC-4626 vault * * A PT is a zero-coupon bond. Spectra's is **ERC-5095 + ERC-2612**, explicitly * NOT ERC-4626 — and the two entry points it does expose are not a fixed-rate * deposit: * * - `deposit()` mints PT **and** YT from the IBT. Holding both is just the IBT * re-wrapped; a fixed-rate position still requires selling the YT. * - `withdraw()` burns **both** back. It is not an exit from a PT-only * position. * - `redeem()` pays par, and only after `maturity`. * * So the only way to hold the fixed rate is to BUY the PT on its Curve * StableSwap-NG pool, and the only way out before maturity is to sell it there. * Every consequence the Pendle provider documents follows verbatim: no share * price, USD-denominated size, `liquidityUsd` is pool DEPTH rather than idle * cash, exclusion from `buildVaultLookup`, and a row that is only valid until * `expiry`. * * Field names deliberately mirror `PendlePtMarket` where the meaning is the * same (`expiry`, `supplyRate`, `impliedApyPercent`, `liquidityUsd`, * `withdrawalMode`), so the generic term-sheet and earn readers — which key off * names, not types — need no Spectra branch. */ interface SpectraPtMarket extends VaultClassificationFields { /** PT (PrincipalToken) address, lowercased. What a holder actually owns. */ address: string; /** * The Curve StableSwap-NG pool, lowercased. Where the trade routes, and the * `liquidityUsd` denominator. * * Named `marketAddress` for parity with the Pendle row even though Spectra's * PT and pool are separate contracts (Pendle's "market" IS its AMM). */ marketAddress?: string; /** * Underlying/accounting asset address, lowercased — what the PT redeems for * at maturity, and the denomination of the fixed rate. */ underlying: string; /** YT address, lowercased. The complement that takes the floating yield. */ ytAddress?: string; /** * The interest-bearing token the PT was minted from, lowercased. * * The analogue of Pendle's SY, but a plain ERC-4626 (or a * `Spectra4626Wrapper` over one) rather than a bespoke standard. */ ibtAddress?: string; /** The vault behind a wrapper IBT, when the IBT is a `Spectra4626Wrapper`. */ baseIbtAddress?: string; /** PT symbol, e.g. `PT-stXRP(FXRP)-2026/12/31`. */ symbol: string; /** Display name for the market. */ name: string; /** * PT decimals. * * **NOT reliably the underlying's** — 2 of 36 live markets disagreed at * integration. The listing publishes both, so no inference is needed; a * market missing either is DROPPED rather than defaulted, since a wrong * value mis-scales every amount silently. */ decimals: number; /** Underlying asset decimals. */ assetDecimals: number; /** Unix SECONDS, straight from the API's `maturity`. */ expiry: number; /** ISO-8601 mirror, derived. Parity with the Pendle row's `expiryIso`. */ expiryIso: string; /** Snapshot at fetch time — recompute from `expiry` for a live countdown. */ secondsToExpiry: number; /** `secondsToExpiry` in days, rounded to 2dp. Convenience for display. */ daysToExpiry: number; /** * The fixed yield to maturity, as a nominal APR percent. * * Converted from the pool's `impliedApy`, which is a compounded APY **already * in percent** — unlike Pendle, whose `details` block is fractions. Getting * that wrong is invisible: a doubled conversion yields 0.2 %, a missing one * yields nothing that looks out of place. */ supplyRate: number; /** Always 0 — SPECTRA emissions accrue to LPs and voters, never PT holders. */ rewardsRate: number; /** `supplyRate + rewardsRate`. The headline. */ depositRate: number; /** Spectra's own `impliedApy`, unconverted. Display parity with their app. */ impliedApyPercent: number; /** * Spectra's `ptApy` — the rate a taker realises after price impact and fee, * as a compounded APY percent. * * **Always ≤ {@link impliedApyPercent}, and the gap measures pool thinness.** * On the deepest market they are 0.06 pp apart; on a $35 market, 9.4 pp; on * one Flare market `ptApy` is negative while the implied rate is positive. * A client that wants to show "what you would actually get" should show this * one — but ranking must use `supplyRate`, or a Spectra row and a Pendle row * in the same column would be answering different questions. */ executableApyPercent?: number; /** * The IBT's own floating yield (percent APR) — what a PT buyer GIVES UP. * Spectra's `ibt.apr.total`, often absent. Context, never the row's rate. */ underlyingApyPercent?: number; /** * Curve's static pool fee as a FRACTION of the traded amount, charged on * BOTH legs. Converted from the API's 1e10-scaled integer string. */ feeRate?: number; /** * Whole-market TVL in USD. * * `tvl.usd` where the API supplies it; otherwise `tvl.underlying × price` * from the caller's price map. **Nullable upstream** (1 of 36 at * integration), and a row sized at 0 sorts to the bottom rather than * disappearing — the quieter failure, hence the fallback. */ totalAssetsUsd: number; /** Same number. The cross-source sort field is `formatted`; see PendlePtMarket. */ totalAssetsFormatted: number; /** * Pool depth in USD — what can actually be traded in or out right now. * For a PT this is the real exit constraint, not a solvency signal. */ liquidityUsd: number; /** TVL denominated in the underlying, which the API always supplies. */ totalAssetsUnderlying?: number; /** * `ptRate` as a fraction of par — `1` means the PT redeems 1:1. * * **The number that says a Spectra PT is not an unconditional par claim.** * Spectra writes `ptRate` DOWN whenever the IBT loses value, and it can only * decrease; their own docs work the example where a halved IBT returns half * the deposit. So "redeems 1:1 at maturity" is the normal case, not the * guaranteed one, and this is the field that distinguishes them. * * 33 of 36 live markets read exactly 1 at integration. */ ptRate: number; /** * How far below par {@link ptRate} sits, in bps. `0` on a healthy market. * * Surfaced separately so a consumer can gate on it without knowing the * base-27 scale, and so a written-down market cannot be presented as a clean * fixed-rate bond just because its APY still parses. */ principalWriteDownBps: number; /** Permissionless — anyone can buy a PT. Always true; kept explicit. */ isMintable: boolean; /** `market-sale`: exit is selling on the Curve pool at the prevailing price. */ withdrawalMode: 'market-sale'; /** Hydrated underlying metadata from the token list, if available. */ asset?: GenericCurrency; /** USD price of one underlying unit, when a price map was supplied. */ priceUsd?: number; /** USD price of one PT, from the pool's own quote when available. */ ptPriceUsd?: number; /** PT price in units of the underlying — the discount that IS the yield. */ ptPriceUnderlying?: number; /** Spectra's tags, e.g. `['stable']`. */ categoryIds?: string[]; /** The protocol behind the IBT, per Spectra, e.g. `Morpho`, `Yearn`. */ protocol?: string; } /** Per-chain map, keyed by lowercased PT address (parity with the other providers). */ type SpectraPtMarkets = { [ptAddress: string]: SpectraPtMarket; }; interface FetchSpectraPtOptions { /** * Include markets that have already matured. **Default `false`.** * * Same rule and same reasoning as the Pendle provider: a matured PT redeems * at par, so its forward yield is zero, while every rate field still carries * the last pre-expiry value. Serving those rows puts a stale fixed APY on top * of an APR-sorted list for a product that no longer exists. * * The Spectra-specific caveat is that this switch may have nothing to return. * The upstream listing appears to drop matured markets itself, so a holder of * a matured Spectra PT is not servable from this source at all — unlike * Pendle, whose `/markets/all` carries them. That is a gap in the data, not a * reason to leave the filter off. */ includeExpired?: boolean; /** * Clock override, unix seconds. Tests only — production always judges * maturity against the real clock, never against a cached flag. */ nowSecs?: number; } /** * Fetch every LIVE Spectra PT market on a chain, modelled as fixed-rate earn * products. * * **HTTP-only in every normal case, and normally ONE request.** Unlike Pendle * there is no global listing — the endpoint is per network — so this issues one * call per chain, cached in-isolate for 60 s and shared between concurrent * callers. Unlike Pendle there is also no second metadata endpoint to need: the * `/pools` response embeds decimals, symbol, name and icon for the PT, YT, IBT * and underlying alike, so the on-chain tier below fires only if that stops * being true. * * The multicall stays a last resort for the reason recorded in the Pendle * provider: viem's `allowFailure` returns `'0x'` per call rather than throwing * when the transport dies, so a decimals batch that fails does not error — it * silently empties a whole chain's listing. * * @param chainId target chain * @param multicallRetry last-resort decimals gap-fill only; normally unused * @param prices price map keyed by oracle key / address — also what * recovers a null `tvl.usd` * @param tokenList token list for PT + underlying metadata * @param options `includeExpired` and a test clock * * @returns map keyed by lowercased PT address; empty on chains without a * Spectra deployment. */ declare const fetchSpectraPtMarkets: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, options?: FetchSpectraPtOptions) => Promise; /** * Spectra V2 public market listing. * * Spectra (ex-APWine, npm org `perspectivefi`) is yield tokenisation with the * same shape as Pendle: an IBT (any yield-bearing ERC-4626) is split into a * **PT** — an ERC-5095 zero-coupon claim on the principal at `maturity` — and a * **YT** carrying the floating yield until then. The PT trades at a discount on * a rate-adjusted Curve StableSwap-NG pool, and that discount is the fixed rate. * * So this provider is the Pendle one with four substitutions, each of which is * a bug if assumed away — they are called out at the field that carries them: * * 1. the route key is a network NAME, not a chain id ({@link SPECTRA_NETWORKS}); * 2. one request PER CHAIN, against Pendle's single global listing; * 3. rates arrive in PERCENT (Pendle's are fractions) and are APYs; * 4. `maturity` is unix SECONDS (Pendle's `expiry` is ISO-8601). * * **There is no documented API.** dev.spectra.finance publishes contracts, * oracles and MetaVaults; the endpoint below is the one app.spectra.finance * itself uses and its rate fields are defined nowhere. Everything asserted here * about their meaning was established by reproducing them from the published * prices — see {@link SpectraApiPool.impliedApy}. * * Docs: https://dev.spectra.finance/ · assessment: SPECTRA.md */ /** * Chain id → the network name the API routes on. * * **This mapping is hand-maintained and undiscoverable.** `/api/v1/1/pools` and * `/api/v1/ethereum/pools` both answer `400 {"error":"Invalid network"}`, and * there is no `/api/v1/networks` route to enumerate the valid names — so a new * Spectra deployment stays invisible until someone adds its name here. That is * the opposite of Pendle, whose global listing surfaces a new chain the moment * it exists. * * Verified live 2026-08-17: every name below answers 200. `polygon`, `linea`, * `gnosis`, `mode` and `fraxtal` are rejected, so Spectra is not on them under * any name we could find. */ declare const SPECTRA_NETWORKS: Readonly>; declare const spectraNetwork: (chainId: string) => string | undefined; declare const hasSpectraMarkets: (chainId: string) => boolean; declare const spectraPoolsUrl: (network: string) => string; /** A token as the listing embeds it. Decimals are always present. */ interface SpectraApiToken { address?: string | null; chainId?: number | null; name?: string | null; symbol?: string | null; decimals?: number | null; logoURI?: string | null; /** The yield venue behind the IBT, e.g. `Morpho`, `Yearn`, `SMARDEX`. */ protocol?: string | null; price?: { underlying?: number | null; usd?: number | null; } | null; /** IBT only — its own floating yield. **PERCENT**, and often `null`. */ apr?: { total?: number | null; details?: Record | null; } | null; /** IBT price in underlying, 1e18-ish fixed point as a decimal string. */ rate?: string | null; spotRate?: string | null; } /** * One entry of `market.pools[]` — the Curve StableSwap-NG pool the PT trades on. * * Every live market carries exactly one (36/36 at integration), but the field * is an array and is read as one: {@link pickPool} takes the deepest rather * than `[0]`, so a second pool on a market cannot silently decide the rate. */ interface SpectraApiPool { address?: string | null; chainId?: number | null; /** * **The PT's fixed rate, as a compounded APY in PERCENT.** * * Established empirically, because nothing documents it: across all 36 live * markets on 7 chains this field reproduces * `((1 / ptPrice.underlying) ** (1 / yearsToMaturity) - 1) * 100` * to within float noise. It is the mid-price implied rate — the same quantity * as Pendle's `impliedApy`, in different units. * * See {@link ptApy} for the number that is NOT this. */ impliedApy?: number | null; /** * The rate a taker would actually realise — price impact and pool fee * included — as a compounded APY in PERCENT. * * **Not the market's rate, and not interchangeable with * {@link impliedApy}.** It is always the lower of the two, by an amount that * tracks pool thinness: 0.06 pp apart on the deepest market, 9.4 pp apart on * a $35 one, and on `PT-stXRP…2026/09/30` it is NEGATIVE (−0.06 %) while the * implied rate is +1.03 %. Carried through as * `SpectraPtMarket.executableApyPercent` because it is genuinely useful — it * is the honest "what you'd get" figure, which Pendle publishes nothing * equivalent to — but the row's `supplyRate` is the implied rate, so that a * Spectra row and a Pendle row mean the same thing when ranked together. */ ptApy?: number | null; /** LP-side total return, PERCENT. Neither PT nor YT earns this. */ lpApy?: { total?: number | null; details?: Record | null; } | null; /** YT leverage multiple. Not a rate. */ ytLeverage?: number | null; liquidity?: { underlying?: number | null; usd?: number | null; } | null; ptPrice?: { underlying?: number | null; usd?: number | null; } | null; ytPrice?: { underlying?: number | null; usd?: number | null; } | null; /** * Curve's static pool fee — a **1e10-scaled integer string**, not a fraction * and not bps. `"1204544"` is 1.2 bps. `midFee` / `outFee` read 0 on every * live pool; `ibtToPtFee` / `ptToIbtFee` are the per-direction dynamic fees. */ feeRate?: string | null; midFee?: string | null; outFee?: string | null; ibtToPtFee?: string | null; ptToIbtFee?: string | null; /** `CURVE_SNG` on every live pool at integration. */ type?: string | null; lpt?: { address?: string | null; decimals?: number | null; } | null; } /** One `pools[]` entry — a MARKET (the naming is the API's, not ours). */ interface SpectraApiMarket { /** The PrincipalToken contract. This is what a holder owns. */ address?: string | null; chainId?: number | null; /** PT name, e.g. `Principal Token: sw-WUSDN(USDN) 2027/01/12`. */ name?: string | null; /** PT symbol, e.g. `PT-sw-WUSDN(USDN)-2027/01/12`. */ symbol?: string | null; /** **PT decimals** — not the underlying's. See `SpectraPtMarket.decimals`. */ decimals?: number | null; /** Unix SECONDS. */ maturity?: number | null; createdAt?: number | null; /** `ptRate`, base-27. Starts at 1e27 and can only fall (negative-yield events). */ rate?: string | null; tvl?: { ibt?: number | null; underlying?: number | null; usd?: number | null; } | null; yt?: SpectraApiToken | null; /** The interest-bearing token the PT is minted from. */ ibt?: SpectraApiToken | null; /** The vault behind a `Spectra4626Wrapper` IBT, when the IBT is a wrapper. */ baseIbt?: SpectraApiToken | null; /** What the PT redeems for at maturity, and the rate's denomination. */ underlying?: SpectraApiToken | null; pools?: SpectraApiPool[] | null; maturityValue?: { underlying?: number | null; usd?: number | null; } | null; tags?: string[] | null; multipliers?: unknown; } /** Lowercased address, or `undefined` for anything that is not one. */ declare const spectraAddress: (v: string | null | undefined) => string | undefined; /** * Is this market still live? * * **Judged against the clock, every time.** The listing appears to filter * matured markets already — zero of 36 had a past maturity on a protocol that * has been running V2 since 2024 — but that is an observation about today's * upstream behaviour, not a contract, and "the API already handles it" is * exactly the assumption `PENDLE_PT.md` §2 exists to refuse. A matured PT keeps * publishing its last pre-expiry implied APY, which on a rate-sorted earn list * puts a dead product on top. * * A market with no parseable maturity is NOT live: an unbounded fixed-rate row * is never the safe default. */ declare function isLiveSpectraMarket(market: SpectraApiMarket, nowSecs?: number): boolean; /** * Pick the pool that prices the market. * * The DEEPEST, not the first. Every live market has exactly one pool today, so * this never fires — which is why it is written now rather than after a second * pool quietly halves a published rate. */ declare function pickPool(pools: SpectraApiPool[] | null | undefined): SpectraApiPool | undefined; /** * Curve's 1e10-scaled fee integer → a FRACTION, the unit * `VaultTermInput.swapFeeRate` is documented in. */ declare function parseCurveFee(raw: string | null | undefined): number | undefined; /** * Bounds a published rate must clear to be believed, in PERCENT. * * **Spectra does not sanity-check its own rate fields.** HyperEVM publishes * `impliedApy: 549844464093797600` and `12307.1` on its two markets — raw fixed * point leaking through on pools holding $318 and $599 — and Hemi publishes * `null` for both rate fields. That is 3 of 36 markets, i.e. ~8 %, and an * unbounded value does not merely look odd: it takes the top of any * rate-sorted earn listing instantly. * * The floor is below zero on purpose. A PT trading ABOVE par is a real, if * unusual, market state and reporting it as such is correct; only the * impossible is rejected. */ declare const SPECTRA_RATE_MIN_PERCENT = -99; declare const SPECTRA_RATE_MAX_PERCENT = 1000; /** * A published rate, or `undefined` when it is absent or impossible. * * Callers DROP the market on `undefined` rather than substituting 0 — a * fixed-rate product whose rate we cannot establish is not an offer, and 0 % * is a specific claim rather than a neutral one. */ declare function sanePercent(v: number | null | undefined): number | undefined; /** * `ptRate` as a fraction of par (`1` = par, `0.5` = half). * * **This is the field that says a Spectra PT does NOT unconditionally redeem * 1:1.** Spectra's docs are explicit: "The `ptRate` starts as 1 and decreases * if the `ibtRate` … decreases. The `ptRate` can only decrease" — their own * worked example has a holder receive half their deposit back after the IBT * halves. Pendle carries the same economic exposure through its SY and * publishes no equivalent number, so this is strictly better disclosure, and it * is why the row surfaces {@link SpectraPtMarket.principalWriteDownBps}. * * It is also a live integrity check that costs nothing. The three markets whose * `impliedApy` is absent or nonsense (Hemi, both HyperEVM) are EXACTLY the * three whose `ptRate` has been written down to ~0 — two independent signals * agreeing that those rows are not offers. */ declare function parsePtRate(raw: string | null | undefined): number | undefined; /** Drop every cached listing. Tests only. */ declare function clearSpectraMarketsCache(): void; /** * Fetch one network's live PT listing. * * Chain filtering, maturity filtering and normalization happen in * `fetchPublic`. A non-200 throws — the caller logs and omits the provider for * that chain rather than serving a partial listing as a complete one. */ declare function fetchSpectraApiMarkets(chainId: string): Promise; /** * Vault interface family, detected via ERC-165 `supportsInterface`. * * - `erc4626` — plain synchronous tokenized vault (the default; most * vaults don't implement ERC-165 at all, which we treat as 4626). * - `erc7540` — asynchronous deposit and/or redeem (request → claim). * - `erc7575` — multi-asset vault (share token split from entrypoints) * that is NOT also 7540. */ type InterfaceKind = 'erc4626' | 'erc7540' | 'erc7575'; /** * ERC-165 interface IDs, verified against EIP-7540 / EIP-7575: * - `0xe3bc4e65` — ERC-7540 operator methods (implemented by ALL 7540 * vaults, async-deposit and async-redeem alike). Best single probe * for "is this a 7540 vault". * - `0x2f0a18c5` — ERC-7575 interface. All 7540 vaults also return true * here, so 7575-but-not-7540 is the pure multi-asset (sync) case. */ declare const INTERFACE_IDS: { readonly erc7540Operator: "0xe3bc4e65"; readonly erc7575: "0x2f0a18c5"; }; /** * Classify each vault address by interface family using two ERC-165 * probes per vault (`supportsInterface(0xe3bc4e65)` for 7540, * `supportsInterface(0x2f0a18c5)` for 7575). Failures / reverts (a vault * that doesn't implement ERC-165) fall back to `erc4626`. * * Returns a map keyed by lowercased address. Pure read — one multicall. */ declare const detectInterfaceKinds: (addresses: string[], chainId: string, multicallRetry: MulticallRetryFunction) => Promise>; /** * The cross-provider subset of static vault metadata + share-price * inputs that every supported provider populates. Lifted out of the * per-provider types so consumers (e.g. a user-balance endpoint that * only needs to know decimals + share/asset ratio) don't have to * branch on provider key. * * Load-bearing fields here MUST be present in every provider's value * type — the structural constraint in `buildVaultLookup`'s * `addEntries` enforces this at compile time. If a * future provider drops one, the TS build fails. */ interface VaultLookupEntry { /** Provider key — `fluid | gearbox | morpho | silo | euler-earn` */ provider: VaultProvider; /** Share token (vault) address, lowercased. */ address: string; /** Underlying ERC-20 address, lowercased. */ underlying: string; /** Share-token symbol (e.g. `fUSDC`, `dWETHV3`). */ symbol: string; /** Share-token name. */ name: string; /** Share decimals. By ERC-4626 convention equals underlying decimals. */ decimals: number; /** Underlying asset decimals. For plain ERC-4626 providers this equals * `decimals`; for providers where the share token and underlying have * different decimals (e.g. Lagoon: 18-decimal shares over USDC/WBTC), * this is the underlying's own decimals. Absent ⇒ assume it equals * `decimals`. Load-bearing for share→asset formatting. */ assetDecimals?: number; /** Total underlying held by the vault, raw integer string. */ totalAssets: string; /** Total shares minted, raw integer string. */ totalSupply: string; /** Vault interface family, when detected via ERC-165 * (`detectInterfaceKinds`). Absent ⇒ not probed; consumers should * treat absent as `erc4626`. Drives deposit/withdraw + withdrawal- * queue routing. */ interfaceKind?: InterfaceKind; /** NAV behaviour — `yield-bearing` (monotonic accrual: LST, savings, * lending supply) vs `volatile` (trading/perp/leveraged strategy). * Set by `stampVaultClassification`; absent ⇒ not classified. */ yieldProfile?: YieldProfile; /** Underlying denomination — `stable` (stablecoin underlying) vs * `volatile`. Orthogonal to `yieldProfile`. */ denomination?: Denomination; /** Uniform share price, folded from the provider's `convertToAssets`/ * `pricePerShare` by `stampVaultClassification`. `sharePriceRaw` is the * asset-scaled raw integer (`'0'` for empty); `sharePrice` is it formatted * to underlying units; `sharePriceUSD` is `sharePrice * priceUsd`. Absent on * hypercore/gmx. */ sharePriceRaw?: string; sharePrice?: number; sharePriceUSD?: number; /** Resolved branded icon URL — share-token logo, else underlying-asset logo. * Carried from `stampVaultClassification`; absent ⇒ none resolved. */ logoURI?: string; /** * How a USER's position in this vault is read. * * Absent ⇒ `balanceOf(account)` on {@link address}, which is right for every * vault whose position IS a share-token balance — i.e. all but one. * * `savings-account` means the address is **not a token at all**: Frankencoin's * savings module is an internal ledger (`savings(address) → (saved, ticks)`) * and `balanceOf` REVERTS on it. That matters because the balance path runs * `allowFailure: true`, so the revert silently became a zero and a real * 162 ZCHF deposit rendered as an empty position. A missing balance that * looks like a legitimate zero is the worst shape this bug could take, which * is why the read is dispatched rather than probed. */ balanceKind?: 'erc20' | 'savings-account'; } /** * Flattens the per-provider maps from `VaultPublicDataAll` into a * single `vaultAddress → VaultLookupEntry` map. * * The per-provider maps key inconsistently — Fluid + Gearbox by * underlying, Morpho/Silo/Euler-Earn by vault address. We iterate * values and use each entry's `.address` field as the canonical key * so the resulting lookup is uniform regardless of provider keying. * * Pure transform — no I/O, no env. Cache wrappers live in callers * (e.g. worker-api's `vaultsShared.ts`). */ declare function buildVaultLookup(data: VaultPublicDataAll): Map; /** Supported ERC-4626 vault providers. */ /** * Every vault provider, as a runtime list. `VaultProvider` is derived from it, * so the two cannot drift — and consumers that need to validate untrusted * input (`parseEarnUid` on an action route's query param) can pass this as * `knownProviders` instead of accepting any `vault.` string. */ declare const VAULT_PROVIDERS: readonly ["fluid", "gearbox", "morpho", "lista", "silo", "euler-earn", "termmax", "lst", "savings", "lagoon", "aave-earn", "upshift", "yearn", "hypercore", "gmx", "pendle", "spectra"]; type VaultProvider = (typeof VAULT_PROVIDERS)[number]; /** * Per-provider payload returned by `getVaultPublicDataAll`. Each entry is * present only when the matching provider was requested AND its fetch * resolved (not when the chain has no deployment — those return an empty * map, which is still recorded here). */ interface VaultPublicDataAll { fluid?: FluidFTokens; gearbox?: GearboxV3Pools; morpho?: MorphoVaults; /** Lista DAO earn vaults (Moolah-fork of MetaMorpho, BNB chain). Same * `MorphoVaults` shape as `morpho`, but fetched on-chain via a dedicated * path since the Morpho API/subgraph doesn't index the Lista fork. */ lista?: MorphoVaults; silo?: SiloVaults; 'euler-earn'?: EulerEarnVaults; termmax?: TermMaxVaults; lst?: LstShareTokens; savings?: SavingsVaults; lagoon?: LagoonVaults; /** Aave Earn ("stable") vaults — curator-run ERC-4626 wrappers over an * Aave v3 supply position. Discovered + priced via the Aave public * GraphQL API, enriched on-chain for `totalSupply`/share price. Keyed by * lowercased vault address. */ 'aave-earn'?: AaveEarnVaults; /** Upshift curator-run ERC-4626-style yield vaults, sourced from the * Upshift public REST listing. Keyed by lowercased receipt-token * address. */ upshift?: UpshiftVaults; /** Yearn V3 vaults (VaultV3 + TokenizedStrategy ERC-4626). Discovered * via the yDaemon API; keyed by lowercased vault address. */ yearn?: YearnVaults; /** Hyperliquid HyperCore (L1) perp/HLP vaults. Only populated for the * Hyperliquid chain ({@link HYPERCORE_PROVIDER_CHAIN}). Keyed by * lowercased vault address. */ hypercore?: HypercoreVaults; /** GMX V2 GM-market and GLV-vault pool tokens (perp-DEX liquidity). * Only populated on chains where GMX is deployed (Arbitrum One, * Avalanche C-Chain). Keyed by lowercased token address. */ gmx?: GmxVaults; /** Per-chain minimum GM/GLV execution fees (wei). Populated alongside * `gmx`. Gas-price-derived, so treat as fresh-at-fetch estimates. */ gmxExecutionFees?: GmxExecutionFees; /** Pendle V2 Principal Tokens as fixed-rate earn products — one row per * LIVE market, keyed by lowercased PT address. **Matured markets are * never included** (an expired PT redeems at par, so its published fixed * APY is stale by definition); pass `pendleIncludeExpired` to override, * which only a holder-facing redeem flow should ever do. Not a share * token: no `totalAssets`/`totalSupply`/share price, USD-denominated * size, and excluded from `buildVaultLookup` — the GMX/HyperCore * precedent. */ pendle?: PendlePtMarkets; /** Spectra V2 Principal Tokens as fixed-rate earn products — one row per * LIVE market, keyed by lowercased PT address. Same instrument and same * rules as `pendle` (matured markets never included; pass * `spectraIncludeExpired` to override), over a rate-adjusted Curve * StableSwap-NG pool instead of Pendle's own AMM. Not a share token: * USD-denominated size, no share price, excluded from `buildVaultLookup`. */ spectra?: SpectraPtMarkets; } interface GetVaultPublicDataAllOptions { /** Narrow Silo to a single protocol version (`v2` or `v3`). */ siloProtocolVersion?: 'v2' | 'v3'; /** Page size hint for Silo's GraphQL query. */ siloLimit?: number; /** * Include MATURED Pendle PT markets. Default `false`. * * Only a flow that services an existing holder (redeem a PT you already * own) should set this. Any listing, ranking or discovery surface must * leave it off: a matured PT still publishes its last pre-expiry implied * APY, and that number is not an offer. */ pendleIncludeExpired?: boolean; /** * Include MATURED Spectra PT markets. Default `false`. * * Same rule as {@link pendleIncludeExpired}, with one caveat worth knowing * before relying on it: Spectra's listing appears to drop matured markets * upstream, so this switch may return nothing at all. The filter stays * regardless — "the API already handles it" is an observation about today, * not a contract. */ spectraIncludeExpired?: boolean; } /** * Combined output of `getVaultPublicDataAll`: the rich per-provider * payload (`data`) plus a flat address-keyed lookup (`lookup`) that * downstream consumers (e.g. user-balance endpoints) use without * branching on provider key. Both views come from the same fetch — * the lookup is built in-process, no extra I/O. */ interface VaultPublicDataResult { data: VaultPublicDataAll; lookup: Map; } /** * Universal vault fetcher — the vaults-side analogue of * `getLenderPublicDataAll`. * * Dispatches each requested provider to its dedicated fetcher and runs * them concurrently. Sources are mixed (Fluid + Gearbox use multicall; * Morpho + Silo are HTTP GraphQL), so there's no API-vs-on-chain split * like the lender path — every provider runs in parallel and a single * provider's failure does not sink the others. * * @param chainId target chain * @param providers which providers to fetch (any subset of * `'fluid' | 'gearbox' | 'morpho' | 'silo' | 'euler-earn'`) * @param multicallRetry provider-level multicall executor — used by * Fluid and Gearbox; ignored when only HTTP * providers are requested * @param prices optional price map keyed by oracle key / * underlying address * @param tokenList optional token list for hydrating `asset` * metadata on each parsed vault * @param options provider-specific knobs (Silo version filter, …) * * @returns `VaultPublicDataAll` — one key per requested provider that * resolved successfully; failed providers are omitted (and * logged when `VAULTS_DEBUG` is on). */ declare const getVaultPublicDataAll: (chainId: string, providers: VaultProvider[], multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, options?: GetVaultPublicDataAllOptions) => Promise; /** * Two orthogonal axes for separating savings-like vaults from volatile ones. * * - `yieldProfile` — NAV behaviour / mechanism. `yield-bearing` = the * share/asset ratio accrues monotonically (LST, staking, savings, lending * supply). `volatile` = a trading / perp / leveraged strategy whose NAV * fluctuates and can draw down (Hyperliquid HLP & friends; any leveraged * strategy vault we explicitly flag). * - `denomination` — is the underlying a stablecoin. Independent of * `yieldProfile`: sUSDe is `yield-bearing` + `stable`, wstETH is * `yield-bearing` + `volatile`, an HLP-USDC vault is `volatile` + `stable`. */ type YieldProfile = 'yield-bearing' | 'volatile'; type Denomination = 'stable' | 'volatile'; interface VaultClassification { yieldProfile: YieldProfile; denomination: Denomination; } /** * Optional cross-provider fields mixed into every per-provider vault type * (and `VaultLookupEntry`) and stamped by `stampVaultClassification`; absent * until then. * * Uniform **share price** — the provider's `convertToAssets` * (fluid/savings/lst/morpho/lista/silo/gearbox/euler-earn) or `pricePerShare` * (lagoon/yearn), so consumers read one set of fields instead of * `convertToAssets ?? pricePerShare`: * - `sharePriceRaw` — raw `convertToAssets(10^shareDecimals)` (asset-scaled * integer string). `'0'` for an empty vault. * - `sharePrice` — that value formatted to underlying units (1 share → X * underlying, a JS number). * - `sharePriceUSD` — `sharePrice * priceUsd` (USD value of 1 share); absent * when no underlying price was supplied/resolved. * All absent on hypercore/gmx (no share/asset ratio). */ interface VaultClassificationFields { yieldProfile?: YieldProfile; denomination?: Denomination; sharePriceRaw?: string; sharePrice?: number; sharePriceUSD?: number; /** * The **share token's own** token-list entry (hydrated from the provided * `tokenList` by `stampVaultClassification`). Unlike `asset` — which is the * *underlying* — this is what the vault/LST *actually is* (e.g. pumpBTC, with * its branded `logoURI` + `assetGroup`), so a UI can show the right icon. * Absent when the caller passed no token list, or the share token isn't in * it. */ shareAsset?: GenericCurrency; /** * Resolved branded icon URL for the vault — the single field a UI should * read to render an icon, so consumers don't re-derive the fallback. Set by * `stampVaultClassification` to the first of: the share token's own logo * (`shareAsset.logoURI`, branded — e.g. pumpBTC), the underlying asset's logo * (`asset.logoURI`), or — for GMX GM/GLV tokens that have no `asset` entry — * the long-leg token's logo. Absent when none of those resolve (e.g. no * token list passed, or a HyperCore vault whose underlying isn't hydrated). */ logoURI?: string; /** * Redemption model — whether a depositor can withdraw underlying cash * synchronously (standard ERC-4626 up to available liquidity) or must go * through a delay/queue/cooldown. * - `sync` — morpho/lista/euler-earn/fluid/gearbox/silo/yearn and `instant` * savings: instant withdraw up to the vault's cash. For these, ~zero * withdrawable liquidity against non-trivial TVL is a genuine red flag * (stuck capital — e.g. an allocator parked in an exploited market). * - `async` — LSTs (unbonding/cooldown/off-chain), cooldown savings (sUSDe), * and epoch/request vaults (lagoon/upshift). Zero *instant* liquidity is * expected by design and must NOT be read as a risk. * Absent on hypercore/gmx (USD-denominated, no share redemption model). */ redemptionType?: 'sync' | 'async'; /** * The structured deal — rate, maturity, exit, fees, backing, counterparty, * availability and principal risk in the SAME shape a lending market uses, * so one consumer renders both. Stamped by `stampVaultTermSheets` right after * this pass; absent until then. * * Mixed in HERE rather than added to sixteen provider types by hand: this * interface is already inherited by every one of them, so the field arrives * everywhere at once and a new provider gets it for free. * * Read `termSheet.coverage` before trusting a silence — an absent block means * "not applicable" or "not wired" and the two are recorded separately. */ termSheet?: TermSheet; } /** * Per-vault overrides for strategy vaults that are volatile despite living * inside a yield provider (e.g. a leveraged Morpho vault, a Lagoon trading * strategy). Keyed by `${chainId}-${lowercasedAddress}`. * * Seeded from the TradingStrategy dataset (kept locally under * `scratch/tradingstrategy/`): vaults flagged `perp_dex_trading_vault` / * `proprietary_trading`. As of the May 2026 snapshot, ALL such vaults belong to * perp-DEX protocols (Hyperliquid, Lighter, GRVT, …) — none fall inside the * EVM providers we index — so this set is intentionally empty today. It is the * extension point: add `${chainId}-${address}` rows here when a leveraged / * trading strategy vault surfaces inside morpho/euler/silo/lagoon/etc. */ declare const VOLATILE_VAULT_OVERRIDES: Set; /** * Curated stablecoin symbol set (normalised uppercase), compiled from the * dataset's `stablecoinish`-flagged denominations. Symbol-first so it is * chain-agnostic. Combined with a `contains 'USD'` fallback (virtually every * USD-substring token is a USD stablecoin) this covers the long tail without * per-chain address tables. */ declare const STABLECOIN_SYMBOLS: Set; /** True when a token symbol denotes a (fiat-pegged) stablecoin. */ declare const isStablecoinSymbol: (symbol: string | undefined) => boolean; interface ClassifyVaultInput { provider: VaultProvider | 'hypercore' | 'gmx'; chainId: string | number; address: string; /** Underlying token symbol, when known (drives `denomination`). */ underlyingSymbol?: string; /** * Underlying token ADDRESS, when known. Consulted against * {@link STABLECOIN_UNDERLYING_OVERRIDES} before the symbol heuristic, for * the underlyings whose ticker cannot identify them. */ underlyingAddress?: string; } /** * Classify a vault on both axes. Pure — no I/O. `yieldProfile` is * provider-default with a per-vault override; `denomination` is * positive-ID by underlying symbol (defaults to `volatile` when unknown). */ declare const classifyVault: (input: ClassifyVaultInput) => VaultClassification; /** * Mutate every vault in a `VaultPublicDataAll` payload, setting * `yieldProfile` + `denomination` via {@link classifyVault}. Called once in * `getVaultPublicDataAll` after the providers resolve and before the lookup * is built, so both the per-provider objects and the lookup carry the fields. * * `denomination` reads the underlying token symbol (`asset.symbol`), falling * back to the share symbol when asset metadata wasn't hydrated — most stable * vaults embed the stablecoin in their share symbol (`steakUSDC`), and * volatile shares rarely contain a stable token name. */ declare const stampVaultClassification: (data: VaultPublicDataAll, chainId: string | number, tokenList?: GenericTokenList) => void; /** * Parsed Fluid fToken entry. * * fTokens are standalone ERC-4626 yield vaults on Fluid's shared Liquidity * Layer. They are NOT a lending market (no collateral, no borrow, no * liquidation) — just a deposit → earn shape. Modeled separately from * Fluid borrow vaults (`src/lending/public-data/fluid/`). */ interface FluidFToken extends VaultClassificationFields { /** fToken (share) contract address, case-preserved from the resolver. */ address: string; /** Lowercased underlying ERC20 address. Fluid's fETH has WETH as its * underlying — the EEE sentinel is not used for fTokens in practice. * Still normalized to `zeroAddress` if ever returned. */ underlying: string; /** Share-token symbol, e.g. `fUSDC`, `fETH`. */ symbol: string; /** Share-token name as returned by `name()`. Falls back to * `Fluid ` when the resolver returns an empty name. */ name: string; /** Cross-provider UI label — `Fluid ${asset.symbol}` (e.g. `Fluid USDC`). * Always non-empty. Mirrors `name` on most chains since Fluid's * on-chain `name()` already returns the same shape; kept as a separate * field for parity with the other vault providers. */ displayName: string; /** Brand label for cross-provider UI parity with `MorphoVault.curatorName` * / `GearboxV3Pool.curatorName`. Fluid is single-curator (the Fluid * team / Instadapp), so this is the constant `'Fluid'`. */ curatorName: string; /** Share and underlying decimals (same — ERC-4626 keeps them aligned). */ decimals: number; /** Total underlying assets held by the vault, raw integer (wei-like) as string. */ totalAssets: string; /** Total shares minted, raw integer as string. */ totalSupply: string; /** `convertToShares(1e)` — raw, 1 underlying → shares ratio. */ convertToShares: string; /** `convertToAssets(1e)` — raw, 1 share → underlying ratio. */ convertToAssets: string; /** Base supply APR in percent (e.g. `2.83` = 2.83 %). From Liquidity Layer utilization. */ supplyRate: number; /** Extra rewards APR in percent, on top of `supplyRate`. */ rewardsRate: number; /** Sum of `supplyRate + rewardsRate` — what a depositor actually earns. */ depositRate: number; /** Capability flag — the fToken supports a `depositNative()` wrapper that * accepts raw ETH. Doesn't imply `underlying === zeroAddress`; for fETH * the underlying is WETH and this flag is true. */ isNativeUnderlying: boolean; /** True if the fToken supports EIP-2612 permit on deposits. */ eip2612Deposits: boolean; /** Hydrated asset metadata from the provided token list, if any. */ asset?: GenericCurrency; /** USD price of one underlying unit, if prices were supplied. */ priceUsd?: number; /** Human-formatted total assets (`totalAssets / 10^decimals`). */ totalAssetsFormatted: number; /** Human-formatted total assets in USD (`totalAssetsFormatted * priceUsd`). */ totalAssetsUsd: number; /** Currently withdrawable underlying (raw integer as string), sourced * from the Liquidity Layer's `withdrawable` field — the fToken's own * supply bounded by the shared-liquidity withdrawal limit. Caps * immediate exits. */ liquidity: string; /** Human-formatted immediate withdrawable liquidity. */ liquidityFormatted: number; /** Human-formatted immediate withdrawable liquidity in USD. */ liquidityUsd: number; /** Borrowed-against-collateral breakdown. fTokens supply into Fluid's * shared Liquidity Layer rather than discrete markets, so "exposure" is * reframed: each entry is a Fluid borrow vault that borrows this fToken's * underlying, showing the collateral it's borrowed against, the amount * borrowed (`assets` in the underlying / `assetsUsd`), the share of the * underlying's total borrows (`weightPct`), and that vault's borrow rate * (`supplyApr`). Ordered by weight descending. Undefined when nothing * borrows the underlying or the borrow-vault fetch is unavailable. */ exposures?: VaultMarketExposure[]; } /** Full parsed payload: per-underlying map for easy UI lookup. */ type FluidFTokens = { /** Keyed by lowercased underlying address (`zeroAddress` for native ETH). */ [underlying: string]: FluidFToken; }; /** * Parser for the single-call fToken fetch built by `buildFluidFTokensCall`. * * data[0] = FTokenDetails[] (from LendingResolver.getFTokensEntireData) * * Returns a map keyed by lowercased underlying address. Native-ETH fTokens * key under `zeroAddress` to match the convention in `calldata-sdk` * (`isNativeAddress` / `normalizeNativeAddress`). */ declare const getFluidFTokensConverter: (chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList) => [(data: any[]) => FluidFTokens | undefined, number]; /** * One-shot fetcher for all Fluid fTokens on a chain. * * Wraps build + multicall + parse so callers that just want "the fTokens * snapshot" don't need to hand-wire the pipeline. * * @param chainId target chain * @param multicallRetry provider-level multicall executor (same one used * by `getLenderPublicData`) * @param prices optional price map keyed by oracle key / underlying * @param tokenList optional token list for hydrating `asset` metadata * * @returns `FluidFTokens` — map keyed by lowercased underlying address, or * an empty object if the chain has no resolver configured. */ declare const fetchFluidFTokens: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList) => Promise; /** * One-shot fetcher for all MetaMorpho vaults on a chain. * * Routes between two data sources, normalized to the same `MorphoVault` * shape: the curated **on-chain** path (via the `morphoTypeVaults` registry) * for chains the hosted API doesn't cover, and the public **Morpho API** * for chains it indexes. The on-chain branch needs a `multicallRetry` * executor; the API branch ignores it (HTTP GraphQL). * * (A Goldsky-subgraph branch used to serve SEI / Celo / Lisk / Soneium / TAC * / Hemi, but all of those now have on-chain registry entries — and the * upstream subgraph endpoints were retired — so the on-chain path covers them * with real `liquidity`/APR and the subgraph path was removed.) * * @param chainId target chain * @param multicallRetry provider-level multicall executor (on-chain branch) * @param prices optional price map keyed by oracle key / underlying * @param tokenList optional token list for hydrating `asset` metadata * * @returns `MorphoVaults` — map keyed by lowercased vault address, or * an empty object when the chain has no Morpho deployment. */ declare const fetchMorphoVaults: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList) => Promise; /** * Fetches MetaMorpho vaults from the public Morpho API for a single chain. * * Covers all chains indexed by `blue-api.morpho.org` (Ethereum mainnet, * Base, Arbitrum, Polygon, Unichain, etc.). Pages through at most * `maxItems` entries — vault counts per chain are well under 200 in * practice but we loop defensively for mainnet where count is highest. */ declare function fetchMorphoVaultsFromApi(chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, maxItems?: number): Promise; interface FetchMorphoVaultsFromChainOptions { /** Restrict to a subset of curator-protocol keys (e.g. `['LISTA_DAO']`). */ protocols?: string[]; /** * Override the registry — `[{ vault, underlying, name? }]`. Lets fork tests * point at a specific vault list without populating the global registry. */ vaultsOverride?: MorphoTypeVaultEntry[]; } /** * On-chain MetaMorpho vault fetcher — multicalls every vault listed in * `morphoTypeVaults()` for the chain (or in `opts.vaultsOverride`) and * normalizes the results to the same `MorphoVaults` shape produced by the * Morpho API and Goldsky paths. * * Intended as a fallback for forks / chains where the Morpho public API and * Goldsky subgraph are unreachable. The supply rate isn't computable from * vault state alone (it requires walking the underlying market allocations), * so `supplyRate` and `rewardsRate` are surfaced as `0` — same convention * used by the Goldsky path when rewards aren't indexed. `liquidity` is the * real withdrawable amount from the allocation walk (idle + Σ min(allocation, * market liquidity)); it falls back to `totalAssets` only when no lens/core is * available for the chain. */ declare const fetchMorphoVaultsFromChain: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, opts?: FetchMorphoVaultsFromChainOptions) => Promise; interface FetchListaVaultsFromChainOptions { /** * Override the registry — `[{ vault, underlying, name? }]`. Lets fork * tests point at a specific vault list without populating the global * registry. */ vaultsOverride?: MorphoTypeVaultEntry[]; } /** * On-chain Lista DAO (Moolah-fork) vault fetcher. Same output shape as * `fetchMorphoVaultsFromChain`, different read path: * * - Phase 1 multicall: `name / symbol / decimals / totalAssets / * totalSupply / fee / feeRecipient / CURATOR` per vault. The role hash * returned by `CURATOR()` is contract-defined, not derivable, so it * must be read on-chain. * - Phase 2 multicall: `getRoleMember(CURATOR_ROLE, 0)` per vault → * surfaces the curator address (Moolah uses * `AccessControlEnumerable`, no direct `curator()` getter). * * `owner` / `guardian` are surfaced as `undefined` (Moolah uses * `DEFAULT_ADMIN_ROLE` for admin and has no guardian role; left out to * avoid extra round-trips). `timelock` is `0` — no timelock pattern in * Moolah. * * APR is computed via an allocation walk: each vault's `withdrawQueue` * enumerates the markets it lends to; `MOOLAH.position(id, vault)` gives * the vault's supply shares per market; the Lista lens supplies each * market's supply APY + share/asset totals. The vault `depositRate` is the * allocation-weighted market supply APR over `totalAssets` (idle assets * earn nothing), net of the vault performance `fee`. If the Moolah core * address or lens is missing for the chain, APR falls back to `0`. */ declare const fetchListaVaultsFromChain: (chainId: string, multicallRetry: MulticallRetryFunction, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, opts?: FetchListaVaultsFromChainOptions) => Promise; /** * One-shot fetcher for all Silo v2/v3 vaults on a chain via the public * `api-v3.silo.finance` GraphQL indexer. * * The same endpoint serves both protocol versions — pass * `protocolVersion` to narrow to just one. Returns `{}` for any chain not * in `SILO_API_SUPPORTED_CHAIN_IDS` (Ethereum, Arbitrum, Avalanche, XDC, * Injective at time of writing). Silo vaults on Sonic must still be * fetched via a dedicated path when the indexer adds coverage. * * Unlike `fetchFluidFTokens` / `fetchGearboxV3Pools`, no multicall * executor is needed — the source is HTTP GraphQL. */ declare const fetchSiloVaults: (chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, options?: { protocolVersion?: "v2" | "v3"; limit?: number; }) => Promise; /** * One-shot fetcher for all Euler Earn vaults on a chain. * * Source strategy — Euler Data API primary, Goldsky subgraph fallback: * - The Euler Data API reads live on-chain figures (APR-converted rates, * correct share decimals, cap-aware liquidity), so it's the source of * record. Used first on every chain. * - The Goldsky subgraph is the fallback when the API is unavailable or * returns nothing for the chain (and only on chains that have a * subgraph configured). It can lag chain head, so it's no longer the * primary path. * * @param chainId target chain * @param prices optional price map keyed by oracle key / underlying * @param tokenList optional token list for hydrating `asset` metadata * @param multicallRetry optional multicall executor — both paths use it to * read each earn vault's real `decimals()` on-chain * (the Data API's `decimals` field is unreliable) * * @returns `EulerEarnVaults` keyed by lowercased vault address — `{}` when * neither source returns data. */ declare const fetchEulerEarnVaults: (chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, multicallRetry?: MulticallRetryFunction) => Promise; /** True when the chain has an Euler Earn Goldsky subgraph configured. */ declare const hasEulerEarnVaultSubgraph: (chainId: string) => boolean; /** * Fetches Euler Earn vaults from the Goldsky subgraph for chains * configured in `EULER_EARN_SUBGRAPH_URLS`. * * Two-phase fetch: * 1. `eulerEarnVaults` + flat strategies (drops the nested `eulerVault` * relation, which throws null-violation errors when un-indexed). * 2. `eulerVaults` filtered to the assets discovered in step 1, used * both for `decimals` (Earn subgraph doesn't expose it) and live * `state.supplyApy` (used to compute the weighted Earn-vault APY). * * Phase 2 failures degrade gracefully — Earn-vault rows are still * returned with `supplyRate = 0` and `decimals` from the token list. */ declare function fetchEulerEarnVaultsFromSubgraph(chainId: string, prices?: { [asset: string]: number; }, tokenList?: GenericTokenList, multicallRetry?: MulticallRetryFunction): Promise; /** * Probe amount for the share-price read. `convertToAssets(x)` is a pure * ratio (assets-per-share), so the result scales with `x` but the * *growth* ratio across two reads is independent of it — we just fix it * at 1e18 for every vault regardless of decimals. */ declare const VAULT_SHARE_PRICE_PROBE: bigint; /** * Read the current ERC-4626 share price (`convertToAssets(1e18)`) for a * batch of vaults in one multicall. The basic, protocol-agnostic yield * signal — every standard 4626 vault exposes it, so this works across * Morpho / Euler / Silo / Fluid / LST / savings / arbitrary 4626 vaults * alike. * * Returns a map of lowercased address → raw share price (string). Vaults * whose call reverts or returns 0 (non-4626, empty, or paused) are * omitted rather than throwing — the caller records what it can. * * This is the *realized* yield signal. See [./README.md](./README.md) for * how it ranks against a vault's *published* intrinsic APR (the published * fetcher wins at the display layer; this is the fallback / drift check). */ declare const readVaultSharePrices: (chainId: string, addresses: string[], multicallRetry: MulticallRetryFunction) => Promise>; /** One recorded share-price sample for a vault. */ interface VaultYieldSnapshot { /** Unix seconds when the sample was taken. */ t: number; /** Raw `convertToAssets(1e18)` share price at `t`, as a string. */ p: string; } /** Per-vault time series, keyed by lowercased address. */ type VaultYieldSeries = { [address: string]: VaultYieldSnapshot[]; }; interface VaultAprResult { /** Annualised realised return from share-price growth (e.g. 0.043 = 4.3%). */ apr: number; /** Share price at the latest sample. */ sharePriceNow: string; /** Share price at the lookback sample used. */ sharePriceThen: string; /** Seconds between the two samples used. */ windowSeconds: number; /** Number of samples in the series. */ samples: number; } interface AppendSnapshotOptions { /** Max ring length — oldest samples drop off. Default 90. */ maxPoints?: number; /** Skip the append if the last sample is newer than this. Default 0 * (always append). Gates the ring to a sane cadence regardless of how * often the recorder fires. */ minIntervalSeconds?: number; } /** * Append a sample to a vault's ring buffer, dropping the oldest beyond * `maxPoints` and skipping samples that arrive sooner than * `minIntervalSeconds` after the last one. Pure — returns a new array. */ declare const appendSnapshot: (points: VaultYieldSnapshot[], snap: VaultYieldSnapshot, options?: AppendSnapshotOptions) => VaultYieldSnapshot[]; interface ComputeVaultAprOptions { /** Target lookback window. Default 7. The sample closest to * `now − windowDays` (and strictly before `now`) is used as the base. */ windowDays?: number; /** Require at least this much elapsed between the two samples, else * return `undefined` (a window too short makes the annualised figure * meaningless). Default 1 hour. */ minWindowSeconds?: number; } /** * Annualise a vault's realised return from its recorded share-price * series: * * apr = (priceNow / priceThen − 1) × (yearSeconds / elapsedSeconds) * * `priceNow` is the latest sample; `priceThen` is the sample closest to * `now − windowDays`. The ratio is computed in bigint (scaled by 1e18) * to preserve the small per-day growth that double precision would lose * on ~1e18-scale prices. * * Returns `undefined` when there aren't two usable samples or the window * is shorter than `minWindowSeconds`. */ declare const computeVaultApr: (points: VaultYieldSnapshot[], options?: ComputeVaultAprOptions) => VaultAprResult | undefined; /** * Resolve the config a position's `mode` slot refers to — aware that the slot * is OVERLOADED, and that the two meanings must not collapse into one lookup. * * On most lenders `modes[posId]` is a CONFIG KEY (an Aave e-mode category, a * Euler controller id): `configs[mode]` either hits or the mode genuinely does * not exist for this market. * * On a PARAMETERIZED market — one whose base config declares an * `openParameter` — the slot carries the borrower's chosen VALUE instead * (LlamaLend's band count `N`). There is no `configs["17"]`, and there never * will be: the config advertises the DOMAIN, the position carries the value. * A plain `configs[mode]` lookup therefore misses, and every caller's * `?? 1` factor fallback then prices the collateral UNDISCOUNTED — health * reads `deposits/debt`, borrow capacity reads NAV-shaped, and a max-loop * quote overshoots what the Controller will accept. That is the bug this * resolver exists to end. * * Resolution: * 1. `configs[mode]` — the ordinary hit, always preferred. * 2. Else, if the base (`'0'`) config declares an `openParameter`: the mode is * a parameter value. Return the base config, with the moved dimension * re-priced from `openParameter.curve` when the curve carries this value — * LlamaLend ships the full 4..50 LTV curve, so a real band count always * resolves. A value the curve does not know (or a curve-less declaration, * e.g. the borrowed-side row whose factors do not move with `N`) falls * back to the base config's numbers — the DEFAULT point, which is at worst * the old pre-`modes` behavior, never factor-1. * 3. Else `undefined` — a genuinely unknown mode on an unparameterized * market, which callers already handle. */ declare function resolveModeConfig(configs: MarketConfigs | null | undefined, mode: string): MarketConfigEntry | undefined; interface EModeAssets { /** marketUids eligible as collateral in this mode */ collateral: string[]; /** marketUids eligible for borrowing in this mode */ borrow: string[]; } type EModeBlockReason = 'collateral_not_supported' | 'debt_not_supported' | 'health_factor'; interface EModeResult { /** Protocol-agnostic mode identifier (replaces legacy `category`) */ modeId: number; /** @deprecated Use `modeId` */ category: number; label: string; /** null means no debt (health is infinite) */ healthFactor: number | null; supportedAssets: EModeAssets; /** false if HF <= 1 after switch, user holds incompatible debt, or user has * collateral-enabled assets that are not valid collateral in this mode */ canSwitch: boolean; /** Reason the switch is blocked; absent when canSwitch=true */ blockReason?: EModeBlockReason; /** marketUids of the positions that block the switch */ blockingAssets?: string[]; } /** * For each available e-mode, compute the hypothetical health factor, * list supported assets, and determine if the switch is valid. */ declare function computeEModeAnalysis(subAccount: UserDataForSubAccount, lenderMeta: LenderCrossPoolMeta, eModes: Record): EModeResult[]; /** * Sumer absorption waterfall: computes total effective collateral in USD. * * For each group, collateral absorbs debt in priority order: * 1. cToken collateral absorbs suToken debt @ intraMintRate (highest) * 2. cToken collateral absorbs cToken debt @ intraCRate * 3. suToken collateral absorbs remaining suToken debt @ intraSuRate * 4. suToken collateral absorbs remaining cToken debt @ intraSuRate * Remaining collateral is valued at inter-group rates. */ declare function computeSumerWaterfall(groups: SumerMarketMeta[], accum: GroupAccumulator): number; /** * Build group-level rate maps and accumulators from a position array. * * Returns: * - groupRatesMap: unique SumerMarketMeta per groupId * - gAccum: per-group accumulators (enabled collateral only) * - gAccumAll: per-group accumulators (all protocol-active markets) */ declare function buildSumerAccumulators(positions: SumerPositionInput[]): { groupRatesMap: Record; gAccum: GroupAccumulator; gAccumAll: GroupAccumulator; }; /** * Clone positions and apply a USD delta to the target market. */ declare function applyPositionDelta(positions: SumerPositionInput[], targetMarketUid: string, depositsDeltaUSD: number, debtDeltaUSD: number, debtStableDeltaUSD: number): SumerPositionInput[]; /** * Compute post-trade balance metrics for a Sumer withdraw operation. * * Re-runs the waterfall absorption with decreased deposits for the target market. */ declare function computeSumerWithdrawDelta(amount: number, price: number, targetMarketUid: string, balanceData: BalanceData, positions: SumerPositionInput[], apr?: AprData, yieldParams?: LenderYields$1): PostTradeMetrics; /** * Compute post-trade balance metrics for a Sumer borrow operation. * * Re-runs the waterfall absorption with increased debt for the target market. * Debt changes affect the waterfall because debt is absorbed by collateral * in the 4-phase process. */ declare function computeSumerBorrowDelta(amount: number, price: number, targetMarketUid: string, balanceData: BalanceData, positions: SumerPositionInput[], apr?: AprData, yieldParams?: LenderYields$1, irMode?: number): PostTradeMetrics; /** * Compute post-trade balance metrics for a Sumer repay operation. * * Re-runs the waterfall absorption with decreased debt for the target market. * Reducing debt frees collateral from intra-group absorption, potentially * increasing the effective collateral valued at inter-group rates. */ declare function computeSumerRepayDelta(amount: number, price: number, targetMarketUid: string, balanceData: BalanceData, positions: SumerPositionInput[], apr?: AprData, yieldParams?: LenderYields$1, irMode?: number): PostTradeMetrics; /** * Active sub-account indexes for an owner (true set; for the next-account * route). Falls back to [0] when the API is unsupported or fails. */ declare function fetchEulerSubAccountIndexes(chainId: string, owner: string): Promise; /** * Derives a sub-account address from an owner address and index (0-255). * Uses the EVC formula: `address(uint160(owner) ^ uint160(accountId))`. * Since accountId is uint8, only the last byte is affected via XOR. */ declare function getSubAccountAddress(owner: string, index: number): string; /** * Extracts the sub-account index from a sub-account address given its owner. * Reverses the EVC formula: `index = lastByte(subAccountAddr) ^ lastByte(owner)`. */ declare function getSubAccountIndex(addr: string, owner?: string): number; /** * One Dolomite sub-account as the READ path sees it: `(owner, number)`. * * For an ordinary account `owner` is the user. For an isolation-mode position * (dGM, dsavETH, dGMX, …) `owner` is the user's per-factory VAULT — the vault, * not the user, owns the DolomiteMargin account — and `isolationMarketId` is * the factory's marketId, which is exactly the `isolationModeMarketId` the * Dolomite routers take to reach that vault again. `id` is the position id * the API exposes: the bare account number for the user's own accounts, and * `iso::` for vault-owned ones, so an action can be * addressed without ever carrying the vault address. */ interface DolomiteSubAccount { id: string; owner: string; number: string; isolationMarketId?: string; } declare const DOLOMITE_ISO_ID_PREFIX = "iso:"; /** Parse a position id back into `(isolationMarketId?, accountNumber)`. */ declare function parseDolomiteSubAccountId(id: string): { isolationMarketId?: string; number: string; }; declare function toDolomiteSubAccountId(number: string, isolationMarketId?: string): string; /** * The vault that owns `user`'s position in isolation market `marketId` * (`IsolationModeVaultFactory.calculateVaultByAccount`), derived offline from * the factory's `vaultInitCodeHash`: `CREATE2(factory, keccak256(user), hash)`. * Verified bit-exact against `getVaultByAccount` on Arbitrum (2026-09-18). * The address is deterministic whether or not the vault exists yet — reading * a not-yet-created vault answers an empty account. `undefined` when the * market is not an isolation market or its hash is unknown (the dead djUSDC * V1 factory predates the getter). */ declare function dolomiteVaultAddress(chainId: string, marketId: string | number | bigint, user: string): string | undefined; /** * Every funded / borrowing sub-account an owner controls — their own accounts * AND the accounts of their isolation-mode vaults — from the Dolomite * subgraph. Memoized 30 s so concurrent build + parse calls share one fetch. * Always includes the user's default account `'0'`. Falls back to just that * on any error. */ declare function fetchDolomiteSubAccounts(chainId: string, owner: string): Promise; /** * The account NUMBERS the owner holds directly (not through a vault) — the * shape `/next-account` and explicit-`accountNumbers` callers work with. * Always includes `'0'`. */ declare function fetchDolomiteAccountNumbers(chainId: string, owner: string): Promise; /** Synchronous read of the sub-accounts a prior build resolved. */ declare function getResolvedDolomiteSubAccounts(chainId: string, account: string): DolomiteSubAccount[] | undefined; /** Legacy shape: the resolved position ids (numbers, or `iso:` ids). */ declare function getResolvedDolomiteAccountNumbers(chainId: string, account: string): string[] | undefined; /** * Isolation-mode facts a Dolomite market row carries. Everything a caller * needs to know that the row's `underlying` is a per-user VAULT FACTORY * (`dGM`, `dsavETH`, `dGMX`, …) and not a token anyone holds: * * - `underlying` is what the user deposits / receives (GM, savETH, GMX); * `factory` is the market token itself. * - `allowedDebtMarketUids` / `allowedCollateralMarketUids` are the vault's * allow-lists as market uids. `undefined` = unrestricted (the on-chain * list is empty) — never confuse with "no debt allowed". * - `wrapper` / `unwrapper` are the trusted converter traders a zap must use * as the last / first hop; `null` = no loop route through this market. * - `async` = GMX V2 / GLV: the wrap is a keeper-executed GMX deposit paid * with `executionFeeWei` of native, and the vault is frozen meanwhile. */ interface DolomiteIsolationRow { factory: string; underlying: string; underlyingSymbol: string; underlyingDecimals: number; allowedDebtMarketUids?: string[]; allowedCollateralMarketUids?: string[]; wrapper: string | null; unwrapper: string | null; wrapperInputMarketUids: string[]; unwrapperOutputMarketUids: string[]; async: boolean; executionFeeWei: string | null; } interface DolomiteRowIdentity { /** The row label (`Dolomite dGM [WETH-USDC]`, `Dolomite WETH`). */ name: string; /** The token-list meta the row renders with (the dToken's, logo-filled from the underlying). */ asset: any; /** * The key the intrinsic-yield join reads: the ASSET GROUP (as every other * lender), and for an isolation market the UNDERLYING's group — a dGM * earns what its GM earns. */ yieldKey: string; isolation?: DolomiteIsolationRow; } /** * Resolve name / asset meta / yield key for one Dolomite market, and the * isolation block when the market is an isolation-mode factory. * * The isolation table comes from `config/dolomite-isolation.json` * (`dolomiteIsolationMarket`); when it is absent for a chain the token-list * overlay's `props.receipt.underlying` still names the underlying, so the * yield join and the label degrade gracefully instead of to `dGM` ×12. */ declare function resolveDolomiteRowIdentity(chainId: string, lender: string, marketId: string | number, token: string, tokenList?: GenericTokenList): DolomiteRowIdentity; interface RawRpcResponse { jsonrpc: '2.0'; id: number; result?: string; error?: { code: number; message: string; }; } declare function parseRawRpcResponses(responses: RawRpcResponse[], callMetadata: PreparedCall[], allowFailure?: boolean): any[]; declare function parseRawRpcBatchResponses(batches: RawRpcBatch[], batchResponses: RawRpcResponse[][], allowFailure?: boolean): any[]; /** * Parses multicall3 aggregate3 responses * The response contains an array of {success, returnData} tuples * Each returnData needs to be decoded using the original call's ABI * * `permanentFailures`, when supplied, is filled with the indices of calls that * failed DETERMINISTICALLY. This path can tell them apart with certainty, which * the viem path can only infer: if the batch response itself came back, the * transport worked, so a `success: false` entry inside it is a revert — the * chain's answer, not a lost read. Only a batch-level error is a lost read. * * The distinction matters downstream: a cross-margin lender voids its whole set * on a lost read, and without this a single always-reverting market would void * a perfectly good position on every request. */ declare function parseMulticallRpcResponses(responses: RawRpcResponse[], batches: MulticallRpcBatch[], allowFailure?: boolean, permanentFailures?: Set): any[]; type TokenEntry = { chainId: string; decimals: number; name: string; address: string; symbol: string; assetGroup: string; currencyId: string; }; interface FetchTokenMetadataOptions { /** * RPCs to read through. Defaults to the curated per-chain overrides — pass * an explicit list from a Worker, where `LIST_OVERRIDES` is not necessarily * the set the deployment is allowed to use. */ rpcUrls?: string[]; maxRetries?: number; } /** * Read `name`/`symbol`/`decimals` for arbitrary addresses on one chain. * * Three properties this function is responsible for, all of which the earlier * `allowFailure: false` version got wrong: * * 1. **One bad address must not sink the batch.** A caller resolving a pasted * address alongside five known ones would otherwise get nothing back. * 2. **A missing `decimals()` means NOT AN ERC-20, and the address is * OMITTED.** It is never defaulted to 18 — every amount in this codebase is * scaled by that number, so a wrong decimals is a wrong transaction, and a * silent default is the shape that produces one. * 3. **`name`/`symbol` are cosmetic and may legitimately be absent.** A token * with real `decimals` and no `symbol` still resolves; the strings fall back * to the address prefix rather than dropping the row. * * @returns entries keyed by LOWERCASED address. Addresses that did not resolve * are absent from the result — the caller must not assume input order or * completeness. */ declare function fetchTokenMetadata(chain: string, addrs: string[], options?: FetchTokenMetadataOptions): Promise>; interface TokenBalanceQuery { chainId: string; account: string; tokens: string[]; } interface TokenBalanceEntry { tokenIndex: number; tokenAddress: string; balance: bigint; } interface ParsedUserBalance { userIndex: number; userAddress: string; balances: Record; } interface ParsedBalanceData { balances: ParsedUserBalance[]; blockNumber: bigint; } interface TokenBalanceResult { [tokenAddress: string]: { balanceRaw: string; balance: bigint; }; } interface PreparedTokenBalanceRpcCalls { call: RawRpcCall; query: TokenBalanceQuery; encodedCalldata: string; } /** * Encodes the calldata for the balance fetcher contract * Format: [uint16 numTokens][uint16 numAccounts][addresses...][tokens...] */ declare function encodeBalanceFetcherCalldata(accounts: string[], tokens: string[]): string; /** * Parses the raw hex response from the balance fetcher contract * Handles both ABI-encoded bytes response and raw packed data * Returns parsed balance data with block number and per-user balances */ declare function parseBalanceFetcherResult(hexData: string, users: string[], tokens: string[]): ParsedBalanceData; /** * Prepares the RPC call for fetching token balances without executing it * Uses the balance fetcher contract for efficient batched balance queries * * @param chainId - The chain ID * @param account - The account address to query balances for * @param tokens - Array of token addresses (use zeroAddress for native balance) * @param blockTag - Block tag for the RPC call, default is 'latest' * @returns The prepared RPC call and metadata needed for parsing */ declare function prepareTokenBalanceRpcCalls(chainId: string, account: string, tokens: string[], blockTag?: string): PreparedTokenBalanceRpcCalls; /** * Converts the raw RPC result into a structured token balance map * * @param rawResult - The raw hex result from the RPC call * @param query - The original query parameters * @returns Map of token address to balance data */ declare function parseTokenBalanceResult(rawResult: string, query: TokenBalanceQuery): TokenBalanceResult; interface FetchTokenBalancesOptions { blockTag?: string; rpcUrl?: string; rpcUrls?: string[]; maxRetries?: number; } /** * End-to-end function that fetches token balances for an account. * Combines prepareTokenBalanceRpcCalls and parseTokenBalanceResult * with retry logic and RPC URL rotation. * * @param chainId - The chain ID * @param account - The account address to query balances for * @param tokens - Array of token addresses * @param options - Optional configuration (blockTag, rpcUrl/rpcUrls, maxRetries) * @returns Map of token address to balance data */ declare function fetchTokenBalances(chainId: string, account: string, tokens: string[], options?: FetchTokenBalancesOptions): Promise; /** * The normalized input the builder reads. * * Deliberately NOT `PoolData` directly: the same market travels through this * codebase in two casings — the in-package shape (`totalDepositsUSD`, * `variableBorrowRate`) and the API-serialized shape (`totalDepositsUsd`, * nested `caps`/`flags`/`config`). The builder must work on both, because it * runs in-package during a fetch AND at the worker while proxying an origin * response. * * So: one tolerant reader (`toTermSheetInput`) normalizes either shape into * this interface, and the builder itself is pure over the normalized form. */ interface TermConfigEntry { category: number | string; label?: string; borrowCollateralFactor?: number; collateralFactor?: number; borrowFactor?: number; liquidationPenalty?: number; closeFactor?: number; targetHealthFactor?: number; collateralDisabled?: boolean; debtDisabled?: boolean; } interface TermRewardInput { asset?: string; depositRate?: number; variableBorrowRate?: number; stableBorrowRate?: number; /** Merkl / points programs mark themselves; absent ⇒ a normal token. */ kind?: string; endsAt?: number; claim?: string; } interface TermMenuInput { termId: number; durationSecs: number; durationDays: number; apr: number; depositApr?: number; available?: number; } /** Normalized market facts the generic builder needs. */ interface TermSheetInput { marketUid: string; lender: string; chainId: string; /** Underlying asset of THIS row. */ asset?: { chainId?: string; address?: string; symbol?: string; name?: string; decimals?: number; assetGroup?: string; logoURI?: string; }; underlying?: string; decimals?: number; depositRate?: number; variableBorrowRate?: number; stableBorrowRate?: number; intrinsicYield?: number; rewards?: TermRewardInput[]; rateModel?: string; originationFee?: number; totalDeposits?: number; totalDebt?: number; totalDebtStable?: number; totalLiquidity?: number; borrowLiquidity?: number; totalDepositsUsd?: number; totalDebtUsd?: number; totalLiquidityUsd?: number; utilization?: number; irmTotalDeposits?: number; irmTotalDebt?: number; lockupRatio?: number; supplyCap?: number; borrowCap?: number; debtCeiling?: string | number; /** Minimum borrow in RAW base units (Compound V3 `baseBorrowMin`). */ minBorrow?: string; isActive?: boolean; isFrozen?: boolean; borrowingEnabled?: boolean; depositsEnabled?: boolean; collateralActive?: boolean; hasStable?: boolean; variableBorrowDisabled?: boolean; config?: Record; closeFactor?: number; targetHealthFactor?: number; fixedTerm?: { model?: string; maturity?: number; fees?: { continuousFeeApr?: number; settlementFee?: number; latePenaltyApr?: number; originationFeePercent?: number; }; earlyRepay?: { kind?: string; }; provider?: { kind?: string; address?: string; }; auction?: Record; }; terms?: TermMenuInput[]; /** Market-level params (`params.market`) when the lender has them. */ market?: Record; } /** * Normalize either the in-package `PoolData`-ish row or an API `LendingMarket` * item into {@link TermSheetInput}. Tolerant by design: a field missing under * one casing is looked up under the other, and nested `caps`/`flags` bundles * are unwrapped. */ declare function toTermSheetInput(row: Record, ctx?: { marketUid?: string; lender?: string; chainId?: string; market?: Record; /** * Item-level fixed-term descriptor. `/lending/latest` attaches `fixedTerm` * to the LENDER item, not to each market row, so without this every * fixed-term market would silently lose its maturity, its fees and its * auction window. A row-level `fixedTerm` (how `/pools/latest` serializes * it) is more specific and wins. */ fixedTerm?: Record; }): TermSheetInput; /** * Accepted-collateral / backing set from the SIBLING rows of the same lender. * * Pooled lenders (Aave, Compound) do NOT record on-chain which collateral * backs which borrow, so the result is the ACCEPTED SET with * `weightBasis: 'unweighted'` and NO `weightPct` on any item. Inventing a * TVL-proxy weight would look authoritative and be false — a large idle * collateral market is not a large exposure. */ declare function buildExposures(input: TermSheetInput, siblings: TermSheetInput[], direction: 'backing' | 'accepted'): ExposureTerms | undefined; /** Deep merge an adapter's partial over the generic result. Arrays REPLACE. */ declare function mergeDeep(base: T, patch: DeepPartial | undefined): T; /** * Fill `info` (headline / description / tags) LAST, after adapters have run, * so the prose always describes the final values rather than the generic * guess. This is the mechanism that stops copy drifting from numbers. */ declare function finalizeInfo(sheet: TermSheet): TermSheet; interface BuildTermSheetOptions { /** Unix seconds; injected so tests are deterministic. */ now?: number; /** Other rows of the SAME lender+chain — used to derive the exposure set. */ siblings?: TermSheetInput[]; /** Adapter output, merged over the generic result. */ patch?: DeepPartial; profileId?: string; } /** Build one complete term sheet for one market row. */ declare function buildTermSheet(input: TermSheetInput, opts?: BuildTermSheetOptions): TermSheet; /** Supply-side tags. Market-level tags are folded in by the caller. */ declare function deriveSupplyTags(supply: SupplyTermSheet, market?: Pick): TermTag[]; /** Borrow-side tags. */ declare function deriveBorrowTags(borrow: BorrowTermSheet, market?: Pick): TermTag[]; /** * Severity model — PURE, derived only from structured fields. * * Space is the binding constraint at every display depth, so ranking has to be * principled rather than per-lender taste. There is deliberately NO * hand-maintained list of "scary markets": a newly integrated lender is * classified correctly the moment its adapter sets the right fields. * * - `critical` — you can lose MORE than the amount at stake, or lose it * without doing anything wrong. This is the only tier that should gate a * signature. * - `warn` — it costs money, or blocks you. * - `info` — everything else. */ type Severity = 'critical' | 'warn' | 'info'; interface SeverityFinding { severity: Severity; /** Stable slug — the join key for UI copy and for tests. */ id: string; /** Ready-to-render sentence. */ message: string; side: 'supply' | 'borrow' | 'market'; } /** Sort findings most-severe-first, stable within a tier. */ declare function rankFindings(findings: SeverityFinding[]): SeverityFinding[]; declare function supplyFindings(supply: SupplyTermSheet): SeverityFinding[]; declare function borrowFindings(borrow: BorrowTermSheet): SeverityFinding[]; /** * All findings for one side of a sheet, ranked most-severe-first. Pass * `side: 'supply' | 'borrow'` — market-level findings are always included * because governance and oracle affect both sides. */ declare function findingsFor(sheet: TermSheet, side: 'supply' | 'borrow'): SeverityFinding[]; /** Does this side carry anything that should gate a signature? */ declare function hasCritical(sheet: TermSheet, side: 'supply' | 'borrow'): boolean; /** `4.1234` → `"4.12 %"`; trims to 2dp, drops a trailing `.00`. */ /** * Format an already-PERCENT value (`49.88` → `"49.88%"`). * * Never pass a fraction. The convention across the term sheet is **rates are * percent, factors/ratios are fractions**, so `liquidationLtv`, `penalty`, * `utilization` and every `*Ratio` must be multiplied by 100 at the call site. * * Trailing zeros are trimmed only inside the FRACTIONAL part: a naive * `/\.?0+$/` strip eats integer zeros whenever `dp = 0` leaves no decimal * point, turning `100` into `"1"` and `1000` into `"1"`. * * No space before the `%` — the portal formats the same numbers without one, * and a headline reading "49.88 %" next to a table cell reading "49.88%" looks * like two different figures. */ declare function pct(value: number | undefined, dp?: number): string; /** Duration in seconds → the coarsest human unit that stays honest. */ declare function duration(secs: number | undefined): string; /** Unix seconds → `"3 Sep 2026"`. Locale-independent so snapshots are stable. */ declare function shortDate(unixSecs: number | undefined): string; /** One fee → a self-contained phrase, correct even for an unrecognised `id`. */ declare function feePhrase(fee: FeeTerm): string; declare function supplyHeadline(s: SupplyTermSheet, /** Underlying asset, so a route minimum in the headline names its unit. */ sheet?: Pick): string; /** Borrow-side headline. */ declare function borrowHeadline(b: BorrowTermSheet): string; /** Supply-side description — 1–3 sentences, market values interpolated. */ declare function supplyDescription(s: SupplyTermSheet, sheet?: Pick): string; /** Borrow-side description. */ declare function borrowDescription(b: BorrowTermSheet): string; declare const TERM_PROFILES: TermProfile[]; declare function getTermProfile(id: string): TermProfile | undefined; /** Fallback used when a family has no dedicated profile yet. */ declare const DEFAULT_PROFILE_ID = "pool.variable@v1"; /** * Vault provider → its base profile. * * EXHAUSTIVE over `VaultProvider` by type, which is the point: adding a * provider without deciding what its terms are is a compile error, and a test * asserts every entry resolves to a real profile. Pendle reached production * before this map existed and inherited savings-vault prose ("check the * cooldown") that is wrong in every clause — a `Record` is * what makes that impossible to repeat. */ declare const VAULT_PROVIDER_PROFILE: Record; /** * Profile for a vault row. * * `solvency` — where a curated classification exists — OVERRIDES the provider * default, because the loss waterfall is the term that matters most and it * varies WITHIN a provider: Strata's senior and junior tranches are both * `savings` rows keyed one address apart, and reading the same "your share * price accrues" prose on a first-loss tranche is the single most misleading * thing this surface could do. */ declare function resolveVaultProfileId(provider: VaultProvider, solvency?: CounterpartyTerms['solvency']): string; /** * `earnUid` — the primary key of the unified earn surface. * * There is NO single unified format, and deliberately so. There are two forms * sharing one grammar (`::`): * * ``` * lending → :: the EXISTING marketUid, verbatim * vault → vault.::
the only new form * ``` * * Reusing the grammar rather than inventing a format is what buys zero * migration on the lending half: a lending row's `earnUid` IS its `marketUid`, * so it is already valid on every existing action route. * * **The uid is OPAQUE.** The meaning of the third segment is venue-dependent — * on the lending side it is the underlying for Aave/Morpho/Compound V3, the * cToken for Compound V2, the silo for Silo, the eVault for Euler, and an * INTEGER marketId for Dolomite (not an address at all). Only `parseEarnUid` * may split an `earnUid`; consumers read `EarnMarket.asset.address` to learn * what to deposit, never the uid. * * See EARN_ENDPOINT_PLAN.md §3. */ /** * Namespace prefix marking the vault form. Vault providers are lowercase and * `Lender` keys are uppercase, so bare provider names would already be * collision-free — but only by case, which one consumer lowercasing a uid * somewhere would silently break. The prefix makes the distinction structural * and `isVaultVenue` a `startsWith`. */ declare const VAULT_VENUE_PREFIX = "vault."; /** A lending venue is a `Lender` key; a vault venue is `vault.`. */ type EarnVenueKind = 'lending' | 'vault'; interface ParsedLendingEarnUid { kind: 'lending'; /** The `Lender` key, e.g. `AAVE_V3`. */ venue: string; chainId: string; /** * The uid's third segment. Venue-dependent — resolve it with the worker's * `parseMarketUid`, never by assuming it is a token address. */ ref: string; /** Byte-identical to the input. Pass straight to `parseMarketUid`. */ marketUid: string; } interface ParsedVaultEarnUid { kind: 'vault'; /** The full venue string, e.g. `vault.savings`. */ venue: string; /** The bare provider, e.g. `savings`. */ provider: VaultProvider; chainId: string; /** Lowercased share-token address (L1 vault address for hypercore). */ address: string; } type ParsedEarnUid = ParsedLendingEarnUid | ParsedVaultEarnUid; /** * Cheap syntactic check — does this uid use the vault form? Does not validate * the provider; use `parseEarnUid` when correctness matters. */ declare function isVaultVenue(uidOrVenue: string): boolean; /** `savings` → `vault.savings`. */ declare function vaultVenue(provider: VaultProvider): string; /** * Mint the vault form from its parts. The address is lowercased to match the * canonical keying used across the vault pipeline (`buildVaultLookup` and the * `${chainId}-${address}` DB convention both lowercase). * * @throws if any part is empty. */ declare function buildVaultEarnUid(provider: VaultProvider, chainId: string, address: string): string; /** * The lending form. This is the IDENTITY FUNCTION over `marketUid` — it exists * so call sites read as a deliberate mapping rather than an assignment, and so * the shape is validated once at the boundary. * * Never RECONSTRUCT a lending uid from a row's fields. `/pools/latest` rows * carry `marketUid` stamped by the public-data parsers; rebuilding it as * `lender:chainId:underlying` is only correct for the default-format lenders * and silently mints a wrong key for Compound V2 (needs the cToken) and * Dolomite (needs the integer marketId). A wrong uid does not degrade a term * sheet — it routes a deposit to the wrong market. See EARN_ENDPOINT_PLAN §3.2. * * @throws if `marketUid` is not three non-empty colon-separated segments, or * if it collides with the `vault.` namespace. */ declare function earnUidFromMarketUid(marketUid: string): string; /** * Split an `earnUid` into its parts, discriminated by form. * * `knownProviders` defaults to undefined, which accepts any `vault.` venue. * Pass the live provider set when parsing untrusted input (an action route's * query param) so an unknown provider fails at the edge with a clear message * rather than deep inside a dispatcher. * * @throws on a malformed uid or an unrecognised provider. */ declare function parseEarnUid(earnUid: string, knownProviders?: readonly VaultProvider[]): ParsedEarnUid; /** Non-throwing `parseEarnUid`. Returns undefined instead of throwing. */ declare function tryParseEarnUid(earnUid: string, knownProviders?: readonly VaultProvider[]): ParsedEarnUid | undefined; /** Which half of the surface a uid belongs to, without a full parse. */ declare function earnVenueKind(earnUid: string): EarnVenueKind; /** * `EarnMarket` — one supply-side opportunity, from either half of the stack. * * This is a PROJECTION, not a replacement. The borrow side, pairs, leverage * and migrate stay on `/v1/data/lending/*`; a lending market appears here as * one row per supplyable asset with a `refs.marketUid` pointer back. * * Everything here is derived from data the two origins already return — the * work is unit normalization and identity, not new fetching. * * See EARN_ENDPOINT_PLAN.md §4. */ interface EarnMarket { /** * TWO FORMS, not one — see `./uid`. OPAQUE: only `parseEarnUid` may split it. * lending → the row's `marketUid`, VERBATIM * vault → `vault.::
` */ earnUid: string; chainId: string; /** `AAVE_V3` (a `Lender` key) or `vault.savings` (`vault.`). */ venue: string; venueKind: EarnVenueKind; /** * Display label — the curator where one exists, else the protocol. * Kept for consumers that want one string; prefer `protocol` + `curator` * when the two need to be told apart. */ brand?: string; /** * The PROTOCOL this venue is built on — Morpho, Euler, Silo, Aave V3. * * Load-bearing for vaults: a MetaMorpho vault and an Euler Earn vault both * render as their curator ("Steakhouse Financial", "TelosC Surge"), and * without this nothing on the row says which lending stack the deposit * actually lands in. Two vaults with the same curator on different protocols * are different risk, and two with different curators on the same protocol * share one. * * On the lending half this is the lender itself. */ protocol?: EarnProtocol; /** Who RUNS this instance, where the venue is curated. Absent ⇒ uncurated. */ curator?: EarnCurator; /** Market or vault display name. */ name?: string; /** * The row's provenance as ONE ready-to-render string — `Gauntlet · Morpho · * Vaults`, `Aave V3 · Lending markets`. * * Published so no client composes it. Assembling it from `curator` + * `brand`/`protocol` + `venueKind` is server vocabulary in a client, and it * broke the moment `protocol.name` became version-free: the frontend kept * rendering it, so Aave V2 and Aave V3 rows read identically. See * `earnRowSubtitle` for the de-duplication rules. */ subtitle?: string; /** * The uid's third segment, lifted out so consumers never parse the uid. * Venue-dependent on the lending side (underlying / cToken / silo / Dolomite * marketId / …); the share token on the vault side. */ ref: string; /** Branded icon URL, when one resolved. */ logoURI?: string; /** What the user deposits. */ asset: EarnAsset; /** Present only when depositing mints a distinct receipt token. */ shareToken?: EarnShareToken; /** * Present when the row's POSITION is a multi-token pool position rather than * a balance of `asset`. Absent ⇒ an ordinary single-asset row. * * `asset` stays what it is — the thing the user hands over — because that is * what a deposit form needs. What it stops being on these rows is what the * user ends up HOLDING, and nothing else on the row says so. */ basket?: EarnBasket; /** What the user earns. ALWAYS PERCENT — see `EarnRate`. */ rate: EarnRate; /** Size and room. */ tvl: EarnAmount; /** What can actually leave right now. Absent ⇒ not reported by the source. */ liquidity?: EarnAmount; /** * Room for new deposits, raw base units. `undefined` = uncapped, * `'0'` = full. Distinct from `availability.canDeposit`, which is a * permission — a vault can be permissionless AND full (3Jane USD3), or * gated AND empty. */ depositCapacity?: string; /** Debt/supply ratio. Lending and lending-backed vaults only. */ utilization?: number; /** * When the deal ENDS. Present only on rows that have a maturity — Pendle * PTs today, and the fixed-term lenders (TermMax, Exactly, Midnight, Term, * Teller) as the term-sheet adapter reaches them. * * Lifted to the row root rather than left inside `termSheet` because for a * fixed-rate product the maturity is not enrichment: a 9 % rate over eleven * days and a 9 % rate over two years are different offers, and a client that * has to opt into `?terms=full` to tell them apart will sort them into the * same column. Absent ⇒ perpetual. */ maturity?: MaturityTerms; /** The deal. Digest by default, full sheet under `?terms=full`. */ termSheet?: TermSheet | TermSheetDigest; exit: EarnExit; availability: EarnAvailability; risk?: EarnRisk; /** How to transact. Empty ⇒ nothing can be done right now. */ capabilities: EarnCapability[]; /** Pointers, never copies. */ refs?: EarnRefs; /** Provider-specific escape hatch. Semantics unchanged from the source. */ providerMeta?: Record; } interface EarnProtocol { /** * The STABLE family key — `MORPHO_BLUE`, `COMPOUND_V3`, `vault.morpho`. * * Deliberately NOT the row's `venue`: on the lending half that is minted per * market (`MORPHO_BLUE_<32-byte id>`), so it identifies one market rather * than the protocol and cannot be filtered or cached on. This can. */ key: string; /** * Display name — `Aave V3`, `Morpho`, `Ethena`. * * What `?protocol=` matches, because a name can be shared where a key * cannot: every `vault.savings` row has one key but names its own protocol. */ name: string; } interface EarnCurator { name?: string; /** Legal/brand entity behind the curator, where the registry carries one. */ entity?: string; } interface EarnAsset { /** The underlying the user supplies, lowercased. */ address: string; symbol: string; decimals: number; /** Price-group key ('ETH', 'USDC') — how the price store is keyed. */ assetGroup?: string; priceUsd?: number; } interface EarnShareToken { address: string; symbol: string; /** * Share decimals. Equals `asset.decimals` for plain ERC-4626, but NOT for * Lagoon (18-decimal shares over 6-decimal USDC) — never assume they match. */ decimals: number; } /** * A position whose unit is a POOL POSITION over several tokens. * * ## Why this is on the row and not in `providerMeta` * * Every earn row is rendered under one asset's name, and for these rows that * name is a half-truth: deposit USDC into a Fluid smart vault and you hold a * claim on USDC *and* ETH, in a split that keeps moving and that you do not * control. Ranked in a list beside ordinary USDC rows it reads as the same * kind of thing, and it is not — it carries the other leg's price risk. The * one place that fact can live is the row. * * It is also the fix for a rate bug that already shipped: on a pool position * the per-leg rate is right PER DOLLAR and is NOT the position's APR, so a * "best APR" taken as the max over legs read 11.81 % where the vault earned * 10.33 %. `rate` on a basket row is always the POSITION's; `legs[].rate` * keeps the per-leg figure so the headline stays auditable. * * ## Deliberately NOT Fluid-shaped * * `legs` is an ARRAY, not a pair. Fluid smart vaults, Lista SmartLP, Uniswap * V2 and GMX GM are all two-token, but Curve 3pool is three, tricrypto is * three and Balancer goes to eight — and a `[Leg, Leg]` tuple is exactly the * kind of shortcut that forces a second, competing vocabulary the first time a * three-token pool arrives. `LP_ACTIONS_PLAN.md` §3's lending-side `lp` * descriptor still spells a 2-tuple because both of its providers are * two-token; when a Curve or Balancer LP reaches the LENDING half, that one * should adopt this shape rather than the reverse. */ interface EarnBasket { /** * How this row's own `asset` relates to the pool position. * * `leg` — the row IS one leg, and the venue emits one row per leg * (Fluid smart: a T4 emits up to four rows for ONE vault). * Consumers must dedupe on `positionUnit` before summing, * or they will count the same position N times. * `positionUnit` — the row IS the pool position; `asset` is the LP token * itself (GMX GM/GLV, a Curve LP gauge, Lista SmartLP). */ rowAsset: 'leg' | 'positionUnit'; /** * The pool's tokens, IN THE POOL'S OWN INDEX ORDER. * * The order is load-bearing, not cosmetic: a two-input deposit form maps its * inputs to it positionally, and swapping them lands the amounts on the * wrong legs. Preserve it verbatim from the source; never sort it. */ legs: EarnBasketLeg[]; /** * The composition is POOL-CONTROLLED — it drifts with price and trading, and * a holder cannot choose to hold one leg on its own. * * True for every constant-function pool (Uniswap V2, Curve, Balancer, Fluid * DEX, GMX GM). It would be FALSE for a position whose split the user sets * and the protocol does not touch, which is why this is a flag rather than * being implied by `legs.length > 1` — a basket and an auto-balanced basket * are different claims, and a UI warning about drift must only fire on the * second. */ autoBalanced: boolean; /** What the position is denominated in, when it is not `asset`. */ positionUnit?: { /** `shares` = an internal share count (Fluid); `lpToken` = a real ERC-20. */ kind: 'shares' | 'lpToken'; /** Present for `lpToken` — this is what a client dedupes leg rows on. */ address?: string; decimals: number; }; /** * Current split by VALUE, parallel to `legs`, fractions summing to ~1. * * The word "current" is the whole point: it is a snapshot of a number that * moves, so it is safe to display and never safe to cache or to treat as the * ratio a future deposit will land at. */ weights?: number[]; /** * Pool venue family — `fluid-dex`, `curve`, `uniswap-v2`, `gmx-gm`, * `lista-smartlp`. Free-form on purpose: it exists so a client can say WHERE * the position lives without parsing `venue`, not so anyone can switch on it. */ pool?: string; /** * Swap fee in bps, so a client can explain what an off-ratio entry costs * without protocol knowledge. Absent ⇒ not published by the source. */ feeBps?: number; } interface EarnBasketLeg { address: string; symbol?: string; decimals?: number; assetGroup?: string; /** * This leg's OWN rate, PERCENT — what the row's `rate.total` was blended * from. Kept because it is not wrong (it is the rate per dollar sitting in * this token) and because hiding it makes the headline unauditable against * the market page the user came from. */ rate?: number; } /** * An amount, carried in whichever scales the source actually provides. * * **The two origins do NOT agree on scale, and this is a 1e18 landmine.** * The vault side reports `tvl.totalAssets` as a RAW base-unit integer string; * the lending side reports `totalDeposits` already through `parseRawAmount`, * which is `formatUnits` — i.e. TOKEN UNITS, despite the field being commented * "raw amounts" at its source. Folding both into one field would sort a $1M * Aave reserve below a dust vault. * * So: `formatted` is the cross-source field a consumer should compare on, and * `raw` is present only where the source genuinely carries base units. Never * infer one from the other without `asset.decimals`. */ interface EarnAmount { /** * Raw base-unit integer STRING — never a number; these overflow float64 well * inside normal 18-decimal balances. Absent when the source is pre-formatted. */ raw?: string; /** Human/token units. The field to compare and sort on. */ formatted?: number; usd?: number; } /** * The headline yield. * * **Every field is a PERCENT** (`4.12` = 4.12 %). This is the single most * dangerous field on the row: the lending origin reports percent, the 4626 * vault providers report percent, but realized APR, HyperCore `apr` and GMX * `apy`/`baseApy`/`bonusApr` are FRACTIONS at the source. Normalizing them on * the way in is not optional — the same column would otherwise be 100× off * between two rows in one list. See `vaults/DATABASE_INTEGRATION.md` §1. */ interface EarnRate { /** The headline: base + rewards + intrinsic. */ total: number; /** Protocol interest / share-price accrual. */ base?: number; /** Incentive programs (Merkl et al). */ rewards?: number; /** The underlying's own yield (stETH staking under an stETH market). */ intrinsic?: number; /** * What THIS venue pays, on top of what the asset would pay in your wallet: * `base + rewards`, excluding `intrinsic`. * * This is the number that answers "what am I being paid for taking this * market's risk". `total` answers "what will my balance do", and for a * yield-bearing collateral the two are very different. */ marketOwn?: number; /** * True when the asset carries its own yield and the venue adds ~nothing — * `intrinsic > 0` and `marketOwn < 0.01 %`. * * These rows are the reason an APR-sorted earn list misleads: an LST market * showing 3 % out-ranks a genuine 2.5 % stablecoin market, but supplying into * it earns you **the same as holding the token**, with the market's * liquidation, oracle and governance risk added for free. Filtered out by * default — see the `passthrough` param on `/v1/data/earn`. * * Distinct from `availability.gating === 'collateral-only'`, which is a * market paying nothing on an asset that also pays nothing. */ passthrough?: boolean; /** * The venue's cut of the yield, PERCENT (`10` = 10 % of interest earned). * * Not subtracted from anything here — every rate on this row is already net * of it. It is carried because "why is this vault's rate below the market it * lends into" has exactly one answer and it is this number, and because a * term sheet built from this row has no other way to state a fee schedule. */ fee?: number; /** * HOW the rate is set — the provenance that stops a leaderboard from lying. * A `variable-curve` 8 % and a `nav-accrual` 8 % are not the same promise. */ kind: RateKind; /** WHERE the number came from. */ source: EarnRateSource; /** Unix seconds — the rate is a snapshot. */ asOf?: number; } type EarnRateSource = /** Read from the chain (IRM, accumulator, share price). */ 'chain' /** Protocol or aggregator API (Morpho, DefiLlama, Strata S3). */ | 'api' /** A price/NAV feed (Re, USPC, Apyx). */ | 'oracle' /** Derived from a share-price series (`computeVaultApr`). */ | 'realized'; /** * How the money gets out. Flattened to the row root deliberately: on * `/v1/data/vaults` this lives in `providerMeta.withdrawalMode`, which is a * known integrator trap. Here it is always at the same path for every venue. */ interface EarnExit { mode: SupplyExitMode; settlement?: SupplyExitTerms['settlement']; cooldownSecs?: number; /** Instant-exit fee, where taking the fast path costs something. */ feeBps?: number; /** * Every leg of the exit, one entry each — the structured form of what * `mode` + `cooldownSecs` + `feeBps` encode between them. * * The three flat fields above cannot describe a CHOICE, and half our modes * are one: on `fee-or-queued` the fee belongs to the instant leg and the * cooldown to the other, so reading them together says "0.5 % AND a week", * which is true of neither leg. They are kept for compatibility; new * consumers should read this. */ routes?: VaultExitRoute[]; /** * The measured half of "how likely is it that funds get stuck": a 30-day * digest of the INSTANT leg's observed capacity, from the recorded hourly * series. ORIGIN-ONLY — the SDK's normalizers cannot derive it (they see one * snapshot), so the edge-merge fallback never carries it and its absence * there is not a finding. Withheld by the origin for modes with no instant * leg (cooldown / queued / request-based): their liquidity is 0 by * definition, and "dry 100 % of the time" would misread a known wait as a * lockup probability. The full per-row picture (p_horizon at a size, dry * episodes, realized-vs-quoted rate) is `/v1/data/earn/metrics`. */ history?: EarnExitHistory; } interface EarnExitHistory { /** window length, days */ days: number; /** hourly point samples observed */ samples: number; /** share of the window actually observed, 0..1 */ coverage: number; /** USD withdrawable at once: worst hour, 5th percentile, median */ capacityUsd: { worst: number | null; p05: number | null; median: number | null; }; /** the same, as a share of TVL (0..1) */ capacityRatio: { worst: number | null; p05: number | null; median: number | null; }; /** share of observed hours with < max($1k, 0.5 % of TVL) withdrawable */ dryShare: number | null; /** maximal dry runs in the window */ dryEpisodes: number; /** longest dry run, hours; null = never dry */ worstDrySpellHours: number | null; currentlyDry: boolean; /** hourly point samples — a trough inside one hour is invisible, so every * figure here underestimates the lockup */ lowerBound: true; } interface EarnAvailability { canDeposit: boolean; canWithdraw: boolean; /** Why not, when `canDeposit` is false. */ gating?: EarnGating; /** Human-readable, for the disabled-CTA tooltip. */ reason?: string; } type EarnGating = /** Contract callers must be governance-approved (Inverse, Fraxlend). */ 'allowlist-contract' | 'kyc' /** Permissionless but at its cap (3Jane USD3, Yield Basis). */ | 'cap-full' | 'paused' | 'frozen' /** * A fixed-term product past its maturity. Entry is closed and the published * rate is stale by construction; the exit leg stays open so a holder can * still redeem. Should be rare — matured rows are filtered upstream — but * the check is repeated here because a recorder lag is exactly how a dead * market ends up at the top of a rate-sorted list. */ | 'matured' /** Deposits are open but this leg earns nothing (collateral-only reserve). */ | 'collateral-only'; interface EarnRisk { /** Monotonic accrual vs a NAV that can fall. */ yieldProfile?: YieldProfile; denomination?: Denomination; /** The trust question, one field. */ counterparty?: CounterpartyTerms['solvency']; /** * `GREATEST(chain, lender, propagated_token)` — **higher is worse**. The rest * of the API lists `<= 4` by default and this surface matches it, so the earn * list does not disagree with `/pools` about what is listable. */ score?: number; /** Human band for `score` — 'low' | 'medium' | 'high' | … */ label?: string; /** Claims a same-block exit but reports zero liquidity. See `isIlliquid`. */ illiquid?: boolean; } interface EarnRefs { /** Lending only — the full market (borrow side, pairs, IRM). */ marketUid?: string; /** Can this asset also be borrowed here? */ borrowable?: boolean; /** What actually backs a curated vault. */ exposures?: VaultMarketExposure[]; /** The market's price feed. Read by `disambiguateEarnNames`, nothing else. */ oracle?: string; /** What that feed prices, e.g. `BTC / USD`. */ oracleDescription?: string; } type EarnActionKind = 'deposit' | 'withdraw' /** Open an async exit (cooldown, queue, keeper ticket). */ | 'request-withdraw' /** Settle a matured request. */ | 'claim' /** Unwind an open request before it settles. */ | 'cancel'; /** * What can be done to this row, and what each op needs. * * This is what makes the flow genuinely unified: the client stops branching on * provider. It renders the CTA from `capabilities`, collects whatever * `requires` names, and posts one shape to `/v1/actions/earn/{action}`. * Yield Basis' mandatory `debt`/`minShares`, Apyx's `tokenId`, GMX's * `executionFee`, an LST's `validator` — all surface as DATA rather than as * tribal knowledge in the integrator's head. */ /** * One asset a deposit will accept, and what that particular path needs. * * The requirement is PER INPUT, not per action: minting mETH with native ETH * needs `minMETHAmount`, while the same vault's ERC-20 path may need nothing. * A flat `requires` on the action cannot express that, and flattening it to the * union would demand inputs the chosen path never reads. */ interface EarnActionInput { /** `'native'` or a lowercased ERC-20 address the mint accepts. */ asset: string; symbol?: string; /** * Build shape: `direct` · `wrap` (wrap a base LST already held) · * `submit-wrap` (native → base → wrapped, two legs) · * `psm-then-deposit` (swap through a PSM first). */ mode?: string; /** Option keys this path REQUIRES. Absent ⇒ none. */ needs?: string[]; /** Option keys this path accepts but does not require. */ optional?: string[]; } interface EarnCapability { action: EarnActionKind; /** * Params required BEYOND the universal set (`earnUid`, `operator`, * `receiver`, `amount`). E.g. `['validator']`, `['debt','minShares']`, * `['tokenId']`. */ requires?: string[]; /** Deposit only — may the user pay an asset other than `asset.address`? */ acceptsPayAsset?: boolean; /** Withdraw only — may the user receive something other than the asset? */ acceptsReceiveAsset?: boolean; /** * Settles later (keeper ticket, cooldown, queue) — the client must poll * `/v1/data/earn/withdrawals` rather than treat the tx as terminal. */ async?: boolean; /** Cost hint for the CTA, before quoting. */ feeBps?: number; /** * HOW the action is executed. * * Absent (or `'native'`) ⇒ a protocol call — deposit/withdraw/redeem against * the venue itself, which is every 4626 vault and every lending market. * * `'swap'` ⇒ **there is no protocol entry point at all**; the position is * acquired and closed by TRADING the instrument. Pendle PTs are the case: * you buy the bond on Pendle's AMM at a discount and sell it back, so both * legs need a slippage tolerance, both are priced by pool depth, and neither * has a "deposit the underlying" path to fall back on. A client that renders * a 4626-style amount box for one of these builds an input the venue cannot * serve. */ via?: 'native' | 'swap'; /** * Assets this action accepts, where the venue takes more than its primary * one — and what each of those paths needs. * * Present ⇒ the pay-asset picker should be LIMITED to these; absent ⇒ the * venue takes its underlying only. The requirement is per input because it * genuinely differs: minting mETH with native ETH needs `minMETHAmount`, * while another path on the same vault may need nothing. */ inputs?: EarnActionInput[]; } /** * `/v1/data/earn` response. The shape NEVER changes — a degraded source is * reported in `sources[]` with the rows that did resolve still served, rather * than switching to a different payload the way `/v1/data/vaults` does when * its origin is down. */ interface EarnResponse { ok: boolean; /** Pagination offset of the first item. */ start: number; /** Items in THIS page. */ count: number; /** Items matching the filter across all pages. */ total: number; /** Stamped so no consumer has to guess. Always `'percent'`. */ rateUnit: 'percent'; items: EarnMarket[]; sources: EarnSourceStatus[]; /** * Rows this endpoint removed by DEFAULT, so a UI can offer them back rather * than a user wondering where a market went. Only the pass-through default * (see `EarnRate.passthrough`) removes anything unasked; every other filter * is opt-in. */ excluded: EarnExclusions; /** * What the server filtered WITHOUT being asked. Echoed so a default is never * invisible — an unseen filter is indistinguishable from missing data. */ appliedDefaults?: EarnAppliedDefaults; /** What a client can filter by, derived from the data. See {@link EarnFacets}. */ facets: EarnFacets; } interface EarnExclusions { /** Rows hidden because the venue adds ~nothing over the asset's own yield. */ passthrough: number; /** Claims a same-block exit but reports zero liquidity. */ illiquid: number; /** Below the TVL floor — an APR there is a rounding artefact. */ lowTvl: number; /** Above the risk ceiling the rest of the API also applies. */ highRisk: number; /** * Rows whose position is a multi-token LP (`basket`), hidden by default. * * Excluded unasked because the row names ONE token while the position holds * several in a split the pool keeps moving — so a depositor reading it as * "USDC at 10 %" ends up carrying the other leg's price risk. Reported here * so a UI can offer `?lp=include` rather than let the rows vanish silently. */ lp: number; /** * Rows removed because their RATE IS NOT A MEASUREMENT — see * `earn/rateSanity.ts`. Two mechanisms, counted together because they have * one consequence: a market pinned at ~100 % utilization publishing an * ever-growing APR against nothing withdrawable, and a rate that is an * artifact of how it was sampled (a few hundred dollars of TVL, or a * fixed-term annualization a few hours from maturity). * * **Unlike every other bucket here this one is not a preference and cannot * be switched off.** The others hide rows a caller might reasonably want; * these publish a number that does not describe the market, on rows a * depositor could enter and not exit. The count is reported for the same * reason the others are — a filter nobody can see is indistinguishable from * missing data. */ unrealizable?: number; /** * Fixed-term rows whose maturity has already passed, hidden by default. * * A matured principal token redeems at par and earns nothing more, so it is * not an opportunity — but its recorded row does not disappear (the vault * table is upsert-only), it FREEZES at the last tick before expiry. For an * instrument whose APR is a price deviation raised to * `365 / daysToMaturity`, that is the worst tick to freeze: 19 such rows * were serving APRs from +267 % to −315 % across 5 chains, contaminating * both ends of every rate sort. `?includeExpired=true` returns them — with a * ZERO rate — for a holder who needs to redeem. */ matured?: number; } interface EarnAppliedDefaults { minTvlUsd: number; maxRiskScore: number; excludePassthrough: boolean; excludeIlliquid: boolean; /** LP / auto-rebalancing positions are hidden unless `?lp=include`. */ excludeLp: boolean; /** Matured fixed-term rows are hidden unless `?includeExpired=true`. */ excludeMatured?: boolean; } /** * The filter vocabulary, published rather than hard-coded. * * **A client must never ship its own list of venues or providers.** The * existing frontend does — `VAULT_PROVIDERS` in `sdk/vaults-helper/types.ts` * is a 13-entry copy of `VaultProvider`, already two behind the SDK's 15, so a * newly-integrated protocol is invisible until the frontend redeploys. Facets * invert that: the server enumerates what exists, the UI renders whatever it * receives, and a new lender or vault provider appears with no client change. * * Counts are computed over the **full merged listing for the requested * chains**, before any filter is applied — so selecting one venue does not make * the other options vanish from the dropdown. */ interface EarnFacets { /** * The PROTOCOL each row is built on — the axis that groups a MetaMorpho * vault with the Morpho markets it allocates into, rather than scattering it * across curators. `brands` answers "who runs it"; this answers "what is it". */ protocols: EarnFacetBucket[]; /** * Third parties that RUN an instance of a protocol — Steakhouse Financial, * Gauntlet, TelosC Surge. * * Distinct from `brands`, which is "curator where there is one, else the * protocol" and therefore mixes the two: a brands-fed curator dropdown lists * Ethena, Lido, Fluid and Silo alongside the real curators, none of which * curate anything. Only genuinely curated rows appear here, so an empty * selection is meaningful and the counts are answerable. */ curators: EarnFacetBucket[]; /** * Underlying assets by SYMBOL. * * Keyed by symbol rather than `assetGroup` because `assetGroup` is frequently * null, which left an asset dropdown built on it mostly empty. A symbol is * always present. */ assets: EarnFacetBucket[]; /** * Venues grouped by BRAND — the dimension a filter UI should offer. * * A chain can carry 20+ `MORPHO_BLUE_` venues; listing each is a wall of * hex nobody filters by. `brands` collapses them to one "Morpho Blue (23)" * entry. `venues` stays for precise, single-market filtering. */ brands: EarnFacetBucket[]; venues: EarnFacetBucket[]; venueKinds: EarnFacetBucket[]; chains: EarnFacetBucket[]; assetGroups: EarnFacetBucket[]; exitModes: EarnFacetBucket[]; /** Distinct actions available anywhere in the listing. */ actions: EarnFacetBucket[]; } interface EarnFacetBucket { /** The value to send back as a filter param. */ key: string; /** * Display label. **Populated for every bucket the server can name**, so a * client renders `label ?? key` and ships no vocabulary of its own. An * unlabelled key renders as itself rather than as a client-side guess. */ label?: string; /** One-line explanation, where the dimension has one (exit modes, kinds). */ description?: string; /** Rows carrying this value in the unfiltered listing. */ count: number; } /** * The display vocabulary, independent of any listing. * * Served by `GET /v1/data/earn/facets` so a client can build its filter UI * (and label a row it already holds) **without fetching the catalogue**, and * without embedding a single enum value of its own. */ interface EarnVocabulary { /** dimension → key → label, e.g. `exitMode['fixed-cooldown'] = 'Cooldown'`. */ labels: Record>; descriptions: Record>; } interface EarnSourceStatus { source: 'pools' | 'vaults'; status: 'ok' | 'degraded' | 'failed'; /** Rows contributed by this source before filtering. */ rows: number; /** Present when not `ok`. */ error?: string; } /** * The normalized vault facts the builder reads. * * Deliberately NOT any one provider's vault type. The same vault reaches this * code in two shapes — the in-package per-provider object * (`SavingsVault`, `PendlePtMarket`, …) during a fetch, and the recorder's * flattened `/vaults/latest` row (`rates.*`, `tvl.*`, `providerMeta.*`) while * the worker proxies an origin response — exactly the split the lending * `TermSheetInput` already documents. One tolerant reader per shape normalizes * into this interface and the builder stays pure over it. * * Every field is optional except the three that key the row: a vault we cannot * identify is dropped upstream, and a vault whose numbers are missing still * gets a sheet that says so through `coverage`. */ interface VaultTermInput { provider: VaultProvider; chainId: string; /** Share-token address, lowercased. The vault's identity. */ address: string; /** `vault.::
` — the earn uid. */ vaultUid?: string; name?: string; brand?: string; curatorName?: string; asset?: TermAssetRef; /** Curated user-facing explainer (`SavingsVault.description`). */ description?: string; supplyRate?: number; rewardsRate?: number; /** The provider's own all-in figure, when it publishes one. */ totalRate?: number; /** * Yearn's `isForwardApr`: is the published number a PROJECTION or a * MEASUREMENT? Yearn ships both under one field name, so this flag is the * only thing that distinguishes them. */ isForwardApr?: boolean; /** Lagoon's `aprWindow` — `weekly` | `monthly` | `yearly` | `inception`. */ aprWindow?: string; totalAssets?: number; totalAssetsUsd?: number; liquidity?: number; liquidityUsd?: number; /** `liquidity / totalAssets`, when the provider computes it itself. */ instantLiquidityRatio?: number; totalBorrowed?: number; /** Gearbox `expectedLiquidity` — the utilization denominator. */ expectedLiquidity?: number; withdrawalMode?: string; withdrawalCooldownSeconds?: number; withdrawFeeBps?: number; /** * The row's per-leg exit routes, passed straight through to * `SupplyExitTerms.routes`. Present on every vault row; when a row predates * the field the builder derives the same list from the flat fields, so the * sheet never loses the split. */ exitRoutes?: VaultExitRoute[]; /** Performance fee on yield, as the provider reports it. */ fee?: number; /** * Management fee on ASSETS, percent per year. A separate field rather than a * second use of `fee` because the two answer different questions: a * performance fee costs nothing in a flat year, a management fee is owed * regardless. Upshift publishes both, up to 20 % + 2 % on one vault. * * Report the EFFECTIVE rate — 0 where a waiver is live — since that is what * is charged today, and the mutability of the schedule is already carried by * `FeeTerm.mutable`. */ managementFee?: number; /** Pendle's AMM swap fee, a FRACTION (`0.0005`), charged on BOTH legs. */ swapFeeRate?: number; /** Signed bps gap between redemption value and fundamental (Yield Basis TRD). */ redemptionDiscountBps?: number; /** `fee-or-queued` vaults only — is the instant leg switched on at all? */ instantRedeemEnabled?: boolean; /** * Seconds a fresh deposit earns nothing (Frankencoin `INTEREST_DELAY`). * NOT a withdrawal lock — see `RateTerms.warmupSecs`. */ yieldWarmupSeconds?: number; /** * Does the accrual COMPOUND, or is it linear? * * Defaults to compounding, which is right for any vault whose share price * grows continuously. Frankencoin's savings module is linear * (`Δticks × saved / 1e6 / 365 days`) and only compounds when someone * happens to call `refresh`, so labelling it per-second would imply an APY * ~2 % relative above what it pays. */ accrual?: 'linear' | 'compounding'; /** * Does a deposit need an ERC-20 approval? * * Defaults to true — nearly every vault pulls with `transferFrom`. False for * Frankencoin's savings modules, whose underlying grants a registered minter * an implicit infinite allowance, so the deposit route emits no approval and * a sheet claiming one would contradict the envelope beside it. */ needsDepositApproval?: boolean; isMintable?: boolean; /** Raw base units. `undefined` = uncapped, `'0'` = full. */ depositCapacity?: string; /** Raw base units (TermMax `supplyCap`). */ supplyCap?: string; paused?: boolean; depositsPaused?: boolean; withdrawalsPaused?: boolean; /** HyperCore — the vault is closed to new deposits. */ isClosed?: boolean; /** Morpho — the vault is on the curator's whitelist. */ whitelisted?: boolean; /** Unix seconds. */ expiry?: number; timelock?: number; /** Addresses that can REALLOCATE the vault between markets, typically with * no timelock — the curation power that moves a depositor's exposure * between blocks. */ allocators?: string[]; owner?: string; curator?: string; guardian?: string; feeRecipient?: string; /** * Curated loss-waterfall / backing classification. Absent ⇒ NOT curated, and * the builder reports that through `coverage.pending` rather than asserting a * comfortable default. */ solvency?: CounterpartyTerms['solvency']; /** NAV feed address, for the share prices published by an operator. */ navOracle?: string; /** `volatile` ⇒ the share price can fall in asset terms. */ yieldProfile?: string; exposures?: VaultMarketExposure[]; } /** * The in-package per-provider vault object, as a permissive union. * * Typed as an index signature rather than `SavingsVault | PendlePtMarket | …` * on purpose: the 15 provider types share no common base beyond * `VaultClassificationFields`, and a discriminated union would have to be * updated in lockstep with every provider — the exact coupling the traits table * exists to avoid. Every read below is guarded. */ type AnyVaultRow = object; /** * Normalize an in-package vault object (the `getVaultPublicDataAll` shape). */ declare function toVaultTermInput(vault: AnyVaultRow, provider: VaultProvider, chainId: string): VaultTermInput | undefined; /** * Normalize an already-normalized `EarnMarket` back into term-sheet input. * * The third door, and it exists for a caching reason rather than a shape one: * `/v1/data/earn` caches the whole MERGED listing and slices pages out of it, * so baking a sheet into every row would inflate the cached blob by thousands * of sheets to serve fifty. Rebuilding from the served page costs nothing and * keeps the cache lean. * * Everything the builder needs survived normalization: the row keeps its * `providerMeta` verbatim, its exposures under `refs`, and its rates — including * the fee — in normalized percent. * * Returns `undefined` for a LENDING row. Those are not vaults and must go * through `buildTermSheet`, which has the borrow side. */ declare function vaultTermInputFromEarnMarket(m: EarnMarket): VaultTermInput | undefined; /** * Normalize a recorder `/vaults/latest` row. * * The origin flattens rates into `rates.*`, sizes into `tvl.*` / `liquidity.*` * and parks everything provider-specific under `providerMeta` — so the same * facts live at different paths than in the SDK shape, and several * (`expiry`, `withdrawalMode`, `depositCapacity`) exist ONLY in `providerMeta`. */ declare function vaultTermInputFromSourceRow(row: Record, chainId: string): VaultTermInput | undefined; interface BuildVaultTermSheetOptions { /** Unix seconds; injected so tests are deterministic. */ now?: number; /** Merged over the generic result, same contract as a lender adapter. */ patch?: DeepPartial; } /** Build one complete term sheet for one vault row. */ declare function buildVaultTermSheet(input: VaultTermInput, opts?: BuildVaultTermSheetOptions): TermSheet; /** * Per-provider INVARIANTS — the facts that are true of every row a provider * emits, and that no row carries a field for. * * This is the vault analogue of the lender `adapters/` directory, collapsed * into a table because vaults differ along far fewer axes than lenders do: * there is no borrow side, no liquidation model and no collateral set, so what * is left is "how is the rate set, how do you get out, and who is on the other * side". Sixteen providers × seven answers fits in one screen and stays * auditable; sixteen adapter files would not. * * `Record` is deliberate: a new provider is a COMPILE ERROR * here, which is the mechanism the plan's §6 enforcement was supposed to * provide and never did. */ interface VaultProviderTraits { /** * How the headline rate is SET. Distinct from where the number was read — * `savings` rows are read from an API or the chain but the rate itself is * governance-set either way. */ rateKind: RateKind; /** WHERE the number came from, for the rows where the provider is uniform. */ rateSource: RateTerms['source']; /** * What period the rate describes. Absent ⇒ `spot`. See `RateTerms.window`: * the providers that MEASURE a return rather than quote a rate must say so, * or a trailing number lands in the same column as a live offer. */ rateWindow?: NonNullable; /** * Exit mode when the row does not carry its own `withdrawalMode`. * * `instant-capped` rather than `instant` for every ERC-4626 vault over * lending markets: those vaults hold a fraction of TVL as cash and the rest * as supply positions, so a withdrawal larger than the liquid balance simply * cannot settle this block. The builder narrows this to `instant` only when * the row PROVES full liquidity (`liquidity >= totalAssets`). */ defaultExitMode: SupplyExitMode; /** Does exiting cost an unknown amount? Vaults redeem at par; PTs do not. */ priceRisk: SupplyExitTerms['priceRisk']; counterpartyKind: CounterpartyTerms['kind']; /** * Solvency when it is a STRUCTURAL property of the provider — true of every * row it emits, with no curation needed. * * Absent ⇒ the answer varies per vault (or has no honest answer in this * vocabulary) and must come from a curated per-vault classification. The * builder then falls back to `overcollateralized` and marks * `coverage.pending.counterparty`, so an assumed answer is never mistaken for * an assessed one. * * The line between the two is whether the protocol can be reasoned about * without knowing WHICH vault: a MetaMorpho allocator only reaches Morpho * Blue markets, and every one of those requires collateral. A Lagoon vault * can hold literally anything its curator picks — one of the live ones is * institutional credit — so no provider-level answer exists. */ solvency?: CounterpartyTerms['solvency']; /** * Does the provider report a `fee` field, and is it a PERFORMANCE fee on * yield? `false` means the provider publishes no fee at all — which must be * reported as unknown, never as an empty fee list (an empty list reads as * "free", and none of these protocols are). */ reportsPerformanceFee: boolean; /** * `true` when the provider reports `fee` as a FRACTION (`0.1` = 10 %) rather * than the percent every other provider uses. * * Exactly one provider does this — `aave-earn`, and its own type says so — * which is precisely why it needs a flag instead of a shared assumption: a * uniform `unit: 'percent'` renders a 10 % curator fee as "0.1 %", a 100× * understatement of the one number on the sheet the curator is paid. */ feeIsFraction?: boolean; /** Does the provider emit `timelock` / role fields? */ reportsGovernance: boolean; /** Does the provider decompose its backing (`VaultMarketExposure[]`)? */ reportsExposures: boolean; /** * Is the underlying position a claim on a decomposable set of markets at * all? `false` for LSTs, savings wrappers, PTs and trading books — for those * an absent `backedBy` is a POSITIVE fact (`coverage.notApplicable`), not a * gap we have yet to fill. */ hasDecomposableBacking: boolean; /** * Does the underlying position have a debt accumulator, i.e. is * `utilization` a meaningful question here? */ hasUtilization: boolean; /** Is the principal at risk in the ordinary course (a trading book)? */ volatilePrincipal?: boolean; } declare const VAULT_PROVIDER_TRAITS: Record; declare function vaultTraits(provider: string): VaultProviderTraits | undefined; /** * Stamp `termSheet` onto every vault in a `VaultPublicDataAll` payload. * * Mirrors `stampVaultClassification` deliberately — same shape, same call site, * same mutate-in-place contract — so both the per-provider objects and the * lookup carry the field, and a consumer never has to ask which pass ran. * * Runs AFTER `stampVaultClassification`, because it reads `yieldProfile` (the * classification's output) to decide whether the principal can draw down. * * Every provider bag is walked, including the ones with thin data: a vault we * know little about still gets a sheet, and `coverage.pending` says what is * missing. Skipping those rows would leave a consumer unable to distinguish * "no sheet built" from "no terms to state". */ declare function stampVaultTermSheets(data: VaultPublicDataAll, chainId: string | number): void; /** * Stamping — the single place term sheets are attached. * * Runs ONCE at the end of the public-data pipeline rather than inside each * lender's converter. That is the whole architecture: ~200 Aave/Compound forks * get correct sheets from the generic builder with zero per-fork work, and * only the ~13 exotic families need an adapter. */ interface StampOptions { /** Unix seconds; injected so tests are deterministic. */ now?: number; /** Attach ranked `implications[]` from the severity model. Default true. */ withImplications?: boolean; /** * Derive `governance` / `oracle` / exposure quality from the rows' own * `oracleInfo` + `risk.breakdown`. Default true — set `false` only to test * the un-enriched builder in isolation. */ enrich?: boolean; } /** * Build sheets for one lender's rows on one chain. * * Siblings matter: the exposure set (`backedBy` / `acceptedCollateral`) is * derived by cross-referencing the OTHER rows of the same lender, so the whole * group has to be built together. */ declare function buildTermSheetsForGroup(rows: Record[], ctx?: { lender?: string; chainId?: string; market?: Record; /** Item-level `fixedTerm` from `/lending/latest` — see `toTermSheetInput`. */ fixedTerm?: Record; }, opts?: StampOptions): Map; /** * Fill `info.implications[]` from the severity model, most severe first. * * Derived rather than hand-written, so a newly integrated lender gets correct * warnings the moment its adapter sets the right structured fields — and a * warning can never contradict the numbers next to it. */ declare function attachImplications(sheet: TermSheet): TermSheet; /** * Build an {@link EnrichmentIndex} from the market rows themselves. * * The governance and oracle screens are NOT a separate fetch: the origin * already ships both on every row — `oracleInfo.feeds[]` (the oracle-risk * classification) and `risk.breakdown[]` (the governance screen under * `category: 'governance'`, the asset screen under `category: 'token'`). So * the join is local to the group being stamped, with no extra round-trip and * no cross-service dependency. * * The per-exposure enrichment falls out of the same data: an exposure item * points at a SIBLING row's `marketUid`, and that sibling is already in this * group — so its oracle and its asset quality are right there. */ declare function enrichmentIndexFromRows(rows: Record[]): EnrichmentIndex; /** * Collapse a sheet to its digest form (`?terms=digest`). * * Drops `items[]` from the exposure sets and the long prose — an Aave market * with 30 accepted collaterals is several kB on its own, and it is the SAME * accepted set repeated on every row of that lender. Every dropped item is * still reachable: each carries a `marketUid` for the bulk endpoint. */ declare function toDigest(sheet: TermSheet): TermSheetDigest; /** Row shape of `~/risk-data/data/oracles/oracle-risk-flat.json`. */ interface OracleRiskRow { marketUid: string; oracle?: string; provider?: string; priceDescription?: string; intendedPair?: string; correctOracle?: boolean | null; denominatorMatch?: boolean | null; fixedRate?: boolean; score?: number; band?: string; flags?: string[]; /** Underlying feeds when an adapter composes several. `oracle` stays the * single source of truth — this is for auditability only. */ components?: string[]; } /** Row shape of `~/risk-data/data/lending/market-governance-flat.json`. */ interface GovernanceRow { marketUid: string; tier?: string; score?: number; ownerKind?: string; signerThreshold?: number | null; signerCount?: number | null; mode?: string; /** Present once the flat builder carries it through (see TERM_SHEET_PLAN §5.4.1). */ delaySeconds?: number | null; } /** Per-asset quality, keyed `chainId → lowercased address`. */ type AssetRiskIndex = Record>; interface EnrichmentIndex { oracleByMarketUid?: Map; governanceByMarketUid?: Map; assetRisk?: AssetRiskIndex; } /** * Merge governance / oracle / asset-quality onto a sheet at the SERVING layer. * * These cannot be computed in-package — they come from the risk-data * screeners, which key on the same `marketUid` grammar (a dictionary lookup, * not a fuzzy match). `margin-fetcher` emits the sheet with these blocks * absent; the worker fills them in, exactly as it already does for * `oracleInfo`. */ declare function enrichTermSheet(sheet: TermSheet, index: EnrichmentIndex): TermSheet; /** * Invariant checker — pure, and the mechanism that turns a derivation bug into * a loud failure instead of a plausible-looking wrong answer. * * Run over live fetched data in CI for every lender key and vault provider. * Every rule here encodes something that WOULD otherwise ship silently. */ interface TermSheetViolation { /** Stable slug, so a test can assert on the specific rule. */ rule: string; message: string; marketUid?: string; } declare function validateTermSheet(sheet: TermSheet): TermSheetViolation[]; /** Validate a batch; returns every violation found, flattened. */ declare function validateTermSheets(sheets: TermSheet[]): TermSheetViolation[]; /** * A term-sheet adapter: returns ONLY what the generic builder cannot derive, * as a `DeepPartial` merged over the generic result. * * Adding a lender is one adapter plus one profile entry — no core edits. The * completeness test fails when a lender family reaches production without a * profile, so the extension point cannot be silently skipped. */ interface TermAdapter { /** Stable id, for tests and debugging. */ id: string; /** Does this adapter handle the given lender key? */ matches: (lender: string) => boolean; /** The profile whose prose this market points at. */ profileId: (input: TermSheetInput) => string; build: (input: TermSheetInput) => DeepPartial; } /** * Order matters only where predicates could overlap; today they are disjoint. * The list is walked front-to-back and the first match wins. */ declare const TERM_ADAPTERS: TermAdapter[]; declare function resolveAdapter(lender: string): TermAdapter | undefined; /** * The STABLE family key behind a venue — `MORPHO_BLUE_1E9D…` → `MORPHO_BLUE`, * `FLUID_1_11` → `FLUID`, `vault.savings` → `vault.savings`. * * This is the identifier a client can filter and cache on. The venue key * itself cannot serve that purpose on the lending half: it is minted per * market, so `?venue=` needs the exact 32-byte Morpho id and a "Morpho" * filter is unexpressible. * * Vault venues are already family-shaped (`vault.`) and pass * through unchanged. */ declare function venueBrandKey(venue: string): string; /** * Collapse a venue key to its display brand. * * `MORPHO_BLUE_1E9D…` → `Morpho Blue`; `FLUID_1_11` → `Fluid`; * `SKY_1_ETH_A` → `Sky`; `vault.savings` → `Savings`. * * Derived from the `Lender` enum, not from a hand-maintained table, so every * integrated lender is named and a new one is named the day its enum member * lands. An unknown key still renders as its own collapsed family — terse but * true, never a guess. */ declare function venueBrand(venue: string): string; /** Every dimension the earn surface labels, in one lookup. */ declare const EARN_LABELS: { readonly venueKind: Record; readonly exitMode: Record; readonly action: Record; readonly gating: Record; readonly rateKind: Record; readonly rateSource: Record; }; declare const EARN_DESCRIPTIONS: { readonly venueKind: Record; readonly exitMode: Record; }; type EarnLabelDimension = keyof typeof EARN_LABELS; /** * Look up a label, falling back to the raw key. * * The fallback is the contract: an unlabelled value renders as itself, so a * newly added mode is legible (if terse) everywhere immediately, and adding its * label later is a pure improvement rather than a bug fix. */ declare function earnLabel(dimension: EarnLabelDimension, key: string): string; declare function earnDescription(dimension: keyof typeof EARN_DESCRIPTIONS, key: string): string | undefined; interface EarnMarketLabelInput { /** The asset being supplied, e.g. `USDC`. */ assetSymbol?: string; /** * Symbols of the collateral(s) that can be posted against this market. * * Length is the whole signal — see {@link earnMarketLabel}. Pass the real * list; do not pre-truncate it, or a shared pool with 30 collaterals becomes * indistinguishable from an isolated pair with 1. */ collateralSymbols?: string[]; /** * The LENDER's own name for this market, as `lenderInfo.name` — * `Morpho cbBTC-USDC 86`, `TermMax RLUSD / USPC — 2026-10-25`, * `Aave V4 Etherfi`, or just `Aave V3` for a shared pool. * * Preferred over the derived pair because it carries what a derived pair * cannot: the **LLTV** that separates three otherwise identical * `USDC · vs WBTC` Morpho markets, the **maturity** on a fixed-term market, * and the **instance** on a multi-spoke deployment. */ lenderMarketName?: string; /** * The row's venue key, used to strip the part of the lender's name that the * brand already states. * * The VENUE and not the brand string, because a brand override can be * SHORTER than the name it has to cancel: `FLUX_FINANCE` displays as "Flux", * so stripping by the brand alone leaves `Flux Finance` → "Finance" and the * row reads "USDT · Finance". Both the display brand and the family key * contribute words. */ venue?: string; /** * The row's maturity, when it has one. * * Appended to whatever label the rules below produce, because a fixed-term * product's date is part of its identity — two PTs on one underlying are * otherwise indistinguishable. See {@link withMaturityLabel}. */ maturity?: MaturityTerms; /** * The name the PUBLIC-DATA FETCHER produced for this row. * * Quality varies by lender and that is the whole point of treating it as a * candidate rather than a fallback: most emit a per-LEG label * (`'Loan ' + symbol`, `'Collateral ' + symbol` — Compound V3, Fluid, Lista, * Term, TermMax, Midnight all do), which says nothing about WHICH market it * is. Euler's parser instead sets the eVault's own on-chain `vaultName`, so * `Prime USDC` was sitting on the row while the label fell through to plain * `USDC` and 530 Euler markets rendered as their asset. * * Leg names are rejected; anything else is used. */ fetcherName?: string; /** * Which slice of a tranched vault this is. * * Strata names its tranches `srUSDe` / `jrUSDe` — two characters carrying * the entire difference between a senior claim and FIRST-LOSS capital that * pays roughly double and can print a negative trailing APR. Read as jargon * or skimmed past, those two rows look like the same product at two rates. * * Taken from `risk.counterparty` (`tranched-senior` / `tranched-junior`), * which the registry already sets per vault — so this states in words what * the data model already knows, rather than parsing a symbol prefix. */ tranche?: 'senior' | 'junior'; /** Used only when there is no asset symbol at all. */ fallbackName?: string; } /** * Which tranche a row is — from `solvency`, and ONLY from `solvency`. * * A senior claim and FIRST-LOSS capital that pays roughly double and can print * a negative trailing APR are different products, and Strata separates them * with two characters: `srUSDe` vs `jrUSDe`. The savings registry already * records which is which (`tranched-senior` / `tranched-junior`) and the SDK * publishes it, so the answer exists — it just does not survive the pipeline: * there is no `savings_solvency` column, so the ingest drops it and `/vaults` * never serves it. Adding that column is the fix. * * **Do not read the tranche off the symbol prefix.** It was tried and measured * against all 396 live chain-1 vault rows: `^(sr|jr)[A-Z]` is clean but tags * only 8 of Strata's 12 (Midas-backed tranches spell it `srmHYPER`, * `jrmM1-USD`), and widening it to `^(sr|jr)[a-zA-Z]` reaches all 12 at the * cost of claiming Reserve's `sreUSD` — a plain savings token — is a senior * tranche. No prefix separates them, and labelling 8 of 12 is worse than * labelling none: it reads as "the other four are not tranches". */ declare function trancheFromCounterparty(counterparty: string | undefined): 'senior' | 'junior' | undefined; /** * The same rule against an EXPLICIT brand, for the rows whose brand does not * come from a venue key. * * A vault's brand is its CURATOR — "Frankencoin", "9Summits", "Lido" — which * `venueBrand('vault.savings')` cannot know, so the vault path needs this * form. It matters because the brand renders in the subtitle beside the name: * without it "Frankencoin Savings Module" sits under "Frankencoin · Vaults" * and "Lido wstETH" under "Lido · Vaults", each saying the brand twice. * * Returns the input unchanged when stripping would leave nothing — a vault * genuinely named after its brand and nothing else ("Ethena") must keep that * name rather than render blank. */ declare function stripLeadingBrand(name: string, brand?: string): string; /** * A market label that actually distinguishes one market from another. * * The problem this solves: an isolated market is a **(collateral, loan) pair**, * but the fetcher emits it as two rows each naming only its own leg — so a * chain with 300 Morpho Blue markets renders 300 rows all reading * "Loan USDC". The identity lives in the relationship between the legs, and * neither leg's name can express it. * * Three sources, in descending order of what they can express: * * **1. The lender's own market name**, once the brand prefix is stripped. This * wins where it exists because it carries what nothing derived can — the LLTV * (`cbBTC-USDC 86`), the maturity (`RLUSD / USPC — 2026-10-25`), the spoke * (`Etherfi`). Three Morpho markets on the very same pair differ ONLY by LLTV, * so a pair-derived label leaves them identical and reproduces the same * complaint one step later. * * The asset is prefixed only when the name does not already state it: * * ``` * 'Morpho cbBTC-USDC 86' + USDC → 'cbBTC-USDC 86' name already says USDC * 'Aave V4 Etherfi' + weETH → 'weETH · Etherfi' name does not * 'Compound USDC' + USDC → (nothing to add — falls through) * 'Aave V3' + WETH → (nothing to add — falls through) * ``` * * **2. The derived pair**, when the lender publishes no name — 12 Fluid and 4 * Silo V3 rows on chain 1 today. **The collateral is named exactly when the * market is ISOLATED**, i.e. exactly one collateral pairs with it: * * ``` * 1 collateral → 'USDC · vs wstETH' the collateral IS the identity * many → 'USDC' a shared pool; naming 1 of 30 misleads * none → 'USDC' collateral-only or unpaired * ``` * * Derived, never configured. No table says "Morpho is isolated, Aave is not" — * the pair count says it, so a newly integrated isolated lender labels itself * correctly with no code change here. * * **3. The asset alone**, which is the right answer for a shared pool: the * brand renders beside it, and picking one of thirty collaterals would assert * something false. */ declare function earnMarketLabel(input: EarnMarketLabelInput): string; /** * Spell out a tranche the name only encodes. * * Skipped when the name already says it in words, so a vault called * "… Senior Tranche" is not stamped twice. The two-letter `sr`/`jr` prefix * does NOT count as saying it — that is the whole reason this exists. */ declare function withTrancheLabel(name: string | undefined, tranche?: 'senior' | 'junior'): string | undefined; /** * Append a fixed-maturity row's date to its name, when the name omits it. * * **A maturity is part of a fixed-term product's identity, not metadata about * it.** Pendle publishes names like `PT wstETH (stETH) Ethereum` with no date, * and on chain 1 alone that produces three collisions — one pair maturing * 2026-08-27 and 2027-12-30, sixteen months apart, rendered as the same row. * Two rows with one name are not a display nit here: they are different bonds * at different prices, and a user picks the wrong one. * * Idempotent by inspection rather than by flag: a venue whose name ALREADY * carries the date (TermMax spells `… — 2026-10-25`, and Pendle's own UI shows * one) must not get it twice. The year is the cheap discriminator — a name * bearing the maturity year is assumed to state the maturity. */ declare function withMaturityLabel(name: string | undefined, maturity: MaturityTerms | undefined): string | undefined; /** * The row's provenance, as ONE ready-to-render string: who runs it · what it * runs on · kind. `Gauntlet · Morpho · Vaults`, `Aave V3 · Lending markets`. * * Published so that no client composes it. Three surfaces were assembling it * from `curator` + `brand`/`protocol` + `venueKind` and all three disagreed — * and it broke silently when `protocol.name` became version-free for grouping, * because the frontend was still rendering that field and Aave V2 and Aave V3 * rows started reading identically. Nothing client-side could catch it: the * rule lives here. * * The SECOND segment is not always the same field, and that is the whole * subtlety: * * - **With a curator**, it is the PROTOCOL. `brand` is documented as "curator * where one exists, else the protocol", and the data bears it out — on * chain 1 every one of the 54 curated rows has `brand === curator.name`. So * `curator · brand` was always the same word twice ("Tulipa Capital · * Tulipa Capital · Vaults"). What the curator's name cannot tell you is * which stack the deposit lands in, which is exactly the protocol. * - **Without one**, it is the BRAND, because that keeps the generation: * `AAVE_V3` arrives as protocol `Aave`, brand `Aave V3`, and an Aave V2 row * reading identically to an Aave V3 row is worse than a less canonical name. * * The dedupe survives either way — it still collapses `Strata · Strata`, and a * future row whose curator and protocol coincide degrades to one segment * rather than to a stutter. * * The ASSET is deliberately absent: every surface shows it separately (a * column in the table, the amount field in the panel), so folding it in here * would duplicate by construction rather than by accident. */ declare function earnRowSubtitle(row: { brand?: string; protocol?: { name: string; }; curator?: { name?: string; }; venueKind: string; }): string; /** Stamp `subtitle` onto every row. */ declare function stampEarnSubtitles(rows: Array[0] & { subtitle?: string; }>): void; /** * Give colliding rows a suffix, and ONLY colliding rows. * * Everything else in this module labels a row from its own fields, which * cannot fix the last case: two markets on the same pair, at the same LLTV, * whose only difference is the oracle. `cbBTC-USDC 86` appears three times on * chain 1 ($532M / $5M / $54k) and one of them prices cbBTC off `BTC / USD` — * which ignores a cbBTC depeg entirely. * * Applied as a whole-listing pass rather than per row because "is this * ambiguous?" is not answerable from one row, and because suffixing every * market would be noise: 450 of 592 Morpho rows would gain a redundant oracle * that restates their own pair. * * Discriminators, in order — the first that actually SEPARATES the group wins, * so a suffix is never added that leaves the rows just as ambiguous: * * 1. what the oracle prices (`BTC / USD`) — a risk difference, and readable * 2. the oracle's address, abbreviated (`0xc7be`) * 3. the market id from the venue key, abbreviated (`0x4fe7`) * * Mutates `name` in place and returns the number of rows changed. */ declare function disambiguateEarnNames(rows: Array<{ chainId: string; brand?: string; venue: string; name?: string; ref?: string; asset: { symbol: string; }; refs?: { oracle?: string; oracleDescription?: string; }; }>): number; /** * Can the money actually leave? * * A venue advertising a same-block exit while reporting ZERO liquidity against * a non-zero balance is enterable but not exitable — the Clearstar shape: $1.8M * of TVL, an "Instant" exit, and nothing to withdraw. * * Two conditions are load-bearing: * * - **Only same-block modes qualify.** A cooldown or request-based vault * legitimately reports no instant liquidity; that is its design, not a * defect, and flagging it would condemn every well-behaved queued vault. * - **`undefined` is not zero.** Several providers do not report liquidity at * all. Treating "not published" as "none" would condemn them on a field they * never sent, so an unknown liquidity is never flagged. * * Mirrored by the `illiquid` column in the `v_earn_latest` migration; a test in * yield-tracer pins the two against each other. */ declare function isIlliquid(input: { exitMode?: string; tvlUsd?: number; liquidityUsd?: number; }): boolean; /** * Can at least `minUsd` actually leave this row, on ANY route? * * The predicate behind `?minLiquidityUsd=`. Its scalar predecessor * (`liquidity.usd >= X`) was 99 % false positive by dollar weight: it dropped * $31.4B of chain-1 TVL of which $31.4B had a working uncapped exit — every * large LST and cooldown vault — while the genuinely stuck rows it exists to * catch totaled ~$300M. Measured by `test/earn/liquidityFilterAudit.ts`, * which pins this predicate against the live catalogue. * * Three rules: * * - **A closed exit fails at any size.** `canWithdraw: false` means no route * is open, whatever the mode says. * - **An uncapped route passes at any size.** The buffer is a latency fact * on these rows, not a capacity fact. * - **On capped routes the buffer IS the capacity** — same-block redemption * (`instant`, `instant-capped`) and market exits (`market-sale`, * `dex-only`, where `liquidity` is book depth) compare it against the * floor. Unreported liquidity is kept, not dropped: the TVL floor's * "unknown is not worthless" rule, which `isIlliquid` already follows. */ declare function meetsLiquidityFloor(input: { exitMode?: string; canWithdraw?: boolean; liquidityUsd?: number; }, minUsd: number): boolean; interface EarnProtocolAndCurator { protocol: { key: string; name: string; }; curator?: { name?: string; entity?: string; }; } /** * Split a row's identity into the protocol it IS and the curator that runs it. * * **One resolver for both halves of the listing.** The lending half used to * assign `protocol` inline, which meant two definitions of the same idea that * could drift — and did: the lending side set `protocol.key` to the PER-MARKET * venue while the vault side set the stable `vault.`, so the one * field a client would cache on meant different things depending on the row. * * Four shapes, all real in the data: * * - **curated vault** (Morpho, Euler, Lagoon, Lista, Gearbox) — protocol * fixed by the provider, brand is a third party: `Morpho` + * `Steakhouse Financial`. * - **category vault** (savings, lst) — the brand IS the protocol: `Ethena`, * `Lido`, with no curator. `vault.savings` spans Sky, Ethena and Maple; * reporting Ethena as a "curator of Savings" inverts the two fields that * exist precisely to be told apart. * - **self-branded vault** (Fluid, Silo, Pendle, GMX, and every uncurated * provider) — brand equals the protocol, so a curator would just repeat it. * - **lending market** — the protocol is the lender family. No lender * publishes a curator today; the parameter is still honoured so that when * one does (a curated Morpho Blue market list, say) it needs no new branch. */ declare function resolveEarnIdentity(venue: string, brand: string | undefined, /** * The protocol as PUBLISHED, from `lender-labels.json`'s `protocols` map by * way of `lenderInfo.protocol`. * * Authoritative when present. `PROTOCOL_ALIASES` below is the fallback for * the window before that map reaches the public-data fetchers — deriving a * name in this module and correcting it downstream is the shape this whole * section exists to end. */ publishedProtocol?: string): EarnProtocolAndCurator; /** * Multiply a formatted (human-unit) amount by a USD price. * * Returns `undefined` when either input is missing, rather than `0` — a vault * we could not price must not sort as worthless next to one that genuinely is. */ declare function usdValue(formatted: number | undefined, priceUsd: number | undefined): number | undefined; /** * Format a raw base-unit integer string to human units. * * Uses BigInt for the integer part so large balances keep full precision, then * appends the fraction — `Number(raw) / 10 ** decimals` silently loses digits * above 2^53, which is well inside normal 18-decimal TVLs. */ declare function formatRaw(raw: string | undefined, decimals: number): number | undefined; /** * Vault half of the `/earn` normalizer: one `/vaults/latest` row → `EarnMarket`. * * The input is the recorder origin's item shape, which is loosely typed by * design (providers add fields without a schema bump), so every read goes * through a tolerant accessor and a missing field yields `undefined` rather * than a throw. A row missing its two load-bearing identifiers — the vault * address and the underlying — is DROPPED, not patched. * * See EARN_ENDPOINT_PLAN.md §4. */ /** The origin's `/vaults/latest` item. Permissive on purpose — see above. */ interface VaultSourceRow { provider?: string; vaultAddress?: string; underlying?: string; symbol?: string; name?: string; displayName?: string; decimals?: number; assetDecimals?: number; curatorName?: string; curatorEntity?: string; /** * The vault origin names this `rating`, not `risk`, and uses `level` where * pools use `label`. Two shapes for one concept — read both explicitly * rather than assuming either. */ rating?: { level?: string; score?: number | string; }; sharePrice?: number | string; sharePriceUsd?: number | string; rates?: { depositRate?: number | string; rewardsRate?: number | string; totalRate?: number | string; supplyRate?: number | string; fee?: number | string; }; tvl?: { totalAssets?: string | number; totalAssetsFormatted?: number | string; totalSupply?: string | number; totalAssetsUsd?: number | string; }; liquidity?: { liquidity?: string | number; liquidityFormatted?: number | string; liquidityUsd?: number | string; }; underlyingInfo?: { asset?: { symbol?: string; decimals?: number; logoURI?: string; }; prices?: { priceUsd?: number | string; }; }; vaultInfo?: { symbol?: string; name?: string; logoURI?: string; assetGroup?: string; yieldProfile?: string; denomination?: string; }; providerMeta?: Record; [key: string]: unknown; } /** * Providers whose rate fields arrive as FRACTIONS (`0.0412`) rather than * percent (`4.12`), and therefore need scaling. * * **EMPTY for the origin path, and that is the verified answer, not a * default.** The underlying hazard is real — `vaults/DATABASE_INTEGRATION.md` * §1 documents HyperCore's `apr` and GMX's `apy`/`baseApy`/`bonusApr` as * fractions where every 4626 provider reports percent — but the recorder * already resolves it before the row reaches us. `mapGmxToListing` / * `mapHypercoreToListing` in the origin's `routes/vaults.ts` run every rate * through `pct()` (`× 100`) and park the raw fraction under * `providerMeta.apr`, explicitly "for sort/filter parity". * * So scaling here would DOUBLE-convert and inflate every GMX and HyperCore row * 100× — the exact bug this constant was written to prevent, in the opposite * direction. * * The set survives as a parameter rather than being deleted because the * SDK-fed path has the opposite convention: `getVaultPublicDataAll` emits the * raw provider fractions with no `pct()` in between. Anything reading the SDK * directly (a recorder, a `?source=live` fallback) must pass * {@link SDK_FRACTION_RATE_PROVIDERS}. */ declare const FRACTION_RATE_PROVIDERS: ReadonlySet; /** * The fraction-reporting providers **as the SDK emits them**, before the * recorder's `pct()` pass. Pass this to {@link earnMarketFromVault} when the * rows come from `getVaultPublicDataAll` rather than from `/vaults/latest`. */ declare const SDK_FRACTION_RATE_PROVIDERS: ReadonlySet; /** Per-call overrides for sources that disagree with the origin's conventions. */ interface EarnVaultNormalizeOptions { /** * Reserved. Term sheets are stamped on the RETURNED PAGE by the worker's * `stampEarnTerms`, not here — building one per row during the merge would * pay for ~1000 sheets to serve 50, and would miss the origin-proxy path * entirely, since those rows never pass through this normalizer. */ terms?: 'none' | 'digest' | 'full'; /** * Providers whose rates need `× 100`. Defaults to * {@link FRACTION_RATE_PROVIDERS} (empty — the origin already normalized). */ fractionRateProviders?: ReadonlySet; } /** * Rate provenance per provider — WHERE the number came from. Sorting a mixed * list on `rate.total` is only honest if the consumer can see that a * `realized` 12 % and a `chain` 12 % are different claims. */ /** * WHERE a provider's rate comes from — exported because the recorder builds * the same row shape from SQL and had no way to say anything but `'chain'`, * so every served row claimed an on-chain read including the ten providers * whose number is an API's. A per-provider fact restated in two languages is * the drift this repo has paid for before (the 100× GMX scaling); one map, * two importers. */ declare const EARN_RATE_SOURCE_BY_PROVIDER: Record; declare function earnMarketFromVault(row: VaultSourceRow, chainId: string, opts?: EarnVaultNormalizeOptions): EarnMarket | undefined; /** * Convert a source rate to PERCENT. * * `undefined` in ⇒ `undefined` out. Zero is preserved (a real 0 % is * meaningful — a collateral-only leg genuinely earns nothing) rather than * being folded into `undefined`. */ declare function ratePercent(value: unknown, provider: string, fractionProviders?: ReadonlySet): number | undefined; /** * Cheap plausibility guard for the fraction/percent question. * * Not a correctness proof — a genuine 300 % vault exists and a genuine 0.02 % * one does too. It catches the systematic case: a whole provider's rows * landing three orders of magnitude off because the origin already normalized * and we normalized again. Call it from a test or a recorder, not per request. */ declare function implausibleRatePercent(percent: number): boolean; /** * The three fields a vault row's TERMS hang off, derived from its provider and * `providerMeta`. * * Exported because the origin's SQL route builds its own row shape and had * none of them: it hardcoded `kind: 'variable-curve'`, defaulted the exit to * `instant`, and emitted no maturity at all. On a Pendle PT — a zero-coupon * bond — that rendered as **"Variable 22.09% · withdraw any time"**, with the * tags `variable-rate, perpetual, exit-instant`. Every clause was false, and * the profile beside it still read `vault.fixed-maturity@v1`. * * That is the exact failure AGENTS.md records ("Pendle inheriting savings-vault * prose whose every clause was wrong for a fixed-maturity bond"), reintroduced * by re-deriving in SQL what this module already derives. One function, called * by both paths, is the fix that keeps them from drifting again. */ declare function earnVaultTerms(provider: string, providerMeta: Record | undefined, size?: { tvl?: VaultSourceRow['tvl']; liquidity?: VaultSourceRow['liquidity']; }): { maturity?: MaturityTerms; exitMode: SupplyExitMode; rateKind: RateKind; }; /** * Has a fixed-term row passed its maturity? Judged against the CLOCK, never * against a stored flag — a recorder that stops writing leaves the row frozen * at its last pre-expiry state, and a cached "live" boolean would keep * advertising a dead fixed rate forever. Mirrors `pendle_pt_is_live()` in the * recorder, deliberately. * * Exported because the ORIGIN builds its earn rows in its own route rather * than through {@link earnMarketFromVault}, and the two must agree — the same * reason {@link earnVaultTerms} is exported. */ declare function isMaturedTerm(maturity?: MaturityTerms, nowSecs?: number): boolean; /** * The forward rate a MATURED fixed-term row may publish, which is **zero**. * * Not a guard and not a clamp: it is the instrument's definition. A matured * principal token redeems for the underlying at par and then sits there — it * earns nothing from that moment on, so 0 is the measured answer, not a * comfortable default. (It is also the worst-ranking value, which is why this * is not the "absent rather than defaulted" case AGENTS.md warns about: there * is nothing we cannot fill.) * * ## Why this function exists at all * * Every layer that could have caught a matured row was scoped to skip one: * * - the providers DROP matured markets (`pendleIncludeExpired` defaults * false), so nothing downstream expected to see one; * - `vaults/rateSanity.ts` bails on `expiry <= now` for exactly that reason; * - the recorder's `pendle_vaults_latest` is UPSERT-ONLY, so when the * provider stops returning the market the row does not disappear — it * FREEZES at the last tick before expiry. * * That last tick is the worst possible one to freeze. A fixed-term APR is a * price deviation raised to `365 / daysToMaturity`, so in the final hours the * exponent amplifies rounding into anything: `PT apxUSD 27 Aug 2026` recorded * 8.28 % a fortnight out, 68 % at T−8 h, 236 % at T−1 h, and served **267.29 % * on $12.1 M** for weeks afterwards off a share price 0.3 bps from par. Across * chains that was 19 rows and $178.7 M of nominal TVL publishing APRs from * +267 % to −315 %, contaminating BOTH ends of every rate sort. * * So the rule is applied where the row is BUILT rather than where it is * filtered — a holder still needs the row to redeem, and a row that is served * must not carry a rate that stopped existing. */ declare function earnRateAtMaturity(rate: EarnRate, maturity?: MaturityTerms, nowSecs?: number): EarnRate; /** * Lending half of the `/earn` normalizer: one `/pools/latest` row → * `EarnMarket`, projecting the SUPPLY side only. * * The borrow side is not dropped, it is *pointed at*: `refs.marketUid` + * `refs.borrowable` let a consumer jump to `/v1/data/lending/*` for the full * market. Duplicating the borrow economics here would double the payload to * serve a question this endpoint does not ask. * * The origin reshapes the SDK's flat `PoolData` into a partly-nested form * (`caps`, `flags`, `underlyingInfo`), and the origin lives in another repo — * so every field is read tolerantly in BOTH shapes. That is deliberate * defensiveness, not indecision: a field that moves nests silently, and a * silently-missing `isFrozen` would advertise a dead market as depositable. * * See EARN_ENDPOINT_PLAN.md §4. */ /** The origin's `/pools/latest` item (`PoolWithMeta`). Permissive by design. */ interface PoolSourceRow { /** Stamped by the public-data parsers. REQUIRED — see `earnMarketFromPool`. */ marketUid?: string; lender?: string; lenderKey?: string; chainId?: string; poolId?: string; underlying?: string; name?: string; asset?: { address?: string; symbol?: string; decimals?: number; logoURI?: string; assetGroup?: string; }; underlyingInfo?: { asset?: { address?: string; symbol?: string; decimals?: number; logoURI?: string; }; assetGroup?: string; prices?: { priceUsd?: number | string; }; }; decimals?: number; /** Origin-computed `depositRate + intrinsicYield`. */ apr?: number | string; price?: number | string; depositRate?: number | string; intrinsicYield?: number | string; variableBorrowRate?: number | string; rewards?: Array<{ asset?: string; depositRate?: number | string; }>; totalDeposits?: string | number; totalDepositsUSD?: number | string; totalDepositsUsd?: number | string; totalLiquidity?: number | string; totalLiquidityUSD?: number | string; totalLiquidityUsd?: number | string; utilization?: number | string; /** * NESTED on the origin response — `risk: { score, label, breakdown }` — not * flat `riskScore`. Reading the flat form silently yielded no risk at all. */ risk?: { score?: number | string; label?: string; }; /** * ROW-LEVEL, and deliberately not under `fluid`: the position's composition * is pool-controlled. Lender-agnostic by design — Lista SmartLP and GMX * GM/GLV are the same shape. */ autoBalanced?: boolean; /** * Fluid's smart-vault descriptor. The ONLY source of leg order today, and * the reason `basket.legs` can be populated at all on the lending half. */ fluid?: { isSmartCol?: boolean; isSmartDebt?: boolean; collateralPair?: string[]; debtPair?: string[]; basketSupplyRate?: number | string; basketBorrowRate?: number | string; supplyDexTradingRate?: number | string; borrowDexTradingRate?: number | string; [key: string]: unknown; }; /** * Oracle provenance per feed. `priceDescription` ("BTC / USD") is the only * field the label uses — it is what distinguishes two markets on the same * pair at the same LLTV. */ oracleInfo?: { feeds?: Array<{ priceDescription?: string; oracle?: string; }>; }; /** * The lender's own identity for this market — `{ key, name, logoURI }`. * `name` is the best market label available (`Morpho cbBTC-USDC 86`), and is * populated for every lender family in the live listing. */ lenderInfo?: { key?: string; name?: string; logoURI?: string; /** * The protocol this market belongs to, from `lender-labels.json`'s * `protocols` map. Absent until that reaches the public-data fetchers, at * which point it supersedes the SDK's fallback table. */ protocol?: string; }; supplyCap?: number | string; caps?: { supplyCap?: number | string; borrowCap?: number | string; }; collateralActive?: boolean; borrowingEnabled?: boolean; depositsEnabled?: boolean; isActive?: boolean; isFrozen?: boolean; flags?: { collateralActive?: boolean; borrowingEnabled?: boolean; depositsEnabled?: boolean; isActive?: boolean; isFrozen?: boolean; }; [key: string]: unknown; } /** * Collateral symbols per venue, derived from the listing ITSELF. * * An isolated market is a (collateral, loan) pair, but the fetcher emits it as * TWO rows — `Collateral cbBTC` and `Loan USDC` — each naming only its own leg. * The pairing is nonetheless recoverable without any extra fetch, because both * legs share the per-market venue key (`MORPHO_BLUE_`, `FLUID_1_11`). * Grouping by venue and keeping the collateral-enabled symbols reconstructs * exactly the input `earnMarketLabel` needs. * * Verified against the live chain-1 listing (1,823 rows): 329 Morpho venues, * 264 of them a clean 2-row pair; Fluid 97, Silo 55, Resupply 13, Frankencoin * 11 the same shape. Shared pools land on the other side of the same rule — * Gearbox averages 3.5 collaterals per pool, a Compound III comet ~10, Aave V3 * ~13 — so they are named by asset alone, which is correct. * * `collateralActive` is published by EVERY lender family in that listing (zero * undefined), so there is no flag-absent fallback to get wrong. * * **Pass the WHOLE scope's rows, not a page.** Given a page, a shared pool * looks isolated and gets a confidently wrong "vs" label — worse than no * label, because it names one arbitrary collateral of thirty. * * Keyed by (chain, venue) rather than venue alone: a per-market key is only * chain-unique when the lender bakes an id into it, and `AAVE_V3` is the same * string on 20 chains. Cross-chain merging would not produce a wrong pairing * (the count only grows, so a market degrades to its plain name) but the key * costs nothing and removes the question. */ declare function collateralSymbolsByVenue(rows: readonly PoolSourceRow[], fallbackChainId?: string): Map; declare function earnMarketFromPool(row: PoolSourceRow, fallbackChainId?: string, venueCollaterals?: ReadonlyMap): EarnMarket | undefined; /** * Below this (in percent) a venue's own yield is treated as nothing. * * Not zero: utilization IRMs report dust — a market at 0.3 % utilization * quotes something like 0.004 %, which is economically nil but would defeat an * `=== 0` test and let every LST market back into the list. */ declare const PASSTHROUGH_RATE_EPSILON = 0.01; /** * Does this row publish a rate a depositor cannot earn? * * Mirrors `isUnrealizableRate`'s two-condition discipline exactly, on the * normalized fields: an implausible APR **and** a market with essentially * nothing withdrawable. Both halves are required and that is the point — a * high rate alone can be a genuine incentive campaign, and zero liquidity * alone is ordinary for a fully-lent market. * * `liquidity` ABSENT is not zero. Several providers never report it, and an * async-exit vault reports zero by design; condemning either on a field they * did not send is the mistake `meetsLiquidityFloor` was written to avoid. */ declare const isUnrealizableEarnRate: (m: EarnMarket) => boolean; /** * Does this row publish a rate that is an artifact of HOW it was measured — * a magnitude no dust vault can support, or a fixed-term annualization * blown up a few hours from maturity? * * Delegates to the cross-provider guard so the thresholds live in one place. */ declare const isUnearnableEarnRate: (m: EarnMarket, nowSecs?: number) => boolean; /** * Blank a TVL that cannot be true, in place. Returns whether it did. * * Blanked rather than dropped, and blanked rather than clamped: the MARKET is * usually real, it is the USD valuation that is broken, and `tvl.usd` * ABSENT already means "unpriced" everywhere else on this surface — unpriced * rows skip the TVL floor and sort last, which is exactly the treatment a * number nobody can stand behind deserves. The token amount is left alone; it * is not the part that is wrong. */ declare const repairImpossibleTvl: (m: EarnMarket) => boolean; interface EarnSanityResult { /** The rows that survived, in their original order. */ items: EarnMarket[]; /** Dropped for publishing a rate nobody can earn (the pinned-market case). */ unrealizable: number; /** Dropped for publishing a measurement artifact (dust / near-maturity). */ unearnable: number; /** Kept, but with a `tvl.usd` that could not be true removed. */ repricedTvl: number; } /** * Apply every sanity guard to a listing or a page. * * Mutates `tvl.usd` on repaired rows (the array is freshly normalized on both * callers) and returns a new array for the survivors, so a caller that needs * the counts for its `excluded` block gets them without a second pass. */ declare const applyEarnSanity: (rows: EarnMarket[], opts?: { nowSecs?: number; }) => EarnSanityResult; /** * Stamp `capabilities[]` onto a normalized row. * * Mutates and returns the row — it is called once per row inside the * normalizer's own loop, and cloning several thousand rows to avoid a local * mutation is a real cost for no benefit. */ declare function stampCapabilities(row: EarnMarket): EarnMarket; /** * Every swap-routed provider must also be book-priced. * * The two facts are independent in principle but must agree in practice: a * provider routed through a swap whose traits claim par pricing would emit a * `via: 'swap'` capability while the rest of the surface treated it as a * redeem-at-par vault. Exported so a test can assert it rather than leaving it * as a comment nobody runs. */ declare function swapRoutedProvidersArePriceConsistent(): string[]; /** * Needs that are a PRICE BOUND rather than a routing choice or a realised * amount. * * `minMETHAmount`, `minRSETHAmountExpected`, `minUsddOut` bound a fill; * `stEthAmount` / `eEthAmount` carry the amount the first leg actually produced * (deterministic — Lido's submit and wrap have no slippage), and `depositPool` * is an address. Only the first class deserves a slippage control. */ declare function isBoundNeed(need: string): boolean; /** * `EarnPosition` — one row of a user's supply-side portfolio, from either half * of the stack. * * The user half of `/v1/data/earn`. Where `EarnMarket` answers "what can I * deposit into", this answers "what do I hold" — and it is deliberately NOT * symmetric with it, because the two halves of the stack carry positions at * different granularities and flattening that difference would be a lie: * * ``` * vault → ONE ROW PER VAULT. A share balance is a standalone position. * lending → ONE ROW PER (chain, lender). A cross-margin account is ONE * position — its markets are legs of a single solvency * calculation, not independent deposits. * ``` * * Splitting a cross-margin account into per-market rows is the failure this * shape exists to prevent: it renders a $100 supply against a $90 debt as two * unrelated $100 and $90 rows, publishes a health factor per leg that does not * exist, and lets a UI sum a column that was never additive. The legs are * still present — on {@link EarnLendingPosition.legs}, each pointing back at * its catalogue row — but the ROW is the account. * * See EARN_ENDPOINT_PLAN.md §7. */ /** * Row identity. **This is NOT an `earnUid`** and must never be passed to an * action route. * * A vault position's `positionUid` happens to equal its `earnUid` — one vault * is one market is one position. A lending position has no `earnUid` at all: * it spans every market in the account, so no single market uid identifies it. * Its uid is deliberately TWO segments (`:`), which * `parseEarnUid` rejects — so a caller that confuses the two fails at the edge * instead of routing a withdrawal to whichever market sorted first. * * To act on a lending position, take the `earnUid` off the individual * {@link EarnPositionLeg}. */ type EarnPositionUid = string; /** `AAVE_V3` + `1` → `AAVE_V3:1`. Two segments, by design — see above. */ declare function buildLendingPositionUid(lender: string, chainId: string): EarnPositionUid; interface EarnPositionAsset { address: string; symbol?: string; decimals?: number; /** Unit price in USD. `0` ⇒ unpriced, NOT worthless. */ priceUsd?: number; /** * Token icon, where the lender metadata resolved one. * * Carried on the leg so a consumer does not need a token list loaded just to * label a position it was already handed — the address alone identifies * nothing to a reader. */ logoURI?: string; } /** Fields both halves carry, so a table can render one row type. */ interface EarnPositionBase { positionUid: EarnPositionUid; chainId: string; /** `AAVE_V3` (a `Lender` key) or `vault.savings` (`vault.`). */ venue: string; venueKind: EarnVenueKind; /** Display label — curator where one exists, else protocol. */ brand?: string; name?: string; logoURI?: string; /** USD value of everything supplied. */ suppliedUsd: number; /** USD value of everything borrowed. Always `0` on the vault half. */ borrowedUsd: number; /** `suppliedUsd - borrowedUsd` — what the position is actually worth. */ netUsd: number; /** * Net APR on the position AS HELD, in PERCENT — deposit yield less borrow * cost, over `netUsd`. NOT the market's headline rate: a 2x loop on a 4 % * market reads ~8 % here and 4 % on the catalogue row. * * Absent ⇒ not computable, which is not the same as zero. */ apr?: number; } /** * One market inside a lending position. * * `earnUid` is the join back to `/v1/data/earn` — present whenever the lender * minted a well-formed `marketUid`, absent rather than reconstructed when it * did not (a rebuilt uid routes to the wrong market for Compound V2 and * Dolomite; see `earnUidFromMarketUid`). */ interface EarnPositionLeg { /** Catalogue join key. Absent ⇒ this leg has no addressable market row. */ earnUid?: string; marketUid: string; /** Present ⇒ the leg is bound to one loan (fixed-term lenders). */ loanId?: string; asset: EarnPositionAsset; /** * Which side of the book this leg sits on. * * **`'none'` is the common case and the important one**: lenders report every * market the account is CONFIGURED in, not just the ones it holds something * in — an Aave V4 account with one USDC debt reports ten legs, nine of them * empty. Marking them here rather than letting each consumer re-derive it is * what stops a UI rendering nine empty markets as nine positions, which is * indistinguishable from nine real ones at a glance. * * Empty legs are KEPT rather than dropped — "markets this account is set up * in" is a real question — but nothing may present them as holdings. */ side: 'supply' | 'borrow' | 'both' | 'none'; deposits: string; depositsUsd: number; debt: string; debtUsd: number; collateralEnabled: boolean; /** Max withdrawable in token units, where the lender reports it. */ withdrawable?: string; } /** * The three legs of a position's yield, each already expressed over NAV and in * PERCENT, so they simply add. * * **They are separate fields upstream and none of them contains another.** * `aprData.apr` is `(depositInterest − borrowInterest) / nav` — market interest * ONLY. `rewardApr` and `intrinsicApr` are computed alongside it over the same * denominator and are omitted from it entirely. Reading `aprData.apr` as "the * net APR" is therefore wrong for exactly the positions where it matters most: * a levered carry trade borrows a cheap asset to hold a yield-bearing one, so * the market leg is the COST side and the asset's own yield — the entire * reason for the trade — lands in `intrinsicApr`. A 22x sDOLA/crvUSD loop * reports about −74 % on the market leg alone and a large positive number once * the collateral's own yield is counted. * * Kept as a breakdown rather than folded into one number so that a headline can * never quietly become un-inspectable: emissions can stop, and an intrinsic * yield is a different promise from an interest rate. */ interface EarnAprBreakdown { /** Deposit interest less borrow interest, over NAV. */ market: number; /** Incentive emissions, over NAV. Can stop. */ rewards: number; /** * The yield the ASSETS carry themselves (sDOLA, sfrxUSD, an LST) net of the * yield accruing on whatever was borrowed, over NAV. */ intrinsic: number; } /** A sub-account within a lender, for the lenders that have more than one. */ interface EarnPositionSubAccount { accountId: string; health: number | null; suppliedUsd: number; borrowedUsd: number; netUsd: number; legs: EarnPositionLeg[]; } /** * A whole lending account on one lender, on one chain — ONE row however many * markets it touches. */ interface EarnLendingPosition extends EarnPositionBase { venueKind: 'lending'; lender: string; account: string; /** * Health factor of the account. Only meaningful when the lender is * cross-margin (`subAccounts.length <= 1`); otherwise `null`, with each * sub-account carrying its own. `null` also means "no debt, so no health". */ health: number | null; /** `deposits / nav`. `1` ⇒ unlevered, `0` ⇒ not computable. */ leverage: number; /** * What `apr` is made of. `market + rewards + intrinsic === apr`, so a * consumer can show the split without re-deriving it — and can see when a * headline rests entirely on emissions or entirely on collateral yield. */ aprBreakdown: EarnAprBreakdown; /** MARKET deposit interest only — see `aprBreakdown` for the other legs. */ depositApr: number; /** MARKET borrow interest only, as a positive cost. */ borrowApr: number; /** * TRUE when the whole position is one solvency calculation, i.e. this row is * the complete picture. FALSE ⇒ read `subAccounts`, and do not present * `health` as the account's. */ crossMargin: boolean; /** Every market leg, flattened across sub-accounts. */ legs: EarnPositionLeg[]; subAccounts: EarnPositionSubAccount[]; /** * Some of this lender's reads did not complete. The legs are real but the * set is a LOWER BOUND — `netUsd`, `apr` and `health` must not be rendered * as fact. Carried straight through from `/lending/user-positions`. */ incomplete?: boolean; /** Served from the last complete snapshot, `staleAgeMs` ago. */ stale?: boolean; staleAgeMs?: number; } /** A share balance in one vault — a standalone position. */ interface EarnVaultPosition extends EarnPositionBase { venueKind: 'vault'; /** * The catalogue row. Unlike the lending half this is always present and * always actionable — pass it straight to an earn action route. */ earnUid: string; provider: VaultProvider; /** Share-token address. */ vault: string; asset: EarnPositionAsset; /** Raw share balance, base units of `shareDecimals`. */ sharesRaw: string; shares: string; /** Share balance converted to underlying at the fair share price. */ assetsRaw: string; assets: string; /** Share-token decimals. Differs from the asset's for Lagoon. */ shareDecimals: number; yieldProfile?: YieldProfile; denomination?: Denomination; /** What the venue pays, PERCENT. */ rate?: EarnRate; /** How the money gets out. */ exit?: EarnExit; /** Whether it can be entered right now, and why not. */ availability?: EarnAvailability; /** What can be done with the position — drives the withdraw CTA. */ capabilities?: EarnCapability[]; } type EarnPosition = EarnLendingPosition | EarnVaultPosition; declare function isVaultPosition(p: EarnPosition): p is EarnVaultPosition; declare function isLendingPosition(p: EarnPosition): p is EarnLendingPosition; /** Per-source health, so a dead half degrades the list rather than the route. */ interface EarnPositionSourceStatus { source: 'lending' | 'vaults'; status: 'ok' | 'degraded' | 'failed'; /** Rows contributed by this source. */ rows: number; /** Present when not `ok`. */ error?: string; } interface EarnPositionTotals { suppliedUsd: number; borrowedUsd: number; netUsd: number; /** `netUsd` of the lending half alone. */ lendingUsd: number; /** `netUsd` of the vault half alone. */ vaultUsd: number; } /** * `/v1/data/earn/positions` response. Same contract as `/v1/data/earn`: the * shape never changes, a degraded source is reported in `sources[]` with * whatever did resolve still served. */ interface EarnPositionsResponse { ok: boolean; account: string; chainIds: string[]; count: number; /** Always `'percent'`, stamped so no consumer has to guess. */ rateUnit: 'percent'; items: EarnPosition[]; totals: EarnPositionTotals; sources: EarnPositionSourceStatus[]; /** Set when any lending entry was `incomplete` — totals are a lower bound. */ partial?: boolean; /** Set when any entry was served from a last-known-good snapshot. */ stale?: boolean; } /** * `LenderDataEntry` → ONE `EarnLendingPosition`. * * The entry is already aggregated per (chain, lender) by `buildSummaries`, so * this is a projection, not a re-summation — the USD figures come off * `balanceData`, which the summary computed from the same legs. The legs are * flattened purely so a row can show what it is made of. */ declare function earnPositionFromLenderEntry(entry: LenderDataEntry): EarnLendingPosition; /** What a caller must supply per vault beyond the cached public metadata. */ interface VaultBalanceInput { /** Raw share balance from `balanceOf(account)`. */ sharesRaw: bigint; /** Underlying unit price in USD. `0` ⇒ unpriced. */ priceUsd?: number; /** * The catalogue row for this vault, where one resolved. Supplies the rate, * the exit and the capabilities — everything about the DEAL, as opposed to * the balance. Absent ⇒ those fields are omitted rather than defaulted; a * missing sheet reads as "unknown", never as "instant, free, 0 %". */ market?: EarnMarket; } /** * ERC-4626 convention: `assets = shares * totalAssets / totalSupply`. * * Returns `0n` for an empty vault or zero shares — both safe for display, and * both distinct from an error. */ declare function vaultSharesToAssets(sharesRaw: bigint, meta: Pick): bigint; /** * `VaultLookupEntry` + a share balance → ONE `EarnVaultPosition`. * * `format` is injected rather than importing viem here so this stays a pure * transform the worker and the tests can both drive; pass `formatUnits`. */ declare function earnPositionFromVaultBalance(meta: VaultLookupEntry, chainId: string, input: VaultBalanceInput, format: (value: bigint, decimals: number) => string): EarnVaultPosition; /** Portfolio totals across both halves. */ declare function earnPositionTotals(items: EarnPosition[]): EarnPositionTotals; declare const dexResolverFor: (chainId: string) => string | undefined; /** Per-1e18-share token amounts for one DEX pool, each in its OWN token decimals. */ interface FluidDexShareState { dex: string; token0PerSupplyShare: bigint; token1PerSupplyShare: bigint; token0PerBorrowShare: bigint; token1PerBorrowShare: bigint; totalSupplyShares: bigint; totalBorrowShares: bigint; } type FluidDexStateMap = { [dexAddress: string]: FluidDexShareState; }; /** * Read every DEX pool's share state for a chain. * * Two multicall rounds: discover the pools, then read each one's state. The * combined `getAllDexEntireDatas()` exists but returns a 205-word struct per * pool and times out on Ethereum's 48 pools against public RPCs, so the * narrower `getDexState` is used instead. * * DISCOVERY IS A UNION, and it has to be. `getAllDexAddresses()` derives pool * addresses by CREATE2 from ONE deployer, so it misses an older DEX generation * that is still backing live vaults — on Ethereum three such pools serve 10 * smart vaults, and they answer `getDexState` perfectly well (they even reuse * dexIds 1-3, so the two generations collide in id space and only the ADDRESS * identifies a pool). Vault-referenced addresses are therefore folded in via * `extraDexAddresses`; the caller sources them from each smart vault's * `constantVariables.supply` / `.borrow`. */ declare const fetchFluidDexState: (chainId: string, multicallRetry: MulticallRetryFunction, extraDexAddresses?: string[]) => Promise; /** Synchronous read of whatever `fetchFluidDexState` last cached for a chain. */ declare const getCachedFluidDexState: (chainId: string) => FluidDexStateMap | undefined; export { type AaveMetadata, type AaveV2Public, type AaveV2UserReserveResponse, type AaveV3Public, type AaveV3UserReserveResponse, type AdditionalYields, type AdminKind, ApiBookSource, type AprData, type AprPercent, type AssetQuality, type AssetRiskIndex, type AuctionWindow, type AvailabilityTerms, type BalanceData, type BaseLendingPosition, type BasicReserveResponse, type BorrowExitTerms, type BorrowTermSheet, type BuildTermSheetOptions, type BuildVaultTermSheetOptions, type ChainDiagnostic, type ChainLinkResponse, type ChainQuery, type ChainSummary, type CompoundV2Metadata, type CompoundV3Public, type CompoundV3UserReserveResponse, type ConfigEntry, type ConvertLenderUserDataOptions, type CoolerDripRaw, type CoolerMarketsRaw, type CoolerPositionInfo, type CoreValidators, type CounterpartyTerms, type CoverageInfo, DEFAULT_MIDNIGHT_API, DEFAULT_OUTLIER_GUARD, DEFAULT_PROFILE_ID, DEFAULT_STALE_REJECT_SECONDS, DEFAULT_TERMMAX_API, DEFAULT_TERM_ORDER_STORE, DOLOMITE_ISO_ID_PREFIX, type DeepPartial, type Denomination, type DepthMap, type DolomiteIsolationRow, type DolomiteSubAccount, type DssMarketRaw, type DssMarketsRaw, type DssPositionInfo, EARN_DESCRIPTIONS, EARN_LABELS, EARN_RATE_SOURCE_BY_PROVIDER, EMPTY_BALANCE, type EModeAssets, type EModeData, type EModeResult, EXACTLY_LENDER_KEY, type EarnActionInput, type EarnActionKind, type EarnAmount, type EarnAppliedDefaults, type EarnAprBreakdown, type EarnAsset, type EarnAvailability, type EarnBasket, type EarnBasketLeg, type EarnCapability, type EarnCurator, type EarnExclusions, type EarnExit, type EarnExitHistory, type EarnFacetBucket, type EarnFacets, type EarnGating, type EarnLabelDimension, type EarnLendingPosition, type EarnMarket, type EarnMarketLabelInput, type EarnPosition, type EarnPositionAsset, type EarnPositionBase, type EarnPositionLeg, type EarnPositionSourceStatus, type EarnPositionSubAccount, type EarnPositionTotals, type EarnPositionUid, type EarnPositionsResponse, type EarnProtocol, type EarnProtocolAndCurator, type EarnRate, type EarnRateSource, type EarnRefs, type EarnResponse, type EarnRisk, type EarnSanityResult, type EarnShareToken, type EarnSourceStatus, type EarnVaultNormalizeOptions, type EarnVaultPosition, type EarnVenueKind, type EarnVocabulary, type EndpointFailure, type EnrichmentIndex, type EulerEarnVault, type EulerEarnVaults, type EulerV2Metadata, type ExactlyFixedPool, type ExactlyFixedPosition, type ExactlyMarketAccount, type ExactlyMarketsRaw, type ExactlyUserFixedPosition, type ExposureEntry, type ExposureTerms, type ExtraValidationCall, FLYING_TULIP_LENDER_KEY, FRACTION_RATE_PROVIDERS, type FeeTerm, type FeeWhen, type FeedObservation, type FeedStat, type FeedStatsMap, type FeedTimestampMap, type FetchListaVaultsFromChainOptions, type FetchMorphoVaultsFromChainOptions, type FetchOraclePricesOptions, type FetchPendlePtOptions, type FetchSpectraPtOptions, type FetchTokenBalancesOptions, type FetchTokenMetadataOptions, type FlattenPriorityConfig, type FluidDexShareState, type FluidDexStateMap, type FluidFToken, type FluidFTokens, type FlyingTulipAssetRaw, type FlyingTulipMarketsRaw, type FrankencoinMarketRaw, type FrankencoinMarketsRaw, type FrankencoinPositionInfo, type FraxlendPairRaw, type FraxlendPairsRaw, type FullLenderRewardsMap, GMX_API_HOSTS, GMX_READ_CONTRACTS, GMX_SUPPORTED_CHAINS, type GenericCurrency, type GenericTokenList, type GetVaultPublicDataAllOptions, type GmxExecutionFees, type GmxPendingDeposit, type GmxPendingWithdrawal, type GmxReadContracts, type GmxUserBalance, type GmxUserPositions, type GmxUserPositionsOptions, type GmxVault, type GmxVaultKind, type GmxVaults, type GmxVaultsFetchOptions, type GovernancePower, type GovernanceRow, type GovernanceTerms, type GroupAccumulator, HYPERCORE_VAULT_REGISTRY, type HypercoreLockStatus, type HypercoreUserPositionsOptions, type HypercoreVault, type HypercoreVaultPosition, type HypercoreVaultRegistryEntry, type HypercoreVaults, type HypercoreVaultsFetchOptions, IDLE_MARKET_ID, INTERFACE_IDS, type IncompleteLenderRead, type IncompleteReason, type InitMetadata, type InitPublic, type InitUserReserveResponse, type InterfaceKind, type InvariantViolation, type InverseMarketRaw, type InverseMarketsRaw, type InversePositionInfo, LAGOON_API_URL, LAGOON_CHAIN_IDS, type LagoonApiVault, type LagoonSyncMode, type LagoonVault, type LagoonVaults, type LenderAssetReward, type LenderConfigData, type LenderConfigMap, type LenderCrossPoolMeta, type LenderData, type LenderDataEntry, type LenderInfo, type LenderInfoMap, type LenderPublicBase, type LenderRewardsMap, type LenderSummary, type LenderToLenderCrossPoolMeta, type LenderTotalAmounts, type LenderUserQuery, type LenderUserResponse, type LenderYieldComplete, type LenderYields$1 as LenderYields, LendingMode, type LiquidationPenaltyTerm, type LiquidationTerms, type LiquityBranchRaw, type LiquityDiscoveredTrove, type LiquityDiscovery, type LiquityMarketsRaw, type LiquitySpInfo, type LiquityTroveInfo, type ListaMarketOverrides, type LlamaLendMarketOverride, type LlamaLendMarketOverrides, type LlamaLendMarketRaw, type LlamaLendMarketsRaw, type LlamaLendPositionInfo, type LoopPostTradeMetrics, type LstDelegation, type LstDelegationKind, type LstValidator, type LstWithdrawalFetchOptions, type LstWithdrawalRegistryEntry, type LstWithdrawalRequest, type LstWithdrawalRequestsByLst, type LstWithdrawalStatus, MORPHO_LENS, MULTICALL_FAILURE, type MaturityTerms, MaxParamThresholds, type MergedUserData, type MidnightBook, type MidnightBookLevel, type MidnightBookSource, type ModeBase, type ModeVariant, type MorphoMarketOverrides, type MorphoSubgraphProxyConfig, type MorphoUserMarketBalance, type MorphoUserReserveResponse, type MorphoVault, type MorphoVaults, type MulticallEndpointOptions, type MulticallRpcBatch, type NumberMap, type Open, type OracleBand, type OracleDiagnostics, type OraclePriceEntry, type OraclePricesResult, type OracleRiskRow, type OracleTerms, type OutlierGuardConfig, PASSTHROUGH_RATE_EPSILON, PENDLE_ASSETS_URL, PENDLE_CHAIN_IDS, PENDLE_MARKETS_URL, type ParsedBalanceData, type ParsedEarnUid, type ParsedLendingEarnUid, type ParsedResponse, type ParsedUserBalance, type ParsedVaultEarnUid, type PendleApiAsset, type PendleApiMarket, type PendleApiMarketDetails, type PendlePtMarket, type PendlePtMarkets, type PermissionKind, type PermissionParams, type PoolData, type PoolSourceRow, type PoolWithMeta, type PortfolioSummary, type PortfolioTotals, type PositionConstraints, type PostTradeMetrics, type PreparedCall, type PreparedMergedMulticallParams, type PreparedMergedRpcCalls, type PreparedTokenBalanceRpcCalls, type PreparedUserDataRpcCalls, type PriceDerivation, type PriceSelection, type ProtocolParams, type ProviderOptions, type RateKind, type RateMenuEntry, type RateTerms, type RawRpcBatch, type RawRpcCall, type RawRpcResponse, type ReadFailurePolicy, type RedemptionTerms, type ResupplyMarketsRaw, type ResupplyPairIdentity, type ResupplyPairRaw, type ResupplyPositionInfo, type ResupplyWrappedMarket, type RewardSourceRef, type RewardStream, type RewardTerm, type RewardTokenRef, type RiverMarketRaw, type RiverMarketsRaw, type RiverPositionInfo, SDK_FRACTION_RATE_PROVIDERS, SPECTRA_NETWORKS, SPECTRA_RATE_MAX_PERCENT, SPECTRA_RATE_MIN_PERCENT, STABLECOIN_SYMBOLS, STCELO_MANAGER_ADDRESS, type SelectPricesOptions, type Severity, type SeverityFinding, type SiloVault, type SiloVaults, type SpectraApiMarket, type SpectraApiPool, type SpectraApiToken, type SpectraPtMarket, type SpectraPtMarkets, type StCeloValidatorGroup, type StaleFeedEntry, type StampOptions, type StructuredOraclePrices, type SubAccountSummary, type SumerPositionInput, type SummaryAprData, type SummaryBalanceData, type SupplyExitMode, type SupplyExitRoute, type SupplyExitTerms, type SupplyTermSheet, TELLER_CALLS_PER_BID, DECIMAL_BASE as TERMMAX_DECIMAL_BASE, TERMMAX_LIQUIDATION_PENALTY, TERMMAX_LIQUIDATION_WINDOW_SECS, TERMMAX_LIQUIDATOR_BONUS, TERMMAX_MARKETS_PER_CALL, TERMMAX_PARTIAL_CLOSE_FACTOR, TERMMAX_PARTIAL_LIQUIDATION_THRESHOLD_USD, TERM_ADAPTERS, TERM_PROFILES, TERM_SHEET_SCHEMA_VERSION, type TellerDiscoveredBid, type TellerDiscovery, type TellerMarketsRaw, type TellerPoolRaw, type TermAdapter, type TermAssetRef, type TermAuctionOrder, type TermAuctionOrders, type TermBookSource, type TermBookTop, type TermFillNow, type TermFillNowSide, type TermInfo, type TermListing, type TermMarketRaw, TermMaxApiSource, type TermMaxBookTop, type TermMaxCurveSegment, type TermMaxDataSource, type TermMaxDiscovery, type TermMaxFeeConfig, type TermMaxMarketConfig, type TermMaxMarketRaw, type TermMaxOrderState, type TermProfile, type TermSheet, type TermSheetDigest, type TermSheetInput, type TermSheetViolation, type TermStoreOrder, TermSubgraphSource, type TermTag, type TokenApprovalMeta, type TokenApprovalParams, type TokenBalanceEntry, type TokenBalanceQuery, type TokenBalanceResult, type TokenEntry, type TokenList, type TokenListInput, type TrackerDiagnostic, UPSHIFT_CHAIN_IDS, UPSHIFT_VAULTS_URL, type USDPriceMap, type UpshiftApiAsset, type UpshiftApiVault, type UpshiftVault, type UpshiftVaults, type UsddMarketRaw, type UsddMarketsRaw, type DssPositionInfo as UsddPositionInfo, type UserApr, type UserConfig, type UserData, type UserDataResult, type UserLendingPosition, type UtilizationTerms, VAULT_PROVIDERS, VAULT_PROVIDER_PROFILE, VAULT_PROVIDER_TRAITS, VAULT_SHARE_PRICE_PROBE, VAULT_VENUE_PREFIX, VOLATILE_VAULT_OVERRIDES, type ValidationResult, type VaultAprResult, type VaultBalanceInput, type VaultClassification, type VaultClassificationFields, type VaultLookupEntry, type VaultMarketExposure, type VaultProvider, type VaultProviderTraits, type VaultPublicDataAll, type VaultPublicDataResult, type VaultSourceRow, type VaultTermInput, type VaultYieldSeries, type VaultYieldSnapshot, type YDaemonVault, YEARN_CHAIN_IDS, YEARN_YDAEMON_BASE, type YearnVault, type YearnVaultKind, type YearnVaults, type YieldProfile, __resetResupplyUserCaches, accountDepositListKey, accountWithdrawalListKey, appendSnapshot, applyEarnSanity, applyPositionDelta, attachImplications, attachPricesToFlashLiquidity, borrowDescription, borrowFindings, borrowHeadline, buildExposures, buildFluidFTokensCall, buildLendingPositionUid, buildLiquityUserCall, buildLoopResult, buildMorphoTypeCall, buildMorphoTypeUserCallWithLens, buildPortfolioTotals, buildSumerAccumulators, buildSummaries, buildTellerUserCall, buildTermMaxUserCall, buildTermSheet, buildTermSheetsForGroup, buildVaultEarnUid, buildVaultLookup, buildVaultTermSheet, calculateLeverage, calculateNetApr, calculateOverallNetApr, calculateWeightedAverage, classifyFreshness, classifyReliability, classifyVault, clearPendleMarketsCache, clearSpectraMarketsCache, collateralSymbolsByVenue, collectFeedObservations, computeBorrowDelta, computeCloseTradeDeltas, computeCollateralSwapDeltas, computeDebtSwapDeltas, computeDepositDelta, computeEModeAnalysis, computeOpenTradeDeltas, computePostTradeMetrics, computeRepayDelta, computeSumerBorrowDelta, computeSumerDepositDelta, computeSumerRepayDelta, computeSumerWaterfall, computeSumerWithdrawDelta, computeVaultApr, computeWithdrawDelta, computeZapTradeDeltas, consensusReference, convertCoolerMarketsToResponse, convertDssMarketsToResponse, convertExactlyMarketsToResponse, convertFlyingTulipMarketsToResponse, convertFrankencoinMarketsToResponse, convertFraxlendPairsToResponse, convertInverseMarketsToResponse, convertLenderUserDataResult, convertLiquityMarketsToResponse, convertLlamaLendMarketsToResponse, convertResupplyMarketsToResponse, convertRiverMarketsToResponse, convertTellerMarketsToResponse, convertTermMarketsToResponse, convertTermMaxMarketsToResponse, convertUsddMarketsToResponse, createMarketUid, createMidnightBookSource, createMulticallRpcCall, createRawRpcCalls, createTermBookSource, createTermMaxDataSource, decodeListaMarkets, decodeMarkets, decodePackedListaUserDataset, decodePackedMorphoUserDataset, deriveBorrowTags, deriveSupplyTags, detectInterfaceKinds, dexResolverFor, disambiguateEarnNames, dolomiteVaultAddress, dssIlkBytes32, dssKeyParts, dssLenderKey, duration, earnDescription, earnLabel, earnMarketFromPool, earnMarketFromVault, earnMarketLabel, earnPositionFromLenderEntry, earnPositionFromVaultBalance, earnPositionTotals, earnRateAtMaturity, earnRowSubtitle, earnUidFromMarketUid, earnVaultTerms, earnVenueKind, encodeBalanceFetcherCalldata, enrichTermSheet, enrichmentIndexFromRows, exactlyPairLtv, exactlyPenaltyRateToAprPercent, exactlyWadRateToPercent, feePhrase, feedKeyOf, feedStatKey, fetchCoolerMarkets, fetchDolomiteAccountNumbers, fetchDolomiteSubAccounts, fetchDssMarkets, fetchEulerEarnVaults, fetchEulerEarnVaultsFromSubgraph, fetchEulerSubAccountIndexes, fetchExactlyMarkets, fetchFlashLiquidityForChain, fetchFluidDexState, fetchFluidFTokens, fetchFlyingTulipMarkets, fetchFrankencoinMarkets, fetchFraxlendPairs, fetchGeneralYields, fetchGeneralYieldsByMarketUid, fetchGmxExecutionFees, fetchGmxTickerPrices, fetchGmxVaults, fetchHypercoreVaults, fetchInverseMarkets, fetchLagoonApiVaults, fetchLagoonVaults, fetchLiquityMarkets, fetchListaVaultsFromChain, fetchLlamaLendMarkets, fetchMorphoUserBalances, fetchMorphoUserPositionMarkets, fetchMorphoVaults, fetchMorphoVaultsFromApi, fetchMorphoVaultsFromChain, fetchOraclePrices, fetchPendleApiAssets, fetchPendleApiMarkets, fetchPendlePrices, fetchPendlePtMarkets, fetchResupplyMarkets, fetchRiverMarkets, fetchSiloVaults, fetchSpectraApiMarkets, fetchSpectraPtMarkets, fetchTellerMarkets, fetchTermMarkets, fetchTermMaxMarkets, fetchTermStoreOrders, fetchTokenBalances, fetchTokenMetadata, fetchUpshiftApiVaults, fetchUpshiftVaults, fetchUsddMarkets, fetchYearnApiVaults, fetchYearnVaults, fillableRemaining, filterActiveLenders, filterLendersByProtocol, finalizeInfo, findingsFor, formatRaw, frankencoinKeyParts, frankencoinLenderKey, fraxlendAssetPerCollateral, fraxlendKeyParts, fraxlendLenderKey, fuseLenderData, generateLendingPools, getAavesForChain, getAssetConfig, getBalanceForMarketUid, getBorrowCapacity, getCachedFluidDexState, getCachedLiquityTroves, getCachedTellerBids, getCachedTermMaxDiscovery, getCachedTermMaxMarket, getCachedTermMaxMarkets, getCoreValidators, getFluidFTokensConverter, getGmxApiHost, getGmxReadContracts, getGmxUserPositions, getHealthFactor, getHypercoreUserPositions, getHypercoreVaultRegistry, getLenderAssets, getLenderPublicData, getLenderPublicDataAll, getLenderPublicDataViaApi, getLenderUserDataMulti, getLenderUserDataResult, getLendersForChain, getLstDelegation, getLstValidators, getLstWithdrawalRegistry, getLstWithdrawalRequests, getMaxAmountClose, getMaxAmountCollateralSwap, getMaxAmountDebtSwap, getMaxAmountOpen, getMergedUserData, getMorphoTypeMarketConverter, getReadFailurePolicy, getResolvedDolomiteAccountNumbers, getResolvedDolomiteSubAccounts, getStCeloValidatorGroups, getSubAccountAddress, getSubAccountIndex, getTermProfile, getVaultPublicDataAll, getVaultWithdrawalRequests, hasCritical, hasEulerEarnVaultSubgraph, hasLagoonVaults, hasMorphoPositionIndex, hasMorphoUserApi, hasMorphoUserSubgraph, hasPendleMarkets, hasSpectraMarkets, hasUpshiftVaults, hasYearnVaults, ilkToKeySegment, implausibleRatePercent, inverseKeyParts, inverseLenderKey, isBoundNeed, isFailedCall, isIlliquid, isLendingPosition, isLiveMarket as isLivePendleMarket, isLiveSpectraMarket, isMaturedTerm, isSecondaryMarketOnly, isStablecoinSymbol, isUnearnableEarnRate, isUnrealizableEarnRate, isVaultPosition, isVaultVenue, isYearnV3, keySegmentToIlk, keysFromMaps, lenderApiOnly, lenderFamily, liquityCandidateTroveIds, liquityKeyParts, liquityLenderKey, llamaLendKeyParts, llamaLendLenderKey, meetsLiquidityFloor, mergeDeep, multicall3Abi, nanTo, needsLenderApproval, needsTokenApproval, noOpResult, normalizeToBytes, organizeUserQueries, parseBalanceFetcherResult, parseDolomiteSubAccountId, parseEarnUid, parseMergedResult, parseMulticallRpcResponses, parseExpirySeconds as parsePendleExpirySeconds, parseRawRpcBatchResponses, parseRawRpcResponses, parseCurveFee as parseSpectraCurveFee, parsePtRate as parseSpectraPtRate, parseTermMaxLtv, parseTokenBalanceResult, pct, assetKey as pendleAssetKey, pickPool as pickSpectraPool, positivePart, predictInverseEscrow, prepareLenderUserDataRpcCalls, prepareMergedMulticallParams, prepareMergedRpcCalls, prepareMulticallInputs, prepareTokenBalanceRpcCalls, priceGlvVaults, priceGmMarkets, probeAaveFeedTimestamps, pruneFeedStats, rankFindings, ratePercent, readVaultSharePrices, rejectOutliers as rejectPriceOutliers, repairImpossibleTvl, resolveAdapter, resolveDerivation, resolveDolomiteRowIdentity, resolveEarnIdentity, resolveModeConfig, resolveStCeloDepositGroup, resolveVaultProfileId, resupplyKeyParts, resupplyLenderKey, resupplyMarketLabel, riverKeyParts, riverLenderKey, sanePercent as saneSpectraPercent, savingsAddresses, savingsBalanceKind, secondaryMarketVault, selectAssetGroupPrices, setMysticApiKey, shortDate, spectraAddress, spectraNetwork, spectraPoolsUrl, splitChainScopedAddress as splitPendleChainScopedAddress, stampCapabilities, stampEarnSubtitles, stampVaultClassification, stampVaultTermSheets, stripLeadingBrand, supplyDescription, supplyFindings, supplyHeadline, swapRoutedProvidersArePriceConsistent, tellerBpsToPercent, tellerImpliedLtv, tellerLenderKey, tellerPoolFromLenderKey, termApiBaseUrl, termLenderKey, termMaxApiBase, borrowNif as termMaxBorrowNif, curveApr as termMaxCurveApr, curveAprNumber as termMaxCurveAprNumber, daysToMaturity as termMaxDaysToMaturity, lendNif as termMaxLendNif, mintGtFeeRatio as termMaxMintGtFeeRatio, ratioToNumber as termMaxRatioToNumber, ratioToPercent as termMaxRatioToPercent, termOfferRateToAprPct, termOrderStoreBaseUrl, tickToAprNumber, tickToPrice, toDigest, toDolomiteSubAccountId, toTermFillNow, toTermSheetInput, toVaultTermInput, trancheFromCounterparty, tryParseEarnUid, unflattenLenderData, updateFeedStats, usdValue, usddIlkBytes32, usddKeyParts, usddLenderKey, validateTermSheet, validateTermSheets, validateUserData, vaultSharesToAssets, vaultTermInputFromEarnMarket, vaultTermInputFromSourceRow, vaultTraits, vaultVenue, venueBrand, venueBrandKey, withMaturityLabel, withTrancheLabel };