@morpho-org/midnight-sdk 1.0.0 → 1.1.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 (54) hide show
  1. package/lib/cjs/api/MidnightApi.js +67 -3
  2. package/lib/cjs/api/helpers.d.ts +11 -1
  3. package/lib/cjs/api/helpers.js +48 -1
  4. package/lib/cjs/errors.d.ts +15 -0
  5. package/lib/cjs/errors.js +21 -1
  6. package/lib/cjs/fetch/Market.js +37 -2
  7. package/lib/cjs/fetch/utils.d.ts +1 -1
  8. package/lib/cjs/fetch/utils.js +9 -2
  9. package/lib/cjs/offers/Offer.d.ts +1 -1
  10. package/lib/cjs/offers/OfferUtils.d.ts +5 -5
  11. package/lib/cjs/offers/OfferUtils.js +26 -13
  12. package/lib/cjs/signatures/EcrecoverRatifierUtils.d.ts +118 -0
  13. package/lib/cjs/signatures/EcrecoverRatifierUtils.js +149 -1
  14. package/lib/cjs/signatures/Group.js +5 -3
  15. package/lib/cjs/signatures/GroupUtils.d.ts +48 -0
  16. package/lib/cjs/signatures/GroupUtils.js +51 -0
  17. package/lib/cjs/signatures/RatifierUtils.js +14 -3
  18. package/lib/cjs/signatures/SetterRatifierUtils.d.ts +31 -0
  19. package/lib/cjs/signatures/SetterRatifierUtils.js +38 -0
  20. package/lib/cjs/signatures/Tree.js +10 -2
  21. package/lib/cjs/signatures/TreeUtils.d.ts +4 -1
  22. package/lib/cjs/signatures/TreeUtils.js +19 -6
  23. package/lib/cjs/signatures/eip712.d.ts +22 -0
  24. package/lib/cjs/signatures/eip712.js +26 -0
  25. package/lib/cjs/signatures/index.d.ts +1 -0
  26. package/lib/cjs/signatures/index.js +1 -0
  27. package/lib/esm/api/MidnightApi.js +68 -4
  28. package/lib/esm/api/helpers.d.ts +11 -1
  29. package/lib/esm/api/helpers.js +48 -2
  30. package/lib/esm/errors.d.ts +15 -0
  31. package/lib/esm/errors.js +19 -0
  32. package/lib/esm/fetch/Market.js +38 -3
  33. package/lib/esm/fetch/utils.d.ts +1 -1
  34. package/lib/esm/fetch/utils.js +9 -2
  35. package/lib/esm/offers/Offer.d.ts +1 -1
  36. package/lib/esm/offers/OfferUtils.d.ts +5 -5
  37. package/lib/esm/offers/OfferUtils.js +28 -15
  38. package/lib/esm/signatures/EcrecoverRatifierUtils.d.ts +118 -0
  39. package/lib/esm/signatures/EcrecoverRatifierUtils.js +151 -3
  40. package/lib/esm/signatures/Group.js +5 -3
  41. package/lib/esm/signatures/GroupUtils.d.ts +48 -0
  42. package/lib/esm/signatures/GroupUtils.js +51 -0
  43. package/lib/esm/signatures/RatifierUtils.js +15 -4
  44. package/lib/esm/signatures/SetterRatifierUtils.d.ts +31 -0
  45. package/lib/esm/signatures/SetterRatifierUtils.js +38 -0
  46. package/lib/esm/signatures/Tree.js +11 -3
  47. package/lib/esm/signatures/TreeUtils.d.ts +4 -1
  48. package/lib/esm/signatures/TreeUtils.js +19 -6
  49. package/lib/esm/signatures/eip712.d.ts +22 -0
  50. package/lib/esm/signatures/eip712.js +22 -0
  51. package/lib/esm/signatures/index.d.ts +1 -0
  52. package/lib/esm/signatures/index.js +1 -0
  53. package/package.json +8 -7
  54. package/src/index.ts +0 -19
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidnightApi = void 0;
4
+ const morpho_ts_1 = require("@morpho-org/morpho-ts");
5
+ const errors_js_1 = require("../errors.js");
6
+ const index_js_1 = require("../math/index.js");
4
7
  const Payload_js_1 = require("../signatures/Payload.js");
5
8
  const helpers_js_1 = require("./helpers.js");
