@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.
- package/lib/cjs/api/MidnightApi.js +67 -3
- package/lib/cjs/api/helpers.d.ts +11 -1
- package/lib/cjs/api/helpers.js +48 -1
- package/lib/cjs/errors.d.ts +15 -0
- package/lib/cjs/errors.js +21 -1
- package/lib/cjs/fetch/Market.js +37 -2
- package/lib/cjs/fetch/utils.d.ts +1 -1
- package/lib/cjs/fetch/utils.js +9 -2
- package/lib/cjs/offers/Offer.d.ts +1 -1
- package/lib/cjs/offers/OfferUtils.d.ts +5 -5
- package/lib/cjs/offers/OfferUtils.js +26 -13
- package/lib/cjs/signatures/EcrecoverRatifierUtils.d.ts +118 -0
- package/lib/cjs/signatures/EcrecoverRatifierUtils.js +149 -1
- package/lib/cjs/signatures/Group.js +5 -3
- package/lib/cjs/signatures/GroupUtils.d.ts +48 -0
- package/lib/cjs/signatures/GroupUtils.js +51 -0
- package/lib/cjs/signatures/RatifierUtils.js +14 -3
- package/lib/cjs/signatures/SetterRatifierUtils.d.ts +31 -0
- package/lib/cjs/signatures/SetterRatifierUtils.js +38 -0
- package/lib/cjs/signatures/Tree.js +10 -2
- package/lib/cjs/signatures/TreeUtils.d.ts +4 -1
- package/lib/cjs/signatures/TreeUtils.js +19 -6
- package/lib/cjs/signatures/eip712.d.ts +22 -0
- package/lib/cjs/signatures/eip712.js +26 -0
- package/lib/cjs/signatures/index.d.ts +1 -0
- package/lib/cjs/signatures/index.js +1 -0
- package/lib/esm/api/MidnightApi.js +68 -4
- package/lib/esm/api/helpers.d.ts +11 -1
- package/lib/esm/api/helpers.js +48 -2
- package/lib/esm/errors.d.ts +15 -0
- package/lib/esm/errors.js +19 -0
- package/lib/esm/fetch/Market.js +38 -3
- package/lib/esm/fetch/utils.d.ts +1 -1
- package/lib/esm/fetch/utils.js +9 -2
- package/lib/esm/offers/Offer.d.ts +1 -1
- package/lib/esm/offers/OfferUtils.d.ts +5 -5
- package/lib/esm/offers/OfferUtils.js +28 -15
- package/lib/esm/signatures/EcrecoverRatifierUtils.d.ts +118 -0
- package/lib/esm/signatures/EcrecoverRatifierUtils.js +151 -3
- package/lib/esm/signatures/Group.js +5 -3
- package/lib/esm/signatures/GroupUtils.d.ts +48 -0
- package/lib/esm/signatures/GroupUtils.js +51 -0
- package/lib/esm/signatures/RatifierUtils.js +15 -4
- package/lib/esm/signatures/SetterRatifierUtils.d.ts +31 -0
- package/lib/esm/signatures/SetterRatifierUtils.js +38 -0
- package/lib/esm/signatures/Tree.js +11 -3
- package/lib/esm/signatures/TreeUtils.d.ts +4 -1
- package/lib/esm/signatures/TreeUtils.js +19 -6
- package/lib/esm/signatures/eip712.d.ts +22 -0
- package/lib/esm/signatures/eip712.js +22 -0
- package/lib/esm/signatures/index.d.ts +1 -0
- package/lib/esm/signatures/index.js +1 -0
- package/package.json +8 -7
- package/src/index.ts +0 -19
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
383
|
+
data: mapBoundTakeableOffers(response.data, {
|
|
384
|
+
maker: input.maker,
|
|
385
|
+
marketIds: input.marketIds,
|
|
386
|
+
groups: input.groups,
|
|
387
|
+
}),
|
|
324
388
|
};
|
|
325
389
|
}
|
|
326
390
|
/**
|
package/lib/esm/api/helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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;
|
package/lib/esm/api/helpers.js
CHANGED
|
@@ -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
|
|
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");
|
package/lib/esm/errors.d.ts
CHANGED
|
@@ -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
|
*
|
package/lib/esm/fetch/Market.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
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
|
+
}
|
package/lib/esm/fetch/utils.d.ts
CHANGED
|
@@ -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
|
|
7
|
+
/** @internal Returns the RPC chain id and rejects configured-chain drift. */
|
|
8
8
|
export declare const resolveChainId: (client: Client) => Promise<number>;
|
package/lib/esm/fetch/utils.js
CHANGED
|
@@ -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
|
|
12
|
-
export const resolveChainId = async (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
|
|
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
|
|
2
|
+
import { type Address, type Hash } from "viem";
|
|
3
3
|
import { type BuildOfferParams, type IOffer, Offer, type OfferStruct } from "./Offer.js";
|
|
4
4
|
/**
|
|
5
5
|
* Object-compatible helpers for Midnight offer construction, grouping, and
|
|
@@ -280,8 +280,8 @@ export declare namespace OfferUtils {
|
|
|
280
280
|
* Validates and normalizes the maximum continuous fee an offer accepts.
|
|
281
281
|
*
|
|
282
282
|
* Use this before `Offer.create` when a maker UI exposes fee tolerance.
|
|
283
|
-
* `Offer.create` calls it internally and defaults omitted values to
|
|
284
|
-
*
|
|
283
|
+
* `Offer.create` calls it internally and defaults omitted values to zero so
|
|
284
|
+
* fresh offers fail closed unless the maker explicitly accepts a fee cap.
|
|
285
285
|
*
|
|
286
286
|
* @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer.
|
|
287
287
|
* @returns Normalized continuous fee cap.
|
|
@@ -339,7 +339,7 @@ export declare namespace OfferUtils {
|
|
|
339
339
|
* @param params.tickSpacing - Optional market tick spacing; defaults to `DEFAULT_TICK_SPACING`.
|
|
340
340
|
* @param params.maxUnits - Optional unit cap; defaults to zero.
|
|
341
341
|
* @param params.maxAssets - Optional buyer or seller asset cap; defaults to zero.
|
|
342
|
-
* @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to
|
|
342
|
+
* @param params.continuousFeeCap - Optional maximum market continuous fee accepted by this offer; defaults to zero.
|
|
343
343
|
* @param params.start - Optional offer start timestamp; defaults to zero.
|
|
344
344
|
* @param params.expiry - Offer expiry timestamp.
|
|
345
345
|
* @param params.receiverIfMakerIsSeller - Optional receiver used when maker is seller.
|
|
@@ -620,7 +620,7 @@ export declare namespace OfferUtils {
|
|
|
620
620
|
* ```
|
|
621
621
|
*/
|
|
622
622
|
function getConsumableUnits(params: {
|
|
623
|
-
readonly offer: Pick<IOffer, "market" | "buy" | "tick" | "maxUnits" | "maxAssets" | "continuousFeeCap">;
|
|
623
|
+
readonly offer: Pick<IOffer, "market" | "buy" | "start" | "expiry" | "tick" | "maxUnits" | "maxAssets" | "continuousFeeCap">;
|
|
624
624
|
readonly consumed: BigIntish;
|
|
625
625
|
readonly timestamp: BigIntish;
|
|
626
626
|
}): bigint;
|
|
@@ -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,
|
|
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
|
-
*
|
|
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 ??
|
|
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
|
|
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 =
|
|
581
|
+
const expectedMaker = first.maker;
|
|
583
582
|
const expectedBuy = first.buy;
|
|
584
|
-
const expectedLoanToken =
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
*
|