@morpho-org/midnight-sdk 1.1.1 → 1.2.1

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 (53) hide show
  1. package/lib/cjs/api/helpers.js +2 -4
  2. package/lib/cjs/api/version.generated.d.ts +2 -0
  3. package/lib/cjs/api/version.generated.js +6 -0
  4. package/lib/cjs/fetch/Position.d.ts +1 -1
  5. package/lib/cjs/fetch/Position.js +5 -1
  6. package/lib/cjs/index.d.ts +1 -0
  7. package/lib/cjs/index.js +1 -0
  8. package/lib/cjs/market/index.d.ts +0 -2
  9. package/lib/cjs/market/index.js +0 -2
  10. package/lib/cjs/offers/Offer.d.ts +14 -10
  11. package/lib/cjs/offers/Offer.js +11 -7
  12. package/lib/cjs/offers/OfferChainUtils.d.ts +179 -0
  13. package/lib/cjs/offers/OfferChainUtils.js +446 -0
  14. package/lib/cjs/offers/index.d.ts +1 -0
  15. package/lib/cjs/offers/index.js +1 -0
  16. package/lib/cjs/{market → position}/Position.d.ts +83 -6
  17. package/lib/cjs/{market → position}/Position.js +83 -5
  18. package/lib/{esm/market → cjs/position}/PositionUtils.d.ts +26 -1
  19. package/lib/cjs/{market → position}/PositionUtils.js +27 -0
  20. package/lib/cjs/position/index.d.ts +2 -0
  21. package/lib/cjs/position/index.js +18 -0
  22. package/lib/cjs/signatures/RatifierUtils.js +4 -5
  23. package/lib/cjs/signatures/Tree.d.ts +3 -2
  24. package/lib/cjs/signatures/Tree.js +7 -4
  25. package/lib/cjs/signatures/TreeUtils.d.ts +4 -7
  26. package/lib/cjs/signatures/TreeUtils.js +22 -16
  27. package/lib/esm/api/helpers.js +1 -3
  28. package/lib/esm/api/version.generated.d.ts +2 -0
  29. package/lib/esm/api/version.generated.js +3 -0
  30. package/lib/esm/fetch/Position.d.ts +1 -1
  31. package/lib/esm/fetch/Position.js +5 -1
  32. package/lib/esm/index.d.ts +1 -0
  33. package/lib/esm/index.js +1 -0
  34. package/lib/esm/market/index.d.ts +0 -2
  35. package/lib/esm/market/index.js +0 -2
  36. package/lib/esm/offers/Offer.d.ts +14 -10
  37. package/lib/esm/offers/Offer.js +12 -8
  38. package/lib/esm/offers/OfferChainUtils.d.ts +179 -0
  39. package/lib/esm/offers/OfferChainUtils.js +443 -0
  40. package/lib/esm/offers/index.d.ts +1 -0
  41. package/lib/esm/offers/index.js +1 -0
  42. package/lib/esm/{market → position}/Position.d.ts +83 -6
  43. package/lib/esm/{market → position}/Position.js +83 -5
  44. package/lib/{cjs/market → esm/position}/PositionUtils.d.ts +26 -1
  45. package/lib/esm/{market → position}/PositionUtils.js +27 -0
  46. package/lib/esm/position/index.d.ts +2 -0
  47. package/lib/esm/position/index.js +2 -0
  48. package/lib/esm/signatures/RatifierUtils.js +5 -6
  49. package/lib/esm/signatures/Tree.d.ts +3 -2
  50. package/lib/esm/signatures/Tree.js +8 -5
  51. package/lib/esm/signatures/TreeUtils.d.ts +4 -7
  52. package/lib/esm/signatures/TreeUtils.js +22 -16
  53. package/package.json +4 -3
@@ -1,4 +1,4 @@
1
- import { zeroAddress } from "viem";
1
+ import { keccak256, zeroAddress } from "viem";
2
2
  import { Market, MarketParams, } from "../market/index.js";
