@morpho-org/midnight-sdk 1.0.0 → 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.
Files changed (86) hide show
  1. package/lib/cjs/api/MidnightApi.js +67 -3
  2. package/lib/cjs/api/helpers.d.ts +11 -1
  3. package/lib/cjs/api/helpers.js +48 -1
  4. package/lib/cjs/errors.d.ts +15 -0
  5. package/lib/cjs/errors.js +21 -1
  6. package/lib/cjs/fetch/Market.js +37 -2
  7. package/lib/cjs/fetch/Position.d.ts +1 -1
  8. package/lib/cjs/fetch/Position.js +5 -1
  9. package/lib/cjs/fetch/utils.d.ts +1 -1
  10. package/lib/cjs/fetch/utils.js +9 -2
  11. package/lib/cjs/index.d.ts +1 -0
  12. package/lib/cjs/index.js +1 -0
  13. package/lib/cjs/market/index.d.ts +0 -2
  14. package/lib/cjs/market/index.js +0 -2
  15. package/lib/cjs/offers/Offer.d.ts +1 -1
  16. package/lib/cjs/offers/OfferChainUtils.d.ts +179 -0
  17. package/lib/cjs/offers/OfferChainUtils.js +446 -0
  18. package/lib/cjs/offers/OfferUtils.d.ts +5 -5
  19. package/lib/cjs/offers/OfferUtils.js +26 -13
  20. package/lib/cjs/offers/index.d.ts +1 -0
  21. package/lib/cjs/offers/index.js +1 -0
  22. package/lib/cjs/{market → position}/Position.d.ts +83 -6
  23. package/lib/cjs/{market → position}/Position.js +83 -5
  24. package/lib/{esm/market → cjs/position}/PositionUtils.d.ts +26 -1
  25. package/lib/cjs/{market → position}/PositionUtils.js +27 -0
  26. package/lib/cjs/position/index.d.ts +2 -0
  27. package/lib/cjs/position/index.js +18 -0
  28. package/lib/cjs/signatures/EcrecoverRatifierUtils.d.ts +118 -0
  29. package/lib/cjs/signatures/EcrecoverRatifierUtils.js +149 -1
  30. package/lib/cjs/signatures/Group.js +5 -3
  31. package/lib/cjs/signatures/GroupUtils.d.ts +48 -0
  32. package/lib/cjs/signatures/GroupUtils.js +51 -0
  33. package/lib/cjs/signatures/RatifierUtils.js +15 -3
  34. package/lib/cjs/signatures/SetterRatifierUtils.d.ts +31 -0
  35. package/lib/cjs/signatures/SetterRatifierUtils.js +38 -0
  36. package/lib/cjs/signatures/Tree.js +13 -3
  37. package/lib/cjs/signatures/TreeUtils.d.ts +6 -3
  38. package/lib/cjs/signatures/TreeUtils.js +35 -13
  39. package/lib/cjs/signatures/eip712.d.ts +22 -0
  40. package/lib/cjs/signatures/eip712.js +26 -0
  41. package/lib/cjs/signatures/index.d.ts +1 -0
  42. package/lib/cjs/signatures/index.js +1 -0
  43. package/lib/esm/api/MidnightApi.js +68 -4
  44. package/lib/esm/api/helpers.d.ts +11 -1
  45. package/lib/esm/api/helpers.js +48 -2
  46. package/lib/esm/errors.d.ts +15 -0
  47. package/lib/esm/errors.js +19 -0
  48. package/lib/esm/fetch/Market.js +38 -3
  49. package/lib/esm/fetch/Position.d.ts +1 -1
  50. package/lib/esm/fetch/Position.js +5 -1
  51. package/lib/esm/fetch/utils.d.ts +1 -1
  52. package/lib/esm/fetch/utils.js +9 -2
  53. package/lib/esm/index.d.ts +1 -0
  54. package/lib/esm/index.js +1 -0
  55. package/lib/esm/market/index.d.ts +0 -2
  56. package/lib/esm/market/index.js +0 -2
  57. package/lib/esm/offers/Offer.d.ts +1 -1
  58. package/lib/esm/offers/OfferChainUtils.d.ts +179 -0
  59. package/lib/esm/offers/OfferChainUtils.js +443 -0
  60. package/lib/esm/offers/OfferUtils.d.ts +5 -5
  61. package/lib/esm/offers/OfferUtils.js +28 -15
  62. package/lib/esm/offers/index.d.ts +1 -0
  63. package/lib/esm/offers/index.js +1 -0
  64. package/lib/esm/{market → position}/Position.d.ts +83 -6
  65. package/lib/esm/{market → position}/Position.js +83 -5
  66. package/lib/{cjs/market → esm/position}/PositionUtils.d.ts +26 -1
  67. package/lib/esm/{market → position}/PositionUtils.js +27 -0
  68. package/lib/esm/position/index.d.ts +2 -0
  69. package/lib/esm/position/index.js +2 -0
  70. package/lib/esm/signatures/EcrecoverRatifierUtils.d.ts +118 -0
  71. package/lib/esm/signatures/EcrecoverRatifierUtils.js +151 -3
  72. package/lib/esm/signatures/Group.js +5 -3
  73. package/lib/esm/signatures/GroupUtils.d.ts +48 -0
  74. package/lib/esm/signatures/GroupUtils.js +51 -0
  75. package/lib/esm/signatures/RatifierUtils.js +16 -4
  76. package/lib/esm/signatures/SetterRatifierUtils.d.ts +31 -0
  77. package/lib/esm/signatures/SetterRatifierUtils.js +38 -0
  78. package/lib/esm/signatures/Tree.js +14 -4
  79. package/lib/esm/signatures/TreeUtils.d.ts +6 -3
  80. package/lib/esm/signatures/TreeUtils.js +35 -13
  81. package/lib/esm/signatures/eip712.d.ts +22 -0
  82. package/lib/esm/signatures/eip712.js +22 -0
  83. package/lib/esm/signatures/index.d.ts +1 -0
  84. package/lib/esm/signatures/index.js +1 -0
  85. package/package.json +8 -7
  86. package/src/index.ts +0 -19
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { type BigIntish } from "@morpho-org/morpho-ts";
2
- import type { Address, Hash } from "viem";
2
+ import { type Address, type Hash } from "viem";
3
3
  import { type BuildOfferParams, type IOffer, Offer, type OfferStruct } from "./Offer.js";
