origin-morpho-utils 0.1.0

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 ADDED
@@ -0,0 +1,264 @@
1
+ # origin-morpho-utils
2
+
3
+ [![CI](https://github.com/OriginProtocol/morpho-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/OriginProtocol/morpho-utils/actions/workflows/ci.yml)
4
+
5
+ Morpho Blue / MetaMorpho vault APY computation, deposit/withdrawal impact simulation, and max-amount binary search.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # From npm
11
+ pnpm add origin-morpho-utils
12
+
13
+ # From GitHub
14
+ pnpm add github:OriginProtocol/morpho-utils
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### RPC URL API (recommended)
20
+
21
+ No viem imports needed — just pass an RPC URL:
22
+
23
+ ```typescript
24
+ import {
25
+ fetchVaultApy,
26
+ computeDepositImpactRpc,
27
+ computeWithdrawalImpactRpc,
28
+ findMaxDepositRpc,
29
+ } from 'origin-morpho-utils'
30
+
31
+ const RPC_URL = 'https://eth.llamarpc.com'
32
+ const vault = '0x5b8b...'
33
+
34
+ // Current APY
35
+ const result = await fetchVaultApy(RPC_URL, 1, vault)
36
+ console.log(result.apy) // e.g. 0.0423 (4.23%)
37
+
38
+ // With per-market breakdown
39
+ const detailed = await fetchVaultApy(RPC_URL, 1, vault, { includeMarkets: true })
40
+ detailed.marketDetails.forEach(m => {
41
+ console.log(m.marketId, m.supplyApy, m.utilization, m.allocationPct)
42
+ })
43
+
44
+ // Deposit impact
45
+ const deposit = await computeDepositImpactRpc(RPC_URL, 1, vault, 1000000000000000000n)
46
+ console.log(deposit.impactBps) // e.g. -12 (APY drops 0.12%)
47
+
48
+ // With per-market details
49
+ const depositDetailed = await computeDepositImpactRpc(RPC_URL, 1, vault, 1000000000000000000n, { includeMarkets: true })
50
+ depositDetailed.markets.forEach(m => {
51
+ console.log(m.marketId, m.current.supplyApy, '→', m.simulated.supplyApy)
52
+ })
53
+
54
+ // Withdrawal impact
55
+ const withdrawal = await computeWithdrawalImpactRpc(RPC_URL, 1, vault, 1000000000000000000n)
56
+ console.log(withdrawal.impactBps)
57
+ console.log(withdrawal.isPartial) // true if insufficient liquidity
58
+
59
+ // Find max deposit within 50 bps APY impact
60
+ const maxDeposit = await findMaxDepositRpc(RPC_URL, 1, vault, 100000n * 10n**18n, 50)
61
+ console.log(maxDeposit.amount) // largest deposit within threshold
62
+ console.log(maxDeposit.isCapped) // true if limited by vault capacity
63
+ ```
64
+
65
+ ### viem PublicClient API
66
+
67
+ If you already have a viem client:
68
+
69
+ ```typescript
70
+ import { createPublicClient, http } from 'viem'
71
+ import { mainnet } from 'viem/chains'
72
+ import { computeDepositImpact, fetchVaultApyViem } from 'origin-morpho-utils'
73
+
74
+ const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) })
75
+ const result = await fetchVaultApyViem(client, vault, '0xBBBB...EFFCb')
76
+ ```
77
+
78
+ ### CommonJS (require)
79
+
80
+ For non-TypeScript projects using `require()`:
81
+
82
+ ```js
83
+ const { fetchVaultApy, computeDepositImpactRpc } = require('origin-morpho-utils')
84
+
85
+ const result = await fetchVaultApy('https://eth.llamarpc.com', 1, '0x5b8b...')
86
+ console.log(result.apy)
87
+ ```
88
+
89
+ ### Math-Only (Zero Dependencies)
90
+
91
+ For offline computation or validation without viem:
92
+
93
+ ```typescript
94
+ import { estimateMarketApy, weightedVaultApyDetailed, simulateDeposit } from 'origin-morpho-utils/math'
95
+
96
+ const { supplyApy, borrowApy } = estimateMarketApy(0, 1000, 800, 0, 3_170_979_198)
97
+ ```
98
+
99
+ ### CLI
100
+
101
+ ```bash
102
+ # Current APY
103
+ npx origin-morpho-utils apy \
104
+ --rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b...
105
+
106
+ # APY with per-market breakdown
107
+ npx origin-morpho-utils apy \
108
+ --rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b... --markets
109
+
110
+ # Deposit impact
111
+ npx origin-morpho-utils deposit-impact \
112
+ --rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b... \
113
+ --amount 1000000000000000000
114
+
115
+ # Find max deposit within 50 bps impact
116
+ npx origin-morpho-utils find-max-deposit \
117
+ --rpc-url https://eth.llamarpc.com --chain-id 1 --vault 0x5b8b... \
118
+ --amount 100000000000000000000 --max-impact 50
119
+
120
+ # Offline compute from JSON stdin
121
+ echo '{"markets": [...], "idleAssets": "0"}' | \
122
+ npx origin-morpho-utils compute --deposit 1000000
123
+ ```
124
+
125
+ CLI outputs JSON to stdout. BigInt fields are string-encoded. Use `--markets` on any command to include per-market details.
126
+
127
+ ## API Reference
128
+
129
+ ### Options
130
+
131
+ All RPC and viem functions accept an `options` object as the last parameter:
132
+
133
+ | Option | Type | Default | Description |
134
+ |--------|------|---------|-------------|
135
+ | `morphoAddress` | string | Auto-detected from chainId | Morpho Blue singleton address |
136
+ | `includeMarkets` | boolean | `false` | Include per-market breakdown in results |
137
+ | `precision` | bigint | 1 whole token | Search granularity for find-max functions |
138
+ | `constraint` | `(state) => boolean \| Promise<boolean>` | — | Custom constraint for find-max functions (see below) |
139
+
140
+ ### RPC URL Functions
141
+
142
+ **`fetchVaultApy(rpcUrl, chainId, vaultAddress, options?)`**
143
+ Returns `VaultApyResult | null` — `{ apy, markets, idleAssets, marketDetails }`.
144
+
145
+ **`fetchVaultMarkets(rpcUrl, chainId, vaultAddress, morphoAddress)`**
146
+ Returns `{ markets, idleAssets }` — raw market data for offline use.
147
+
148
+ **`computeDepositImpactRpc(rpcUrl, chainId, vaultAddress, depositAmount, options?)`**
149
+ Returns `{ currentApy, newApy, impact, impactBps, markets }`.
150
+
151
+ **`computeWithdrawalImpactRpc(rpcUrl, chainId, vaultAddress, withdrawAmount, options?)`**
152
+ Returns `{ currentApy, newApy, impact, impactBps, isPartial, withdrawableAmount, markets }`.
153
+
154
+ **`findMaxDepositRpc(rpcUrl, chainId, vaultAddress, maxAmount, maxImpactBps, options?)`**
155
+ Binary search for largest deposit within APY impact threshold.
156
+ Returns `{ amount, maxPossibleAmount, isMaxAmount, isCapped, impact }`.
157
+
158
+ **`findMaxWithdrawalRpc(rpcUrl, chainId, vaultAddress, maxAmount, maxImpactBps, options?)`**
159
+ Binary search for largest withdrawal within APY impact threshold.
160
+
161
+ ### viem PublicClient Functions
162
+
163
+ Same as RPC functions but take a viem `PublicClient` instead of `rpcUrl`/`chainId`:
164
+
165
+ - `fetchVaultApyViem(client, vaultAddress, morphoAddress, options?)`
166
+ - `computeDepositImpact(client, chainId, vaultAddress, amount, options?)`
167
+ - `computeWithdrawalImpact(client, chainId, vaultAddress, amount, options?)`
168
+ - `findMaxDeposit(client, chainId, vaultAddress, maxAmount, maxImpactBps, options?)`
169
+ - `findMaxWithdrawal(client, chainId, vaultAddress, maxAmount, maxImpactBps, options?)`
170
+
171
+ ### Pure Math (`origin-morpho-utils/math`)
172
+
173
+ **`estimateMarketApy(depositAmt, totalSupply, totalBorrows, fee, rateAtTarget)`**
174
+ Compute supply and borrow APY for a single Morpho Blue market.
175
+
176
+ **`weightedVaultApy(markets, depositSim?, withdrawalSim?)`**
177
+ Position-weighted vault APY. Returns a single number.
178
+
179
+ **`weightedVaultApyDetailed(markets, depositSim?, withdrawalSim?)`**
180
+ Same but returns `{ apy, markets: MarketApyDetail[] }` with per-market details.
181
+
182
+ **`simulateDeposit(markets, depositAmount)`** / **`simulateWithdrawal(markets, withdrawAmount, idleAssets)`**
183
+ Simulate how deposits/withdrawals flow through the queue.
184
+
185
+ **`findMaxDepositAmount(markets, maxAmount, maxImpactBps, options?)`** / **`findMaxWithdrawalAmount(markets, idleAssets, maxAmount, maxImpactBps, options?)`**
186
+ Pure math binary search (no RPC). Options: `{ precision?, includeMarkets? }`.
187
+
188
+ ### Addresses
189
+
190
+ **`MORPHO_BLUE_ADDRESSES`** / **`getMorphoBlueAddress(chainId)`**
191
+
192
+ ## Per-Market Details
193
+
194
+ When `includeMarkets: true`, impact results include a `markets` array of `MarketImpactDetail`:
195
+
196
+ ```typescript
197
+ {
198
+ marketId: string
199
+ name: string // "WETH/USDC" (collateral/loan symbols)
200
+ current: {
201
+ supplyApy: number // 0-8.0
202
+ borrowApy: number // 0-8.0
203
+ utilization: number // 0-1
204
+ allocationPct: number // 0-1
205
+ vaultSupplyAssets: bigint
206
+ }
207
+ simulated: { /* same shape */ }
208
+ }
209
+ ```
210
+
211
+ For `fetchVaultApy`, the `marketDetails` array contains `MarketApyDetail` (same fields as above, without the `current`/`simulated` nesting).
212
+
213
+ ## Custom Constraints
214
+
215
+ The `findMaxDeposit*` and `findMaxWithdrawal*` functions accept an optional `constraint` callback that adds custom criteria beyond APY impact. The binary search finds the largest amount satisfying **both** the APY impact threshold and the custom constraint.
216
+
217
+ ```typescript
218
+ const result = await findMaxDepositRpc(RPC_URL, 1, vault, 100000n * 10n**18n, 50, {
219
+ includeMarkets: true,
220
+ constraint: (state) => {
221
+ // Keep target market utilization under 80%
222
+ const market = state.markets.find(m => m.marketId === targetId)
223
+ return !market || market.simulated.utilization <= 0.8
224
+ },
225
+ })
226
+ ```
227
+
228
+ The callback receives `FindMaxConstraintState`:
229
+
230
+ ```typescript
231
+ {
232
+ amount: bigint // candidate amount being tested
233
+ currentApy: number // vault APY before the action
234
+ simulatedApy: number // vault APY after the action
235
+ impactBps: number // APY change in basis points
236
+ markets: MarketImpactDetail[] // per-market current vs simulated
237
+ isPartial?: boolean // withdrawal only: liquidity-constrained
238
+ withdrawableAmount?: bigint // withdrawal only: actual removable amount
239
+ }
240
+ ```
241
+
242
+ The constraint can be sync or async (return `boolean` or `Promise<boolean>`).
243
+
244
+ **Important:** The constraint must be **monotonic** — if amount X fails, all amounts > X must also fail. Non-monotonic constraints (e.g., "utilization between 70% and 85%") will produce incorrect results. For constraint-only mode (no APY threshold), set `maxImpactBps` to `10000` (100%).
245
+
246
+ ## Algorithm Documentation
247
+
248
+ See [docs/algorithm.md](docs/algorithm.md) for detailed math, pseudocode, and on-chain data requirements.
249
+
250
+ ## Development
251
+
252
+ ```bash
253
+ pnpm install
254
+ pnpm build # ESM + CJS + types
255
+ pnpm test # Unit tests
256
+ pnpm dev -- apy --rpc-url $RPC --chain-id 1 --vault 0x5b8b... # Run CLI from source
257
+ ```
258
+
259
+ ### Publishing
260
+
261
+ ```bash
262
+ pnpm build
263
+ npm publish
264
+ ```