@1delta/margin-fetcher

中文文档
5.0.152发布于 2026/11/06ISCTypeScriptESM + CJS近 30 天下载 6611

Multi-protocol lending data fetcher supporting Morpho Blue, Aave V2/V3, Compound V2/V3, Euler, Init, Lista DAO, and Morpho Midnight. Provides public market data (rates, TVL, configs) and per-user position data (balances, shares, collateral) in a unified f

原始使用文档
文档目录16 节
  1. How Morpho Blue Works
  2. Protocol overview
  3. Interest rate model
  4. Share-based accounting
  5. Market data representation
  6. Whitelisted vs unlisted markets
  7. Architecture
  8. Data fetching strategy
  9. API path (GraphQL)
  10. On-chain path (Morpho Lens)
  11. User data path
  12. File structure
  13. Lista DAO extension
  14. Fixed-term broker ( LendingBroker )
  15. Morpho Midnight extension
  16. API-first fetching

@1delta/margin-fetcher

Multi-protocol lending data fetcher supporting Morpho Blue, Aave V2/V3, Compound V2/V3, Euler, Init, Lista DAO, and Morpho Midnight. Provides public market data (rates, TVL, configs) and per-user position data (balances, shares, collateral) in a unified format.

How Morpho Blue Works

Protocol overview

Morpho Blue is an isolated-market lending protocol. Unlike pooled protocols (Aave, Compound) where all assets share a single pool, each Morpho market is a standalone pair defined by five parameters:

ParameterDescription
loanTokenThe asset that can be supplied and borrowed
collateralTokenThe asset deposited as collateral
oraclePrice oracle for the pair
irmInterest Rate Model contract
lltvLiquidation Loan-To-Value ratio (18-decimal WAD)

These five parameters are hashed into a uniqueKey (bytes32) that identifies the market on-chain.

Interest rate model

Morpho uses an adaptive curve IRM that adjusts rates based on utilization relative to a 90% target:

utilization = totalBorrowAssets / totalSupplyAssets

If utilization > 90%: rateAtTarget increases over time (via exponential adjustment)
If utilization < 90%: rateAtTarget decreases over time

borrowRate = curve(rateAtTarget, utilization)
supplyRate = borrowRate * utilization * (1 - fee)

The curve uses a steepness factor of 4x, meaning at full utilization the rate is 4x the rate-at-target. Rates are stored as per-second WAD values and compounded continuously.

Key constants:

  • Target utilization: 90%
  • Curve steepness: 4x
  • Min rate at target: ~0.1% APY
  • Max rate at target: ~200% APY
  • Adjustment speed: 50x/year (exponential)

Share-based accounting

Positions are tracked via shares rather than raw assets. This allows interest to accrue without storage updates per-user:

assets = shares * (totalAssets + 1) / (totalShares + VIRTUAL_SHARES)

Where VIRTUAL_SHARES = 1e6 prevents share inflation attacks.

Market data representation

