@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,5 +1,8 @@
1
+ import { MathLib } from "@morpho-org/morpho-ts";
2
+ import { InvalidMidnightApiResponseError } from "../errors.js";
3
+ import { TickLib } from "../math/index.js";
1
4
  import { Payload } from "../signatures/Payload.js";
2
- import { buildBookPath, mapBookMarket, mapPriceLevel, mapTakeableOffer, parseValidationResponse, requestMidnightApi, } from "./helpers.js";
5
+ import { buildBookPath, mapBookMarket, mapBoundTakeableOffers, mapPriceLevel, parseValidationResponse, requestMidnightApi, } from "./helpers.js";
3
6
  /**
4
7
  * Midnight API client and stateless helper surface for books and mempool data.
5
8
  *
@@ -217,7 +220,10 @@ export class MidnightApi {
217
220
  }),
218
221
  });
219
222
  return {
220
- data: response.data.map(mapTakeableOffer),
223
+ data: mapBoundTakeableOffers(response.data, {
224
+ marketId: input.marketId,
225
+ side: input.side,
226
+ }),
221
227
  };
222
228
  }
223
229
  /**
@@ -267,7 +273,61 @@ export class MidnightApi {
267
273
  assets: input.assets,
268
274
  },
269
275
  });
270
- const takeableOffers = response.data.takeable_offers.map(mapTakeableOffer);
276
+ const takeableOffers = mapBoundTakeableOffers(response.data.takeable_offers, {
277
+ marketId: input.marketId,
278
+ side: input.side,
279
+ });
280
+ const averageWorstPrice = "averageWorstPrice" in input && input.averageWorstPrice != null
281
+ ? input.averageWorstPrice
282
+ : "slippage" in input && input.slippage != null
283
+ ? response.data.average_worst_price
284
+ : undefined;
285
+ if (averageWorstPrice != null) {
286
+ const guard = BigInt(averageWorstPrice);
287
+ let filledUnits = 0n;
288
+ let weightedPrice = 0n;
289
+ if ("units" in input && input.units != null) {
290
+ let remainingUnits = BigInt(input.units);
291
+ for (const take of takeableOffers) {
292
+ if (remainingUnits === 0n)
293
+ break;
294
+ const price = TickLib.tickToPrice(take.offer.tick);
295
+ const filled = take.units < remainingUnits ? take.units : remainingUnits;
296
+ filledUnits += filled;
297
+ weightedPrice += filled * price;
298
+ remainingUnits -= filled;
299
+ }
300
+ }
301
+ else {
302
+ let remainingAssets = BigInt(input.assets);
303
+ for (const take of takeableOffers) {
304
+ if (remainingAssets === 0n)
305
+ break;
306
+ const price = TickLib.tickToPrice(take.offer.tick);
307
+ const takeAssets = input.side === "asks"
308
+ ? MathLib.mulDivUp(take.units, price, MathLib.WAD)
309
+ : MathLib.mulDivDown(take.units, price, MathLib.WAD);
310
+ if (takeAssets === 0n)
311
+ continue;
312
+ const fillsEntireTake = takeAssets <= remainingAssets;
313
+ const filled = fillsEntireTake
314
+ ? take.units
315
+ : MathLib.mulDiv(remainingAssets, MathLib.WAD, price, input.side === "asks" ? "Down" : "Up");
316
+ if (filled === 0n)
317
+ continue;
318
+ filledUnits += filled;
319
+ weightedPrice += filled * price;
320
+ remainingAssets = fillsEntireTake ? remainingAssets - takeAssets : 0n;
321
+ }
322
+ }
323
+ if (filledUnits > 0n) {
324
+ const averagePrice = MathLib.mulDivDown(weightedPrice, 1n, filledUnits);
325
+ const violatesGuard = input.side === "asks" ? averagePrice > guard : averagePrice < guard;
326
+ if (violatesGuard) {
327
+ throw new InvalidMidnightApiResponseError(`Midnight API quote takeable offers imply average price "${averagePrice}" outside average_worst_price guard "${guard}".`);
328
+ }
329
+ }
330
+ }
271
331
  return {
272
332
  data: {
273
333
  averageBestPrice: response.data.average_best_price,
@@ -320,7 +380,11 @@ export class MidnightApi {
320
380
  });
321
381
  return {
322
382
  cursor: response.cursor,
323
- data: response.data.map(mapTakeableOffer),
383
+ data: mapBoundTakeableOffers(response.data, {
384
+ maker: input.maker,
385
+ marketIds: input.marketIds,
386
+ groups: input.groups,
387
+ }),
324
388
  };
325
389
  }
326
390
  /**
@@ -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;
@@ -1,9 +1,11 @@
1
- import { BLUE_API_BASE_URL } from "@morpho-org/morpho-ts";
1
+ import { BLUE_API_BASE_URL, isHexEqual } from "@morpho-org/morpho-ts";
2
+ import { isAddressEqual } from "viem";
2
3
  import { InvalidMidnightApiResponseError, MidnightApiError, } from "../errors.js";
4
+ import { MarketUtils } from "../market/index.js";
3
5
  const DEFAULT_MIDNIGHT_API_URL = new URL("/v0/midnight", BLUE_API_BASE_URL);
4
6
  // Keep this literal in source so the browser ESM build does not import package.json
5
7
  // through Node-only createRequire/module shims.
6
- const MIDNIGHT_SDK_VERSION = "0.1.0";
8
+ const MIDNIGHT_SDK_VERSION = "1.0.1";
7
9
  /** @internal Sends one Midnight API request and maps non-2xx responses to SDK errors. */
