@1delta/margin-fetcher 5.0.17 → 5.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,33 @@
1
1
  # @1delta/margin-fetcher
2
2
 
3
- 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.
3
+ Multi-protocol lending data fetcher supporting Morpho Blue, Aave V2/V3, Compound
4
+ V2/V3, Euler, Init, Lista DAO, and Morpho Midnight. Provides public market data
5
+ (rates, TVL, configs) and per-user position data (balances, shares, collateral)
6
+ in a unified format.
4
7
 
5
8
  ## How Morpho Blue Works
6
9
 
7
10
  ### Protocol overview
8
11
 
9
- 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:
12
+ Morpho Blue is an **isolated-market** lending protocol. Unlike pooled protocols
13
+ (Aave, Compound) where all assets share a single pool, each Morpho market is a
14
+ **standalone pair** defined by five parameters:
10
15
 
11
- | Parameter | Description |
12
- |-----------|-------------|
13
- | `loanToken` | The asset that can be supplied and borrowed |
14
- | `collateralToken` | The asset deposited as collateral |
15
- | `oracle` | Price oracle for the pair |
16
- | `irm` | Interest Rate Model contract |
17
- | `lltv` | Liquidation Loan-To-Value ratio (18-decimal WAD) |
16
+ | Parameter | Description |
17
+ | ----------------- | ------------------------------------------------ |
18
+ | `loanToken` | The asset that can be supplied and borrowed |
19
+ | `collateralToken` | The asset deposited as collateral |
20
+ | `oracle` | Price oracle for the pair |
21
+ | `irm` | Interest Rate Model contract |
22
+ | `lltv` | Liquidation Loan-To-Value ratio (18-decimal WAD) |
18
23
 
19
- These five parameters are hashed into a `uniqueKey` (bytes32) that identifies the market on-chain.
24
+ These five parameters are hashed into a `uniqueKey` (bytes32) that identifies
25
+ the market on-chain.
20
26
 
21
27
  ### Interest rate model
22
28
 
23
- Morpho uses an **adaptive curve IRM** that adjusts rates based on utilization relative to a 90% target:
29
+ Morpho uses an **adaptive curve IRM** that adjusts rates based on utilization
30
+ relative to a 90% target:
24
31
 
25
32
  ```
26
33
  utilization = totalBorrowAssets / totalSupplyAssets
@@ -32,9 +39,12 @@ borrowRate = curve(rateAtTarget, utilization)
32
39
  supplyRate = borrowRate * utilization * (1 - fee)
33
40
  ```
34
41
 
35
- 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.
42
+ The curve uses a steepness factor of 4x, meaning at full utilization the rate is
43
+ 4x the rate-at-target. Rates are stored as per-second WAD values and compounded
44
+ continuously.
36
45
 
37
46
  Key constants:
47
+
38
48
  - **Target utilization**: 90%
39
49
  - **Curve steepness**: 4x
40
50
  - **Min rate at target**: ~0.1% APY
@@ -43,7 +53,8 @@ Key constants:
43
53
 
44
54
  ### Share-based accounting
45
55
 
46
- Positions are tracked via **shares** rather than raw assets. This allows interest to accrue without storage updates per-user:
56
+ Positions are tracked via **shares** rather than raw assets. This allows
57
+ interest to accrue without storage updates per-user:
47
58
 
48
59
  ```
49
60
  assets = shares * (totalAssets + 1) / (totalShares + VIRTUAL_SHARES)
@@ -67,13 +78,17 @@ Each Morpho market produces **two entries** in the normalized output:
67
78
 
68
79
  ### Whitelisted vs unlisted markets
69
80
 
70
- 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.
81
+ Morpho markets can be **whitelisted** (curated, visible in the Morpho UI) or
82
+ **unlisted** (removed from curation). The `includeUnlistedMorphoMarkets` flag
83
+ controls whether unlisted markets are fetched from the API. Unlisted markets are
84
+ tagged with `isListed: false` on the `MorphoMarket` params.
71
85
 
72
86
  ## Architecture
73
87
 
74
88
  ### Data fetching strategy
75
89
 
76
- The fetcher uses a **hybrid approach** that routes each lender to either an API or on-chain path:
90
+ The fetcher uses a **hybrid approach** that routes each lender to either an API
91
+ or on-chain path:
77
92
 
78
93
  ```