6
9
  /**
@@ -220,7 +223,10 @@ class MidnightApi {
220
223
  }),
221
224
  });
222
225
  return {
223
- data: response.data.map(helpers_js_1.mapTakeableOffer),
226
+ data: (0, helpers_js_1.mapBoundTakeableOffers)(response.data, {
227
+ marketId: input.marketId,
228
+ side: input.side,
229
+ }),
224
230
  };
225
231
  }
226
232
  /**
@@ -270,7 +276,61 @@ class MidnightApi {
270
276
  assets: input.assets,
271
277
  },
272
278
  });
273
- const takeableOffers = response.data.takeable_offers.map(helpers_js_1.mapTakeableOffer);
279
+ const takeableOffers = (0, helpers_js_1.mapBoundTakeableOffers)(response.data.takeable_offers, {
280
+ marketId: input.marketId,
281
+ side: input.side,
282
+ });
283
+ const averageWorstPrice = "averageWorstPrice" in input && input.averageWorstPrice != null
284
+ ? input.averageWorstPrice
285
+ : "slippage" in input && input.slippage != null
286
+ ? response.data.average_worst_price
287
+ : undefined;
288
+ if (averageWorstPrice != null) {
289
+ const guard = BigInt(averageWorstPrice);
290
+ let filledUnits = 0n;
291
+ let weightedPrice = 0n;
292
+ if ("units" in input && input.units != null) {
293
+ let remainingUnits = BigInt(input.units);
294
+ for (const take of takeableOffers) {
295
+ if (remainingUnits === 0n)
296
+ break;
297
+ const price = index_js_1.TickLib.tickToPrice(take.offer.tick);
298
+ const filled = take.units < remainingUnits ? take.units : remainingUnits;
299
+ filledUnits += filled;
300
+ weightedPrice += filled * price;
301
+ remainingUnits -= filled;
302
+ }
303
+ }
304
+ else {
305
+ let remainingAssets = BigInt(input.assets);
306
+ for (const take of takeableOffers) {
307
+ if (remainingAssets === 0n)
308
+ break;
309
+ const price = index_js_1.TickLib.tickToPrice(take.offer.tick);
310
+ const takeAssets = input.side === "asks"
311
+ ? morpho_ts_1.MathLib.mulDivUp(take.units, price, morpho_ts_1.MathLib.WAD)
312
+ : morpho_ts_1.MathLib.mulDivDown(take.units, price, morpho_ts_1.MathLib.WAD);
313
+ if (takeAssets === 0n)
314
+ continue;
315
+ const fillsEntireTake = takeAssets <= remainingAssets;
316
+ const filled = fillsEntireTake
317
+ ? take.units
318
+ : morpho_ts_1.MathLib.mulDiv(remainingAssets, morpho_ts_1.MathLib.WAD, price, input.side === "asks" ? "Down" : "Up");
319
+ if (filled === 0n)
320
+ continue;
321
+ filledUnits += filled;
322
+ weightedPrice += filled * price;
323
+ remainingAssets = fillsEntireTake ? remainingAssets - takeAssets : 0n;
324
+ }
325
+ }
326
+ if (filledUnits > 0n) {
327
+ const averagePrice = morpho_ts_1.MathLib.mulDivDown(weightedPrice, 1n, filledUnits);
328
+ const violatesGuard = input.side === "asks" ? averagePrice > guard : averagePrice < guard;
329
+ if (violatesGuard) {
330
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API quote takeable offers imply average price "${averagePrice}" outside average_worst_price guard "${guard}".`);
331
+ }
332
+ }
333
+ }
274
334
  return {
275
335
  data: {
276
336
  averageBestPrice: response.data.average_best_price,
@@ -323,7 +383,11 @@ class MidnightApi {
323
383
  });
324
384
  return {
325
385
  cursor: response.cursor,
326
- data: response.data.map(helpers_js_1.mapTakeableOffer),
386
+ data: (0, helpers_js_1.mapBoundTakeableOffers)(response.data, {
387
+ maker: input.maker,
388
+ marketIds: input.marketIds,
389
+ groups: input.groups,
390
+ }),
327
391
  };
328
392
  }
329
393
  /**
@@ -1,4 +1,4 @@
1
- import type { Hash } from "viem";
1
+ import { type Address, type Hash } from "viem";
2
2
  import type { ApiBookMarketResponse, ApiCollateralResponse, ApiPriceLevelResponse, ApiRequestParams, ApiTakeableOfferResponse, MempoolPayloadValidationResult, MidnightApiBookMarket, MidnightApiBookSide, MidnightApiCollateral, MidnightApiPriceLevel, MidnightApiTake } from "./types.js";
3
3
  /** @internal Sends one Midnight API request and maps non-2xx responses to SDK errors. */
