@morpho-org/midnight-sdk 1.1.1 → 1.2.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/lib/cjs/fetch/Position.d.ts +1 -1
- package/lib/cjs/fetch/Position.js +5 -1
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/market/index.d.ts +0 -2
- package/lib/cjs/market/index.js +0 -2
- package/lib/cjs/offers/OfferChainUtils.d.ts +179 -0
- package/lib/cjs/offers/OfferChainUtils.js +446 -0
- package/lib/cjs/offers/index.d.ts +1 -0
- package/lib/cjs/offers/index.js +1 -0
- package/lib/cjs/{market → position}/Position.d.ts +83 -6
- package/lib/cjs/{market → position}/Position.js +83 -5
- package/lib/{esm/market → cjs/position}/PositionUtils.d.ts +26 -1
- package/lib/cjs/{market → position}/PositionUtils.js +27 -0
- package/lib/cjs/position/index.d.ts +2 -0
- package/lib/cjs/position/index.js +18 -0
- package/lib/cjs/signatures/RatifierUtils.js +2 -1
- package/lib/cjs/signatures/Tree.js +3 -1
- package/lib/cjs/signatures/TreeUtils.d.ts +2 -2
- package/lib/cjs/signatures/TreeUtils.js +17 -8
- package/lib/esm/fetch/Position.d.ts +1 -1
- package/lib/esm/fetch/Position.js +5 -1
- package/lib/esm/index.d.ts +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/market/index.d.ts +0 -2
- package/lib/esm/market/index.js +0 -2
- package/lib/esm/offers/OfferChainUtils.d.ts +179 -0
- package/lib/esm/offers/OfferChainUtils.js +443 -0
- package/lib/esm/offers/index.d.ts +1 -0
- package/lib/esm/offers/index.js +1 -0
- package/lib/esm/{market → position}/Position.d.ts +83 -6
- package/lib/esm/{market → position}/Position.js +83 -5
- package/lib/{cjs/market → esm/position}/PositionUtils.d.ts +26 -1
- package/lib/esm/{market → position}/PositionUtils.js +27 -0
- package/lib/esm/position/index.d.ts +2 -0
- package/lib/esm/position/index.js +2 -0
- package/lib/esm/signatures/RatifierUtils.js +2 -1
- package/lib/esm/signatures/Tree.js +3 -1
- package/lib/esm/signatures/TreeUtils.d.ts +2 -2
- package/lib/esm/signatures/TreeUtils.js +17 -8
- package/package.json +2 -2
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { MathLib, Time } from "@morpho-org/morpho-ts";
|
|
2
|
+
import { formatUnits, parseUnits } from "viem";
|
|
3
|
+
import { DEFAULT_TICK_SPACING, MAX_TICK } from "../constants.js";
|
|
4
|
+
import { InvalidOfferParameterError } from "../errors.js";
|
|
5
|
+
import { TickLib } from "../math/index.js";
|
|
6
|
+
const FAVORABLE_RATE_DRIFT = 0.1;
|
|
7
|
+
const MAX_EXPIRY_TTM_NUMERATOR = 75n;
|
|
8
|
+
const MAX_EXPIRY_TTM_DENOMINATOR = 100n;
|
|
9
|
+
const MAX_CHAIN_LEGS = Number(MAX_TICK) + 1;
|
|
10
|
+
const SECONDS_PER_YEAR = Number(Time.s.from.y(1n));
|
|
11
|
+
/**
|
|
12
|
+
* Utilities for building time-bounded Midnight offer chains.
|
|
13
|
+
*
|
|
14
|
+
* A Midnight offer has one fixed price, so its displayed yearly rate changes
|
|
15
|
+
* as maturity approaches. The markets app uses these helpers when a maker wants
|
|
16
|
+
* to post, for example, a 5% order for several months: the app builds several
|
|
17
|
+
* adjacent offers sharing the same reserve, each with a different tick and time
|
|
18
|
+
* window, so the order reviews and renders as a stable 5% order across the
|
|
19
|
+
* selected window instead of drifting upward as time passes.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
24
|
+
*
|
|
25
|
+
* const legs = OfferChainUtils.buildLendFixedRateOfferChain({
|
|
26
|
+
* targetRate: 0.05,
|
|
27
|
+
* tickSpacing: 4n,
|
|
28
|
+
* maturityTimestamp: 1_798_761_600n,
|
|
29
|
+
* chainStartTimestamp: 1_767_225_600n,
|
|
30
|
+
* chainEndTimestamp: 1_791_153_600n,
|
|
31
|
+
* });
|
|
32
|
+
* console.log(legs[0]?.tick);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export var OfferChainUtils;
|
|
36
|
+
(function (OfferChainUtils) {
|
|
37
|
+
/**
|
|
38
|
+
* Maximum fraction of the initial time-to-maturity covered by a fixed-rate offer chain.
|
|
39
|
+
*
|
|
40
|
+
* Use this cap before displaying an expiry picker for a fixed-rate maker
|
|
41
|
+
* order. The last part of the maturity window is intentionally excluded
|
|
42
|
+
* because tiny time changes cause large displayed-rate drift.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
47
|
+
*
|
|
48
|
+
* console.log(OfferChainUtils.MAX_EXPIRY_TTM_FRACTION);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
OfferChainUtils.MAX_EXPIRY_TTM_FRACTION = 0.75;
|
|
52
|
+
/**
|
|
53
|
+
* Builds borrow-side sell-offer legs that approximate one fixed maker rate over time.
|
|
54
|
+
*
|
|
55
|
+
* Use before a make-borrow or supply-collateral-and-make-borrow flow when a
|
|
56
|
+
* borrower wants one displayed fixed borrow rate across a longer window. Map
|
|
57
|
+
* the returned legs to `Offer.create({ buy: false, ... })`, then group and
|
|
58
|
+
* submit them through the maker flow.
|
|
59
|
+
*
|
|
60
|
+
* Borrow chains read their target rate at each leg's expiry edge. If a leg
|
|
61
|
+
* would need to extend past `chainEndTimestamp`, it is dropped rather than
|
|
62
|
+
* clamped so the expiry edge remains the recoverable target-rate edge.
|
|
63
|
+
*
|
|
64
|
+
* @param params - Fixed-rate offer-chain parameters without a side field.
|
|
65
|
+
* @param params.targetRate - Target yearly fixed rate expressed as a decimal number.
|
|
66
|
+
* @param params.tickSpacing - Tick spacing enforced by the market.
|
|
67
|
+
* @param params.maturityTimestamp - Market maturity timestamp in seconds.
|
|
68
|
+
* @param params.chainStartTimestamp - First timestamp the chain should cover.
|
|
69
|
+
* @param params.chainEndTimestamp - Latest timestamp the chain may cover.
|
|
70
|
+
* @returns Borrow-side offer legs that can be mapped to `Offer.create({ buy: false, ... })`.
|
|
71
|
+
* @throws {InvalidOfferParameterError} when an input is invalid or the end timestamp exceeds the supported horizon.
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { Offer, OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
75
|
+
*
|
|
76
|
+
* const legs = OfferChainUtils.buildBorrowFixedRateOfferChain({
|
|
77
|
+
* targetRate: 0.08,
|
|
78
|
+
* tickSpacing: market.tickSpacing,
|
|
79
|
+
* maturityTimestamp: market.params.maturity,
|
|
80
|
+
* chainStartTimestamp: now,
|
|
81
|
+
* chainEndTimestamp: expiry,
|
|
82
|
+
* });
|
|
83
|
+
* const offers = legs.map((leg) =>
|
|
84
|
+
* Offer.create({
|
|
85
|
+
* ...baseOffer,
|
|
86
|
+
* buy: false,
|
|
87
|
+
* tick: leg.tick,
|
|
88
|
+
* start: leg.startTimestamp,
|
|
89
|
+
* expiry: leg.expiryTimestamp,
|
|
90
|
+
* }),
|
|
91
|
+
* );
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
function buildBorrowFixedRateOfferChain(params) {
|
|
95
|
+
if (!Number.isFinite(params.targetRate)) {
|
|
96
|
+
throw new InvalidOfferParameterError({
|
|
97
|
+
parameter: "targetRate",
|
|
98
|
+
value: params.targetRate,
|
|
99
|
+
instruction: "Use a finite positive yearly rate.",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (params.targetRate <= 0) {
|
|
103
|
+
throw new InvalidOfferParameterError({
|
|
104
|
+
parameter: "targetRate",
|
|
105
|
+
value: params.targetRate,
|
|
106
|
+
instruction: "Use a positive yearly rate.",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const tickSpacing = normalizeSafeInteger("tickSpacing", params.tickSpacing);
|
|
110
|
+
if (tickSpacing <= 0n || DEFAULT_TICK_SPACING % tickSpacing !== 0n) {
|
|
111
|
+
throw new InvalidOfferParameterError({
|
|
112
|
+
parameter: "tickSpacing",
|
|
113
|
+
value: tickSpacing,
|
|
114
|
+
instruction: `Use a positive tick spacing that divides "${DEFAULT_TICK_SPACING}".`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const maturityTimestamp = normalizeTimestamp("maturityTimestamp", params.maturityTimestamp);
|
|
118
|
+
const chainStartTimestamp = normalizeTimestamp("chainStartTimestamp", params.chainStartTimestamp);
|
|
119
|
+
const chainEndTimestamp = normalizeTimestamp("chainEndTimestamp", params.chainEndTimestamp);
|
|
120
|
+
if (maturityTimestamp <= chainStartTimestamp) {
|
|
121
|
+
throw new InvalidOfferParameterError({
|
|
122
|
+
parameter: "maturityTimestamp",
|
|
123
|
+
value: maturityTimestamp,
|
|
124
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (chainEndTimestamp <= chainStartTimestamp) {
|
|
128
|
+
throw new InvalidOfferParameterError({
|
|
129
|
+
parameter: "chainEndTimestamp",
|
|
130
|
+
value: chainEndTimestamp,
|
|
131
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const maxChainEndTimestamp = getMaxFixedRateOfferChainEndTimestamp({
|
|
135
|
+
maturityTimestamp,
|
|
136
|
+
chainStartTimestamp,
|
|
137
|
+
});
|
|
138
|
+
if (chainEndTimestamp > maxChainEndTimestamp) {
|
|
139
|
+
throw new InvalidOfferParameterError({
|
|
140
|
+
parameter: "chainEndTimestamp",
|
|
141
|
+
value: chainEndTimestamp,
|
|
142
|
+
instruction: `Use a timestamp no greater than "${maxChainEndTimestamp}".`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const tauInitial = Number(maturityTimestamp - chainStartTimestamp) / SECONDS_PER_YEAR;
|
|
146
|
+
const tauStop = Number(maturityTimestamp - chainEndTimestamp) / SECONDS_PER_YEAR;
|
|
147
|
+
const lowerRate = params.targetRate * (1 - FAVORABLE_RATE_DRIFT);
|
|
148
|
+
/* v8 ignore next: positive finite targetRate validation keeps lowerRate positive for practical JS numbers. */
|
|
149
|
+
if (lowerRate <= 0)
|
|
150
|
+
return [];
|
|
151
|
+
const legs = [];
|
|
152
|
+
let tauTop = tauInitial;
|
|
153
|
+
let iteration = 0;
|
|
154
|
+
for (; iteration < MAX_CHAIN_LEGS; iteration++) {
|
|
155
|
+
const price = 1 / (1 + lowerRate * tauTop);
|
|
156
|
+
let priceInput;
|
|
157
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
158
|
+
priceInput = 0n;
|
|
159
|
+
}
|
|
160
|
+
else if (price >= 1) {
|
|
161
|
+
priceInput = MathLib.WAD;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
priceInput = parseUnits(price.toFixed(18), 18);
|
|
165
|
+
}
|
|
166
|
+
const rawTick = TickLib.priceToTick(priceInput, 1n) - 1n;
|
|
167
|
+
if (rawTick < 0n || rawTick > MAX_TICK)
|
|
168
|
+
break;
|
|
169
|
+
const tick = floorTickToSpacing(rawTick, tickSpacing);
|
|
170
|
+
if (legs.at(-1)?.tick === tick)
|
|
171
|
+
break;
|
|
172
|
+
const tauMax = rateTau(tick, lowerRate);
|
|
173
|
+
const tauMin = rateTau(tick, params.targetRate);
|
|
174
|
+
if (tauMin >= tauTop)
|
|
175
|
+
break;
|
|
176
|
+
if (tauMin < tauStop)
|
|
177
|
+
break;
|
|
178
|
+
const startTimestamp = maturityTimestamp -
|
|
179
|
+
BigInt(Math.round(Math.min(tauMax, tauTop) * SECONDS_PER_YEAR));
|
|
180
|
+
const expiryTimestamp = maturityTimestamp - BigInt(Math.round(tauMin * SECONDS_PER_YEAR));
|
|
181
|
+
/* v8 ignore next: tests cover positive legs; zero-width legs are skipped only on rounding ties. */
|
|
182
|
+
if (expiryTimestamp > startTimestamp) {
|
|
183
|
+
legs.push({ tick, startTimestamp, expiryTimestamp });
|
|
184
|
+
}
|
|
185
|
+
tauTop = tauMin;
|
|
186
|
+
}
|
|
187
|
+
/* v8 ignore next: the monotonic tick walk exits before exhausting the finite tick range. */
|
|
188
|
+
if (iteration >= MAX_CHAIN_LEGS) {
|
|
189
|
+
throw new InvalidOfferParameterError({
|
|
190
|
+
parameter: "tickSpacing",
|
|
191
|
+
value: tickSpacing,
|
|
192
|
+
instruction: `Borrow chain exceeded "${MAX_CHAIN_LEGS}" legs.`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return legs;
|
|
196
|
+
}
|
|
197
|
+
OfferChainUtils.buildBorrowFixedRateOfferChain = buildBorrowFixedRateOfferChain;
|
|
198
|
+
/**
|
|
199
|
+
* Builds lend-side buy-offer legs that approximate one fixed maker rate over time.
|
|
200
|
+
*
|
|
201
|
+
* Use before a make-lend flow when a lender wants one displayed fixed lend
|
|
202
|
+
* rate across a longer window. Map the returned legs to
|
|
203
|
+
* `Offer.create({ buy: true, ... })`, then group and submit them through the
|
|
204
|
+
* maker flow.
|
|
205
|
+
*
|
|
206
|
+
* Lend chains read their target rate at each leg's start edge. The first leg
|
|
207
|
+
* may start before `chainStartTimestamp`; clamping that edge would move the
|
|
208
|
+
* recoverable display rate below the maker's target.
|
|
209
|
+
*
|
|
210
|
+
* @param params - Fixed-rate offer-chain parameters without a side field.
|
|
211
|
+
* @param params.targetRate - Target yearly fixed rate expressed as a decimal number.
|
|
212
|
+
* @param params.tickSpacing - Tick spacing enforced by the market.
|
|
213
|
+
* @param params.maturityTimestamp - Market maturity timestamp in seconds.
|
|
214
|
+
* @param params.chainStartTimestamp - First timestamp the chain should cover.
|
|
215
|
+
* @param params.chainEndTimestamp - Latest timestamp the chain may cover.
|
|
216
|
+
* @returns Lend-side offer legs that can be mapped to `Offer.create({ buy: true, ... })`.
|
|
217
|
+
* @throws {InvalidOfferParameterError} when an input is invalid or the end timestamp exceeds the supported horizon.
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* import { Offer, OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
221
|
+
*
|
|
222
|
+
* const legs = OfferChainUtils.buildLendFixedRateOfferChain({
|
|
223
|
+
* targetRate: 0.05,
|
|
224
|
+
* tickSpacing: market.tickSpacing,
|
|
225
|
+
* maturityTimestamp: market.params.maturity,
|
|
226
|
+
* chainStartTimestamp: now,
|
|
227
|
+
* chainEndTimestamp: expiry,
|
|
228
|
+
* });
|
|
229
|
+
* const offers = legs.map((leg) =>
|
|
230
|
+
* Offer.create({
|
|
231
|
+
* ...baseOffer,
|
|
232
|
+
* buy: true,
|
|
233
|
+
* tick: leg.tick,
|
|
234
|
+
* start: leg.startTimestamp,
|
|
235
|
+
* expiry: leg.expiryTimestamp,
|
|
236
|
+
* }),
|
|
237
|
+
* );
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
function buildLendFixedRateOfferChain(params) {
|
|
241
|
+
if (!Number.isFinite(params.targetRate)) {
|
|
242
|
+
throw new InvalidOfferParameterError({
|
|
243
|
+
parameter: "targetRate",
|
|
244
|
+
value: params.targetRate,
|
|
245
|
+
instruction: "Use a finite positive yearly rate.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (params.targetRate <= 0) {
|
|
249
|
+
throw new InvalidOfferParameterError({
|
|
250
|
+
parameter: "targetRate",
|
|
251
|
+
value: params.targetRate,
|
|
252
|
+
instruction: "Use a positive yearly rate.",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const tickSpacing = normalizeSafeInteger("tickSpacing", params.tickSpacing);
|
|
256
|
+
if (tickSpacing <= 0n || DEFAULT_TICK_SPACING % tickSpacing !== 0n) {
|
|
257
|
+
throw new InvalidOfferParameterError({
|
|
258
|
+
parameter: "tickSpacing",
|
|
259
|
+
value: tickSpacing,
|
|
260
|
+
instruction: `Use a positive tick spacing that divides "${DEFAULT_TICK_SPACING}".`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const maturityTimestamp = normalizeTimestamp("maturityTimestamp", params.maturityTimestamp);
|
|
264
|
+
const chainStartTimestamp = normalizeTimestamp("chainStartTimestamp", params.chainStartTimestamp);
|
|
265
|
+
const chainEndTimestamp = normalizeTimestamp("chainEndTimestamp", params.chainEndTimestamp);
|
|
266
|
+
if (maturityTimestamp <= chainStartTimestamp) {
|
|
267
|
+
throw new InvalidOfferParameterError({
|
|
268
|
+
parameter: "maturityTimestamp",
|
|
269
|
+
value: maturityTimestamp,
|
|
270
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (chainEndTimestamp <= chainStartTimestamp) {
|
|
274
|
+
throw new InvalidOfferParameterError({
|
|
275
|
+
parameter: "chainEndTimestamp",
|
|
276
|
+
value: chainEndTimestamp,
|
|
277
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const maxChainEndTimestamp = getMaxFixedRateOfferChainEndTimestamp({
|
|
281
|
+
maturityTimestamp,
|
|
282
|
+
chainStartTimestamp,
|
|
283
|
+
});
|
|
284
|
+
if (chainEndTimestamp > maxChainEndTimestamp) {
|
|
285
|
+
throw new InvalidOfferParameterError({
|
|
286
|
+
parameter: "chainEndTimestamp",
|
|
287
|
+
value: chainEndTimestamp,
|
|
288
|
+
instruction: `Use a timestamp no greater than "${maxChainEndTimestamp}".`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const tauInitial = Number(maturityTimestamp - chainStartTimestamp) / SECONDS_PER_YEAR;
|
|
292
|
+
const tauStop = Number(maturityTimestamp - chainEndTimestamp) / SECONDS_PER_YEAR;
|
|
293
|
+
const upperRate = params.targetRate * (1 + FAVORABLE_RATE_DRIFT);
|
|
294
|
+
const maxAlignedTick = floorTickToSpacing(MAX_TICK, tickSpacing);
|
|
295
|
+
const highestDiscountTick = floorTickToSpacing(TickLib.priceToTick(MathLib.WAD, 1n) - 1n, tickSpacing);
|
|
296
|
+
const ceilingTick = highestDiscountTick < maxAlignedTick
|
|
297
|
+
? highestDiscountTick
|
|
298
|
+
: maxAlignedTick;
|
|
299
|
+
const tauCeiling = rateTau(ceilingTick, upperRate);
|
|
300
|
+
let tauBottom = Math.max(tauStop, tauCeiling);
|
|
301
|
+
if (tauBottom >= tauInitial)
|
|
302
|
+
return [];
|
|
303
|
+
const tauLegs = [];
|
|
304
|
+
let iteration = 0;
|
|
305
|
+
for (; iteration < MAX_CHAIN_LEGS; iteration++) {
|
|
306
|
+
const price = 1 / (1 + upperRate * tauBottom);
|
|
307
|
+
let priceInput;
|
|
308
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
309
|
+
priceInput = 0n;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
/* v8 ignore next: positive rates that survive the grid-floor check keep lend-chain prices below one. */
|
|
313
|
+
if (price >= 1) {
|
|
314
|
+
priceInput = MathLib.WAD;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
priceInput = parseUnits(price.toFixed(18), 18);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const rawTick = TickLib.priceToTick(priceInput, 1n);
|
|
321
|
+
/* v8 ignore next: priceToTick returns in-range ticks; maxAlignedTick only tightens non-dividing spacing. */
|
|
322
|
+
if (rawTick < 0n || rawTick > MAX_TICK || rawTick > maxAlignedTick)
|
|
323
|
+
break;
|
|
324
|
+
const alignedTick = ((rawTick + tickSpacing - 1n) / tickSpacing) * tickSpacing;
|
|
325
|
+
/* v8 ignore next: non-dividing spacing boundary clamp is defensive; normal alignment is covered. */
|
|
326
|
+
const tick = alignedTick < maxAlignedTick ? alignedTick : maxAlignedTick;
|
|
327
|
+
if (tauLegs.at(-1)?.tick === tick)
|
|
328
|
+
break;
|
|
329
|
+
const tauMax = rateTau(tick, params.targetRate);
|
|
330
|
+
const tauMin = rateTau(tick, upperRate);
|
|
331
|
+
if (tauMax <= tauBottom)
|
|
332
|
+
break;
|
|
333
|
+
tauLegs.push({ tick, tauMax, tauMin: Math.max(tauMin, tauBottom) });
|
|
334
|
+
if (tauMax >= tauInitial)
|
|
335
|
+
break;
|
|
336
|
+
tauBottom = tauMax;
|
|
337
|
+
}
|
|
338
|
+
/* v8 ignore next: the monotonic tick walk exits before exhausting the finite tick range. */
|
|
339
|
+
if (iteration >= MAX_CHAIN_LEGS) {
|
|
340
|
+
throw new InvalidOfferParameterError({
|
|
341
|
+
parameter: "tickSpacing",
|
|
342
|
+
value: tickSpacing,
|
|
343
|
+
instruction: `Lend chain exceeded "${MAX_CHAIN_LEGS}" legs.`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const leftmostLeg = tauLegs.at(-1);
|
|
347
|
+
if (leftmostLeg && leftmostLeg.tauMax < tauInitial)
|
|
348
|
+
return [];
|
|
349
|
+
return tauLegs
|
|
350
|
+
.reverse()
|
|
351
|
+
.map((leg) => ({
|
|
352
|
+
tick: leg.tick,
|
|
353
|
+
startTimestamp: maturityTimestamp - BigInt(Math.round(leg.tauMax * SECONDS_PER_YEAR)),
|
|
354
|
+
expiryTimestamp: maturityTimestamp - BigInt(Math.round(leg.tauMin * SECONDS_PER_YEAR)),
|
|
355
|
+
}))
|
|
356
|
+
.filter((leg) => leg.expiryTimestamp > leg.startTimestamp);
|
|
357
|
+
}
|
|
358
|
+
OfferChainUtils.buildLendFixedRateOfferChain = buildLendFixedRateOfferChain;
|
|
359
|
+
/**
|
|
360
|
+
* Returns the latest supported end timestamp for a fixed-rate offer chain.
|
|
361
|
+
*
|
|
362
|
+
* Chains intentionally stop before the final part of the maturity window
|
|
363
|
+
* because rate sensitivity accelerates near maturity and would require too
|
|
364
|
+
* many short-lived offers for a stable maker quote.
|
|
365
|
+
* Call this when constraining a make-offer expiry selector, then pass the
|
|
366
|
+
* selected timestamp to the lend-only or borrow-only chain builder.
|
|
367
|
+
*
|
|
368
|
+
* @param params - Maturity and chain-start timestamps.
|
|
369
|
+
* @param params.maturityTimestamp - Market maturity timestamp in seconds.
|
|
370
|
+
* @param params.chainStartTimestamp - First timestamp the chain should cover.
|
|
371
|
+
* @returns Latest accepted chain end timestamp.
|
|
372
|
+
* @throws {InvalidOfferParameterError} when a timestamp is invalid.
|
|
373
|
+
* @example
|
|
374
|
+
* ```ts
|
|
375
|
+
* import { OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
376
|
+
*
|
|
377
|
+
* const maxExpiry = OfferChainUtils.getMaxFixedRateOfferChainEndTimestamp({
|
|
378
|
+
* maturityTimestamp: market.params.maturity,
|
|
379
|
+
* chainStartTimestamp: now,
|
|
380
|
+
* });
|
|
381
|
+
* console.log(maxExpiry);
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
function getMaxFixedRateOfferChainEndTimestamp(params) {
|
|
385
|
+
const maturityTimestamp = normalizeTimestamp("maturityTimestamp", params.maturityTimestamp);
|
|
386
|
+
const chainStartTimestamp = normalizeTimestamp("chainStartTimestamp", params.chainStartTimestamp);
|
|
387
|
+
if (maturityTimestamp <= chainStartTimestamp) {
|
|
388
|
+
throw new InvalidOfferParameterError({
|
|
389
|
+
parameter: "maturityTimestamp",
|
|
390
|
+
value: maturityTimestamp,
|
|
391
|
+
instruction: "Use a maturity timestamp greater than chainStartTimestamp.",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return (chainStartTimestamp +
|
|
395
|
+
((maturityTimestamp - chainStartTimestamp) * MAX_EXPIRY_TTM_NUMERATOR) /
|
|
396
|
+
MAX_EXPIRY_TTM_DENOMINATOR);
|
|
397
|
+
}
|
|
398
|
+
OfferChainUtils.getMaxFixedRateOfferChainEndTimestamp = getMaxFixedRateOfferChainEndTimestamp;
|
|
399
|
+
})(OfferChainUtils || (OfferChainUtils = {}));
|
|
400
|
+
function normalizeSafeInteger(parameter, value) {
|
|
401
|
+
let normalized;
|
|
402
|
+
try {
|
|
403
|
+
normalized = BigInt(value);
|
|
404
|
+
}
|
|
405
|
+
catch (cause) {
|
|
406
|
+
throw new InvalidOfferParameterError({
|
|
407
|
+
parameter,
|
|
408
|
+
value,
|
|
409
|
+
instruction: "Use a safe integer value.",
|
|
410
|
+
cause,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const numberValue = Number(normalized);
|
|
414
|
+
if (!Number.isSafeInteger(numberValue)) {
|
|
415
|
+
throw new InvalidOfferParameterError({
|
|
416
|
+
parameter,
|
|
417
|
+
value,
|
|
418
|
+
instruction: "Use a JavaScript-safe integer value.",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return normalized;
|
|
422
|
+
}
|
|
423
|
+
function normalizeTimestamp(parameter, value) {
|
|
424
|
+
const normalized = normalizeSafeInteger(parameter, value);
|
|
425
|
+
if (normalized < 0n) {
|
|
426
|
+
throw new InvalidOfferParameterError({
|
|
427
|
+
parameter,
|
|
428
|
+
value,
|
|
429
|
+
instruction: "Use a non-negative timestamp.",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return normalized;
|
|
433
|
+
}
|
|
434
|
+
function rateTau(tick, rate) {
|
|
435
|
+
const price = Number(formatUnits(TickLib.tickToPrice(tick), 18));
|
|
436
|
+
/* v8 ignore next: builders pass positive rates and discount ticks; this guards direct internal misuse. */
|
|
437
|
+
if (price <= 0 || price >= 1 || rate <= 0)
|
|
438
|
+
return 0;
|
|
439
|
+
return (1 / price - 1) / rate;
|
|
440
|
+
}
|
|
441
|
+
function floorTickToSpacing(tick, spacing) {
|
|
442
|
+
return tick - (tick % spacing);
|
|
443
|
+
}
|
package/lib/esm/offers/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BigIntish } from "@morpho-org/morpho-ts";
|
|
2
|
-
import type { Address } from "viem";
|
|
3
|
-
import { type IMarket, Market } from "
|
|
2
|
+
import type { Address, Hash } from "viem";
|
|
3
|
+
import { type IMarket, Market } from "../market/Market.js";
|
|
4
4
|
/**
|
|
5
5
|
* Plain input accepted by {@link Position}.
|
|
6
6
|
*
|
|
@@ -9,6 +9,8 @@ import { type IMarket, Market } from "./Market.js";
|
|
|
9
9
|
* import type { IPosition } from "@morpho-org/midnight-sdk";
|
|
10
10
|
*
|
|
11
11
|
* const position: IPosition = {
|
|
12
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
13
|
+
* marketId: "0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
12
14
|
* credit: 0n,
|
|
13
15
|
* pendingFee: 0n,
|
|
14
16
|
* lastLossFactor: 0n,
|
|
@@ -20,6 +22,10 @@ import { type IMarket, Market } from "./Market.js";
|
|
|
20
22
|
* ```
|
|
21
23
|
*/
|
|
22
24
|
export interface IPosition {
|
|
25
|
+
/** User holding this position. */
|
|
26
|
+
readonly user: Address;
|
|
27
|
+
/** Id of the market on which this position is held. */
|
|
28
|
+
readonly marketId: Hash;
|
|
23
29
|
/** User credit. */
|
|
24
30
|
readonly credit: BigIntish;
|
|
25
31
|
/** Pending continuous fee. */
|
|
@@ -43,6 +49,8 @@ export interface IPosition {
|
|
|
43
49
|
* import { Position } from "@morpho-org/midnight-sdk";
|
|
44
50
|
*
|
|
45
51
|
* const position = new Position({
|
|
52
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
53
|
+
* marketId: "0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
46
54
|
* credit: 0n,
|
|
47
55
|
* pendingFee: 0n,
|
|
48
56
|
* lastLossFactor: 0n,
|
|
@@ -54,7 +62,11 @@ export interface IPosition {
|
|
|
54
62
|
* console.log(position.debt);
|
|
55
63
|
* ```
|
|
56
64
|
*/
|
|
57
|
-
export declare class Position {
|
|
65
|
+
export declare class Position implements IPosition {
|
|
66
|
+
/** User holding this position. */
|
|
67
|
+
readonly user: Address;
|
|
68
|
+
/** Id of the market on which this position is held. */
|
|
69
|
+
readonly marketId: Hash;
|
|
58
70
|
/** User credit. */
|
|
59
71
|
readonly credit: bigint;
|
|
60
72
|
/** Pending continuous fee. */
|
|
@@ -79,6 +91,8 @@ export declare class Position {
|
|
|
79
91
|
* import { Position } from "@morpho-org/midnight-sdk";
|
|
80
92
|
*
|
|
81
93
|
* const position = new Position({
|
|
94
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
95
|
+
* marketId: "0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
82
96
|
* credit: 1_000n,
|
|
83
97
|
* pendingFee: 100n,
|
|
84
98
|
* lastLossFactor: 0n,
|
|
@@ -92,6 +106,9 @@ export declare class Position {
|
|
|
92
106
|
*/
|
|
93
107
|
get faceValue(): bigint;
|
|
94
108
|
}
|
|
109
|
+
/** Plain input shape for a Midnight position paired with hydrated market state. */
|
|
110
|
+
export interface IAccrualPosition extends Omit<IPosition, "marketId"> {
|
|
111
|
+
}
|
|
95
112
|
/**
|
|
96
113
|
* Midnight position paired with its hydrated market.
|
|
97
114
|
*
|
|
@@ -116,6 +133,7 @@ export declare class Position {
|
|
|
116
133
|
*
|
|
117
134
|
* const position = new AccrualPosition(
|
|
118
135
|
* {
|
|
136
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
119
137
|
* credit: 1_000n,
|
|
120
138
|
* pendingFee: 100n,
|
|
121
139
|
* lastLossFactor: 0n,
|
|
@@ -154,10 +172,65 @@ export declare class Position {
|
|
|
154
172
|
* console.log(position.market.id);
|
|
155
173
|
* ```
|
|
156
174
|
*/
|
|
157
|
-
export declare class AccrualPosition extends Position {
|
|
175
|
+
export declare class AccrualPosition extends Position implements IAccrualPosition {
|
|
176
|
+
protected readonly _market: Market;
|
|
177
|
+
constructor(position: IAccrualPosition, market: IMarket | Market);
|
|
158
178
|
/** Hydrated market for this position. */
|
|
159
|
-
|
|
160
|
-
|
|
179
|
+
get market(): Market;
|
|
180
|
+
/**
|
|
181
|
+
* Returns the maximum credit units currently withdrawable from this position.
|
|
182
|
+
*
|
|
183
|
+
* The protocol capacity is limited by both the accrued position credit and
|
|
184
|
+
* the market's currently withdrawable liquidity. Default redemption flows may
|
|
185
|
+
* still choose the position face value.
|
|
186
|
+
*
|
|
187
|
+
* @returns Maximum credit units currently withdrawable.
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* import { AccrualPosition } from "@morpho-org/midnight-sdk";
|
|
191
|
+
*
|
|
192
|
+
* const position = new AccrualPosition(
|
|
193
|
+
* {
|
|
194
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
195
|
+
* credit: 1_000n,
|
|
196
|
+
* pendingFee: 100n,
|
|
197
|
+
* lastLossFactor: 0n,
|
|
198
|
+
* lastAccrual: 1_000n,
|
|
199
|
+
* debt: 0n,
|
|
200
|
+
* collateralBitmap: 1n,
|
|
201
|
+
* collateral: [50n],
|
|
202
|
+
* },
|
|
203
|
+
* {
|
|
204
|
+
* params: {
|
|
205
|
+
* chainId: 8453,
|
|
206
|
+
* midnight: "0x0000000000000000000000000000000000001000",
|
|
207
|
+
* loanToken: "0x0000000000000000000000000000000000006000",
|
|
208
|
+
* collateralParams: [
|
|
209
|
+
* {
|
|
210
|
+
* token: "0x0000000000000000000000000000000000007000",
|
|
211
|
+
* lltv: 770000000000000000n,
|
|
212
|
+
* liquidationCursor: 250000000000000000n,
|
|
213
|
+
* oracle: "0x0000000000000000000000000000000000008000",
|
|
214
|
+
* },
|
|
215
|
+
* ],
|
|
216
|
+
* maturity: 54_000n,
|
|
217
|
+
* rcfThreshold: 0n,
|
|
218
|
+
* enterGate: "0x0000000000000000000000000000000000000000",
|
|
219
|
+
* liquidatorGate: "0x0000000000000000000000000000000000000000",
|
|
220
|
+
* },
|
|
221
|
+
* totalUnits: 1_000n,
|
|
222
|
+
* lossFactor: 0n,
|
|
223
|
+
* withdrawable: 950n,
|
|
224
|
+
* continuousFeeCredit: 0n,
|
|
225
|
+
* settlementFeeCbps: [1, 2, 3, 4, 5, 6, 7],
|
|
226
|
+
* continuousFee: 10,
|
|
227
|
+
* tickSpacing: 4,
|
|
228
|
+
* },
|
|
229
|
+
* );
|
|
230
|
+
* console.log(position.withdrawable); // 950n
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
get withdrawable(): bigint;
|
|
161
234
|
/**
|
|
162
235
|
* Returns the position collateral balance at an index configured by the market.
|
|
163
236
|
*
|
|
@@ -184,6 +257,7 @@ export declare class AccrualPosition extends Position {
|
|
|
184
257
|
*
|
|
185
258
|
* const position = new AccrualPosition(
|
|
186
259
|
* {
|
|
260
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
187
261
|
* credit: 1_000n,
|
|
188
262
|
* pendingFee: 100n,
|
|
189
263
|
* lastLossFactor: 0n,
|
|
@@ -251,6 +325,7 @@ export declare class AccrualPosition extends Position {
|
|
|
251
325
|
* const collateralToken = "0x0000000000000000000000000000000000007000";
|
|
252
326
|
* const position = new AccrualPosition(
|
|
253
327
|
* {
|
|
328
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
254
329
|
* credit: 1_000n,
|
|
255
330
|
* pendingFee: 100n,
|
|
256
331
|
* lastLossFactor: 0n,
|
|
@@ -317,6 +392,7 @@ export declare class AccrualPosition extends Position {
|
|
|
317
392
|
*
|
|
318
393
|
* const position = new AccrualPosition(
|
|
319
394
|
* {
|
|
395
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
320
396
|
* credit: 1_000n,
|
|
321
397
|
* pendingFee: 100n,
|
|
322
398
|
* lastLossFactor: 0n,
|
|
@@ -386,6 +462,7 @@ export declare class AccrualPosition extends Position {
|
|
|
386
462
|
*
|
|
387
463
|
* const position = new AccrualPosition(
|
|
388
464
|
* {
|
|
465
|
+
* user: "0x0000000000000000000000000000000000009000",
|
|
389
466
|
* credit: 1_000n,
|
|
390
467
|
* pendingFee: 100n,
|
|
391
468
|
* lastLossFactor: 0n,
|