@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.
- package/lib/cjs/api/MidnightApi.js +67 -3
- package/lib/cjs/api/helpers.d.ts +11 -1
- package/lib/cjs/api/helpers.js +48 -1
- package/lib/cjs/errors.d.ts +15 -0
- package/lib/cjs/errors.js +21 -1
- package/lib/cjs/fetch/Market.js +37 -2
- package/lib/cjs/fetch/Position.d.ts +1 -1
- package/lib/cjs/fetch/Position.js +5 -1
- package/lib/cjs/fetch/utils.d.ts +1 -1
- package/lib/cjs/fetch/utils.js +9 -2
- 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/Offer.d.ts +1 -1
- package/lib/cjs/offers/OfferChainUtils.d.ts +179 -0
- package/lib/cjs/offers/OfferChainUtils.js +446 -0
- package/lib/cjs/offers/OfferUtils.d.ts +5 -5
- package/lib/cjs/offers/OfferUtils.js +26 -13
- 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/EcrecoverRatifierUtils.d.ts +118 -0
- package/lib/cjs/signatures/EcrecoverRatifierUtils.js +149 -1
- package/lib/cjs/signatures/Group.js +5 -3
- package/lib/cjs/signatures/GroupUtils.d.ts +48 -0
- package/lib/cjs/signatures/GroupUtils.js +51 -0
- package/lib/cjs/signatures/RatifierUtils.js +15 -3
- package/lib/cjs/signatures/SetterRatifierUtils.d.ts +31 -0
- package/lib/cjs/signatures/SetterRatifierUtils.js +38 -0
- package/lib/cjs/signatures/Tree.js +13 -3
- package/lib/cjs/signatures/TreeUtils.d.ts +6 -3
- package/lib/cjs/signatures/TreeUtils.js +35 -13
- package/lib/cjs/signatures/eip712.d.ts +22 -0
- package/lib/cjs/signatures/eip712.js +26 -0
- package/lib/cjs/signatures/index.d.ts +1 -0
- package/lib/cjs/signatures/index.js +1 -0
- package/lib/esm/api/MidnightApi.js +68 -4
- package/lib/esm/api/helpers.d.ts +11 -1
- package/lib/esm/api/helpers.js +48 -2
- package/lib/esm/errors.d.ts +15 -0
- package/lib/esm/errors.js +19 -0
- package/lib/esm/fetch/Market.js +38 -3
- package/lib/esm/fetch/Position.d.ts +1 -1
- package/lib/esm/fetch/Position.js +5 -1
- package/lib/esm/fetch/utils.d.ts +1 -1
- package/lib/esm/fetch/utils.js +9 -2
- 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/Offer.d.ts +1 -1
- package/lib/esm/offers/OfferChainUtils.d.ts +179 -0
- package/lib/esm/offers/OfferChainUtils.js +443 -0
- package/lib/esm/offers/OfferUtils.d.ts +5 -5
- package/lib/esm/offers/OfferUtils.js +28 -15
- 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/EcrecoverRatifierUtils.d.ts +118 -0
- package/lib/esm/signatures/EcrecoverRatifierUtils.js +151 -3
- package/lib/esm/signatures/Group.js +5 -3
- package/lib/esm/signatures/GroupUtils.d.ts +48 -0
- package/lib/esm/signatures/GroupUtils.js +51 -0
- package/lib/esm/signatures/RatifierUtils.js +16 -4
- package/lib/esm/signatures/SetterRatifierUtils.d.ts +31 -0
- package/lib/esm/signatures/SetterRatifierUtils.js +38 -0
- package/lib/esm/signatures/Tree.js +14 -4
- package/lib/esm/signatures/TreeUtils.d.ts +6 -3
- package/lib/esm/signatures/TreeUtils.js +35 -13
- package/lib/esm/signatures/eip712.d.ts +22 -0
- package/lib/esm/signatures/eip712.js +22 -0
- package/lib/esm/signatures/index.d.ts +1 -0
- package/lib/esm/signatures/index.js +1 -0
- package/package.json +8 -7
- package/src/index.ts +0 -19
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OfferChainUtils = void 0;
|
|
4
|
+
const morpho_ts_1 = require("@morpho-org/morpho-ts");
|
|
5
|
+
const viem_1 = require("viem");
|
|
6
|
+
const constants_js_1 = require("../constants.js");
|
|
7
|
+
const errors_js_1 = require("../errors.js");
|
|
8
|
+
const index_js_1 = require("../math/index.js");
|
|
9
|
+
const FAVORABLE_RATE_DRIFT = 0.1;
|
|
10
|
+
const MAX_EXPIRY_TTM_NUMERATOR = 75n;
|
|
11
|
+
const MAX_EXPIRY_TTM_DENOMINATOR = 100n;
|
|
12
|
+
const MAX_CHAIN_LEGS = Number(constants_js_1.MAX_TICK) + 1;
|
|
13
|
+
const SECONDS_PER_YEAR = Number(morpho_ts_1.Time.s.from.y(1n));
|
|
14
|
+
/**
|
|
15
|
+
* Utilities for building time-bounded Midnight offer chains.
|
|
16
|
+
*
|
|
17
|
+
* A Midnight offer has one fixed price, so its displayed yearly rate changes
|
|
18
|
+
* as maturity approaches. The markets app uses these helpers when a maker wants
|
|
19
|
+
* to post, for example, a 5% order for several months: the app builds several
|
|
20
|
+
* adjacent offers sharing the same reserve, each with a different tick and time
|
|
21
|
+
* window, so the order reviews and renders as a stable 5% order across the
|
|
22
|
+
* selected window instead of drifting upward as time passes.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
27
|
+
*
|
|
28
|
+
* const legs = OfferChainUtils.buildLendFixedRateOfferChain({
|
|
29
|
+
* targetRate: 0.05,
|
|
30
|
+
* tickSpacing: 4n,
|
|
31
|
+
* maturityTimestamp: 1_798_761_600n,
|
|
32
|
+
* chainStartTimestamp: 1_767_225_600n,
|
|
33
|
+
* chainEndTimestamp: 1_791_153_600n,
|
|
34
|
+
* });
|
|
35
|
+
* console.log(legs[0]?.tick);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
var OfferChainUtils;
|
|
39
|
+
(function (OfferChainUtils) {
|
|
40
|
+
/**
|
|
41
|
+
* Maximum fraction of the initial time-to-maturity covered by a fixed-rate offer chain.
|
|
42
|
+
*
|
|
43
|
+
* Use this cap before displaying an expiry picker for a fixed-rate maker
|
|
44
|
+
* order. The last part of the maturity window is intentionally excluded
|
|
45
|
+
* because tiny time changes cause large displayed-rate drift.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
50
|
+
*
|
|
51
|
+
* console.log(OfferChainUtils.MAX_EXPIRY_TTM_FRACTION);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
OfferChainUtils.MAX_EXPIRY_TTM_FRACTION = 0.75;
|
|
55
|
+
/**
|
|
56
|
+
* Builds borrow-side sell-offer legs that approximate one fixed maker rate over time.
|
|
57
|
+
*
|
|
58
|
+
* Use before a make-borrow or supply-collateral-and-make-borrow flow when a
|
|
59
|
+
* borrower wants one displayed fixed borrow rate across a longer window. Map
|
|
60
|
+
* the returned legs to `Offer.create({ buy: false, ... })`, then group and
|
|
61
|
+
* submit them through the maker flow.
|
|
62
|
+
*
|
|
63
|
+
* Borrow chains read their target rate at each leg's expiry edge. If a leg
|
|
64
|
+
* would need to extend past `chainEndTimestamp`, it is dropped rather than
|
|
65
|
+
* clamped so the expiry edge remains the recoverable target-rate edge.
|
|
66
|
+
*
|
|
67
|
+
* @param params - Fixed-rate offer-chain parameters without a side field.
|
|
68
|
+
* @param params.targetRate - Target yearly fixed rate expressed as a decimal number.
|
|
69
|
+
* @param params.tickSpacing - Tick spacing enforced by the market.
|
|
70
|
+
* @param params.maturityTimestamp - Market maturity timestamp in seconds.
|
|
71
|
+
* @param params.chainStartTimestamp - First timestamp the chain should cover.
|
|
72
|
+
* @param params.chainEndTimestamp - Latest timestamp the chain may cover.
|
|
73
|
+
* @returns Borrow-side offer legs that can be mapped to `Offer.create({ buy: false, ... })`.
|
|
74
|
+
* @throws {InvalidOfferParameterError} when an input is invalid or the end timestamp exceeds the supported horizon.
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { Offer, OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
78
|
+
*
|
|
79
|
+
* const legs = OfferChainUtils.buildBorrowFixedRateOfferChain({
|
|
80
|
+
* targetRate: 0.08,
|
|
81
|
+
* tickSpacing: market.tickSpacing,
|
|
82
|
+
* maturityTimestamp: market.params.maturity,
|
|
83
|
+
* chainStartTimestamp: now,
|
|
84
|
+
* chainEndTimestamp: expiry,
|
|
85
|
+
* });
|
|
86
|
+
* const offers = legs.map((leg) =>
|
|
87
|
+
* Offer.create({
|
|
88
|
+
* ...baseOffer,
|
|
89
|
+
* buy: false,
|
|
90
|
+
* tick: leg.tick,
|
|
91
|
+
* start: leg.startTimestamp,
|
|
92
|
+
* expiry: leg.expiryTimestamp,
|
|
93
|
+
* }),
|
|
94
|
+
* );
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
function buildBorrowFixedRateOfferChain(params) {
|
|
98
|
+
if (!Number.isFinite(params.targetRate)) {
|
|
99
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
100
|
+
parameter: "targetRate",
|
|
101
|
+
value: params.targetRate,
|
|
102
|
+
instruction: "Use a finite positive yearly rate.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (params.targetRate <= 0) {
|
|
106
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
107
|
+
parameter: "targetRate",
|
|
108
|
+
value: params.targetRate,
|
|
109
|
+
instruction: "Use a positive yearly rate.",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const tickSpacing = normalizeSafeInteger("tickSpacing", params.tickSpacing);
|
|
113
|
+
if (tickSpacing <= 0n || constants_js_1.DEFAULT_TICK_SPACING % tickSpacing !== 0n) {
|
|
114
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
115
|
+
parameter: "tickSpacing",
|
|
116
|
+
value: tickSpacing,
|
|
117
|
+
instruction: `Use a positive tick spacing that divides "${constants_js_1.DEFAULT_TICK_SPACING}".`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const maturityTimestamp = normalizeTimestamp("maturityTimestamp", params.maturityTimestamp);
|
|
121
|
+
const chainStartTimestamp = normalizeTimestamp("chainStartTimestamp", params.chainStartTimestamp);
|
|
122
|
+
const chainEndTimestamp = normalizeTimestamp("chainEndTimestamp", params.chainEndTimestamp);
|
|
123
|
+
if (maturityTimestamp <= chainStartTimestamp) {
|
|
124
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
125
|
+
parameter: "maturityTimestamp",
|
|
126
|
+
value: maturityTimestamp,
|
|
127
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (chainEndTimestamp <= chainStartTimestamp) {
|
|
131
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
132
|
+
parameter: "chainEndTimestamp",
|
|
133
|
+
value: chainEndTimestamp,
|
|
134
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const maxChainEndTimestamp = getMaxFixedRateOfferChainEndTimestamp({
|
|
138
|
+
maturityTimestamp,
|
|
139
|
+
chainStartTimestamp,
|
|
140
|
+
});
|
|
141
|
+
if (chainEndTimestamp > maxChainEndTimestamp) {
|
|
142
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
143
|
+
parameter: "chainEndTimestamp",
|
|
144
|
+
value: chainEndTimestamp,
|
|
145
|
+
instruction: `Use a timestamp no greater than "${maxChainEndTimestamp}".`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const tauInitial = Number(maturityTimestamp - chainStartTimestamp) / SECONDS_PER_YEAR;
|
|
149
|
+
const tauStop = Number(maturityTimestamp - chainEndTimestamp) / SECONDS_PER_YEAR;
|
|
150
|
+
const lowerRate = params.targetRate * (1 - FAVORABLE_RATE_DRIFT);
|
|
151
|
+
/* v8 ignore next: positive finite targetRate validation keeps lowerRate positive for practical JS numbers. */
|
|
152
|
+
if (lowerRate <= 0)
|
|
153
|
+
return [];
|
|
154
|
+
const legs = [];
|
|
155
|
+
let tauTop = tauInitial;
|
|
156
|
+
let iteration = 0;
|
|
157
|
+
for (; iteration < MAX_CHAIN_LEGS; iteration++) {
|
|
158
|
+
const price = 1 / (1 + lowerRate * tauTop);
|
|
159
|
+
let priceInput;
|
|
160
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
161
|
+
priceInput = 0n;
|
|
162
|
+
}
|
|
163
|
+
else if (price >= 1) {
|
|
164
|
+
priceInput = morpho_ts_1.MathLib.WAD;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
priceInput = (0, viem_1.parseUnits)(price.toFixed(18), 18);
|
|
168
|
+
}
|
|
169
|
+
const rawTick = index_js_1.TickLib.priceToTick(priceInput, 1n) - 1n;
|
|
170
|
+
if (rawTick < 0n || rawTick > constants_js_1.MAX_TICK)
|
|
171
|
+
break;
|
|
172
|
+
const tick = floorTickToSpacing(rawTick, tickSpacing);
|
|
173
|
+
if (legs.at(-1)?.tick === tick)
|
|
174
|
+
break;
|
|
175
|
+
const tauMax = rateTau(tick, lowerRate);
|
|
176
|
+
const tauMin = rateTau(tick, params.targetRate);
|
|
177
|
+
if (tauMin >= tauTop)
|
|
178
|
+
break;
|
|
179
|
+
if (tauMin < tauStop)
|
|
180
|
+
break;
|
|
181
|
+
const startTimestamp = maturityTimestamp -
|
|
182
|
+
BigInt(Math.round(Math.min(tauMax, tauTop) * SECONDS_PER_YEAR));
|
|
183
|
+
const expiryTimestamp = maturityTimestamp - BigInt(Math.round(tauMin * SECONDS_PER_YEAR));
|
|
184
|
+
/* v8 ignore next: tests cover positive legs; zero-width legs are skipped only on rounding ties. */
|
|
185
|
+
if (expiryTimestamp > startTimestamp) {
|
|
186
|
+
legs.push({ tick, startTimestamp, expiryTimestamp });
|
|
187
|
+
}
|
|
188
|
+
tauTop = tauMin;
|
|
189
|
+
}
|
|
190
|
+
/* v8 ignore next: the monotonic tick walk exits before exhausting the finite tick range. */
|
|
191
|
+
if (iteration >= MAX_CHAIN_LEGS) {
|
|
192
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
193
|
+
parameter: "tickSpacing",
|
|
194
|
+
value: tickSpacing,
|
|
195
|
+
instruction: `Borrow chain exceeded "${MAX_CHAIN_LEGS}" legs.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return legs;
|
|
199
|
+
}
|
|
200
|
+
OfferChainUtils.buildBorrowFixedRateOfferChain = buildBorrowFixedRateOfferChain;
|
|
201
|
+
/**
|
|
202
|
+
* Builds lend-side buy-offer legs that approximate one fixed maker rate over time.
|
|
203
|
+
*
|
|
204
|
+
* Use before a make-lend flow when a lender wants one displayed fixed lend
|
|
205
|
+
* rate across a longer window. Map the returned legs to
|
|
206
|
+
* `Offer.create({ buy: true, ... })`, then group and submit them through the
|
|
207
|
+
* maker flow.
|
|
208
|
+
*
|
|
209
|
+
* Lend chains read their target rate at each leg's start edge. The first leg
|
|
210
|
+
* may start before `chainStartTimestamp`; clamping that edge would move the
|
|
211
|
+
* recoverable display rate below the maker's target.
|
|
212
|
+
*
|
|
213
|
+
* @param params - Fixed-rate offer-chain parameters without a side field.
|
|
214
|
+
* @param params.targetRate - Target yearly fixed rate expressed as a decimal number.
|
|
215
|
+
* @param params.tickSpacing - Tick spacing enforced by the market.
|
|
216
|
+
* @param params.maturityTimestamp - Market maturity timestamp in seconds.
|
|
217
|
+
* @param params.chainStartTimestamp - First timestamp the chain should cover.
|
|
218
|
+
* @param params.chainEndTimestamp - Latest timestamp the chain may cover.
|
|
219
|
+
* @returns Lend-side offer legs that can be mapped to `Offer.create({ buy: true, ... })`.
|
|
220
|
+
* @throws {InvalidOfferParameterError} when an input is invalid or the end timestamp exceeds the supported horizon.
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* import { Offer, OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
224
|
+
*
|
|
225
|
+
* const legs = OfferChainUtils.buildLendFixedRateOfferChain({
|
|
226
|
+
* targetRate: 0.05,
|
|
227
|
+
* tickSpacing: market.tickSpacing,
|
|
228
|
+
* maturityTimestamp: market.params.maturity,
|
|
229
|
+
* chainStartTimestamp: now,
|
|
230
|
+
* chainEndTimestamp: expiry,
|
|
231
|
+
* });
|
|
232
|
+
* const offers = legs.map((leg) =>
|
|
233
|
+
* Offer.create({
|
|
234
|
+
* ...baseOffer,
|
|
235
|
+
* buy: true,
|
|
236
|
+
* tick: leg.tick,
|
|
237
|
+
* start: leg.startTimestamp,
|
|
238
|
+
* expiry: leg.expiryTimestamp,
|
|
239
|
+
* }),
|
|
240
|
+
* );
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
function buildLendFixedRateOfferChain(params) {
|
|
244
|
+
if (!Number.isFinite(params.targetRate)) {
|
|
245
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
246
|
+
parameter: "targetRate",
|
|
247
|
+
value: params.targetRate,
|
|
248
|
+
instruction: "Use a finite positive yearly rate.",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (params.targetRate <= 0) {
|
|
252
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
253
|
+
parameter: "targetRate",
|
|
254
|
+
value: params.targetRate,
|
|
255
|
+
instruction: "Use a positive yearly rate.",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const tickSpacing = normalizeSafeInteger("tickSpacing", params.tickSpacing);
|
|
259
|
+
if (tickSpacing <= 0n || constants_js_1.DEFAULT_TICK_SPACING % tickSpacing !== 0n) {
|
|
260
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
261
|
+
parameter: "tickSpacing",
|
|
262
|
+
value: tickSpacing,
|
|
263
|
+
instruction: `Use a positive tick spacing that divides "${constants_js_1.DEFAULT_TICK_SPACING}".`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const maturityTimestamp = normalizeTimestamp("maturityTimestamp", params.maturityTimestamp);
|
|
267
|
+
const chainStartTimestamp = normalizeTimestamp("chainStartTimestamp", params.chainStartTimestamp);
|
|
268
|
+
const chainEndTimestamp = normalizeTimestamp("chainEndTimestamp", params.chainEndTimestamp);
|
|
269
|
+
if (maturityTimestamp <= chainStartTimestamp) {
|
|
270
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
271
|
+
parameter: "maturityTimestamp",
|
|
272
|
+
value: maturityTimestamp,
|
|
273
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (chainEndTimestamp <= chainStartTimestamp) {
|
|
277
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
278
|
+
parameter: "chainEndTimestamp",
|
|
279
|
+
value: chainEndTimestamp,
|
|
280
|
+
instruction: "Use a timestamp greater than chainStartTimestamp.",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const maxChainEndTimestamp = getMaxFixedRateOfferChainEndTimestamp({
|
|
284
|
+
maturityTimestamp,
|
|
285
|
+
chainStartTimestamp,
|
|
286
|
+
});
|
|
287
|
+
if (chainEndTimestamp > maxChainEndTimestamp) {
|
|
288
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
289
|
+
parameter: "chainEndTimestamp",
|
|
290
|
+
value: chainEndTimestamp,
|
|
291
|
+
instruction: `Use a timestamp no greater than "${maxChainEndTimestamp}".`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const tauInitial = Number(maturityTimestamp - chainStartTimestamp) / SECONDS_PER_YEAR;
|
|
295
|
+
const tauStop = Number(maturityTimestamp - chainEndTimestamp) / SECONDS_PER_YEAR;
|
|
296
|
+
const upperRate = params.targetRate * (1 + FAVORABLE_RATE_DRIFT);
|
|
297
|
+
const maxAlignedTick = floorTickToSpacing(constants_js_1.MAX_TICK, tickSpacing);
|
|
298
|
+
const highestDiscountTick = floorTickToSpacing(index_js_1.TickLib.priceToTick(morpho_ts_1.MathLib.WAD, 1n) - 1n, tickSpacing);
|
|
299
|
+
const ceilingTick = highestDiscountTick < maxAlignedTick
|
|
300
|
+
? highestDiscountTick
|
|
301
|
+
: maxAlignedTick;
|
|
302
|
+
const tauCeiling = rateTau(ceilingTick, upperRate);
|
|
303
|
+
let tauBottom = Math.max(tauStop, tauCeiling);
|
|
304
|
+
if (tauBottom >= tauInitial)
|
|
305
|
+
return [];
|
|
306
|
+
const tauLegs = [];
|
|
307
|
+
let iteration = 0;
|
|
308
|
+
for (; iteration < MAX_CHAIN_LEGS; iteration++) {
|
|
309
|
+
const price = 1 / (1 + upperRate * tauBottom);
|
|
310
|
+
let priceInput;
|
|
311
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
312
|
+
priceInput = 0n;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
/* v8 ignore next: positive rates that survive the grid-floor check keep lend-chain prices below one. */
|
|
316
|
+
if (price >= 1) {
|
|
317
|
+
priceInput = morpho_ts_1.MathLib.WAD;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
priceInput = (0, viem_1.parseUnits)(price.toFixed(18), 18);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const rawTick = index_js_1.TickLib.priceToTick(priceInput, 1n);
|
|
324
|
+
/* v8 ignore next: priceToTick returns in-range ticks; maxAlignedTick only tightens non-dividing spacing. */
|
|
325
|
+
if (rawTick < 0n || rawTick > constants_js_1.MAX_TICK || rawTick > maxAlignedTick)
|
|
326
|
+
break;
|
|
327
|
+
const alignedTick = ((rawTick + tickSpacing - 1n) / tickSpacing) * tickSpacing;
|
|
328
|
+
/* v8 ignore next: non-dividing spacing boundary clamp is defensive; normal alignment is covered. */
|
|
329
|
+
const tick = alignedTick < maxAlignedTick ? alignedTick : maxAlignedTick;
|
|
330
|
+
if (tauLegs.at(-1)?.tick === tick)
|
|
331
|
+
break;
|
|
332
|
+
const tauMax = rateTau(tick, params.targetRate);
|
|
333
|
+
const tauMin = rateTau(tick, upperRate);
|
|
334
|
+
if (tauMax <= tauBottom)
|
|
335
|
+
break;
|
|
336
|
+
tauLegs.push({ tick, tauMax, tauMin: Math.max(tauMin, tauBottom) });
|
|
337
|
+
if (tauMax >= tauInitial)
|
|
338
|
+
break;
|
|
339
|
+
tauBottom = tauMax;
|
|
340
|
+
}
|
|
341
|
+
/* v8 ignore next: the monotonic tick walk exits before exhausting the finite tick range. */
|
|
342
|
+
if (iteration >= MAX_CHAIN_LEGS) {
|
|
343
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
344
|
+
parameter: "tickSpacing",
|
|
345
|
+
value: tickSpacing,
|
|
346
|
+
instruction: `Lend chain exceeded "${MAX_CHAIN_LEGS}" legs.`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const leftmostLeg = tauLegs.at(-1);
|
|
350
|
+
if (leftmostLeg && leftmostLeg.tauMax < tauInitial)
|
|
351
|
+
return [];
|
|
352
|
+
return tauLegs
|
|
353
|
+
.reverse()
|
|
354
|
+
.map((leg) => ({
|
|
355
|
+
tick: leg.tick,
|
|
356
|
+
startTimestamp: maturityTimestamp - BigInt(Math.round(leg.tauMax * SECONDS_PER_YEAR)),
|
|
357
|
+
expiryTimestamp: maturityTimestamp - BigInt(Math.round(leg.tauMin * SECONDS_PER_YEAR)),
|
|
358
|
+
}))
|
|
359
|
+
.filter((leg) => leg.expiryTimestamp > leg.startTimestamp);
|
|
360
|
+
}
|
|
361
|
+
OfferChainUtils.buildLendFixedRateOfferChain = buildLendFixedRateOfferChain;
|
|
362
|
+
/**
|
|
363
|
+
* Returns the latest supported end timestamp for a fixed-rate offer chain.
|
|
364
|
+
*
|
|
365
|
+
* Chains intentionally stop before the final part of the maturity window
|
|
366
|
+
* because rate sensitivity accelerates near maturity and would require too
|
|
367
|
+
* many short-lived offers for a stable maker quote.
|
|
368
|
+
* Call this when constraining a make-offer expiry selector, then pass the
|
|
369
|
+
* selected timestamp to the lend-only or borrow-only chain builder.
|
|
370
|
+
*
|
|
371
|
+
* @param params - Maturity and chain-start timestamps.
|
|
372
|
+
* @param params.maturityTimestamp - Market maturity timestamp in seconds.
|
|
373
|
+
* @param params.chainStartTimestamp - First timestamp the chain should cover.
|
|
374
|
+
* @returns Latest accepted chain end timestamp.
|
|
375
|
+
* @throws {InvalidOfferParameterError} when a timestamp is invalid.
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* import { OfferChainUtils } from "@morpho-org/midnight-sdk";
|
|
379
|
+
*
|
|
380
|
+
* const maxExpiry = OfferChainUtils.getMaxFixedRateOfferChainEndTimestamp({
|
|
381
|
+
* maturityTimestamp: market.params.maturity,
|
|
382
|
+
* chainStartTimestamp: now,
|
|
383
|
+
* });
|
|
384
|
+
* console.log(maxExpiry);
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
function getMaxFixedRateOfferChainEndTimestamp(params) {
|
|
388
|
+
const maturityTimestamp = normalizeTimestamp("maturityTimestamp", params.maturityTimestamp);
|
|
389
|
+
const chainStartTimestamp = normalizeTimestamp("chainStartTimestamp", params.chainStartTimestamp);
|
|
390
|
+
if (maturityTimestamp <= chainStartTimestamp) {
|
|
391
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
392
|
+
parameter: "maturityTimestamp",
|
|
393
|
+
value: maturityTimestamp,
|
|
394
|
+
instruction: "Use a maturity timestamp greater than chainStartTimestamp.",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return (chainStartTimestamp +
|
|
398
|
+
((maturityTimestamp - chainStartTimestamp) * MAX_EXPIRY_TTM_NUMERATOR) /
|
|
399
|
+
MAX_EXPIRY_TTM_DENOMINATOR);
|
|
400
|
+
}
|
|
401
|
+
OfferChainUtils.getMaxFixedRateOfferChainEndTimestamp = getMaxFixedRateOfferChainEndTimestamp;
|
|
402
|
+
})(OfferChainUtils || (exports.OfferChainUtils = OfferChainUtils = {}));
|
|
403
|
+
function normalizeSafeInteger(parameter, value) {
|
|
404
|
+
let normalized;
|
|
405
|
+
try {
|
|
406
|
+
normalized = BigInt(value);
|
|
407
|
+
}
|
|
408
|
+
catch (cause) {
|
|
409
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
410
|
+
parameter,
|
|
411
|
+
value,
|
|
412
|
+
instruction: "Use a safe integer value.",
|
|
413
|
+
cause,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const numberValue = Number(normalized);
|
|
417
|
+
if (!Number.isSafeInteger(numberValue)) {
|
|
418
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
419
|
+
parameter,
|
|
420
|
+
value,
|
|
421
|
+
instruction: "Use a JavaScript-safe integer value.",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return normalized;
|
|
425
|
+
}
|
|
426
|
+
function normalizeTimestamp(parameter, value) {
|
|
427
|
+
const normalized = normalizeSafeInteger(parameter, value);
|
|
428
|
+
if (normalized < 0n) {
|
|
429
|
+
throw new errors_js_1.InvalidOfferParameterError({
|
|
430
|
+
parameter,
|
|
431
|
+
value,
|
|
432
|
+
instruction: "Use a non-negative timestamp.",
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return normalized;
|
|
436
|
+
}
|
|
437
|
+
function rateTau(tick, rate) {
|
|
438
|
+
const price = Number((0, viem_1.formatUnits)(index_js_1.TickLib.tickToPrice(tick), 18));
|
|
439
|
+
/* v8 ignore next: builders pass positive rates and discount ticks; this guards direct internal misuse. */
|
|
440
|
+
if (price <= 0 || price >= 1 || rate <= 0)
|
|
441
|
+
return 0;
|
|
442
|
+
return (1 / price - 1) / rate;
|
|
443
|
+
}
|
|
444
|
+
function floorTickToSpacing(tick, spacing) {
|
|
445
|
+
return tick - (tick % spacing);
|
|
446
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type BigIntish } from "@morpho-org/morpho-ts";
|
|
2
|
-
import type
|
|
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
|
-
*
|
|
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
|
|
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;
|
|
@@ -8,7 +8,6 @@ const errors_js_1 = require("../errors.js");
|
|
|
8
8
|
const index_js_1 = require("../market/index.js");
|
|
9
9
|
const index_js_2 = require("../math/index.js");
|
|
10
10
|
const Offer_js_1 = require("./Offer.js");
|
|
11
|
-
const comparableHex = (value) => value.toLowerCase();
|
|
12
11
|
const offerHashParams = [
|
|
13
12
|
{ name: "typehash", type: "bytes32" },
|
|
14
13
|
{ name: "marketHash", type: "bytes32" },
|
|
@@ -417,8 +416,8 @@ var OfferUtils;
|
|
|
417
416
|
* Validates and normalizes the maximum continuous fee an offer accepts.
|
|
418
417
|
*
|
|
419
418
|
* Use this before `Offer.create` when a maker UI exposes fee tolerance.
|
|
420
|
-
* `Offer.create` calls it internally and defaults omitted values to
|
|
421
|
-
*
|
|
419
|
+
* `Offer.create` calls it internally and defaults omitted values to zero so
|
|
420
|
+
* fresh offers fail closed unless the maker explicitly accepts a fee cap.
|
|
422
421
|
*
|
|
423
422
|
* @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer.
|
|
424
423
|
* @returns Normalized continuous fee cap.
|
|
@@ -432,7 +431,7 @@ var OfferUtils;
|
|
|
432
431
|
* ```
|
|
433
432
|
*/
|
|
434
433
|
function validateContinuousFeeCap(params) {
|
|
435
|
-
const continuousFeeCap = BigInt(params.continuousFeeCap ??
|
|
434
|
+
const continuousFeeCap = BigInt(params.continuousFeeCap ?? 0n);
|
|
436
435
|
if (continuousFeeCap < 0n) {
|
|
437
436
|
throw new errors_js_1.InvalidOfferParameterError({
|
|
438
437
|
parameter: "continuousFeeCap",
|
|
@@ -491,7 +490,7 @@ var OfferUtils;
|
|
|
491
490
|
* @param params.tickSpacing - Optional market tick spacing; defaults to `DEFAULT_TICK_SPACING`.
|
|
492
491
|
* @param params.maxUnits - Optional unit cap; defaults to zero.
|
|
493
492
|
* @param params.maxAssets - Optional buyer or seller asset cap; defaults to zero.
|
|
494
|
-
* @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to
|
|
493
|
+
* @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to zero.
|
|
495
494
|
* @param params.start - Optional offer start timestamp; defaults to zero.
|
|
496
495
|
* @param params.expiry - Offer expiry timestamp.
|
|
497
496
|
* @param params.receiverIfMakerIsSeller - Optional receiver used when maker is seller.
|
|
@@ -582,13 +581,15 @@ var OfferUtils;
|
|
|
582
581
|
}
|
|
583
582
|
const offers = offerInputs.map((offer) => Offer_js_1.Offer.from(offer));
|
|
584
583
|
const first = offers[0];
|
|
585
|
-
const expectedMaker =
|
|
584
|
+
const expectedMaker = first.maker;
|
|
586
585
|
const expectedBuy = first.buy;
|
|
587
|
-
const expectedLoanToken =
|
|
586
|
+
const expectedLoanToken = index_js_1.MarketParams.from(first.market).loanToken;
|
|
587
|
+
const expectedChainId = index_js_1.MarketParams.from(first.market).chainId;
|
|
588
|
+
const expectedMidnight = index_js_1.MarketParams.from(first.market).midnight;
|
|
588
589
|
const expectedMaxUnits = first.maxUnits;
|
|
589
590
|
const expectedMaxAssets = first.maxAssets;
|
|
590
591
|
for (const offer of offers) {
|
|
591
|
-
if (
|
|
592
|
+
if (!(0, viem_1.isAddressEqual)(offer.maker, expectedMaker)) {
|
|
592
593
|
throw new errors_js_1.InvalidOfferGroupError("All offers in a group must use the same maker.");
|
|
593
594
|
}
|
|
594
595
|
if (offer.buy !== expectedBuy) {
|
|
@@ -601,17 +602,21 @@ var OfferUtils;
|
|
|
601
602
|
(offer.maxAssets === 0n) === (offer.maxUnits === 0n)) {
|
|
602
603
|
throw new errors_js_1.InvalidOfferGroupError("Every offer must set exactly one non-zero uint128 cap.");
|
|
603
604
|
}
|
|
604
|
-
if (
|
|
605
|
-
expectedLoanToken) {
|
|
605
|
+
if (!(0, viem_1.isAddressEqual)(index_js_1.MarketParams.from(offer.market).loanToken, expectedLoanToken)) {
|
|
606
606
|
throw new errors_js_1.InvalidOfferGroupError("All offers in a group must use the same loan token.");
|
|
607
607
|
}
|
|
608
|
+
if (index_js_1.MarketParams.from(offer.market).chainId !== expectedChainId) {
|
|
609
|
+
throw new errors_js_1.InvalidOfferGroupError("All offers in a group must use the same chain id.");
|
|
610
|
+
}
|
|
611
|
+
if (!(0, viem_1.isAddressEqual)(index_js_1.MarketParams.from(offer.market).midnight, expectedMidnight)) {
|
|
612
|
+
throw new errors_js_1.InvalidOfferGroupError("All offers in a group must use the same Midnight contract.");
|
|
613
|
+
}
|
|
608
614
|
if (offer.maxUnits !== expectedMaxUnits ||
|
|
609
615
|
offer.maxAssets !== expectedMaxAssets) {
|
|
610
616
|
throw new errors_js_1.InvalidOfferGroupError("All offers in a group must use the same cap mode and value.");
|
|
611
617
|
}
|
|
612
618
|
if (offer.buy &&
|
|
613
|
-
|
|
614
|
-
comparableHex(viem_1.zeroAddress)) {
|
|
619
|
+
!(0, viem_1.isAddressEqual)(offer.receiverIfMakerIsSeller, viem_1.zeroAddress)) {
|
|
615
620
|
throw new errors_js_1.InvalidOfferGroupError("Buy offers must use the zero address as receiverIfMakerIsSeller.");
|
|
616
621
|
}
|
|
617
622
|
}
|
|
@@ -837,10 +842,16 @@ var OfferUtils;
|
|
|
837
842
|
function getConsumableUnits(params) {
|
|
838
843
|
const offer = params.offer;
|
|
839
844
|
const consumed = BigInt(params.consumed);
|
|
845
|
+
const timestamp = BigInt(params.timestamp);
|
|
846
|
+
const start = BigInt(offer.start);
|
|
847
|
+
const expiry = BigInt(offer.expiry);
|
|
840
848
|
const maxUnits = BigInt(offer.maxUnits);
|
|
841
849
|
const maxAssets = BigInt(offer.maxAssets);
|
|
842
850
|
const continuousFeeCap = BigInt(offer.continuousFeeCap);
|
|
843
851
|
(0, morpho_ts_1.assertNonNegative)("consumed", consumed);
|
|
852
|
+
(0, morpho_ts_1.assertNonNegative)("timestamp", timestamp);
|
|
853
|
+
(0, morpho_ts_1.assertNonNegative)("offer.start", start);
|
|
854
|
+
(0, morpho_ts_1.assertNonNegative)("offer.expiry", expiry);
|
|
844
855
|
(0, morpho_ts_1.assertNonNegative)("offer.maxUnits", maxUnits);
|
|
845
856
|
(0, morpho_ts_1.assertNonNegative)("offer.maxAssets", maxAssets);
|
|
846
857
|
(0, morpho_ts_1.assertNonNegative)("offer.continuousFeeCap", continuousFeeCap);
|
|
@@ -853,12 +864,14 @@ var OfferUtils;
|
|
|
853
864
|
});
|
|
854
865
|
}
|
|
855
866
|
const market = index_js_1.Market.from(offer.market);
|
|
867
|
+
if (timestamp < start || timestamp > expiry)
|
|
868
|
+
return 0n;
|
|
856
869
|
if (continuousFeeCap < BigInt(market.continuousFee))
|
|
857
870
|
return 0n;
|
|
858
871
|
if (maxUnits > 0n)
|
|
859
872
|
return morpho_ts_1.MathLib.zeroFloorSub(maxUnits, consumed);
|
|
860
873
|
const remainingAssets = morpho_ts_1.MathLib.zeroFloorSub(maxAssets, consumed);
|
|
861
|
-
const settlementFee = market.getSettlementFee(market.timeToMaturity(
|
|
874
|
+
const settlementFee = market.getSettlementFee(market.timeToMaturity(timestamp));
|
|
862
875
|
if (!offer.buy) {
|
|
863
876
|
const { sellerPrice } = index_js_2.TakeAmountsLib.prices({
|
|
864
877
|
offer,
|
package/lib/cjs/offers/index.js
CHANGED
|
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./Offer.js"), exports);
|
|
18
|
+
__exportStar(require("./OfferChainUtils.js"), exports);
|
|
18
19
|
__exportStar(require("./OfferUtils.js"), exports);
|