4
4
  export declare function requestMidnightApi<Response = unknown>(params: ApiRequestParams): Promise<Response>;
@@ -16,5 +16,15 @@ export declare function mapCollateral(collateral: ApiCollateralResponse): Midnig
16
16
  export declare function mapPriceLevel(level: ApiPriceLevelResponse): MidnightApiPriceLevel;
17
17
  /** @internal Maps a takeable-offer API payload to the SDK response shape. */
18
18
  export declare function mapTakeableOffer(takeableOffer: ApiTakeableOfferResponse): MidnightApiTake;
19
+ /** @internal API response context required for a takeable offer to be executable for the caller's request. */
20
+ export interface TakeableOfferContext {
21
+ readonly marketId?: Hash;
22
+ readonly side?: MidnightApiBookSide;
23
+ readonly maker?: Address;
24
+ readonly marketIds?: readonly Hash[];
25
+ readonly groups?: readonly Hash[];
26
+ }
27
+ /** @internal Maps, validates, and best-price-sorts API takeable offers. */
28
+ export declare function mapBoundTakeableOffers(takeableOffers: readonly ApiTakeableOfferResponse[], context: TakeableOfferContext): MidnightApiTake[];
19
29
  /** @internal Parses the mempool validation response envelope. */
20
30
  export declare function parseValidationResponse(response: unknown): MempoolPayloadValidationResult;
@@ -6,13 +6,16 @@ exports.mapBookMarket = mapBookMarket;
6
6
  exports.mapCollateral = mapCollateral;
7
7
  exports.mapPriceLevel = mapPriceLevel;
8
8
  exports.mapTakeableOffer = mapTakeableOffer;
9
+ exports.mapBoundTakeableOffers = mapBoundTakeableOffers;
9
10
  exports.parseValidationResponse = parseValidationResponse;
10
11
  const morpho_ts_1 = require("@morpho-org/morpho-ts");
12
+ const viem_1 = require("viem");
11
13
  const errors_js_1 = require("../errors.js");
14
+ const index_js_1 = require("../market/index.js");
12
15
  const DEFAULT_MIDNIGHT_API_URL = new URL("/v0/midnight", morpho_ts_1.BLUE_API_BASE_URL);
13
16
  // Keep this literal in source so the browser ESM build does not import package.json
14
17
  // through Node-only createRequire/module shims.
15
- const MIDNIGHT_SDK_VERSION = "0.1.0";
18
+ const MIDNIGHT_SDK_VERSION = "1.0.1";
16
19
  /** @internal Sends one Midnight API request and maps non-2xx responses to SDK errors. */
17
20
  async function requestMidnightApi(params) {
18
21
  const baseUrl = new URL(params.baseUrl ?? DEFAULT_MIDNIGHT_API_URL);
@@ -162,6 +165,50 @@ function mapTakeableOffer(takeableOffer) {
162
165
  ratifierData: takeableOffer.ratifier_data,
163
166
  };
164
167
  }