Each Morpho market produces two entries in the normalized output:

  1. Loan entry — the borrowable asset

    • Has borrowingEnabled: true, collateralActive: false
    • Contains deposit/borrow APRs, total supply/debt, liquidity
    • Rewards (MORPHO token incentives) attached here
  2. Collateral entry — the collateral asset

    • Has borrowingEnabled: false, collateralActive: true
    • LTV factors derived from the market's lltv
    • No interest rates (collateral doesn't earn yield in Morpho)

Whitelisted vs unlisted markets

Morpho markets can be whitelisted (curated, visible in the Morpho UI) or unlisted (removed from curation). The includeUnlistedMorphoMarkets flag controls whether unlisted markets are fetched from the API. Unlisted markets are tagged with isListed: false on the MorphoMarket params.

Architecture

Data fetching strategy

The fetcher uses a hybrid approach that routes each lender to either an API or on-chain path:

getLenderPublicDataAll(chainId, lenders, ...)
├── lenderCanUseApi(lender, chainId)?
│   ├── YES → getLenderPublicDataViaApi()  →  Morpho GraphQL API
│   └── NO  → getLenderPublicData()        →  On-chain multicall via Morpho Lens
└── Promise.all([onChain, api]) → merged result

Morpho Blue uses the API on most chains. On-chain fallback is used for chains without API support (OP Mainnet, Soneium, Hemi, Berachain, Sei).

API path (GraphQL)

Fetches from https://blue-api.morpho.org/graphql:

fetchMorphoMarkets(chainId, includeUnlisted)
  → GraphQL query (paginated: 200/page, 2 pages for Ethereum mainnet)
  → GetMarketsResponse
  → convertMarketsToMorphoResponse()
  → { [marketId]: MorphoGeneralPublicResponse }

The API provides pre-computed APYs, USD values, and reward data. APY→APR conversion is applied during normalization.

On-chain path (Morpho Lens)

Uses a custom lens contract that returns compact binary-encoded market data:

buildMorphoCall(chainId)
  → multicall to MORPHO_LENS.getMarketDataCompact(morpho, marketHashes[])
  → chunks of 50 markets per call (calldata size limit)
  → raw bytes response

getMorphoMarketDataConverter()
  → decodeMarkets(bytes)           // 256-byte records → Market[]
  → MathLib.getBorrowApy(...)      // compute rates from rateAtTarget + utilization
  → MathLib.getSupplyApy(...)
  → { [marketId]: MorphoGeneralPublicResponse }

Binary encoding (256 bytes per market)

Offset  Size   Field
0       20     loanToken (address)
20      20     collateralToken (address)
40      20     oracle (address)
60      20     irm (address)
80      16     lltv (uint128)
96      32     price (uint256) — collateral/loan exchange rate in WAD
128     32     rateAtTarget (uint256) — per-second interest rate
160     16     totalSupplyAssets (uint128)
176     16     totalSupplyShares (uint128)
192     16     totalBorrowAssets (uint128)
208     16     totalBorrowShares (uint128)
224     16     lastUpdate (uint128) — timestamp
240     16     fee (uint128)

User data path

buildMorphoUserCallWithLens(chainId, account, lender, marketIds)
  → MORPHO_LENS.getUserDataCompact(marketHashes[], account, morpho)
  → chunks of 100 markets per call

decodePackedMorphoUserDataset(hex)
  → [uint16 count] + [130-byte records × count]
  → BalanceInfo[] { index, supplyShares, borrowShares, supplyAssets, borrowAssets, collateral }

getMorphoUserDataConverterWithLens()
  → map balances to market IDs via index
  → separate loan positions (supply/borrow) from collateral positions
  → format to UserData with USD values

File structure

src/lending/
├── public-data/
│   ├── fetchLenderAll.ts          # Hybrid router (API vs on-chain)
│   ├── fetchLender.ts             # On-chain multicall dispatcher
│   ├── fetchLenderExt.ts          # API dispatcher
│   └── morpho/
│       ├── fetchPublic.ts         # GraphQL query + fetch logic
│       ├── convertPublic.ts       # API response → normalized format
│       ├── publicCallBuild.ts     # Morpho Lens call builder
│       ├── getMarketsFromChain.ts # On-chain response → normalized format
│       └── utils/
│           ├── evmParser.ts       # Binary decoder (256-byte records)
│           ├── mathLib.ts         # WAD math, rate calculations
│           └── parsers.ts         # LTV parsing, number formatting
├── user-data/
│   └── morpho/
│       ├── userCallBuild.ts       # User position call builder
│       ├── decoder.ts             # User data binary decoder
│       ├── userCallParse.ts       # Balance → UserData conversion
│       ├── morphoLib.ts           # Share ↔ asset conversion
│       └── types.ts               # Parameter position enums
└── ...

src/types/lender/
└── morpho-types.ts                # GetMarketsResponse, MorphoMarket, MorphoGeneralPublicResponse

Lista DAO extension

Lista is a Morpho Blue fork with additional per-market fields. Uses a 357-byte binary record instead of 256:

Extra fieldTypeDescription
minLoanuint128Minimum loan amount
hasWhitelistboolWhether the market has access control
loanProvideraddressYield source for loan token
collateralProvideraddressYield source for collateral
brokeraddressAuthorized broker contract

User data also includes per-market whitelist flags prepended before the balance records.

Fixed-term broker (LendingBroker)

Some Lista markets register a LendingBroker — the mandatory debt-side gateway of a Moolah (Morpho-fork) market. Collateral supply/withdraw still go through the normal Moolah path, but borrow and repay run through the broker, which overlays a richer debt model on top of the raw Moolah position. The on-chain broker is an ERC1967 proxy (e.g. BNB 0x1Fa2…8b54 → impl 0xb680…58bb), verified source LendingBroker.sol / BrokerMath.sol. Fetcher support lives in lending/public-data/lista/listaBroker.ts; the composer encoders live in contracts-delegation (ListaBrokerLending.sol, CalldataLib.encodeListaBroker*).

Two debt buckets per user. A brokered market has no flexible/variable borrowing at the Moolah layer — instead each user holds:

  • N fixed-term positions — each a {posId, principal, apr, start, end, …} tranche with a locked APR and a maturity. Identified by posId.
  • One dynamic position — a variable-rate position, the catch-all bucket. Targeted on repay by the sentinel posId == type(uint128).max (LISTA_BROKER_DYNAMIC_POS).

broker.getUserTotalDebt(user) is the authoritative total (fixed + dynamic + all interest).

Borrow

The composer calls borrow(amount, termId, user, receiver). user (the position owner) is always the authenticated caller and must have authorized the composer in Moolah (setAuthorization) — debt can never be opened against someone else. The broker borrows from Moolah and forwards the loan token to receiver.

Native: the broker can unwrap WBNB→BNB, but only on its EOA-direct borrow(amount) / borrow(amount, termId) overloads that pay msg.sender. The receiver-variant the composer must use (Moolah requires receiver == broker when a broker is registered) always sends ERC20 WBNB, never native. To deliver native BNB, chain an unwrap step after the borrow.

Repay

encodeListaBrokerRepay(loanToken, assets, native, broker, posId, onBehalf) → repay(amount, posId, onBehalf) (fixed) or repay(amount, onBehalf) (dynamic, sentinel posId).

  • Interest-first, then principal.
  • onBehalf is taken straight from calldata (exactly like Morpho's repay onBehalfOf) — repaying only ever pays debt down and refunds excess to the composer, so repay-on-behalf is permissionless. Must be non-zero (broker reverts ZeroAddress); there is no caller fallback.
  • assets == 0 repays the composer's full balance (balanceOf / selfbalance); the broker refunds any excess. The clean "close it out" pattern: over-fund slightly, repay with assets == 0, take the dust.
  • Native: both repay overloads are payable and wrap msg.value internally (gated on LOAN_TOKEN == WBNB, else NativeNotSupported). The composer's native path forwards the value and skips the ERC20 approve. Excess refunds as native BNB to the composer (un-swept on explicit amounts).

Early-repayment penalty

Repaying a fixed position's principal before end incurs a penalty (BrokerMath.getPenaltyForFixedPosition):

penalty = ceil( ceil(repayPrincipal × aprPerSecond / RATE_SCALE) × timeLeft / 2 )

≈ half the interest that principal would still accrue over the remaining term. It's skimmed off the principal payment and supplied to the Moolah vault as revenue, so it is additive — on top of outstanding + accruedInterest. Penalty is 0 once matured (block.timestamp > end). The fetcher surfaces it per loan as earlyRepayPenalty (computed for the full outstanding principal; scale linearly for partials). Use the broker's previewRepayFixedLoanPosition(user, amount, posId) for the exact (interest, penalty, principal) split.

Maturity & refinancing

A fixed position's interest freezes at end (getAccruedInterestForFixedPosition caps at position.end); after maturity it accrues no further fixed interest and carries no penalty.

refinanceMaturedFixedPositions(user, posIds) is a bot-only function (onlyRole(BOT)) — users and the composer cannot call it, so it is not exposed as an SDK/composer op. It migrates matured fixed positions into the dynamic bucket: net principal + frozen interest are normalized at the current dynamic rate and added to the dynamic position, and the fixed position is deleted. The window between maturity and the bot's call is interest-free (the protocol absorbs it).

Repaying late

WhenTargetCost
Before endfixed posIdprincipal + accrued interest + early penalty
After end, before bot refinancesfixed posIdprincipal + frozen interest, no penalty — cheapest
After bot refinancesdynamic sentinelprincipal + interest captured at refinance + dynamic-rate interest since

Refinance is a race. A repay targeting a matured posId reverts (position not found) if the bot refinances first; fall back to the dynamic repay. The matured-but-not-yet-refinanced window (isMatured: true, earlyRepayPenalty: '0') is the cheapest time to repay.

Data shape

Brokered markets emit no variable debt — the position's debt is 0, the authoritative total sits in debtStable, and each fixed loan is itemized in a terms[] array (ListaTermLoan):

FieldDescription
loanIdposId — the repay target for the LISTA_BROKER_REPAY composer op
debtoutstanding (principal + accrued interest), loan-token units
aprFractionlocked fixed APR as a fraction
maturity / termDaysunix maturity timestamp / term length
accruedInterestoutstanding accrued interest
earlyRepayPenaltypenalty to close the loan now; 0 once matured
isMaturednow >= end

Note: terms[] currently itemizes only fixed loans. The dynamic position (post-refinance) is included in the debtStable total but not yet surfaced as its own repayable term.

Morpho Midnight extension

Morpho Midnight is a Base-only, fixed-rate / fixed-maturity lending protocol built on an order book of off-chain signed offers (zero-coupon credit/debt units) — it is NOT a Morpho Blue fork, so it is modeled as its own 'midnight' provider (Lender.MORPHO_MIDNIGHT, isMidnight), not isMorphoType. There is no pool and no utilization curve: rates are derived from each offer's tick → price and its time-to-maturity, knowable only via the API. Each maturity is its own isolated market, keyed MORPHO_MIDNIGHT_<marketId>.

API-first fetching

Unlike Morpho Blue's GraphQL-then-on-chain hybrid, Midnight public data is API-only — there is no on-chain public fallback in fetchLenderAll. The fetcher lives in lending/public-data/midnight/:

  • apiClient / fetchPublic — an ApiBookSource reads the order book per market (GET /books/{id}) from the Midnight API. The base URL resolves override → config → hosted default (https://api.morpho.org/v0/midnight); the worker can point it at a dev API via MIDNIGHT_API_BASE (see data-sdk setMidnightApiBase).
  • math.ts — ported TickLib (tickToApr etc.), bit-verified against @morpho-org/midnight-sdk.
  • convertPublic — normalizes the book into a MorphoGeneralPublicResponse keyed by MORPHO_MIDNIGHT_<id>, emitting the fixed borrow/supply APR and a single-term params.market.terms[] ({termId, durationSecs, durationDays, apr}) carrying the maturity — so the maturity flows through the same generic terms[] path as Lista's fixed loans. Units are in the loan token's decimals (not 18): assets = units × price / WAD, so units ≈ assets.

User positions ({credit, debt, collateral[128], collateralBitmap}) are read via multicall in lending/user-data/midnight/, one UserData per market.

Scope: the fetcher and direct spot actions (supply collateral / borrow-via-take / repay / withdraw) are wired. Composer-routed leverage and native ETH are deferred — see calldata-sdk/src/generic/midnight/COMPOSER.md.

版本与发布记录

@1delta/margin-fetcher 共发布 471 个版本,首次发布于 2025/07/08;过去 12 个月发布了 369 个版本。

版本发布日期距今
5.0.152latest2026/11/06今天
5.0.1512026/11/06今天
5.0.1502026/11/042 天前
5.0.1492026/11/032 天前
5.0.1472026/10/306 天前
5.0.1452026/10/307 天前

关于 @1delta/margin-fetcher 的常见问题

@1delta/margin-fetcher 是什么?

Multi-protocol lending data fetcher supporting Morpho Blue, Aave V2/V3, Compound V2/V3, Euler, Init, Lista DAO, and Morpho Midnight. Provides public market data (rates, TVL, configs) and per-user position data (balances, shares, collateral) in a unified f

如何安装 @1delta/margin-fetcher?

在项目目录运行 npm install @1delta/margin-fetcher,或使用 pnpm add / yarn add / bun add。

@1delta/margin-fetcher 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@1delta/margin-fetcher 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@1delta/margin-fetcher 可以商用吗?

@1delta/margin-fetcher 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@1delta/margin-fetcher 的下载量有多少?

根据 npm 官方下载统计,@1delta/margin-fetcher 在 2026/10/05 – 2026/11/03 这 30 天内被下载 6,611 次,日均约 220 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@1delta/margin-fetcher 还在维护吗?

最新版本 5.0.152 发布于 2026/11/06(今天);至今共发布 471 个版本,其中过去 12 个月发布了 369 个;首次发布于 2025/07/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。