79
94
  getLenderPublicDataAll(chainId, lenders, ...)
@@ -83,7 +98,8 @@ getLenderPublicDataAll(chainId, lenders, ...)
83
98
  └── Promise.all([onChain, api]) → merged result
84
99
  ```
85
100
 
86
- **Morpho Blue uses the API** on most chains. On-chain fallback is used for chains without API support (OP Mainnet, Soneium, Hemi, Berachain, Sei).
101
+ **Morpho Blue uses the API** on most chains. On-chain fallback is used for
102
+ chains without API support (OP Mainnet, Soneium, Hemi, Berachain, Sei).
87
103
 
88
104
  ### API path (GraphQL)
89
105
 
@@ -97,7 +113,8 @@ fetchMorphoMarkets(chainId, includeUnlisted)
97
113
  → { [marketId]: MorphoGeneralPublicResponse }
98
114
  ```
99
115
 
100
- The API provides pre-computed APYs, USD values, and reward data. APY→APR conversion is applied during normalization.
116
+ The API provides pre-computed APYs, USD values, and reward data. APY→APR
117
+ conversion is applied during normalization.
101
118
 
102
119
  ### On-chain path (Morpho Lens)
103
120
 
@@ -184,64 +201,77 @@ src/types/lender/
184
201
 
185
202
  ## Lista DAO extension
186
203
 
187
- Lista is a Morpho Blue fork with additional per-market fields. Uses a 357-byte binary record instead of 256:
204
+ Lista is a Morpho Blue fork with additional per-market fields. Uses a 357-byte
205
+ binary record instead of 256:
188
206
 
189
- | Extra field | Type | Description |
190
- |-------------|------|-------------|
191
- | `minLoan` | uint128 | Minimum loan amount |
192
- | `hasWhitelist` | bool | Whether the market has access control |
193
- | `loanProvider` | address | Yield source for loan token |
194
- | `collateralProvider` | address | Yield source for collateral |
195
- | `broker` | address | Authorized broker contract |
207
+ | Extra field | Type | Description |
208
+ | -------------------- | ------- | ------------------------------------- |
209
+ | `minLoan` | uint128 | Minimum loan amount |
210
+ | `hasWhitelist` | bool | Whether the market has access control |
211
+ | `loanProvider` | address | Yield source for loan token |
212
+ | `collateralProvider` | address | Yield source for collateral |
213
+ | `broker` | address | Authorized broker contract |
196
214
 
197
- User data also includes per-market whitelist flags prepended before the balance records.
215
+ User data also includes per-market whitelist flags prepended before the balance
216
+ records.
198
217
 
199
218
  ### Fixed-term broker (`LendingBroker`)
200
219
 
201
- Some Lista markets register a **`LendingBroker`** — the mandatory debt-side gateway of a Moolah
202
- (Morpho-fork) market. Collateral supply/withdraw still go through the normal Moolah path, but
203
- **borrow and repay run through the broker**, which overlays a richer debt model on top of the raw
204
- Moolah position. The on-chain broker is an ERC1967 proxy (e.g. BNB `0x1Fa2…8b54` impl `0xb680…58bb`),
205
- verified source `LendingBroker.sol` / `BrokerMath.sol`. Fetcher support lives in
206
- [`lending/public-data/lista/listaBroker.ts`](src/lending/public-data/lista/listaBroker.ts); the composer
207
- encoders live in `contracts-delegation` (`ListaBrokerLending.sol`, `CalldataLib.encodeListaBroker*`).
208
-
209
- **Two debt buckets per user.** A brokered market has **no flexible/variable borrowing** at the
210
- Moolah layer — instead each user holds:
211
-
212
- - **N fixed-term positions** each a `{posId, principal, apr, start, end, …}` tranche with a *locked*
213
- APR and a maturity. Identified by `posId`.
214
- - **One dynamic position** — a variable-rate position, the catch-all bucket. Targeted on repay by the
215
- sentinel `posId == type(uint128).max` (`LISTA_BROKER_DYNAMIC_POS`).
216
-
217
- `broker.getUserTotalDebt(user)` is the authoritative total (fixed + dynamic + all interest).
220
+ Some Lista markets register a **`LendingBroker`** — the mandatory debt-side
221
+ gateway of a Moolah (Morpho-fork) market. Collateral supply/withdraw still go
222
+ through the normal Moolah path, but **borrow and repay run through the broker**,
223
+ which overlays a richer debt model on top of the raw Moolah position. The
224
+ on-chain broker is an ERC1967 proxy (e.g. BNB `0x1Fa2…8b54` impl
225
+ `0xb680…58bb`), verified source `LendingBroker.sol` / `BrokerMath.sol`. Fetcher
226
+ support lives in
227
+ [`lending/public-data/lista/listaBroker.ts`](src/lending/public-data/lista/listaBroker.ts);
228
+ the composer encoders live in `contracts-delegation` (`ListaBrokerLending.sol`,
229
+ `CalldataLib.encodeListaBroker*`).
230
+
231
+ **Two debt buckets per user.** A brokered market has **no flexible/variable
232
+ borrowing** at the Moolah layer instead each user holds:
233
+
234
+ - **N fixed-term positions** — each a `{posId, principal, apr, start, end, …}`
235
+ tranche with a _locked_ APR and a maturity. Identified by `posId`.
236
+ - **One dynamic position** a variable-rate position, the catch-all bucket.
237
+ Targeted on repay by the sentinel `posId == type(uint128).max`
238
+ (`LISTA_BROKER_DYNAMIC_POS`).
239
+
240
+ `broker.getUserTotalDebt(user)` is the authoritative total (fixed + dynamic +
241
+ all interest).
218
242
 
