npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@1delta/margin-fetcher

v0.0.323

Published

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

Readme

@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:

| Parameter | Description | |-----------|-------------| | loanToken | The asset that can be supplied and borrowed | | collateralToken | The asset deposited as collateral | | oracle | Price oracle for the pair | | irm | Interest Rate Model contract | | lltv | Liquidation 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 field | Type | Description | |-------------|------|-------------| | minLoan | uint128 | Minimum loan amount | | hasWhitelist | bool | Whether the market has access control | | loanProvider | address | Yield source for loan token | | collateralProvider | address | Yield source for collateral | | broker | address | Authorized 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

| When | Target | Cost | |------|--------|------| | Before end | fixed posId | principal + accrued interest + early penalty | | After end, before bot refinances | fixed posId | principal + frozen interest, no penalty — cheapest | | After bot refinances | dynamic sentinel | principal + 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):

| Field | Description | |-------|-------------| | loanId | posId — the repay target for the LISTA_BROKER_REPAY composer op | | debt | outstanding (principal + accrued interest), loan-token units | | aprFraction | locked fixed APR as a fraction | | maturity / termDays | unix maturity timestamp / term length | | accruedInterest | outstanding accrued interest | | earlyRepayPenalty | penalty to close the loan now; 0 once matured | | isMatured | now >= 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/evm/generic/midnight/COMPOSER_PLAN.md.