4
4
  /**
5
5
  * Object-compatible helpers for Midnight offer construction, grouping, and
@@ -280,8 +280,8 @@ export declare namespace OfferUtils {
280
280
  * Validates and normalizes the maximum continuous fee an offer accepts.
281
281
  *
282
282
  * Use this before `Offer.create` when a maker UI exposes fee tolerance.
283
- * `Offer.create` calls it internally and defaults omitted values to
284
- * {@link MAX_CONTINUOUS_FEE}.
283
+ * `Offer.create` calls it internally and defaults omitted values to zero so
284
+ * fresh offers fail closed unless the maker explicitly accepts a fee cap.
285
285
  *
286
286
  * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer.
287
287
  * @returns Normalized continuous fee cap.
@@ -339,7 +339,7 @@ export declare namespace OfferUtils {
339
339
  * @param params.tickSpacing - Optional market tick spacing; defaults to `DEFAULT_TICK_SPACING`.
340
340
  * @param params.maxUnits - Optional unit cap; defaults to zero.
341
341
  * @param params.maxAssets - Optional buyer or seller asset cap; defaults to zero.
342
- * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to `MAX_CONTINUOUS_FEE`.
342
+ * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to zero.
343
343
  * @param params.start - Optional offer start timestamp; defaults to zero.
344
344
  * @param params.expiry - Offer expiry timestamp.
345
345
  * @param params.receiverIfMakerIsSeller - Optional receiver used when maker is seller.
@@ -620,7 +620,7 @@ export declare namespace OfferUtils {
620
620
  * ```
621
621
  */
622
622
  function getConsumableUnits(params: {
623
- readonly offer: Pick<IOffer, "market" | "buy" | "tick" | "maxUnits" | "maxAssets" | "continuousFeeCap">;
623
+ readonly offer: Pick<IOffer, "market" | "buy" | "start" | "expiry" | "tick" | "maxUnits" | "maxAssets" | "continuousFeeCap">;
624
624
  readonly consumed: BigIntish;
625
625
  readonly timestamp: BigIntish;
626
626
  }): bigint;