219
243
  #### Borrow
220
244
 
221
- The composer calls `borrow(amount, termId, user, receiver)`. `user` (the position owner) is **always
222
- the authenticated caller** and must have authorized the composer in Moolah (`setAuthorization`) — debt
223
- can never be opened against someone else. The broker borrows from Moolah and forwards the loan token to
245
+ The composer calls `borrow(amount, termId, user, receiver)`. `user` (the
246
+ position owner) is **always the authenticated caller** and must have authorized
247
+ the composer in Moolah (`setAuthorization`) debt can never be opened against
248
+ someone else. The broker borrows from Moolah and forwards the loan token to
224
249
  `receiver`.
225
250
 
226
- > **Native:** the broker *can* unwrap WBNB→BNB, but only on its EOA-direct `borrow(amount)` /
227
- > `borrow(amount, termId)` overloads that pay `msg.sender`. The `receiver`-variant the composer must use
228
- > (Moolah requires `receiver == broker` when a broker is registered) **always sends ERC20 WBNB, never
229
- > native**. To deliver native BNB, chain an unwrap step after the borrow.
251
+ > **Native:** the broker _can_ unwrap WBNB→BNB, but only on its EOA-direct
252
+ > `borrow(amount)` / `borrow(amount, termId)` overloads that pay `msg.sender`.
253
+ > The `receiver`-variant the composer must use (Moolah requires
254
+ > `receiver == broker` when a broker is registered) **always sends ERC20 WBNB,
255
+ > never native**. To deliver native BNB, chain an unwrap step after the borrow.
230
256
 
231
257
  #### Repay
232
258
 
233
259
  `encodeListaBrokerRepay(loanToken, assets, native, broker, posId, onBehalf)` →
234
- `repay(amount, posId, onBehalf)` (fixed) or `repay(amount, onBehalf)` (dynamic, sentinel `posId`).
260
+ `repay(amount, posId, onBehalf)` (fixed) or `repay(amount, onBehalf)` (dynamic,
261
+ sentinel `posId`).
235
262
 
236
263
  - **Interest-first, then principal.**