8
10
  export async function requestMidnightApi(params) {
9
11
  const baseUrl = new URL(params.baseUrl ?? DEFAULT_MIDNIGHT_API_URL);
@@ -153,6 +155,50 @@ export function mapTakeableOffer(takeableOffer) {
153
155
  ratifierData: takeableOffer.ratifier_data,
154
156
  };
155
157
  }
158
+ /** @internal Maps, validates, and best-price-sorts API takeable offers. */
159
+ export function mapBoundTakeableOffers(takeableOffers, context) {
160
+ const mapped = takeableOffers.map((takeableOffer) => {
161
+ const take = mapTakeableOffer(takeableOffer);
162
+ const embeddedMarketId = MarketUtils.toId(take.offer.market);
163
+ if (!isHexEqual(embeddedMarketId, take.marketId)) {
164
+ throw new InvalidMidnightApiResponseError(`Midnight API takeable offer market_id "${take.marketId}" does not match embedded offer market "${embeddedMarketId}".`);
165
+ }
166
+ if (context.marketId != null &&
167
+ !isHexEqual(take.marketId, context.marketId)) {
168
+ throw new InvalidMidnightApiResponseError(`Midnight API takeable offer market_id "${take.marketId}" does not match requested market "${context.marketId}".`);
169
+ }
170
+ if (context.marketIds != null &&
171
+ context.marketIds.length > 0 &&
172
+ !context.marketIds.some((marketId) => isHexEqual(marketId, take.marketId))) {
173
+ throw new InvalidMidnightApiResponseError(`Midnight API takeable offer market_id "${take.marketId}" is outside the requested market_ids filter.`);
174
+ }
175
+ if (context.side != null) {
176
+ const expectedBuy = context.side === "bids";
177
+ if (take.offer.buy !== expectedBuy) {
178
+ throw new InvalidMidnightApiResponseError(`Midnight API ${context.side} takeable offer returned offer.buy "${take.offer.buy}".`);
179
+ }
180
+ }
181
+ if (context.maker != null &&
182
+ !isAddressEqual(take.offer.maker, context.maker)) {
183
+ throw new InvalidMidnightApiResponseError(`Midnight API takeable offer maker "${take.offer.maker}" does not match requested maker "${context.maker}".`);
184
+ }
185
+ if (context.groups != null &&
186
+ context.groups.length > 0 &&
187
+ !context.groups.some((group) => isHexEqual(group, take.offer.group))) {
188
+ throw new InvalidMidnightApiResponseError(`Midnight API takeable offer group "${take.offer.group}" is outside the requested groups filter.`);
189
+ }
190
+ return take;
191
+ });
192
+ if (context.side == null)
193
+ return mapped;
194
+ return [...mapped].sort((a, b) => {
195
+ if (a.offer.tick === b.offer.tick)
196
+ return 0;
197
+ if (context.side === "asks")
198
+ return a.offer.tick < b.offer.tick ? -1 : 1;
199
+ return a.offer.tick > b.offer.tick ? -1 : 1;
200
+ });
201
+ }
156
202
  /** @internal Parses the mempool validation response envelope. */
157
203
  export function parseValidationResponse(response) {
158
204
  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/esm/errors.js CHANGED
@@ -325,6 +325,25 @@ export class InvalidTypedDataSignatureError extends Error {
325
325
  this.signer = signer;
326
326
  }
327
327
  }
