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 +264 -0
- package/dist/chunk-35AORYVF.cjs +709 -0
- package/dist/chunk-35AORYVF.cjs.map +1 -0
- package/dist/chunk-37KGZP4O.cjs +150 -0
- package/dist/chunk-37KGZP4O.cjs.map +1 -0
- package/dist/chunk-K2ZKPGSF.js +150 -0
- package/dist/chunk-K2ZKPGSF.js.map +1 -0
- package/dist/chunk-M43IOBUD.js +709 -0
- package/dist/chunk-M43IOBUD.js.map +1 -0
- package/dist/cli.cjs +207 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +207 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +59 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +314 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/math-ClwpjVti.d.cts +164 -0
- package/dist/math-ClwpjVti.d.ts +164 -0
- package/dist/math.cjs +17 -0
- package/dist/math.cjs.map +1 -0
- package/dist/math.d.cts +1 -0
- package/dist/math.d.ts +1 -0
- package/dist/math.js +17 -0
- package/dist/math.js.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# origin-morpho-utils
|
|
2
|
+
|
|
3
|
+
[](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
|
+
```
|