237
- - **`onBehalf`** is taken straight from calldata (exactly like Morpho's repay `onBehalfOf`) — repaying
238
- only ever pays debt down and refunds excess to the composer, so **repay-on-behalf is permissionless**.
239
- Must be non-zero (broker reverts `ZeroAddress`); there is no caller fallback.
240
- - **`assets == 0`** repays the composer's full balance (`balanceOf` / `selfbalance`); the broker refunds
241
- any excess. The clean "close it out" pattern: over-fund slightly, repay with `assets == 0`, take the dust.
242
- - **Native:** both repay overloads are `payable` and wrap `msg.value` internally (gated on
243
- `LOAN_TOKEN == WBNB`, else `NativeNotSupported`). The composer's native path forwards the value and
244
- skips the ERC20 approve. Excess refunds as native BNB to the composer (un-swept on explicit amounts).
264
+ - **`onBehalf`** is taken straight from calldata (exactly like Morpho's repay
265
+ `onBehalfOf`) — repaying only ever pays debt down and refunds excess to the
266
+ composer, so **repay-on-behalf is permissionless**. Must be non-zero (broker
267
+ reverts `ZeroAddress`); there is no caller fallback.
268
+ - **`assets == 0`** repays the composer's full balance (`balanceOf` /
269
+ `selfbalance`); the broker refunds any excess. The clean "close it out"
270
+ pattern: over-fund slightly, repay with `assets == 0`, take the dust.
271
+ - **Native:** both repay overloads are `payable` and wrap `msg.value` internally
272
+ (gated on `LOAN_TOKEN == WBNB`, else `NativeNotSupported`). The composer's
273
+ native path forwards the value and skips the ERC20 approve. Excess refunds as
274
+ native BNB to the composer (un-swept on explicit amounts).
245
275
 
246
276
  #### Early-repayment penalty
247
277
 
@@ -252,83 +282,101 @@ Repaying a fixed position's **principal before `end`** incurs a penalty
252
282
  penalty = ceil( ceil(repayPrincipal × aprPerSecond / RATE_SCALE) × timeLeft / 2 )