328
+ /**
329
+ * Thrown when decoded Ecrecover ratifier data contains a non-canonical signature `v` value.
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * import { InvalidEcrecoverSignatureVError } from "@morpho-org/midnight-sdk";
334
+ *
335
+ * throw new InvalidEcrecoverSignatureVError(29);
336
+ * ```
337
+ */
338
+ export class InvalidEcrecoverSignatureVError extends Error {
339
+ /** Invalid ECDSA recovery id. */
340
+ v;
341
+ constructor(v) {
342
+ super(`Ecrecover signature v "${v}" is invalid. Use canonical Ethereum v 27 or 28.`);
343
+ this.name = "InvalidEcrecoverSignatureVError";
344
+ this.v = v;
345
+ }
346
+ }
328
347
  /**
329
348
  * Thrown when the Midnight API returns a non-2xx response.
330
349
  *
@@ -1,7 +1,8 @@
1
1
  import { getChainAddress } from "@morpho-org/morpho-ts";
2
2
  import { readContract } from "viem/actions";
3
3
  import { midnightAbi } from "../abis.js";
4
- import { Market, MarketParams } from "../market/index.js";
4
+ import { InvalidMarketParameterError } from "../errors.js";
5
+ import { Market, MarketParams, MarketUtils } from "../market/index.js";
5
6
  import { callParameters, resolveChainId } from "./utils.js";
6
7
  /**
7
8
  * Fetches immutable market params by id.
@@ -41,7 +42,11 @@ export async function fetchMarketParams(client, params) {
41
42
  functionName: "toMarket",
42
43
  args: [params.marketId],
43
44
  });
44
- return new MarketParams(market);
45
+ return normalizeFetchedMarketParams(market, {
46
+ marketId: params.marketId,
47
+ chainId,
48
+ midnight,
49
+ });
45
50
  }
46
51
  /**
47
52
  * Fetches a hydrated market by id.
@@ -92,7 +97,11 @@ export async function fetchMarket(client, params) {
92
97
  ]);
93
98
  const [totalUnits, lossFactor, withdrawable, continuousFeeCredit, settlementFeeCbps0, settlementFeeCbps1, settlementFeeCbps2, settlementFeeCbps3, settlementFeeCbps4, settlementFeeCbps5, settlementFeeCbps6, continuousFee, tickSpacing,] = state;
94
99
  return new Market({
95
- params: new MarketParams(market),
100
+ params: normalizeFetchedMarketParams(market, {
101
+ marketId: params.marketId,
102
+ chainId,
103
+ midnight,
104
+ }),
96
105
  totalUnits,
97
106
  lossFactor,
98
107
  withdrawable,
@@ -110,3 +119,29 @@ export async function fetchMarket(client, params) {
110
119
  tickSpacing,
111
120
  });
112
121
  }
122
+ function normalizeFetchedMarketParams(market, expected) {
123
+ const marketParams = new MarketParams(market);
124
+ if (marketParams.chainId !== BigInt(expected.chainId)) {
125
+ throw new InvalidMarketParameterError({
126
+ parameter: "chainId",
127
+ value: marketParams.chainId,
128
+ instruction: `Fetched market must use client chain id "${expected.chainId}".`,
129
+ });
130
+ }
131
+ if (marketParams.midnight.toLowerCase() !== expected.midnight.toLowerCase()) {
132
+ throw new InvalidMarketParameterError({
133
+ parameter: "midnight",
134
+ value: marketParams.midnight,
135
+ instruction: `Fetched market must use Midnight contract "${expected.midnight}".`,
136
+ });
137
+ }
138
+ const actualMarketId = MarketUtils.toId(marketParams);
139
+ if (actualMarketId.toLowerCase() !== expected.marketId.toLowerCase()) {
140
+ throw new InvalidMarketParameterError({
141
+ parameter: "marketId",
142
+ value: actualMarketId,
143
+ instruction: `Fetched market must match requested market id "${expected.marketId}".`,
144
+ });
145
+ }
146
+ return marketParams;
147
+ }
@@ -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>;
@@ -1,4 +1,5 @@
1
1
  import { getChainId } from "viem/actions";
2
+ import { ChainIdMismatchError } from "../errors.js";
2
3
  /** @internal Extracts viem call parameters from Midnight fetch inputs. */