3
3
  import { OfferUtils } from "./OfferUtils.js";
4
4
  /**
@@ -8,10 +8,11 @@ import { OfferUtils } from "./OfferUtils.js";
8
8
  * `Group.create` for shared-consumption ladders or directly to `Tree.create`
9
9
  * for standalone offers. API/take-side code can convert a plain `IOffer`
10
10
  * into this class before ABI encoding. The class resolves the onchain `group`
11
- * field lazily. Standalone offers derive their group id from the offer hash
12
- * with the zero group id; `Group.create` copies offers and overrides the group
13
- * on those copies when several offers share one consumption bucket. Offers
14
- * hydrated from API or decoded data may also carry a known group id.
11
+ * field lazily. Standalone offers derive their content-addressed singleton
12
+ * group id from the offer's zero-group hash; `Group.create` copies offers and
13
+ * overrides the group on those copies when several offers share one
14
+ * consumption bucket. Offers hydrated from API or decoded data may also carry
15
+ * a known group id.
15
16
  *
16
17
  * @example
17
18
  * ```ts
@@ -159,8 +160,11 @@ export class Offer {
159
160
  * Consumption group id encoded into this offer.
160
161
  *
161
162
  * When no group id was provided, the value is computed on first access by
162
- * hashing this offer with the protocol zero group id. The computed value is
163
- * cached because offer hashing is resource-intensive.
163
+ * hashing this offer with the protocol zero group id, then hashing that
164
+ * result as a singleton group. The computed value is cached because offer
165
+ * hashing is resource-intensive. This is equivalent to
166
+ * `GroupUtils.hash([offer])`, but is computed directly to keep the offers
167
+ * layer from importing signature helpers and creating a circular dependency.
164
168
  *
165
169
  * @returns Consumption group id.
166
170
  * @example
@@ -205,7 +209,7 @@ export class Offer {
205
209
  * ```
206
210
  */
207
211
  get group() {
208
- this.cachedGroup ??= OfferUtils.groupHash(this);
212
+ this.cachedGroup ??= keccak256(OfferUtils.groupHash(this));
209
213
  return this.cachedGroup;
210
214
  }