168
+ /** @internal Maps, validates, and best-price-sorts API takeable offers. */
169
+ function mapBoundTakeableOffers(takeableOffers, context) {
170
+ const mapped = takeableOffers.map((takeableOffer) => {
171
+ const take = mapTakeableOffer(takeableOffer);
172
+ const embeddedMarketId = index_js_1.MarketUtils.toId(take.offer.market);
173
+ if (!(0, morpho_ts_1.isHexEqual)(embeddedMarketId, take.marketId)) {
174
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API takeable offer market_id "${take.marketId}" does not match embedded offer market "${embeddedMarketId}".`);
175
+ }
176
+ if (context.marketId != null &&
177
+ !(0, morpho_ts_1.isHexEqual)(take.marketId, context.marketId)) {
178
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API takeable offer market_id "${take.marketId}" does not match requested market "${context.marketId}".`);
179
+ }
180
+ if (context.marketIds != null &&
181
+ context.marketIds.length > 0 &&
182
+ !context.marketIds.some((marketId) => (0, morpho_ts_1.isHexEqual)(marketId, take.marketId))) {
183
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API takeable offer market_id "${take.marketId}" is outside the requested market_ids filter.`);
184
+ }
185
+ if (context.side != null) {
186
+ const expectedBuy = context.side === "bids";
187
+ if (take.offer.buy !== expectedBuy) {
188
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API ${context.side} takeable offer returned offer.buy "${take.offer.buy}".`);
189
+ }
190
+ }
191
+ if (context.maker != null &&
192
+ !(0, viem_1.isAddressEqual)(take.offer.maker, context.maker)) {
193
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API takeable offer maker "${take.offer.maker}" does not match requested maker "${context.maker}".`);
194
+ }
195
+ if (context.groups != null &&
196
+ context.groups.length > 0 &&
197
+ !context.groups.some((group) => (0, morpho_ts_1.isHexEqual)(group, take.offer.group))) {
198
+ throw new errors_js_1.InvalidMidnightApiResponseError(`Midnight API takeable offer group "${take.offer.group}" is outside the requested groups filter.`);
199
+ }
200
+ return take;
201
+ });
202
+ if (context.side == null)
203
+ return mapped;
204
+ return [...mapped].sort((a, b) => {
205
+ if (a.offer.tick === b.offer.tick)
206
+ return 0;
207
+ if (context.side === "asks")
208
+ return a.offer.tick < b.offer.tick ? -1 : 1;
209
+ return a.offer.tick > b.offer.tick ? -1 : 1;
210
+ });
211
+ }
165
212
  /** @internal Parses the mempool validation response envelope. */
166
213
  function parseValidationResponse(response) {
167
214
  const parsedResponse = requireRecord(response, "validation response");
@@ -277,6 +277,21 @@ export declare class InvalidTypedDataSignatureError extends Error {
277
277
  readonly signer: Address;
278
278
  constructor(signer: Address, cause?: unknown);
279
279
  }
280
+ /**
281
+ * Thrown when decoded Ecrecover ratifier data contains a non-canonical signature `v` value.
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * import { InvalidEcrecoverSignatureVError } from "@morpho-org/midnight-sdk";
286
+ *
287
+ * throw new InvalidEcrecoverSignatureVError(29);
288
+ * ```
289
+ */
290
+ export declare class InvalidEcrecoverSignatureVError extends Error {
291
+ /** Invalid ECDSA recovery id. */
292
+ readonly v: number;
293
+ constructor(v: number);
294
+ }
280
295
  /**
281
296
  * Thrown when the Midnight API returns a non-2xx response.
282
297
  *
package/lib/cjs/errors.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.InvalidMidnightApiResponseError = exports.MidnightApiError = exports.InvalidTypedDataSignatureError = exports.MidnightMempoolValidationError = exports.ChainIdMismatchError = exports.InvalidTreeError = exports.InvalidTreeHeightError = exports.PayloadDecodeError = exports.InvalidOfferParameterError = exports.UnknownCollateralIndexError = exports.InvalidMarketParameterError = exports.InvalidPositionAccrualStateError = exports.InvalidPositionLossFactorError = exports.InvalidPositionAccrualTimestampError = exports.SettlementFeeExceedsPriceError = exports.InvalidTickSpacingError = exports.PriceGreaterThanOneError = exports.TickOutOfRangeError = exports.InvalidOfferGroupError = void 0;
3
+ exports.InvalidMidnightApiResponseError = exports.MidnightApiError = exports.InvalidEcrecoverSignatureVError = exports.InvalidTypedDataSignatureError = exports.MidnightMempoolValidationError = exports.ChainIdMismatchError = exports.InvalidTreeError = exports.InvalidTreeHeightError = exports.PayloadDecodeError = exports.InvalidOfferParameterError = exports.UnknownCollateralIndexError = exports.InvalidMarketParameterError = exports.InvalidPositionAccrualStateError = exports.InvalidPositionLossFactorError = exports.InvalidPositionAccrualTimestampError = exports.SettlementFeeExceedsPriceError = exports.InvalidTickSpacingError = exports.PriceGreaterThanOneError = exports.TickOutOfRangeError = exports.InvalidOfferGroupError = void 0;
4
4
  /**
5
5
  * Thrown when a Midnight offer group violates protocol-level group mechanics.
6
6
  *
@@ -345,6 +345,26 @@ class InvalidTypedDataSignatureError extends Error {
345
345
  }
346
346
  }
347
347
  exports.InvalidTypedDataSignatureError = InvalidTypedDataSignatureError;
348
+ /**
349
+ * Thrown when decoded Ecrecover ratifier data contains a non-canonical signature `v` value.
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * import { InvalidEcrecoverSignatureVError } from "@morpho-org/midnight-sdk";
354
+ *
355
+ * throw new InvalidEcrecoverSignatureVError(29);
356
+ * ```
357
+ */
358
+ class InvalidEcrecoverSignatureVError extends Error {
359
+ /** Invalid ECDSA recovery id. */
360
+ v;
361
+ constructor(v) {
362
+ super(`Ecrecover signature v "${v}" is invalid. Use canonical Ethereum v 27 or 28.`);
363
+ this.name = "InvalidEcrecoverSignatureVError";
364
+ this.v = v;
365
+ }
366
+ }
367
+ exports.InvalidEcrecoverSignatureVError = InvalidEcrecoverSignatureVError;
348
368
  /**
349
369
  * Thrown when the Midnight API returns a non-2xx response.
350
370
  *
@@ -5,6 +5,7 @@ exports.fetchMarket = fetchMarket;
5
5
  const morpho_ts_1 = require("@morpho-org/morpho-ts");
6
6
  const actions_1 = require("viem/actions");
7
7
  const abis_js_1 = require("../abis.js");
8
+ const errors_js_1 = require("../errors.js");
8
9
  const index_js_1 = require("../market/index.js");
9
10
  const utils_js_1 = require("./utils.js");
10
11
  /**
@@ -45,7 +46,11 @@ async function fetchMarketParams(client, params) {
45
46
  functionName: "toMarket",
46
47
  args: [params.marketId],
47
48
  });
48
- return new index_js_1.MarketParams(market);
49
+ return normalizeFetchedMarketParams(market, {
50
+ marketId: params.marketId,
51
+ chainId,
52
+ midnight,
53
+ });
49
54
  }
50
55
  /**
51
56
  * Fetches a hydrated market by id.
@@ -96,7 +101,11 @@ async function fetchMarket(client, params) {
96
101
  ]);
97
102
  const [totalUnits, lossFactor, withdrawable, continuousFeeCredit, settlementFeeCbps0, settlementFeeCbps1, settlementFeeCbps2, settlementFeeCbps3, settlementFeeCbps4, settlementFeeCbps5, settlementFeeCbps6, continuousFee, tickSpacing,] = state;
98
103
  return new index_js_1.Market({
99
- params: new index_js_1.MarketParams(market),
104
+ params: normalizeFetchedMarketParams(market, {
105
+ marketId: params.marketId,
106
+ chainId,
107
+ midnight,
108
+ }),
100
109
  totalUnits,
101
110
  lossFactor,
102
111
  withdrawable,
@@ -114,3 +123,29 @@ async function fetchMarket(client, params) {
114
123
  tickSpacing,
115
124
  });
116
125
  }
126
+ function normalizeFetchedMarketParams(market, expected) {
127
+ const marketParams = new index_js_1.MarketParams(market);
128
+ if (marketParams.chainId !== BigInt(expected.chainId)) {
129
+ throw new errors_js_1.InvalidMarketParameterError({
130
+ parameter: "chainId",
131
+ value: marketParams.chainId,
132
+ instruction: `Fetched market must use client chain id "${expected.chainId}".`,
133
+ });
134
+ }
135
+ if (marketParams.midnight.toLowerCase() !== expected.midnight.toLowerCase()) {
136
+ throw new errors_js_1.InvalidMarketParameterError({
137
+ parameter: "midnight",
138
+ value: marketParams.midnight,
139
+ instruction: `Fetched market must use Midnight contract "${expected.midnight}".`,
140
+ });
141
+ }
142
+ const actualMarketId = index_js_1.MarketUtils.toId(marketParams);
143
+ if (actualMarketId.toLowerCase() !== expected.marketId.toLowerCase()) {
144
+ throw new errors_js_1.InvalidMarketParameterError({
145
+ parameter: "marketId",
146
+ value: actualMarketId,
147
+ instruction: `Fetched market must match requested market id "${expected.marketId}".`,
148
+ });
149
+ }
150
+ return marketParams;
151
+ }
@@ -4,5 +4,5 @@ import type { DeploylessFetchParameters, MidnightCallParameters } from "./types.
4
4
  export declare const callParameters: (params: MidnightCallParameters) => MidnightCallParameters;
5
5
  /** @internal Returns the configured deployless mode, defaulting to enabled. */
6
6
  export declare const shouldUseDeployless: (params: DeploylessFetchParameters) => boolean | "force";
7
- /** @internal Returns the configured viem client chain id or fetches it from RPC. */
7
+ /** @internal Returns the RPC chain id and rejects configured-chain drift. */
8
8
  export declare const resolveChainId: (client: Client) => Promise<number>;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveChainId = exports.shouldUseDeployless = exports.callParameters = void 0;
4
4
  const actions_1 = require("viem/actions");
5
+ const errors_js_1 = require("../errors.js");
5
6
  /** @internal Extracts viem call parameters from Midnight fetch inputs. */
6
7
  const callParameters = (params) => ({
7
8
  account: params.account,
@@ -13,6 +14,12 @@ exports.callParameters = callParameters;
13
14
  /** @internal Returns the configured deployless mode, defaulting to enabled. */
14
15
  const shouldUseDeployless = (params) => params.deployless ?? true;
15
16
  exports.shouldUseDeployless = shouldUseDeployless;
16
- /** @internal Returns the configured viem client chain id or fetches it from RPC. */
17
- const resolveChainId = async (client) => client.chain?.id ?? (await (0, actions_1.getChainId)(client));
17
+ /** @internal Returns the RPC chain id and rejects configured-chain drift. */
18
+ const resolveChainId = async (client) => {
19
+ const rpcChainId = await (0, actions_1.getChainId)(client);
20
+ if (client.chain?.id != null && client.chain.id !== rpcChainId) {
21
+ throw new errors_js_1.ChainIdMismatchError(client.chain.id, BigInt(rpcChainId));
22
+ }
23
+ return rpcChainId;
24
+ };
18
25
  exports.resolveChainId = resolveChainId;
@@ -721,7 +721,7 @@ export interface BuildOfferParams {
721
721
  readonly maxUnits?: BigIntish;
722
722
  /** Maximum buyer or seller assets; defaults to zero. Exactly one of `maxUnits` and `maxAssets` must be non-zero. */
723
723
  readonly maxAssets?: BigIntish;
724
- /** Maximum market continuous fee accepted by this offer; defaults to the protocol maximum. */
724
+ /** Maximum market continuous fee accepted by this offer; defaults to zero. */
725
725
  readonly continuousFeeCap?: BigIntish;
726
726
  /** Offer start timestamp; defaults to zero. */
727
727
  readonly start?: BigIntish;
@@ -1,5 +1,5 @@
1
1
  import { type BigIntish } from "@morpho-org/morpho-ts";
2
- import type { Address, Hash } from "viem";
2
+ import { type Address, type Hash } from "viem";
3
3
  import { type BuildOfferParams, type IOffer, Offer, type OfferStruct } from "./Offer.js";
4
4
  /**
5
5
  * Object-compatible helpers for Midnight offer construction, grouping, and
@@ -280,8 +280,8 @@ export declare namespace OfferUtils {
280
280
  * Validates and normalizes the maximum continuous fee an offer accepts.
281
281
  *
282
282
  * Use this before `Offer.create` when a maker UI exposes fee tolerance.
283
- * `Offer.create` calls it internally and defaults omitted values to
284
- * {@link MAX_CONTINUOUS_FEE}.
283
+ * `Offer.create` calls it internally and defaults omitted values to zero so
284
+ * fresh offers fail closed unless the maker explicitly accepts a fee cap.
285
285
  *
286
286
  * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer.
287
287
  * @returns Normalized continuous fee cap.
@@ -339,7 +339,7 @@ export declare namespace OfferUtils {
339
339
  * @param params.tickSpacing - Optional market tick spacing; defaults to `DEFAULT_TICK_SPACING`.
340
340
  * @param params.maxUnits - Optional unit cap; defaults to zero.
341
341
  * @param params.maxAssets - Optional buyer or seller asset cap; defaults to zero.
342
- * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to `MAX_CONTINUOUS_FEE`.
342
+ * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to zero.
343
343
  * @param params.start - Optional offer start timestamp; defaults to zero.
344
344
  * @param params.expiry - Offer expiry timestamp.
345
345
  * @param params.receiverIfMakerIsSeller - Optional receiver used when maker is seller.
@@ -620,7 +620,7 @@ export declare namespace OfferUtils {
620
620
  * ```
621
621
  */
622
622
  function getConsumableUnits(params: {
623
- readonly offer: Pick<IOffer, "market" | "buy" | "tick" | "maxUnits" | "maxAssets" | "continuousFeeCap">;
623
+ readonly offer: Pick<IOffer, "market" | "buy" | "start" | "expiry" | "tick" | "maxUnits" | "maxAssets" | "continuousFeeCap">;
624
624
  readonly consumed: BigIntish;
625
625
  readonly timestamp: BigIntish;
626
626
  }): bigint;
@@ -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
- * {@link MAX_CONTINUOUS_FEE}.
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 ?? constants_js_1.MAX_CONTINUOUS_FEE);
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 `MAX_CONTINUOUS_FEE`.
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 = comparableHex(first.maker);
584
+ const expectedMaker = first.maker;
586
585
  const expectedBuy = first.buy;
587
- const expectedLoanToken = comparableHex(index_js_1.MarketParams.from(first.market).loanToken);
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 (comparableHex(offer.maker) !== expectedMaker) {
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 (comparableHex(index_js_1.MarketParams.from(offer.market).loanToken) !==
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
- comparableHex(offer.receiverIfMakerIsSeller) !==
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(params.timestamp));
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,
@@ -1,5 +1,6 @@
1
1
  import { type BigIntish } from "@morpho-org/morpho-ts";
2
2
  import { type Account, type Address, type Chain, type Client, type Hash, type Hex, type Signature, type Transport } from "viem";
3
+ import { type IOffer } from "../offers/index.js";
3
4
  import type { Payload } from "./Payload.js";
4
5
  import { type RatifierTreeInput, type TreeProof } from "./TreeUtils.js";
5
6
  declare const typedDataTypes: {
@@ -365,6 +366,40 @@ export interface EcrecoverRatifierDataParams {
365
366
  /** Ecrecover signature for the tree root. */
366
367
  readonly signature: EcrecoverSignatureInput;
367
368
  }
369
+ /** Parameters for reconstructing an EcrecoverRatifier digest from decoded ratifier data fields. */
370
+ export interface EcrecoverRatifierRootDigestParams {
371
+ /** Observed EIP-155 chain id from the log or execution context. Must match `offer.market.chainId`. */
372
+ readonly chainId: BigIntish;
373
+ /** Offer whose ratifier address and market chain id define the EIP-712 domain. */
374
+ readonly offer: IOffer;
375
+ /** Merkle root embedded in the ratifier data. */
376
+ readonly root: Hash;
377
+ /** Number of sibling hashes in the Merkle proof. */
378
+ readonly proofLength: number;
379
+ }
380
+ /** Parameters for reconstructing an EcrecoverRatifier digest from encoded ratifier data. */
381
+ export interface EcrecoverRatifierDataDigestParams {
382
+ /** Observed EIP-155 chain id from the log or execution context. Must match `offer.market.chainId`. */
383
+ readonly chainId: BigIntish;
384
+ /** Offer carried by the payload item. */
385
+ readonly offer: IOffer;
386
+ /** ABI-encoded Ecrecover ratifier data carried by the payload item. */
387
+ readonly ratifierData: Hex;
388
+ }
389
+ /** Parameters for locally verifying Ecrecover ratifier data attached to one payload item. */
390
+ export interface EcrecoverRatifierDataVerificationParams {
391
+ /** Observed EIP-155 chain id from the log or execution context. Must match `offer.market.chainId`. */
392
+ readonly chainId: BigIntish;
393
+ /** Offer carried by the payload item. */
394
+ readonly offer: IOffer;
395
+ /** ABI-encoded Ecrecover ratifier data carried by the payload item. */
396
+ readonly ratifierData: Hex;
397
+ }
398
+ /** Decoded Ecrecover ratifier data after local proof verification and signature recovery. */
399
+ export interface VerifiedEcrecoverRatifierData extends DecodedEcrecoverRatifierData {
400
+ /** Signer recovered from the Ecrecover typed-data digest. */
401
+ readonly signer: Address;
402
+ }
368
403
  /**
369
404
  * EcrecoverRatifier-specific pure utilities.
370
405
  *
@@ -501,6 +536,89 @@ export declare namespace EcrecoverRatifierUtils {
501
536
  * ```
502
537
  */
503
538
  function digest(params: EcrecoverRatifierTypedDataParams): `0x${string}`;
539
+ /**
540
+ * Builds the EcrecoverRatifier digest from a payload offer, root, and proof height.
541
+ *
542
+ * Use this on the take-side or in indexers after decoding ratifier data, when
543
+ * the original full tree is not available but the payload item includes the
544
+ * offer and proof metadata needed to reconstruct the signed digest. Pass the
545
+ * observed log or execution chain id so cross-chain-spoofed payloads are
546
+ * rejected before signature recovery.
547
+ *
548
+ * @param params.chainId - Observed EIP-155 chain id from the log or execution context.
549
+ * @param params.offer - Offer carried by the payload item.
550
+ * @param params.root - Merkle root embedded in the ratifier data.
551
+ * @param params.proofLength - Number of sibling hashes in the Merkle proof.
552
+ * @returns EIP-712 digest signed by the maker or authorized signer.
553
+ * @throws {ChainIdMismatchError} when `params.chainId` does not match `offer.market.chainId`.
554
+ * @throws {InvalidTreeHeightError} when `proofLength` exceeds 20.
555
+ * @example
556
+ * ```ts
557
+ * import { EcrecoverRatifierUtils } from "@morpho-org/midnight-sdk";
558
+ * import { zeroHash } from "viem";
559
+ *
560
+ * const digest = EcrecoverRatifierUtils.digestForRoot({
561
+ * chainId: 8453n,
562
+ * offer,
563
+ * root: zeroHash,
564
+ * proofLength: 0,
565
+ * });
566
+ * console.log(digest);
567
+ * ```
568
+ */
569
+ function digestForRoot(params: EcrecoverRatifierRootDigestParams): Hash;
570
+ /**
571
+ * Builds the EcrecoverRatifier digest from one payload item's encoded ratifier data.
572
+ *
573
+ * @param params.chainId - Observed EIP-155 chain id from the log or execution context.
574
+ * @param params.offer - Offer carried by the payload item.
575
+ * @param params.ratifierData - ABI-encoded Ecrecover ratifier data.
576
+ * @returns EIP-712 digest signed by the maker or authorized signer.
577
+ * @throws {ChainIdMismatchError} when `params.chainId` does not match `offer.market.chainId`.
578
+ * @throws {InvalidTreeHeightError} when the proof height exceeds 20.
579
+ * @example
580
+ * ```ts
581
+ * import { EcrecoverRatifierUtils } from "@morpho-org/midnight-sdk";
582
+ *
583
+ * const digest = EcrecoverRatifierUtils.digestRatifierData({
584
+ * chainId: 8453n,
585
+ * offer,
586
+ * ratifierData,
587
+ * });
588
+ * console.log(digest);
589
+ * ```
590
+ */
591
+ function digestRatifierData(params: EcrecoverRatifierDataDigestParams): Hash;
592
+ /**
593
+ * Verifies an Ecrecover ratifier-data proof and recovers its signer.
594
+ *
595
+ * This helper intentionally does not check `Midnight.isAuthorized` or
596
+ * `EcrecoverRatifier.isRootCanceled` state. Consumers must query both at the
597
+ * observed block before treating a recovered item as executable: the returned
598
+ * signer must be the maker or authorized by the maker, the offer ratifier must
599
+ * be authorized by the maker, and `isRootCanceled(maker, root)` must be false.
600
+ *
601
+ * @param params.chainId - Observed EIP-155 chain id from the log or execution context.
602
+ * @param params.offer - Offer carried by the payload item.
603
+ * @param params.ratifierData - ABI-encoded Ecrecover ratifier data.
604
+ * @returns Decoded ratifier data plus recovered signer.
605
+ * @throws {ChainIdMismatchError} when `params.chainId` does not match `offer.market.chainId`.
606
+ * @throws {InvalidEcrecoverSignatureVError} when the decoded signature `v` is not 27 or 28.
607
+ * @throws {InvalidTreeError} when the proof does not include `offer` in `root`.
608
+ * @throws {InvalidTreeHeightError} when the proof height exceeds 20.
609
+ * @example
610
+ * ```ts
611
+ * import { EcrecoverRatifierUtils } from "@morpho-org/midnight-sdk";
612
+ *
613
+ * const verified = await EcrecoverRatifierUtils.verifyRatifierData({
614
+ * chainId: 8453n,
615
+ * offer,
616
+ * ratifierData,
617
+ * });
618
+ * console.log(verified.signer);
619
+ * ```
620
+ */
621
+ function verifyRatifierData(params: EcrecoverRatifierDataVerificationParams): Promise<VerifiedEcrecoverRatifierData>;
504
622
  /**
505
623
  * Signs EcrecoverRatifier typed data through a viem client.
506
624
  *