@@ -1,11 +1,10 @@
1
1
  import { assertNonNegative, DivisionByZeroError, MathLib, } from "@morpho-org/morpho-ts";
2
- import { encodeAbiParameters, keccak256, zeroAddress, zeroHash } from "viem";
3
- import { DEFAULT_TICK_SPACING, MAX_CONTINUOUS_FEE, MAX_OFFER_CAP, MAX_TICK, OFFER_TYPEHASH, } from "../constants.js";
2
+ import { encodeAbiParameters, isAddressEqual, keccak256, zeroAddress, zeroHash, } from "viem";
3
+ import { DEFAULT_TICK_SPACING, MAX_OFFER_CAP, MAX_TICK, OFFER_TYPEHASH, } from "../constants.js";
4
4
  import { InvalidOfferGroupError, InvalidOfferParameterError, } from "../errors.js";
5
5
  import { Market, MarketParams, MarketUtils } from "../market/index.js";
6
6
  import { TakeAmountsLib, TickLib } from "../math/index.js";
7
7
  import { Offer, } from "./Offer.js";
8
- const comparableHex = (value) => value.toLowerCase();
9
8
  const offerHashParams = [
10
9
  { name: "typehash", type: "bytes32" },
11
10
  { name: "marketHash", type: "bytes32" },
@@ -414,8 +413,8 @@ export var OfferUtils;
414
413
  * Validates and normalizes the maximum continuous fee an offer accepts.
415
414
  *
416
415
  * Use this before `Offer.create` when a maker UI exposes fee tolerance.
417
- * `Offer.create` calls it internally and defaults omitted values to
418
- * {@link MAX_CONTINUOUS_FEE}.
416
+ * `Offer.create` calls it internally and defaults omitted values to zero so
417
+ * fresh offers fail closed unless the maker explicitly accepts a fee cap.
419
418
  *
420
419
  * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer.
421
420
  * @returns Normalized continuous fee cap.
@@ -429,7 +428,7 @@ export var OfferUtils;
429
428
  * ```
430
429
  */
431
430
  function validateContinuousFeeCap(params) {
432
- const continuousFeeCap = BigInt(params.continuousFeeCap ?? MAX_CONTINUOUS_FEE);
431
+ const continuousFeeCap = BigInt(params.continuousFeeCap ?? 0n);
433
432
  if (continuousFeeCap < 0n) {
434
433
  throw new InvalidOfferParameterError({
435
434
  parameter: "continuousFeeCap",
@@ -488,7 +487,7 @@ export var OfferUtils;
488
487
  * @param params.tickSpacing - Optional market tick spacing; defaults to `DEFAULT_TICK_SPACING`.
489
488
  * @param params.maxUnits - Optional unit cap; defaults to zero.
490
489
  * @param params.maxAssets - Optional buyer or seller asset cap; defaults to zero.
491
- * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to `MAX_CONTINUOUS_FEE`.
490
+ * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to zero.
492
491
  * @param params.start - Optional offer start timestamp; defaults to zero.
493
492
  * @param params.expiry - Offer expiry timestamp.
494
493
  * @param params.receiverIfMakerIsSeller - Optional receiver used when maker is seller.
@@ -579,13 +578,15 @@ export var OfferUtils;
579
578
  }
580
579
  const offers = offerInputs.map((offer) => Offer.from(offer));
581
580
  const first = offers[0];
582
- const expectedMaker = comparableHex(first.maker);
581
+ const expectedMaker = first.maker;
583
582
  const expectedBuy = first.buy;
584
- const expectedLoanToken = comparableHex(MarketParams.from(first.market).loanToken);
583
+ const expectedLoanToken = MarketParams.from(first.market).loanToken;
584
+ const expectedChainId = MarketParams.from(first.market).chainId;
585
+ const expectedMidnight = MarketParams.from(first.market).midnight;
585
586
  const expectedMaxUnits = first.maxUnits;
586
587
  const expectedMaxAssets = first.maxAssets;
587
588
  for (const offer of offers) {
588
- if (comparableHex(offer.maker) !== expectedMaker) {
589
+ if (!isAddressEqual(offer.maker, expectedMaker)) {
589
590
  throw new InvalidOfferGroupError("All offers in a group must use the same maker.");
590
591
  }
591
592
  if (offer.buy !== expectedBuy) {
@@ -598,17 +599,21 @@ export var OfferUtils;
598
599
  (offer.maxAssets === 0n) === (offer.maxUnits === 0n)) {
599
600
  throw new InvalidOfferGroupError("Every offer must set exactly one non-zero uint128 cap.");
600
601
  }
601
- if (comparableHex(MarketParams.from(offer.market).loanToken) !==
602
- expectedLoanToken) {
602
+ if (!isAddressEqual(MarketParams.from(offer.market).loanToken, expectedLoanToken)) {
603
603
  throw new InvalidOfferGroupError("All offers in a group must use the same loan token.");
604
604
  }
605
+ if (MarketParams.from(offer.market).chainId !== expectedChainId) {
606
+ throw new InvalidOfferGroupError("All offers in a group must use the same chain id.");
607
+ }
608
+ if (!isAddressEqual(MarketParams.from(offer.market).midnight, expectedMidnight)) {
609
+ throw new InvalidOfferGroupError("All offers in a group must use the same Midnight contract.");
610
+ }
605
611
  if (offer.maxUnits !== expectedMaxUnits ||
606
612
  offer.maxAssets !== expectedMaxAssets) {
607
613
  throw new InvalidOfferGroupError("All offers in a group must use the same cap mode and value.");
608
614
  }
609
615
  if (offer.buy &&
610
- comparableHex(offer.receiverIfMakerIsSeller) !==
611
- comparableHex(zeroAddress)) {
616
+ !isAddressEqual(offer.receiverIfMakerIsSeller, zeroAddress)) {
612
617
  throw new InvalidOfferGroupError("Buy offers must use the zero address as receiverIfMakerIsSeller.");
613
618
  }
614
619
  }
@@ -834,10 +839,16 @@ export var OfferUtils;
834
839
  function getConsumableUnits(params) {
835
840
  const offer = params.offer;
836
841
  const consumed = BigInt(params.consumed);
842
+ const timestamp = BigInt(params.timestamp);
843
+ const start = BigInt(offer.start);
844
+ const expiry = BigInt(offer.expiry);
837
845
  const maxUnits = BigInt(offer.maxUnits);
838
846
  const maxAssets = BigInt(offer.maxAssets);
839
847
  const continuousFeeCap = BigInt(offer.continuousFeeCap);
840
848
  assertNonNegative("consumed", consumed);
849
+ assertNonNegative("timestamp", timestamp);
850
+ assertNonNegative("offer.start", start);
851
+ assertNonNegative("offer.expiry", expiry);
841
852
  assertNonNegative("offer.maxUnits", maxUnits);
842
853
  assertNonNegative("offer.maxAssets", maxAssets);
843
854
  assertNonNegative("offer.continuousFeeCap", continuousFeeCap);
@@ -850,12 +861,14 @@ export var OfferUtils;
850
861
  });
851
862
  }
852
863
  const market = Market.from(offer.market);
864
+ if (timestamp < start || timestamp > expiry)
865
+ return 0n;
853
866
  if (continuousFeeCap < BigInt(market.continuousFee))
854
867
  return 0n;
855
868
  if (maxUnits > 0n)
856
869
  return MathLib.zeroFloorSub(maxUnits, consumed);
857
870
  const remainingAssets = MathLib.zeroFloorSub(maxAssets, consumed);
858
- const settlementFee = market.getSettlementFee(market.timeToMaturity(params.timestamp));
871
+ const settlementFee = market.getSettlementFee(market.timeToMaturity(timestamp));
859
872
  if (!offer.buy) {
860
873
  const { sellerPrice } = TakeAmountsLib.prices({
861
874
  offer,
@@ -1,2 +1,3 @@
1
1
  export * from "./Offer.js";
2
+ export * from "./OfferChainUtils.js";
2
3
  export * from "./OfferUtils.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./Offer.js";
2
+ export * from "./OfferChainUtils.js";
2
3
  export * from "./OfferUtils.js";