211
215
  /**
@@ -0,0 +1,179 @@
1
+ import { type BigIntish } from "@morpho-org/morpho-ts";
2
+ /** One time-bounded offer leg to convert into a Midnight `Offer.create` input. */
3
+ export interface FixedRateOfferChainLeg {
4
+ /** Spacing-aligned Midnight tick for this offer. */
5
+ readonly tick: bigint;
6
+ /** First timestamp at which the offer leg is active. */
7
+ readonly startTimestamp: bigint;
8
+ /** Last timestamp at which the offer leg is active. */
9
+ readonly expiryTimestamp: bigint;
10
+ }
11
+ /** Parameters shared by lend-only and borrow-only fixed-rate offer-chain builders. */
12
+ export interface BuildFixedRateOfferChainParams {
13
+ /** Target yearly fixed rate as a decimal number, for example `0.05` for 5%. */
14
+ readonly targetRate: number;
15
+ /** Tick spacing enforced by the market. */
16
+ readonly tickSpacing: BigIntish;
17
+ /** Market maturity timestamp in seconds. */
18
+ readonly maturityTimestamp: BigIntish;
19
+ /** First timestamp the chain should cover. */
20
+ readonly chainStartTimestamp: BigIntish;
21
+ /** Latest timestamp the chain may cover. */
22
+ readonly chainEndTimestamp: BigIntish;
23
+ }
24
+ /**
25
+ * Utilities for building time-bounded Midnight offer chains.
26
+ *
27
+ * A Midnight offer has one fixed price, so its displayed yearly rate changes
28
+ * as maturity approaches. The markets app uses these helpers when a maker wants
29
+ * to post, for example, a 5% order for several months: the app builds several
30
+ * adjacent offers sharing the same reserve, each with a different tick and time
31
+ * window, so the order reviews and renders as a stable 5% order across the
32
+ * selected window instead of drifting upward as time passes.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { OfferChainUtils } from "@morpho-org/midnight-sdk";
37
+ *
38
+ * const legs = OfferChainUtils.buildLendFixedRateOfferChain({
39
+ * targetRate: 0.05,
40
+ * tickSpacing: 4n,
41
+ * maturityTimestamp: 1_798_761_600n,
42
+ * chainStartTimestamp: 1_767_225_600n,
43
+ * chainEndTimestamp: 1_791_153_600n,
44
+ * });
45
+ * console.log(legs[0]?.tick);
46
+ * ```
47
+ */
48
+ export declare namespace OfferChainUtils {
49
+ /**
50
+ * Maximum fraction of the initial time-to-maturity covered by a fixed-rate offer chain.
51
+ *
52
+ * Use this cap before displaying an expiry picker for a fixed-rate maker
53
+ * order. The last part of the maturity window is intentionally excluded
54
+ * because tiny time changes cause large displayed-rate drift.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { OfferChainUtils } from "@morpho-org/midnight-sdk";
59
+ *
60
+ * console.log(OfferChainUtils.MAX_EXPIRY_TTM_FRACTION);
61
+ * ```
62
+ */
63
+ const MAX_EXPIRY_TTM_FRACTION = 0.75;
64
+ /**
65
+ * Builds borrow-side sell-offer legs that approximate one fixed maker rate over time.
66
+ *
67
+ * Use before a make-borrow or supply-collateral-and-make-borrow flow when a
68
+ * borrower wants one displayed fixed borrow rate across a longer window. Map
69
+ * the returned legs to `Offer.create({ buy: false, ... })`, then group and
70
+ * submit them through the maker flow.
71
+ *
72
+ * Borrow chains read their target rate at each leg's expiry edge. If a leg
73
+ * would need to extend past `chainEndTimestamp`, it is dropped rather than
74
+ * clamped so the expiry edge remains the recoverable target-rate edge.
75
+ *
76
+ * @param params - Fixed-rate offer-chain parameters without a side field.
77
+ * @param params.targetRate - Target yearly fixed rate expressed as a decimal number.
78
+ * @param params.tickSpacing - Tick spacing enforced by the market.
79
+ * @param params.maturityTimestamp - Market maturity timestamp in seconds.
80
+ * @param params.chainStartTimestamp - First timestamp the chain should cover.
81
+ * @param params.chainEndTimestamp - Latest timestamp the chain may cover.
82
+ * @returns Borrow-side offer legs that can be mapped to `Offer.create({ buy: false, ... })`.
83
+ * @throws {InvalidOfferParameterError} when an input is invalid or the end timestamp exceeds the supported horizon.
84
+ * @example
85
+ * ```ts
86
+ * import { Offer, OfferChainUtils } from "@morpho-org/midnight-sdk";
87
+ *
88
+ * const legs = OfferChainUtils.buildBorrowFixedRateOfferChain({
89
+ * targetRate: 0.08,
90
+ * tickSpacing: market.tickSpacing,
91
+ * maturityTimestamp: market.params.maturity,
92
+ * chainStartTimestamp: now,
93
+ * chainEndTimestamp: expiry,
94
+ * });
95
+ * const offers = legs.map((leg) =>
96
+ * Offer.create({
97
+ * ...baseOffer,
98
+ * buy: false,
99
+ * tick: leg.tick,
100
+ * start: leg.startTimestamp,
101
+ * expiry: leg.expiryTimestamp,
102
+ * }),
103
+ * );
104
+ * ```
105
+ */
106
+ function buildBorrowFixedRateOfferChain(params: BuildFixedRateOfferChainParams): readonly FixedRateOfferChainLeg[];
107
+ /**
108
+ * Builds lend-side buy-offer legs that approximate one fixed maker rate over time.
109
+ *
110
+ * Use before a make-lend flow when a lender wants one displayed fixed lend
111
+ * rate across a longer window. Map the returned legs to
112
+ * `Offer.create({ buy: true, ... })`, then group and submit them through the
113
+ * maker flow.
114
+ *
115
+ * Lend chains read their target rate at each leg's start edge. The first leg
116
+ * may start before `chainStartTimestamp`; clamping that edge would move the
117
+ * recoverable display rate below the maker's target.
118
+ *
119
+ * @param params - Fixed-rate offer-chain parameters without a side field.
120
+ * @param params.targetRate - Target yearly fixed rate expressed as a decimal number.
121
+ * @param params.tickSpacing - Tick spacing enforced by the market.
122
+ * @param params.maturityTimestamp - Market maturity timestamp in seconds.
123
+ * @param params.chainStartTimestamp - First timestamp the chain should cover.
124
+ * @param params.chainEndTimestamp - Latest timestamp the chain may cover.
125
+ * @returns Lend-side offer legs that can be mapped to `Offer.create({ buy: true, ... })`.
126
+ * @throws {InvalidOfferParameterError} when an input is invalid or the end timestamp exceeds the supported horizon.
127
+ * @example
128
+ * ```ts
129
+ * import { Offer, OfferChainUtils } from "@morpho-org/midnight-sdk";
130
+ *
131
+ * const legs = OfferChainUtils.buildLendFixedRateOfferChain({
132
+ * targetRate: 0.05,
133
+ * tickSpacing: market.tickSpacing,
134
+ * maturityTimestamp: market.params.maturity,
135
+ * chainStartTimestamp: now,
136
+ * chainEndTimestamp: expiry,
137
+ * });
138
+ * const offers = legs.map((leg) =>
139
+ * Offer.create({
140
+ * ...baseOffer,
141
+ * buy: true,
142
+ * tick: leg.tick,
143
+ * start: leg.startTimestamp,
144
+ * expiry: leg.expiryTimestamp,
145
+ * }),
146
+ * );
147
+ * ```
148
+ */
149
+ function buildLendFixedRateOfferChain(params: BuildFixedRateOfferChainParams): readonly FixedRateOfferChainLeg[];
150
+ /**
151
+ * Returns the latest supported end timestamp for a fixed-rate offer chain.
152
+ *
153
+ * Chains intentionally stop before the final part of the maturity window
154
+ * because rate sensitivity accelerates near maturity and would require too
155
+ * many short-lived offers for a stable maker quote.
156
+ * Call this when constraining a make-offer expiry selector, then pass the
157
+ * selected timestamp to the lend-only or borrow-only chain builder.
158
+ *
159
+ * @param params - Maturity and chain-start timestamps.
160
+ * @param params.maturityTimestamp - Market maturity timestamp in seconds.
161
+ * @param params.chainStartTimestamp - First timestamp the chain should cover.
162
+ * @returns Latest accepted chain end timestamp.
163
+ * @throws {InvalidOfferParameterError} when a timestamp is invalid.
164
+ * @example
165
+ * ```ts
166
+ * import { OfferChainUtils } from "@morpho-org/midnight-sdk";
167
+ *
168
+ * const maxExpiry = OfferChainUtils.getMaxFixedRateOfferChainEndTimestamp({
169
+ * maturityTimestamp: market.params.maturity,
170
+ * chainStartTimestamp: now,
171
+ * });
172
+ * console.log(maxExpiry);
173
+ * ```
174
+ */
175
+ function getMaxFixedRateOfferChainEndTimestamp(params: {
176
+ readonly maturityTimestamp: BigIntish;
177
+ readonly chainStartTimestamp: BigIntish;
178
+ }): bigint;
179
+ }
@@ -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,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";