253
283
  ```
254
284
 
255
- ≈ **half the interest that principal would still accrue over the remaining term**. It's skimmed off the
256
- principal payment and supplied to the Moolah vault as revenue, so it is **additive** — on top of
257
- `outstanding + accruedInterest`. Penalty is **0 once matured** (`block.timestamp > end`). The fetcher
258
- surfaces it per loan as `earlyRepayPenalty` (computed for the *full outstanding principal*; scale linearly
259
- for partials). Use the broker's `previewRepayFixedLoanPosition(user, amount, posId)` for the exact
285
+ ≈ **half the interest that principal would still accrue over the remaining
286
+ term**. It's skimmed off the principal payment and supplied to the Moolah vault
287
+ as revenue, so it is **additive** on top of `outstanding + accruedInterest`.
288
+ Penalty is **0 once matured** (`block.timestamp > end`). The fetcher surfaces it
289
+ per loan as `earlyRepayPenalty` (computed for the _full outstanding principal_;
290
+ scale linearly for partials). Use the broker's
291
+ `previewRepayFixedLoanPosition(user, amount, posId)` for the exact
260
292
  `(interest, penalty, principal)` split.
261
293
 
262
294
  #### Maturity & refinancing
263
295
 
264
- A fixed position's **interest freezes at `end`** (`getAccruedInterestForFixedPosition` caps at
265
- `position.end`); after maturity it accrues no further fixed interest and carries no penalty.
296
+ A fixed position's **interest freezes at `end`**
297
+ (`getAccruedInterestForFixedPosition` caps at `position.end`); after maturity it
298
+ accrues no further fixed interest and carries no penalty.
266
299
 
267
- `refinanceMaturedFixedPositions(user, posIds)` is a **bot-only** function (`onlyRole(BOT)`) — users and
268
- the composer **cannot** call it, so it is **not** exposed as an SDK/composer op. It migrates matured fixed
269
- positions into the dynamic bucket: net principal + frozen interest are normalized at the current dynamic
270
- rate and added to the dynamic position, and the fixed position is **deleted**. The window between maturity
271
- and the bot's call is interest-free (the protocol absorbs it).
300
+ `refinanceMaturedFixedPositions(user, posIds)` is a **bot-only** function
301
+ (`onlyRole(BOT)`) — users and the composer **cannot** call it, so it is **not**
302
+ exposed as an SDK/composer op. It migrates matured fixed positions into the
303
+ dynamic bucket: net principal + frozen interest are normalized at the current
304
+ dynamic rate and added to the dynamic position, and the fixed position is
305
+ **deleted**. The window between maturity and the bot's call is interest-free
306
+ (the protocol absorbs it).
272
307
 
273
308
  #### Repaying late
274
309
 
275
- | When | Target | Cost |
276
- |------|--------|------|
277
- | Before `end` | fixed `posId` | principal + accrued interest + **early penalty** |
278
- | After `end`, before bot refinances | fixed `posId` | principal + **frozen** interest, **no penalty** — cheapest |
279
- | After bot refinances | **dynamic** sentinel | principal + interest captured at refinance + **dynamic-rate interest since** |
310
+ | When | Target | Cost |
311
+ | ---------------------------------- | -------------------- | ---------------------------------------------------------------------------- |
312
+ | Before `end` | fixed `posId` | principal + accrued interest + **early penalty** |
313
+ | After `end`, before bot refinances | fixed `posId` | principal + **frozen** interest, **no penalty** — cheapest |
314
+ | After bot refinances | **dynamic** sentinel | principal + interest captured at refinance + **dynamic-rate interest since** |
280
315
 
281
- > **Refinance is a race.** A repay targeting a matured `posId` **reverts** (`position not found`) if the
282
- > bot refinances first; fall back to the dynamic repay. The matured-but-not-yet-refinanced window
283
- > (`isMatured: true`, `earlyRepayPenalty: '0'`) is the cheapest time to repay.
316
+ > **Refinance is a race.** A repay targeting a matured `posId` **reverts**
317
+ > (`position not found`) if the bot refinances first; fall back to the dynamic
318
+ > repay. The matured-but-not-yet-refinanced window (`isMatured: true`,
319
+ > `earlyRepayPenalty: '0'`) is the cheapest time to repay.
284
320
 
285
321
  #### Data shape
286
322
 
287
- Brokered markets emit **no variable debt** — the position's `debt` is `0`, the authoritative total sits in
288
- `debtStable`, and each fixed loan is itemized in a `terms[]` array (`ListaTermLoan`):
323
+ Brokered markets emit **no variable debt** — the position's `debt` is `0`, the
324
+ authoritative total sits in `debtStable`, and each fixed loan is itemized in a
325
+ `terms[]` array (`ListaTermLoan`):
289
326
 
290
- | Field | Description |
291
- |-------|-------------|
292
- | `loanId` | `posId` — the repay target for the `LISTA_BROKER_REPAY` composer op |
293
- | `debt` | outstanding (principal + accrued interest), loan-token units |
294
- | `aprFraction` | locked fixed APR as a fraction |
295
- | `maturity` / `termDays` | unix maturity timestamp / term length |
296
- | `accruedInterest` | outstanding accrued interest |
297
- | `earlyRepayPenalty` | penalty to close the loan now; `0` once matured |
298
- | `isMatured` | `now >= end` |
327
+ | Field | Description |
328
+ | ----------------------- | ------------------------------------------------------------------- |
329
+ | `loanId` | `posId` — the repay target for the `LISTA_BROKER_REPAY` composer op |
330
+ | `debt` | outstanding (principal + accrued interest), loan-token units |
331
+ | `aprFraction` | locked fixed APR as a fraction |
332
+ | `maturity` / `termDays` | unix maturity timestamp / term length |
333
+ | `accruedInterest` | outstanding accrued interest |
334
+ | `earlyRepayPenalty` | penalty to close the loan now; `0` once matured |
335
+ | `isMatured` | `now >= end` |
299
336
 
300
- > **Note:** `terms[]` currently itemizes only fixed loans. The dynamic position (post-refinance) is
301
- > included in the `debtStable` total but not yet surfaced as its own repayable term.
337
+ > **Note:** `terms[]` currently itemizes only fixed loans. The dynamic position
338
+ > (post-refinance) is included in the `debtStable` total but not yet surfaced as
339
+ > its own repayable term.
302
340
 
303
341
  ## Morpho Midnight extension
304
342
 
305
- Morpho Midnight is a Base-only, **fixed-rate / fixed-maturity** lending protocol built on an
306
- **order book of off-chain signed offers** (zero-coupon credit/debt units) — it is **NOT a Morpho
307
- Blue fork**, so it is modeled as its own `'midnight'` provider (`Lender.MORPHO_MIDNIGHT`,
308
- `isMidnight`), not `isMorphoType`. There is no pool and no utilization curve: rates are derived
309
- from each offer's `tick` price and its time-to-maturity, knowable only via the API. Each
310
- maturity is its own isolated market, keyed `MORPHO_MIDNIGHT_<marketId>`.
343
+ Morpho Midnight is a Base-only, **fixed-rate / fixed-maturity** lending protocol
344
+ built on an **order book of off-chain signed offers** (zero-coupon credit/debt
345
+ units) — it is **NOT a Morpho Blue fork**, so it is modeled as its own
346
+ `'midnight'` provider (`Lender.MORPHO_MIDNIGHT`, `isMidnight`), not
347
+ `isMorphoType`. There is no pool and no utilization curve: rates are derived
348
+ from each offer's `tick` price and its time-to-maturity, knowable only via the
349
+ API. Each maturity is its own isolated market, keyed
350
+ `MORPHO_MIDNIGHT_<marketId>`.
311
351
 
312
352
  ### API-first fetching
313
353
 
314
- Unlike Morpho Blue's GraphQL-then-on-chain hybrid, Midnight public data is **API-only** — there
315
- is no on-chain public fallback in `fetchLenderAll`. The fetcher lives in
354
+ Unlike Morpho Blue's GraphQL-then-on-chain hybrid, Midnight public data is
355
+ **API-only** — there is no on-chain public fallback in `fetchLenderAll`. The
356
+ fetcher lives in
316
357
  [`lending/public-data/midnight/`](src/lending/public-data/midnight/):
317
358
 
318
- - **`apiClient` / `fetchPublic`** — an `ApiBookSource` reads the order book per market
319
- (`GET /books/{id}`) from the Midnight API. The base URL resolves override → config → hosted
320
- default (`https://api.morpho.org/v0/midnight`); the worker can point it at a dev API via
321
- `MIDNIGHT_API_BASE` (see `data-sdk` `setMidnightApiBase`).
322
- - **`math.ts`** — ported `TickLib` (`tickToApr` etc.), bit-verified against `@morpho-org/midnight-sdk`.
323
- - **`convertPublic`** — normalizes the book into a `MorphoGeneralPublicResponse` keyed by
324
- `MORPHO_MIDNIGHT_<id>`, emitting the fixed borrow/supply APR and a single-term
325
- `params.market.terms[]` (`{termId, durationSecs, durationDays, apr}`) carrying the maturity — so
326
- the maturity flows through the same generic `terms[]` path as Lista's fixed loans. Units are in
327
- the **loan token's decimals** (not 18): `assets = units × price / WAD`, so `units ≈ assets`.
328
-
329
- User positions (`{credit, debt, collateral[128], collateralBitmap}`) are read via multicall in
330
- [`lending/user-data/midnight/`](src/lending/user-data/midnight/), one `UserData` per market.
331
-
332
- > **Scope:** the fetcher and direct spot actions (supply collateral / borrow-via-`take` / repay /
333
- > withdraw) are wired. Composer-routed leverage and native ETH are deferred — see
359
+ - **`apiClient` / `fetchPublic`** — an `ApiBookSource` reads the order book per
360
+ market (`GET /books/{id}`) from the Midnight API. The base URL resolves
361
+ override → config → hosted default (`https://api.morpho.org/v0/midnight`); the
362
+ worker can point it at a dev API via `MIDNIGHT_API_BASE` (see `data-sdk`
363
+ `setMidnightApiBase`).
364
+ - **`math.ts`** — ported `TickLib` (`tickToApr` etc.), bit-verified against
365
+ `@morpho-org/midnight-sdk`.
366
+ - **`convertPublic`** normalizes the book into a `MorphoGeneralPublicResponse`
367
+ keyed by `MORPHO_MIDNIGHT_<id>`, emitting the fixed borrow/supply APR and a
368
+ single-term `params.market.terms[]`
369
+ (`{termId, durationSecs, durationDays, apr}`) carrying the maturity — so the
370
+ maturity flows through the same generic `terms[]` path as Lista's fixed loans.
371
+ Units are in the **loan token's decimals** (not 18):
372
+ `assets = units × price / WAD`, so `units ≈ assets`.
373
+
374
+ User positions (`{credit, debt, collateral[128], collateralBitmap}`) are read
375
+ via multicall in
376
+ [`lending/user-data/midnight/`](src/lending/user-data/midnight/), one `UserData`
377
+ per market.
378
+
379
+ > **Scope:** the fetcher and direct spot actions (supply collateral /
380
+ > borrow-via-`take` / repay / withdraw) are wired. Composer-routed leverage and
381
+ > native ETH are deferred — see
334
382
  > [`calldata-sdk/src/evm/generic/midnight/COMPOSER.md`](../calldata-sdk/src/evm/generic/midnight/COMPOSER.md).