@morpho-org/midnight-sdk 1.3.1 → 1.4.0-next.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.
@@ -166,12 +166,15 @@ export declare class MidnightApi {
166
166
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
167
167
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
168
168
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
169
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
169
170
  * @param params.baseUrl - Optional Midnight API base URL override.
170
171
  * @param params.fetch - Optional fetch implementation override.
171
172
  * @param params.request - Optional fetch options forwarded to this request.
172
173
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
174
+ * @throws {NegativeValueError} when `settlementFee` is negative.
173
175
  * @throws {MidnightApiError} when the API returns a non-2xx response.
174
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
176
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
177
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
175
178
  * @example
176
179
  * ```ts
177
180
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -411,9 +414,12 @@ export declare class MidnightApi {
411
414
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
412
415
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
413
416
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
417
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
414
418
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
419
+ * @throws {NegativeValueError} when `settlementFee` is negative.
415
420
  * @throws {MidnightApiError} when the API returns a non-2xx response.
416
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
421
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
422
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
417
423
  * @example
418
424
  * ```ts
419
425
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -242,12 +242,15 @@ class MidnightApi {
242
242
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
243
243
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
244
244
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
245
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
245
246
  * @param params.baseUrl - Optional Midnight API base URL override.
246
247
  * @param params.fetch - Optional fetch implementation override.
247
248
  * @param params.request - Optional fetch options forwarded to this request.
248
249
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
250
+ * @throws {NegativeValueError} when `settlementFee` is negative.
249
251
  * @throws {MidnightApiError} when the API returns a non-2xx response.
250
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
252
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
253
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
251
254
  * @example
252
255
  * ```ts
253
256
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -263,6 +266,8 @@ class MidnightApi {
263
266
  */
264
267
  static async fetchBookQuote(params) {
265
268
  const input = params;
269
+ const settlementFee = BigInt(input.settlementFee ?? 0n);
270
+ (0, morpho_ts_1.assertNonNegative)("settlementFee", settlementFee);
266
271
  const response = await (0, helpers_js_1.requestMidnightApi)({
267
272
  ...input,
268
273
  method: "GET",
@@ -290,16 +295,20 @@ class MidnightApi {
290
295
  if (averageWorstPrice != null) {
291
296
  const guard = BigInt(averageWorstPrice);
292
297
  let filledUnits = 0n;
293
- let weightedPrice = 0n;
298
+ let filledAssets = 0n;
294
299
  if ("units" in input && input.units != null) {
295
300
  let remainingUnits = BigInt(input.units);
296
301
  for (const take of takeableOffers) {
297
302
  if (remainingUnits === 0n)
298
303
  break;
299
- const price = index_js_1.TickLib.tickToPrice(take.offer.tick);
304
+ const { buyerPrice, sellerPrice } = index_js_1.TakeAmountsLib.prices({
305
+ offer: take.offer,
306
+ settlementFee,
307
+ });
308
+ const price = input.side === "asks" ? buyerPrice : sellerPrice;
300
309
  const filled = take.units < remainingUnits ? take.units : remainingUnits;
301
310
  filledUnits += filled;
302
- weightedPrice += filled * price;
311
+ filledAssets += morpho_ts_1.MathLib.mulDiv(filled, price, morpho_ts_1.MathLib.WAD, input.side === "asks" ? "Up" : "Down");
303
312
  remainingUnits -= filled;
304
313
  }
305
314
  }
@@ -308,25 +317,30 @@ class MidnightApi {
308
317
  for (const take of takeableOffers) {
309
318
  if (remainingAssets === 0n)
310
319
  break;
311
- const price = index_js_1.TickLib.tickToPrice(take.offer.tick);
312
- const takeAssets = input.side === "asks"
313
- ? morpho_ts_1.MathLib.mulDivUp(take.units, price, morpho_ts_1.MathLib.WAD)
314
- : morpho_ts_1.MathLib.mulDivDown(take.units, price, morpho_ts_1.MathLib.WAD);
315
- if (takeAssets === 0n)
316
- continue;
317
- const fillsEntireTake = takeAssets <= remainingAssets;
320
+ const { buyerPrice, sellerPrice } = index_js_1.TakeAmountsLib.prices({
321
+ offer: take.offer,
322
+ settlementFee,
323
+ });
324
+ const makerPrice = input.side === "asks" ? sellerPrice : buyerPrice;
325
+ const settlementPrice = input.side === "asks" ? buyerPrice : sellerPrice;
326
+ const takeMakerAssets = input.side === "asks"
327
+ ? morpho_ts_1.MathLib.mulDivUp(take.units, makerPrice, morpho_ts_1.MathLib.WAD)
328
+ : morpho_ts_1.MathLib.mulDivDown(take.units, makerPrice, morpho_ts_1.MathLib.WAD);
329
+ const fillsEntireTake = takeMakerAssets <= remainingAssets;
318
330
  const filled = fillsEntireTake
319
331
  ? take.units
320
- : morpho_ts_1.MathLib.mulDiv(remainingAssets, morpho_ts_1.MathLib.WAD, price, input.side === "asks" ? "Down" : "Up");
332
+ : morpho_ts_1.MathLib.mulDiv(remainingAssets, morpho_ts_1.MathLib.WAD, makerPrice, input.side === "asks" ? "Down" : "Up");
321
333
  if (filled === 0n)
322
334
  continue;
323
335
  filledUnits += filled;
324
- weightedPrice += filled * price;
325
- remainingAssets = fillsEntireTake ? remainingAssets - takeAssets : 0n;
336
+ filledAssets += morpho_ts_1.MathLib.mulDiv(filled, settlementPrice, morpho_ts_1.MathLib.WAD, input.side === "asks" ? "Up" : "Down");
337
+ remainingAssets = fillsEntireTake
338
+ ? remainingAssets - takeMakerAssets
339
+ : 0n;
326
340
  }
327
341
  }
328
342
  if (filledUnits > 0n) {
329
- const averagePrice = morpho_ts_1.MathLib.mulDivDown(weightedPrice, 1n, filledUnits);
343
+ const averagePrice = morpho_ts_1.MathLib.mulDiv(filledAssets, morpho_ts_1.MathLib.WAD, filledUnits, input.side === "asks" ? "Up" : "Down");
330
344
  const violatesGuard = input.side === "asks" ? averagePrice > guard : averagePrice < guard;
331
345
  if (violatesGuard) {
332
346
  throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API quote takeable offers imply average price "${averagePrice}" outside average_worst_price guard "${guard}".`);
@@ -636,9 +650,12 @@ class MidnightApi {
636
650
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
637
651
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
638
652
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
653
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
639
654
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
655
+ * @throws {NegativeValueError} when `settlementFee` is negative.
640
656
  * @throws {MidnightApiError} when the API returns a non-2xx response.
641
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
657
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
658
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
642
659
  * @example
643
660
  * ```ts
644
661
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -177,6 +177,8 @@ export type FetchBookQuoteParams = MidnightApiConfig & {
177
177
  readonly marketId: Hash;
178
178
  /** Book side to quote. */
179
179
  readonly side: MidnightApiBookSide;
180
+ /** Current WAD-scaled settlement fee used for local guard validation. Defaults to zero. */
181
+ readonly settlementFee?: BigIntish;
180
182
  } & (MidnightApiQuoteAssetsTarget | MidnightApiQuoteUnitsTarget) & (MidnightApiQuoteAverageWorstPriceGuard | MidnightApiQuoteSlippageGuard | MidnightApiQuoteWithoutGuard);
181
183
  /**
182
184
  * Parameters for {@link MidnightApi.fetchTakeableOffers}.
@@ -1,2 +1,2 @@
1
1
  /** @internal Installed Midnight SDK package version. */
2
- export declare const MIDNIGHT_SDK_VERSION = "1.3.1";
2
+ export declare const MIDNIGHT_SDK_VERSION = "1.4.0-next.0";
@@ -3,4 +3,4 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.MIDNIGHT_SDK_VERSION = void 0;
5
5
  /** @internal Installed Midnight SDK package version. */
6
- exports.MIDNIGHT_SDK_VERSION = "1.3.1";
6
+ exports.MIDNIGHT_SDK_VERSION = "1.4.0-next.0";
@@ -817,18 +817,13 @@ var EcrecoverRatifierUtils;
817
817
  account: params.account,
818
818
  }));
819
819
  }
820
- const items = [];
821
- for (const offer of tree.offers) {
822
- items.push({
823
- offer,
824
- ratifierData: ratifierData({
825
- tree,
826
- leafIndex: items.length,
827
- signature,
828
- }),
829
- });
830
- }
831
- return items;
820
+ return tree.offers.map((offer, leafIndex) => ({
821
+ offer,
822
+ ratifierData: encodeRatifierData({
823
+ signature,
824
+ ...TreeUtils_js_1.TreeUtils.buildProof({ tree, leafIndex }),
825
+ }),
826
+ }));
832
827
  }
833
828
  EcrecoverRatifierUtils.ratify = ratify;
834
829
  })(EcrecoverRatifierUtils || (exports.EcrecoverRatifierUtils = EcrecoverRatifierUtils = {}));
@@ -76,8 +76,8 @@ export declare namespace RatifierUtils {
76
76
  * @param params.tree - Tree-like object or raw offer/group input.
77
77
  * @param params.label - Ratifier label used in validation errors.
78
78
  * @returns Normalized tree-like data and its shared ratifier.
79
- * @throws {InvalidTreeError} when the tree is empty or contains multiple ratifiers.
80
- * @throws {InvalidTreeHeightError} when normalized raw input exceeds the supported tree height.
79
+ * @throws {InvalidTreeError} when the tree fields disagree, the tree is empty, or it contains multiple ratifiers.
80
+ * @throws {InvalidTreeHeightError} when the tree height is unsupported.
81
81
  * @example
82
82
  * ```ts
83
83
  * import { Offer, RatifierUtils } from "@morpho-org/midnight-sdk";
@@ -5,6 +5,7 @@ const errors_js_1 = require("../errors.js");
5
5
  const index_js_1 = require("../offers/index.js");
6
6
  const Group_js_1 = require("./Group.js");
7
7
  const GroupUtils_js_1 = require("./GroupUtils.js");
8
+ const offerStructInternal_js_1 = require("./offerStructInternal.js");
8
9
  const TreeUtils_js_1 = require("./TreeUtils.js");
9
10
  function isTreeLike(tree) {
10
11
  return (!Array.isArray(tree) &&
@@ -16,6 +17,49 @@ function isTreeLike(tree) {
16
17
  }
17
18
  function normalizeTree(tree) {
18
19
  if (isTreeLike(tree)) {
20
+ if (!Number.isInteger(tree.height) || tree.height < 0 || tree.height > 20) {
21
+ throw new errors_js_1.InvalidTreeHeightError(tree.height);
22
+ }
23
+ const expectedLength = 2 ** tree.height;
24
+ if (tree.paddedOffers.length !== expectedLength ||
25
+ tree.leaves.length !== expectedLength ||
26
+ tree.offers.length > expectedLength) {
27
+ throw new errors_js_1.InvalidTreeError("Tree offers, leaves, and height describe different trees.");
28
+ }
29
+ const computedLeaves = tree.paddedOffers.map(index_js_1.OfferUtils.hashStruct);
30
+ const seen = new Set();
31
+ for (const [index, offer] of tree.offers.entries()) {
32
+ if ((0, offerStructInternal_js_1.isEmptyOfferStruct)(tree.paddedOffers[index], {
33
+ allowDefaultGroup: true,
34
+ })) {
35
+ throw new errors_js_1.InvalidTreeError("Visible offers must not contain tree padding.");
36
+ }
37
+ const hash = index_js_1.OfferUtils.hashStruct(index_js_1.OfferUtils.toStruct({ offer }));
38
+ if (hash.toLowerCase() !== computedLeaves[index].toLowerCase() ||
39
+ seen.has(hash.toLowerCase())) {
40
+ throw new errors_js_1.InvalidTreeError("Visible offers do not match the tree leaves.");
41
+ }
42
+ seen.add(hash.toLowerCase());
43
+ }
44
+ if (tree.paddedOffers
45
+ .slice(tree.offers.length)
46
+ .some((offer) => !(0, offerStructInternal_js_1.isEmptyOfferStruct)(offer, { allowDefaultGroup: true }))) {
47
+ throw new errors_js_1.InvalidTreeError("Tree padding contains offers hidden from the visible offer list.");
48
+ }
49
+ if (computedLeaves.some((leaf, index) => leaf.toLowerCase() !== tree.leaves[index].toLowerCase())) {
50
+ throw new errors_js_1.InvalidTreeError("Tree leaves do not match its padded offers.");
51
+ }
52
+ let level = computedLeaves;
53
+ while (level.length > 1) {
54
+ const next = [];
55
+ for (let index = 0; index < level.length; index += 2) {
56
+ next.push(TreeUtils_js_1.TreeUtils.hashNode(level[index], level[index + 1]));
57
+ }
58
+ level = next;
59
+ }
60
+ if (level[0]?.toLowerCase() !== tree.root.toLowerCase()) {
61
+ throw new errors_js_1.InvalidTreeError("Tree root does not match its leaves.");
62
+ }
19
63
  return tree;
20
64
  }
21
65
  const entries = Array.isArray(tree) ? tree : [tree];
@@ -77,8 +121,8 @@ var RatifierUtils;
77
121
  * @param params.tree - Tree-like object or raw offer/group input.
78
122
  * @param params.label - Ratifier label used in validation errors.
79
123
  * @returns Normalized tree-like data and its shared ratifier.
80
- * @throws {InvalidTreeError} when the tree is empty or contains multiple ratifiers.
81
- * @throws {InvalidTreeHeightError} when normalized raw input exceeds the supported tree height.
124
+ * @throws {InvalidTreeError} when the tree fields disagree, the tree is empty, or it contains multiple ratifiers.
125
+ * @throws {InvalidTreeHeightError} when the tree height is unsupported.
82
126
  * @example
83
127
  * ```ts
84
128
  * import { Offer, RatifierUtils } from "@morpho-org/midnight-sdk";
@@ -244,17 +244,10 @@ var SetterRatifierUtils;
244
244
  tree: params.tree,
245
245
  label: "Setter",
246
246
  });
247
- const items = [];
248
- for (const offer of tree.offers) {
249
- items.push({
250
- offer,
251
- ratifierData: ratifierData({
252
- tree,
253
- leafIndex: items.length,
254
- }),
255
- });
256
- }
257
- return items;
247
+ return tree.offers.map((offer, leafIndex) => ({
248
+ offer,
249
+ ratifierData: encodeRatifierData(TreeUtils_js_1.TreeUtils.buildProof({ tree, leafIndex })),
250
+ }));
258
251
  }
259
252
  SetterRatifierUtils.ratify = ratify;
260
253
  })(SetterRatifierUtils || (exports.SetterRatifierUtils = SetterRatifierUtils = {}));
@@ -32,7 +32,7 @@ exports.EMPTY_OFFER_STRUCT = {
32
32
  maxAssets: 0n,
33
33
  continuousFeeCap: 0n,
34
34
  };
35
- const EMPTY_OFFER_DEFAULT_GROUP = index_js_1.OfferUtils.hashStruct(exports.EMPTY_OFFER_STRUCT);
35
+ const EMPTY_OFFER_DEFAULT_GROUP = (0, viem_1.keccak256)(index_js_1.OfferUtils.hashStruct(exports.EMPTY_OFFER_STRUCT));
36
36
  /** @internal Returns whether an address value is the zero address. */
37
37
  function isZeroAddress(value) {
38
38
  return value.toLowerCase() === viem_1.zeroAddress;
@@ -44,9 +44,9 @@ function isZeroAddress(value) {
44
44
  * decoding only treats the canonical zero-group empty struct as padding.
45
45
  */
46
46
  function isEmptyOfferStruct(offer, options = {}) {
47
- const hasEmptyGroup = offer.group === viem_1.zeroHash ||
48
- (options.allowDefaultGroup === true &&
49
- offer.group === EMPTY_OFFER_DEFAULT_GROUP);
47
+ const group = offer.group.toLowerCase();
48
+ const hasEmptyGroup = group === viem_1.zeroHash ||
49
+ (options.allowDefaultGroup === true && group === EMPTY_OFFER_DEFAULT_GROUP);
50
50
  return (offer.market.chainId === 0n &&
51
51
  isZeroAddress(offer.market.midnight) &&
52
52
  isZeroAddress(offer.market.loanToken) &&
@@ -166,12 +166,15 @@ export declare class MidnightApi {
166
166
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
167
167
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
168
168
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
169
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
169
170
  * @param params.baseUrl - Optional Midnight API base URL override.
170
171
  * @param params.fetch - Optional fetch implementation override.
171
172
  * @param params.request - Optional fetch options forwarded to this request.
172
173
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
174
+ * @throws {NegativeValueError} when `settlementFee` is negative.
173
175
  * @throws {MidnightApiError} when the API returns a non-2xx response.
174
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
176
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
177
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
175
178
  * @example
176
179
  * ```ts
177
180
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -411,9 +414,12 @@ export declare class MidnightApi {
411
414
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
412
415
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
413
416
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
417
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
414
418
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
419
+ * @throws {NegativeValueError} when `settlementFee` is negative.
415
420
  * @throws {MidnightApiError} when the API returns a non-2xx response.
416
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
421
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
422
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
417
423
  * @example
418
424
  * ```ts
419
425
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -1,6 +1,6 @@
1
- import { MathLib } from "@morpho-org/morpho-ts";
1
+ import { assertNonNegative, MathLib } from "@morpho-org/morpho-ts";
2
2
  import { InvalidMidnightApiResponseError } from "../errors.js";
3
- import { TickLib } from "../math/index.js";
3
+ import { TakeAmountsLib } from "../math/index.js";
4
4
  import { Payload } from "../signatures/Payload.js";
5
5
  import { buildBookPath, mapBoundBookMarket, mapBoundBooks, mapBoundTakeableOffers, mapPriceLevel, parseValidationResponse, requestMidnightApi, } from "./helpers.js";
6
6
  export { MempoolPayloadValidationRule } from "./types.js";
@@ -238,12 +238,15 @@ export class MidnightApi {
238
238
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
239
239
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
240
240
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
241
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
241
242
  * @param params.baseUrl - Optional Midnight API base URL override.
242
243
  * @param params.fetch - Optional fetch implementation override.
243
244
  * @param params.request - Optional fetch options forwarded to this request.
244
245
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
246
+ * @throws {NegativeValueError} when `settlementFee` is negative.
245
247
  * @throws {MidnightApiError} when the API returns a non-2xx response.
246
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
248
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
249
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
247
250
  * @example
248
251
  * ```ts
249
252
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -259,6 +262,8 @@ export class MidnightApi {
259
262
  */
260
263
  static async fetchBookQuote(params) {
261
264
  const input = params;
265
+ const settlementFee = BigInt(input.settlementFee ?? 0n);
266
+ assertNonNegative("settlementFee", settlementFee);
262
267
  const response = await requestMidnightApi({
263
268
  ...input,
264
269
  method: "GET",
@@ -286,16 +291,20 @@ export class MidnightApi {
286
291
  if (averageWorstPrice != null) {
287
292
  const guard = BigInt(averageWorstPrice);
288
293
  let filledUnits = 0n;
289
- let weightedPrice = 0n;
294
+ let filledAssets = 0n;
290
295
  if ("units" in input && input.units != null) {
291
296
  let remainingUnits = BigInt(input.units);
292
297
  for (const take of takeableOffers) {
293
298
  if (remainingUnits === 0n)
294
299
  break;
295
- const price = TickLib.tickToPrice(take.offer.tick);
300
+ const { buyerPrice, sellerPrice } = TakeAmountsLib.prices({
301
+ offer: take.offer,
302
+ settlementFee,
303
+ });
304
+ const price = input.side === "asks" ? buyerPrice : sellerPrice;
296
305
  const filled = take.units < remainingUnits ? take.units : remainingUnits;
297
306
  filledUnits += filled;
298
- weightedPrice += filled * price;
307
+ filledAssets += MathLib.mulDiv(filled, price, MathLib.WAD, input.side === "asks" ? "Up" : "Down");
299
308
  remainingUnits -= filled;
300
309
  }
301
310
  }
@@ -304,25 +313,30 @@ export class MidnightApi {
304
313
  for (const take of takeableOffers) {
305
314
  if (remainingAssets === 0n)
306
315
  break;
307
- const price = TickLib.tickToPrice(take.offer.tick);
308
- const takeAssets = input.side === "asks"
309
- ? MathLib.mulDivUp(take.units, price, MathLib.WAD)
310
- : MathLib.mulDivDown(take.units, price, MathLib.WAD);
311
- if (takeAssets === 0n)
312
- continue;
313
- const fillsEntireTake = takeAssets <= remainingAssets;
316
+ const { buyerPrice, sellerPrice } = TakeAmountsLib.prices({
317
+ offer: take.offer,
318
+ settlementFee,
319
+ });
320
+ const makerPrice = input.side === "asks" ? sellerPrice : buyerPrice;
321
+ const settlementPrice = input.side === "asks" ? buyerPrice : sellerPrice;
322
+ const takeMakerAssets = input.side === "asks"
323
+ ? MathLib.mulDivUp(take.units, makerPrice, MathLib.WAD)
324
+ : MathLib.mulDivDown(take.units, makerPrice, MathLib.WAD);
325
+ const fillsEntireTake = takeMakerAssets <= remainingAssets;
314
326
  const filled = fillsEntireTake
315
327
  ? take.units
316
- : MathLib.mulDiv(remainingAssets, MathLib.WAD, price, input.side === "asks" ? "Down" : "Up");
328
+ : MathLib.mulDiv(remainingAssets, MathLib.WAD, makerPrice, input.side === "asks" ? "Down" : "Up");
317
329
  if (filled === 0n)
318
330
  continue;
319
331
  filledUnits += filled;
320
- weightedPrice += filled * price;
321
- remainingAssets = fillsEntireTake ? remainingAssets - takeAssets : 0n;
332
+ filledAssets += MathLib.mulDiv(filled, settlementPrice, MathLib.WAD, input.side === "asks" ? "Up" : "Down");
333
+ remainingAssets = fillsEntireTake
334
+ ? remainingAssets - takeMakerAssets
335
+ : 0n;
322
336
  }
323
337
  }
324
338
  if (filledUnits > 0n) {
325
- const averagePrice = MathLib.mulDivDown(weightedPrice, 1n, filledUnits);
339
+ const averagePrice = MathLib.mulDiv(filledAssets, MathLib.WAD, filledUnits, input.side === "asks" ? "Up" : "Down");
326
340
  const violatesGuard = input.side === "asks" ? averagePrice > guard : averagePrice < guard;
327
341
  if (violatesGuard) {
328
342
  throw new InvalidMidnightApiResponseError(`Midnight API quote takeable offers imply average price "${averagePrice}" outside average_worst_price guard "${guard}".`);
@@ -632,9 +646,12 @@ export class MidnightApi {
632
646
  * @param params.units - Optional target unit amount. Mutually exclusive with `params.assets`.
633
647
  * @param params.averageWorstPrice - Optional WAD-scaled average worst price guard. Mutually exclusive with `params.slippage`.
634
648
  * @param params.slippage - Optional slippage percentage used to derive the guard. Mutually exclusive with `params.averageWorstPrice`.
649
+ * @param params.settlementFee - Optional current WAD-scaled settlement fee used for local guard validation. Defaults to zero.
635
650
  * @returns Quote and signed ABI-ready take caps mapped from the API response.
651
+ * @throws {NegativeValueError} when `settlementFee` is negative.
636
652
  * @throws {MidnightApiError} when the API returns a non-2xx response.
637
- * @throws {InvalidMidnightApiResponseError} when the API success response is not JSON.
653
+ * @throws {InvalidMidnightApiResponseError} when the API returns a malformed success response or the returned offers imply a rounded aggregate settlement price outside the effective average-worst-price guard.
654
+ * @throws {SettlementFeeExceedsPriceError} when a bid's settlement fee exceeds its offer price.
638
655
  * @example
639
656
  * ```ts
640
657
  * import { MidnightApi } from "@morpho-org/midnight-sdk/api";
@@ -177,6 +177,8 @@ export type FetchBookQuoteParams = MidnightApiConfig & {
177
177
  readonly marketId: Hash;
178
178
  /** Book side to quote. */
179
179
  readonly side: MidnightApiBookSide;
180
+ /** Current WAD-scaled settlement fee used for local guard validation. Defaults to zero. */
181
+ readonly settlementFee?: BigIntish;
180
182
  } & (MidnightApiQuoteAssetsTarget | MidnightApiQuoteUnitsTarget) & (MidnightApiQuoteAverageWorstPriceGuard | MidnightApiQuoteSlippageGuard | MidnightApiQuoteWithoutGuard);
181
183
  /**
182
184
  * Parameters for {@link MidnightApi.fetchTakeableOffers}.
@@ -1,2 +1,2 @@
1
1
  /** @internal Installed Midnight SDK package version. */
2
- export declare const MIDNIGHT_SDK_VERSION = "1.3.1";
2
+ export declare const MIDNIGHT_SDK_VERSION = "1.4.0-next.0";
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/release/generate-midnight-package-version.mjs. Do not edit.
2
2
  /** @internal Installed Midnight SDK package version. */
3
- export const MIDNIGHT_SDK_VERSION = "1.3.1";
3
+ export const MIDNIGHT_SDK_VERSION = "1.4.0-next.0";
@@ -814,18 +814,13 @@ export var EcrecoverRatifierUtils;
814
814
  account: params.account,
815
815
  }));
816
816
  }
817
- const items = [];
818
- for (const offer of tree.offers) {
819
- items.push({
820
- offer,
821
- ratifierData: ratifierData({
822
- tree,
823
- leafIndex: items.length,
824
- signature,
825
- }),
826
- });
827
- }
828
- return items;
817
+ return tree.offers.map((offer, leafIndex) => ({
818
+ offer,
819
+ ratifierData: encodeRatifierData({
820
+ signature,
821
+ ...TreeUtils.buildProof({ tree, leafIndex }),
822
+ }),
823
+ }));
829
824
  }
830
825
  EcrecoverRatifierUtils.ratify = ratify;
831
826
  })(EcrecoverRatifierUtils || (EcrecoverRatifierUtils = {}));
@@ -76,8 +76,8 @@ export declare namespace RatifierUtils {
76
76
  * @param params.tree - Tree-like object or raw offer/group input.
77
77
  * @param params.label - Ratifier label used in validation errors.
78
78
  * @returns Normalized tree-like data and its shared ratifier.
79
- * @throws {InvalidTreeError} when the tree is empty or contains multiple ratifiers.
80
- * @throws {InvalidTreeHeightError} when normalized raw input exceeds the supported tree height.
79
+ * @throws {InvalidTreeError} when the tree fields disagree, the tree is empty, or it contains multiple ratifiers.
80
+ * @throws {InvalidTreeHeightError} when the tree height is unsupported.
81
81
  * @example
82
82
  * ```ts
83
83
  * import { Offer, RatifierUtils } from "@morpho-org/midnight-sdk";
@@ -1,7 +1,8 @@
1
- import { InvalidTreeError } from "../errors.js";
2
- import { Offer } from "../offers/index.js";
1
+ import { InvalidTreeError, InvalidTreeHeightError } from "../errors.js";
2
+ import { Offer, OfferUtils } from "../offers/index.js";
3
3
  import { Group } from "./Group.js";
4
4
  import { GroupUtils } from "./GroupUtils.js";
5
+ import { isEmptyOfferStruct } from "./offerStructInternal.js";
5
6
  import { TreeUtils } from "./TreeUtils.js";
6
7
  function isTreeLike(tree) {
7
8
  return (!Array.isArray(tree) &&
@@ -13,6 +14,49 @@ function isTreeLike(tree) {
13
14
  }
14
15
  function normalizeTree(tree) {
15
16
  if (isTreeLike(tree)) {
17
+ if (!Number.isInteger(tree.height) || tree.height < 0 || tree.height > 20) {
18
+ throw new InvalidTreeHeightError(tree.height);
19
+ }
20
+ const expectedLength = 2 ** tree.height;
21
+ if (tree.paddedOffers.length !== expectedLength ||
22
+ tree.leaves.length !== expectedLength ||
23
+ tree.offers.length > expectedLength) {
24
+ throw new InvalidTreeError("Tree offers, leaves, and height describe different trees.");
25
+ }
26
+ const computedLeaves = tree.paddedOffers.map(OfferUtils.hashStruct);
27
+ const seen = new Set();
28
+ for (const [index, offer] of tree.offers.entries()) {
29
+ if (isEmptyOfferStruct(tree.paddedOffers[index], {
30
+ allowDefaultGroup: true,
31
+ })) {
32
+ throw new InvalidTreeError("Visible offers must not contain tree padding.");
33
+ }
34
+ const hash = OfferUtils.hashStruct(OfferUtils.toStruct({ offer }));
35
+ if (hash.toLowerCase() !== computedLeaves[index].toLowerCase() ||
36
+ seen.has(hash.toLowerCase())) {
37
+ throw new InvalidTreeError("Visible offers do not match the tree leaves.");
38
+ }
39
+ seen.add(hash.toLowerCase());
40
+ }
41
+ if (tree.paddedOffers
42
+ .slice(tree.offers.length)
43
+ .some((offer) => !isEmptyOfferStruct(offer, { allowDefaultGroup: true }))) {
44
+ throw new InvalidTreeError("Tree padding contains offers hidden from the visible offer list.");
45
+ }
46
+ if (computedLeaves.some((leaf, index) => leaf.toLowerCase() !== tree.leaves[index].toLowerCase())) {
47
+ throw new InvalidTreeError("Tree leaves do not match its padded offers.");
48
+ }
49
+ let level = computedLeaves;
50
+ while (level.length > 1) {
51
+ const next = [];
52
+ for (let index = 0; index < level.length; index += 2) {
53
+ next.push(TreeUtils.hashNode(level[index], level[index + 1]));
54
+ }
55
+ level = next;
56
+ }
57
+ if (level[0]?.toLowerCase() !== tree.root.toLowerCase()) {
58
+ throw new InvalidTreeError("Tree root does not match its leaves.");
59
+ }
16
60
  return tree;
17
61
  }
18
62
  const entries = Array.isArray(tree) ? tree : [tree];
@@ -74,8 +118,8 @@ export var RatifierUtils;
74
118
  * @param params.tree - Tree-like object or raw offer/group input.
75
119
  * @param params.label - Ratifier label used in validation errors.
76
120
  * @returns Normalized tree-like data and its shared ratifier.
77
- * @throws {InvalidTreeError} when the tree is empty or contains multiple ratifiers.
78
- * @throws {InvalidTreeHeightError} when normalized raw input exceeds the supported tree height.
121
+ * @throws {InvalidTreeError} when the tree fields disagree, the tree is empty, or it contains multiple ratifiers.
122
+ * @throws {InvalidTreeHeightError} when the tree height is unsupported.
79
123
  * @example
80
124
  * ```ts
81
125
  * import { Offer, RatifierUtils } from "@morpho-org/midnight-sdk";
@@ -241,17 +241,10 @@ export var SetterRatifierUtils;
241
241
  tree: params.tree,
242
242
  label: "Setter",
243
243
  });
244
- const items = [];
245
- for (const offer of tree.offers) {
246
- items.push({
247
- offer,
248
- ratifierData: ratifierData({
249
- tree,
250
- leafIndex: items.length,
251
- }),
252
- });
253
- }
254
- return items;
244
+ return tree.offers.map((offer, leafIndex) => ({
245
+ offer,
246
+ ratifierData: encodeRatifierData(TreeUtils.buildProof({ tree, leafIndex })),
247
+ }));
255
248
  }
256
249
  SetterRatifierUtils.ratify = ratify;
257
250
  })(SetterRatifierUtils || (SetterRatifierUtils = {}));
@@ -1,4 +1,4 @@
1
- import { zeroAddress, zeroHash } from "viem";
1
+ import { keccak256, zeroAddress, zeroHash } from "viem";
2
2
  import { OfferUtils } from "../offers/index.js";
3
3
  /** @internal Empty protocol offer struct used for Merkle padding. */
4
4
  export const EMPTY_OFFER_STRUCT = {
@@ -27,7 +27,7 @@ export const EMPTY_OFFER_STRUCT = {
27
27
  maxAssets: 0n,
28
28
  continuousFeeCap: 0n,
29
29
  };
30
- const EMPTY_OFFER_DEFAULT_GROUP = OfferUtils.hashStruct(EMPTY_OFFER_STRUCT);
30
+ const EMPTY_OFFER_DEFAULT_GROUP = keccak256(OfferUtils.hashStruct(EMPTY_OFFER_STRUCT));
31
31
  /** @internal Returns whether an address value is the zero address. */
32
32
  export function isZeroAddress(value) {
33
33
  return value.toLowerCase() === zeroAddress;
@@ -39,9 +39,9 @@ export function isZeroAddress(value) {
39
39
  * decoding only treats the canonical zero-group empty struct as padding.
40
40
  */
41
41
  export function isEmptyOfferStruct(offer, options = {}) {
42
- const hasEmptyGroup = offer.group === zeroHash ||
43
- (options.allowDefaultGroup === true &&
44
- offer.group === EMPTY_OFFER_DEFAULT_GROUP);
42
+ const group = offer.group.toLowerCase();
43
+ const hasEmptyGroup = group === zeroHash ||
44
+ (options.allowDefaultGroup === true && group === EMPTY_OFFER_DEFAULT_GROUP);
45
45
  return (offer.market.chainId === 0n &&
46
46
  isZeroAddress(offer.market.midnight) &&
47
47
  isZeroAddress(offer.market.loanToken) &&
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morpho-org/midnight-sdk",
3
3
  "description": "Viem-based package for Morpho Midnight that exports protocol utilities, fetch helpers, and Midnight API utilities under a dedicated API subpath.",
4
- "version": "1.3.1",
4
+ "version": "1.4.0-next.0",
5
5
  "author": "Morpho Association <contact@morpho.org>",
6
6
  "contributors": [
7
7
  "Rubilmax <rmilon@gmail.com>"
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "devDependencies": {
50
50
  "fast-check": "^4.9.0",
51
- "typescript": "^7.0.2",
51
+ "typescript": "7.0.2",
52
52
  "viem": "^2.55.10",
53
53
  "vitest": "^4.1.10",
54
54
  "@morpho-org/morpho-ts": "^2.11.1",