3
4
  export const callParameters = (params) => ({
4
5
  account: params.account,
@@ -8,5 +9,11 @@ export const callParameters = (params) => ({
8
9
  });
9
10
  /** @internal Returns the configured deployless mode, defaulting to enabled. */
10
11
  export const shouldUseDeployless = (params) => params.deployless ?? true;
11
- /** @internal Returns the configured viem client chain id or fetches it from RPC. */
12
- export const resolveChainId = async (client) => client.chain?.id ?? (await getChainId(client));
12
+ /** @internal Returns the RPC chain id and rejects configured-chain drift. */
13
+ export const resolveChainId = async (client) => {
14
+ const rpcChainId = await getChainId(client);
15
+ if (client.chain?.id != null && client.chain.id !== rpcChainId) {
16
+ throw new ChainIdMismatchError(client.chain.id, BigInt(rpcChainId));
17
+ }
18
+ return rpcChainId;
19
+ };
@@ -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;
@@ -1,11 +1,10 @@
1
1
  import { assertNonNegative, DivisionByZeroError, MathLib, } from "@morpho-org/morpho-ts";
2
- import { encodeAbiParameters, keccak256, zeroAddress, zeroHash } from "viem";
3
- import { DEFAULT_TICK_SPACING, MAX_CONTINUOUS_FEE, MAX_OFFER_CAP, MAX_TICK, OFFER_TYPEHASH, } from "../constants.js";
2
+ import { encodeAbiParameters, isAddressEqual, keccak256, zeroAddress, zeroHash, } from "viem";
3
+ import { DEFAULT_TICK_SPACING, MAX_OFFER_CAP, MAX_TICK, OFFER_TYPEHASH, } from "../constants.js";
4
4
  import { InvalidOfferGroupError, InvalidOfferParameterError, } from "../errors.js";
5
5
  import { Market, MarketParams, MarketUtils } from "../market/index.js";
6
6
  import { TakeAmountsLib, TickLib } from "../math/index.js";
7
7
  import { Offer, } from "./Offer.js";
8
- const comparableHex = (value) => value.toLowerCase();
9
8
  const offerHashParams = [
10
9
  { name: "typehash", type: "bytes32" },
11
10
  { name: "marketHash", type: "bytes32" },
@@ -414,8 +413,8 @@ export var OfferUtils;
414
413
  * Validates and normalizes the maximum continuous fee an offer accepts.
415
414
  *
416
415
  * Use this before `Offer.create` when a maker UI exposes fee tolerance.
417
- * `Offer.create` calls it internally and defaults omitted values to
418
- * {@link MAX_CONTINUOUS_FEE}.
416
+ * `Offer.create` calls it internally and defaults omitted values to zero so
417
+ * fresh offers fail closed unless the maker explicitly accepts a fee cap.
419
418
  *
420
419
  * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer.
421
420
  * @returns Normalized continuous fee cap.
@@ -429,7 +428,7 @@ export var OfferUtils;
429
428
  * ```
430
429
  */
431
430
  function validateContinuousFeeCap(params) {
432
- const continuousFeeCap = BigInt(params.continuousFeeCap ?? MAX_CONTINUOUS_FEE);
431
+ const continuousFeeCap = BigInt(params.continuousFeeCap ?? 0n);
433
432
  if (continuousFeeCap < 0n) {
434
433
  throw new InvalidOfferParameterError({
435
434
  parameter: "continuousFeeCap",
@@ -488,7 +487,7 @@ export var OfferUtils;
488
487
  * @param params.tickSpacing - Optional market tick spacing; defaults to `DEFAULT_TICK_SPACING`.
489
488
  * @param params.maxUnits - Optional unit cap; defaults to zero.
490
489
  * @param params.maxAssets - Optional buyer or seller asset cap; defaults to zero.
491
- * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to `MAX_CONTINUOUS_FEE`.
490
+ * @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to zero.
492
491
  * @param params.start - Optional offer start timestamp; defaults to zero.
493
492
  * @param params.expiry - Offer expiry timestamp.
494
493
  * @param params.receiverIfMakerIsSeller - Optional receiver used when maker is seller.
@@ -579,13 +578,15 @@ export var OfferUtils;
579
578
  }
580
579
  const offers = offerInputs.map((offer) => Offer.from(offer));
581
580
  const first = offers[0];
582
- const expectedMaker = comparableHex(first.maker);
581
+ const expectedMaker = first.maker;
583
582
  const expectedBuy = first.buy;
584
- const expectedLoanToken = comparableHex(MarketParams.from(first.market).loanToken);
583
+ const expectedLoanToken = MarketParams.from(first.market).loanToken;
584
+ const expectedChainId = MarketParams.from(first.market).chainId;
585
+ const expectedMidnight = MarketParams.from(first.market).midnight;
585
586
  const expectedMaxUnits = first.maxUnits;
586
587
  const expectedMaxAssets = first.maxAssets;
587
588
  for (const offer of offers) {
588
- if (comparableHex(offer.maker) !== expectedMaker) {
589
+ if (!isAddressEqual(offer.maker, expectedMaker)) {
589
590
  throw new InvalidOfferGroupError("All offers in a group must use the same maker.");
590
591
  }
591
592
  if (offer.buy !== expectedBuy) {
@@ -598,17 +599,21 @@ export var OfferUtils;
598
599
  (offer.maxAssets === 0n) === (offer.maxUnits === 0n)) {
599
600
  throw new InvalidOfferGroupError("Every offer must set exactly one non-zero uint128 cap.");
600
601
  }
601
- if (comparableHex(MarketParams.from(offer.market).loanToken) !==
602
- expectedLoanToken) {
602
+ if (!isAddressEqual(MarketParams.from(offer.market).loanToken, expectedLoanToken)) {
603
603
  throw new InvalidOfferGroupError("All offers in a group must use the same loan token.");
604
604
  }
605
+ if (MarketParams.from(offer.market).chainId !== expectedChainId) {
606
+ throw new InvalidOfferGroupError("All offers in a group must use the same chain id.");
607
+ }
608
+ if (!isAddressEqual(MarketParams.from(offer.market).midnight, expectedMidnight)) {
609
+ throw new InvalidOfferGroupError("All offers in a group must use the same Midnight contract.");
610
+ }
605
611
  if (offer.maxUnits !== expectedMaxUnits ||
606
612
  offer.maxAssets !== expectedMaxAssets) {
607
613
  throw new InvalidOfferGroupError("All offers in a group must use the same cap mode and value.");
608
614
  }
609
615
  if (offer.buy &&
610
- comparableHex(offer.receiverIfMakerIsSeller) !==
611
- comparableHex(zeroAddress)) {
616
+ !isAddressEqual(offer.receiverIfMakerIsSeller, zeroAddress)) {
612
617
  throw new InvalidOfferGroupError("Buy offers must use the zero address as receiverIfMakerIsSeller.");
613
618
  }
614
619
  }
@@ -834,10 +839,16 @@ export var OfferUtils;
834
839
  function getConsumableUnits(params) {
835
840
  const offer = params.offer;
836
841
  const consumed = BigInt(params.consumed);
842
+ const timestamp = BigInt(params.timestamp);
843
+ const start = BigInt(offer.start);
844
+ const expiry = BigInt(offer.expiry);
837
845
  const maxUnits = BigInt(offer.maxUnits);
838
846
  const maxAssets = BigInt(offer.maxAssets);
839
847
  const continuousFeeCap = BigInt(offer.continuousFeeCap);
840
848
  assertNonNegative("consumed", consumed);
849
+ assertNonNegative("timestamp", timestamp);
850
+ assertNonNegative("offer.start", start);
851
+ assertNonNegative("offer.expiry", expiry);
841
852
  assertNonNegative("offer.maxUnits", maxUnits);
842
853
  assertNonNegative("offer.maxAssets", maxAssets);
843
854
  assertNonNegative("offer.continuousFeeCap", continuousFeeCap);
@@ -850,12 +861,14 @@ export var OfferUtils;
850
861
  });
851
862
  }
852
863
  const market = Market.from(offer.market);
864
+ if (timestamp < start || timestamp > expiry)
865
+ return 0n;
853
866
  if (continuousFeeCap < BigInt(market.continuousFee))
854
867
  return 0n;
855
868
  if (maxUnits > 0n)
856
869
  return MathLib.zeroFloorSub(maxUnits, consumed);
857
870
  const remainingAssets = MathLib.zeroFloorSub(maxAssets, consumed);
858
- const settlementFee = market.getSettlementFee(market.timeToMaturity(params.timestamp));
871
+ const settlementFee = market.getSettlementFee(market.timeToMaturity(timestamp));
859
872
  if (!offer.buy) {
860
873
  const { sellerPrice } = TakeAmountsLib.prices({
861
874
  offer,
@@ -1,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
  *