@morpho-org/midnight-sdk 0.1.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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -46
  3. package/lib/cjs/abis.d.ts +1185 -996
  4. package/lib/cjs/abis.js +1445 -1211
  5. package/lib/cjs/api/MidnightApi.d.ts +22 -16
  6. package/lib/cjs/api/MidnightApi.js +89 -19
  7. package/lib/cjs/api/helpers.d.ts +11 -1
  8. package/lib/cjs/api/helpers.js +54 -4
  9. package/lib/cjs/api/types.d.ts +35 -9
  10. package/lib/cjs/constants.d.ts +12 -11
  11. package/lib/cjs/constants.js +14 -12
  12. package/lib/cjs/errors.d.ts +89 -1
  13. package/lib/cjs/errors.js +114 -3
  14. package/lib/cjs/fetch/Market.js +37 -3
  15. package/lib/cjs/fetch/index.d.ts +0 -1
  16. package/lib/cjs/fetch/index.js +0 -1
  17. package/lib/cjs/fetch/utils.d.ts +1 -1
  18. package/lib/cjs/fetch/utils.js +9 -2
  19. package/lib/cjs/index.d.ts +1 -1
  20. package/lib/cjs/index.js +2 -23
  21. package/lib/cjs/market/Market.d.ts +99 -33
  22. package/lib/cjs/market/Market.js +137 -28
  23. package/lib/cjs/market/MarketUtils.d.ts +61 -41
  24. package/lib/cjs/market/MarketUtils.js +108 -48
  25. package/lib/cjs/market/Position.d.ts +16 -11
  26. package/lib/cjs/market/Position.js +18 -12
  27. package/lib/cjs/market/PositionUtils.d.ts +6 -4
  28. package/lib/cjs/market/PositionUtils.js +9 -6
  29. package/lib/cjs/math/TickLib.d.ts +22 -0
  30. package/lib/cjs/math/TickLib.js +30 -0
  31. package/lib/cjs/math/index.d.ts +0 -1
  32. package/lib/cjs/math/index.js +0 -1
  33. package/lib/cjs/offers/Offer.d.ts +218 -12
  34. package/lib/cjs/offers/Offer.js +215 -7
  35. package/lib/cjs/offers/OfferUtils.d.ts +249 -143
  36. package/lib/cjs/offers/OfferUtils.js +326 -45
  37. package/lib/cjs/queries/GetPosition.d.ts +1 -1
  38. package/lib/cjs/queries/GetPosition.js +1 -1
  39. package/lib/cjs/signatures/EcrecoverRatifierUtils.d.ts +184 -38
  40. package/lib/cjs/signatures/EcrecoverRatifierUtils.js +296 -80
  41. package/lib/cjs/signatures/Group.d.ts +12 -4
  42. package/lib/cjs/signatures/Group.js +17 -7
  43. package/lib/cjs/signatures/GroupUtils.d.ts +60 -4
  44. package/lib/cjs/signatures/GroupUtils.js +57 -2
  45. package/lib/cjs/signatures/Payload.d.ts +277 -277
  46. package/lib/cjs/signatures/Payload.js +517 -534
  47. package/lib/cjs/signatures/RatifierUtils.d.ts +57 -0
  48. package/lib/cjs/signatures/RatifierUtils.js +105 -0
  49. package/lib/cjs/signatures/SetterRatifierUtils.d.ts +52 -13
  50. package/lib/cjs/signatures/SetterRatifierUtils.js +62 -19
  51. package/lib/cjs/signatures/Tree.d.ts +28 -16
  52. package/lib/cjs/signatures/Tree.js +35 -15
  53. package/lib/cjs/signatures/TreeUtils.d.ts +185 -63
  54. package/lib/cjs/signatures/TreeUtils.js +89 -79
  55. package/lib/cjs/signatures/eip712.d.ts +22 -0
  56. package/lib/cjs/signatures/eip712.js +26 -0
  57. package/lib/cjs/signatures/index.d.ts +2 -0
  58. package/lib/cjs/signatures/index.js +4 -0
  59. package/lib/cjs/signatures/offerStructInternal.d.ts +14 -0
  60. package/lib/cjs/signatures/offerStructInternal.js +72 -0
  61. package/lib/esm/abis.d.ts +1185 -996
  62. package/lib/esm/abis.js +1445 -1211
  63. package/lib/esm/api/MidnightApi.d.ts +22 -16
  64. package/lib/esm/api/MidnightApi.js +91 -21
  65. package/lib/esm/api/helpers.d.ts +11 -1
  66. package/lib/esm/api/helpers.js +54 -5
  67. package/lib/esm/api/types.d.ts +35 -9
  68. package/lib/esm/constants.d.ts +12 -11
  69. package/lib/esm/constants.js +13 -11
  70. package/lib/esm/errors.d.ts +89 -1
  71. package/lib/esm/errors.js +108 -2
  72. package/lib/esm/fetch/Market.js +38 -4
  73. package/lib/esm/fetch/index.d.ts +0 -1
  74. package/lib/esm/fetch/index.js +0 -1
  75. package/lib/esm/fetch/utils.d.ts +1 -1
  76. package/lib/esm/fetch/utils.js +9 -2
  77. package/lib/esm/index.d.ts +1 -1
  78. package/lib/esm/index.js +1 -1
  79. package/lib/esm/market/Market.d.ts +99 -33
  80. package/lib/esm/market/Market.js +137 -28
  81. package/lib/esm/market/MarketUtils.d.ts +61 -41
  82. package/lib/esm/market/MarketUtils.js +109 -49
  83. package/lib/esm/market/Position.d.ts +16 -11
  84. package/lib/esm/market/Position.js +19 -13
  85. package/lib/esm/market/PositionUtils.d.ts +6 -4
  86. package/lib/esm/market/PositionUtils.js +9 -6
  87. package/lib/esm/math/TickLib.d.ts +22 -0
  88. package/lib/esm/math/TickLib.js +31 -1
  89. package/lib/esm/math/index.d.ts +0 -1
  90. package/lib/esm/math/index.js +0 -1
  91. package/lib/esm/offers/Offer.d.ts +218 -12
  92. package/lib/esm/offers/Offer.js +216 -8
  93. package/lib/esm/offers/OfferUtils.d.ts +249 -143
  94. package/lib/esm/offers/OfferUtils.js +329 -48
  95. package/lib/esm/queries/GetPosition.d.ts +1 -1
  96. package/lib/esm/queries/GetPosition.js +1 -1
  97. package/lib/esm/signatures/EcrecoverRatifierUtils.d.ts +184 -38
  98. package/lib/esm/signatures/EcrecoverRatifierUtils.js +298 -82
  99. package/lib/esm/signatures/Group.d.ts +12 -4
  100. package/lib/esm/signatures/Group.js +17 -7
  101. package/lib/esm/signatures/GroupUtils.d.ts +60 -4
  102. package/lib/esm/signatures/GroupUtils.js +57 -2
  103. package/lib/esm/signatures/Payload.d.ts +277 -277
  104. package/lib/esm/signatures/Payload.js +517 -531
  105. package/lib/esm/signatures/RatifierUtils.d.ts +57 -0
  106. package/lib/esm/signatures/RatifierUtils.js +105 -0
  107. package/lib/esm/signatures/SetterRatifierUtils.d.ts +52 -13
  108. package/lib/esm/signatures/SetterRatifierUtils.js +62 -19
  109. package/lib/esm/signatures/Tree.d.ts +28 -16
  110. package/lib/esm/signatures/Tree.js +36 -16
  111. package/lib/esm/signatures/TreeUtils.d.ts +185 -63
  112. package/lib/esm/signatures/TreeUtils.js +91 -81
  113. package/lib/esm/signatures/eip712.d.ts +22 -0
  114. package/lib/esm/signatures/eip712.js +22 -0
  115. package/lib/esm/signatures/index.d.ts +2 -0
  116. package/lib/esm/signatures/index.js +2 -0
  117. package/lib/esm/signatures/offerStructInternal.d.ts +14 -0
  118. package/lib/esm/signatures/offerStructInternal.js +67 -0
  119. package/package.json +31 -34
  120. package/lib/cjs/fetch/Offer.d.ts +0 -74
  121. package/lib/cjs/fetch/Offer.js +0 -119
  122. package/lib/cjs/math/ConsumableUnitsLib.d.ts +0 -57
  123. package/lib/cjs/math/ConsumableUnitsLib.js +0 -77
  124. package/lib/esm/fetch/Offer.d.ts +0 -74
  125. package/lib/esm/fetch/Offer.js +0 -116
  126. package/lib/esm/math/ConsumableUnitsLib.d.ts +0 -57
  127. package/lib/esm/math/ConsumableUnitsLib.js +0 -74
  128. package/src/index.ts +0 -19
@@ -1,564 +1,550 @@
1
1
  import { MathLib } from "@morpho-org/morpho-ts";
2
2
  import { bytesToHex, decodeAbiParameters, encodeAbiParameters, hexToBytes, } from "viem";
3
- import { LIQUIDATION_CURSOR_HIGH, LIQUIDATION_CURSOR_LOW, MAX_COLLATERALS, MAX_TICK, } from "../constants.js";
4
- import { InvalidOfferParameterError } from "../errors.js";
5
- import { MarketUtils } from "../market/index.js";
3
+ import { MAX_COLLATERALS, MAX_OFFER_CAP, MAX_TICK } from "../constants.js";
4
+ import { InvalidOfferParameterError, PayloadDecodeError } from "../errors.js";
6
5
  import { Offer, OfferUtils, } from "../offers/index.js";
6
+ import { isEmptyOfferStruct, isZeroAddress } from "./offerStructInternal.js";
7
7
  /**
8
- * Current Midnight mempool payload wire version.
8
+ * Midnight mempool payload codec namespace.
9
9
  *
10
- * @example
11
- * ```ts
12
- * import { Payload } from "@morpho-org/midnight-sdk";
13
- *
14
- * console.log(Payload.CURRENT_VERSION);
15
- * ```
16
- */
17
- export const CURRENT_VERSION = 1;
18
- const VERSION_PREFIX_BYTES = 1;
19
- const LENGTH_PREFIX_BYTES = 4;
20
- const HEADER_BYTES = VERSION_PREFIX_BYTES + LENGTH_PREFIX_BYTES;
21
- /**
22
- * Upper bound on opaque bytes appended after the gzip stream (e.g. an app
23
- * attribution tag). The suffix is meant to be small, so `decode` rejects a
24
- * larger one: this keeps the `MAX_PAYLOAD_BYTES` ceiling meaningful for the
25
- * whole input. Without it a tiny declared `gzipLen` could smuggle an
26
- * arbitrarily large suffix past validation and into the indexer while still
27
- * forcing the API to receive and hex-decode the whole blob.
28
- *
29
- * @example
30
- * ```ts
31
- * import { Payload } from "@morpho-org/midnight-sdk";
32
- *
33
- * console.log(Payload.MAX_ATTRIBUTION_SUFFIX_BYTES);
34
- * ```
35
- */
36
- export const MAX_ATTRIBUTION_SUFFIX_BYTES = 256;
37
- /**
38
- * Largest fully framed wire payload, in bytes, matching the router/onchain
39
- * mempool cap.
40
- *
41
- * @example
42
- * ```ts
43
- * import { Payload } from "@morpho-org/midnight-sdk";
44
- *
45
- * console.log(Payload.MAX_PAYLOAD_BYTES);
46
- * ```
47
- */
48
- export const MAX_PAYLOAD_BYTES = 1_000_000;
49
- /**
50
- * Maximum gzip-compressed item bytes accepted by the payload codec.
51
- *
52
- * The budget reserves room for the fixed header and the largest accepted
53
- * attribution suffix so the full wire payload never exceeds
54
- * {@link MAX_PAYLOAD_BYTES}.
55
- *
56
- * @example
57
- * ```ts
58
- * import { Payload } from "@morpho-org/midnight-sdk";
59
- *
60
- * console.log(Payload.MAX_COMPRESSED_ITEMS_BYTES);
61
- * ```
10
+ * Use `Payload.encode` to turn ratified offer items into wire bytes for mempool
11
+ * publication or raw payload API validation, and `Payload.decode` to inspect
12
+ * published maker offers, ratifier data, or attribution-tagged payloads.
62
13
  */
63
- export const MAX_COMPRESSED_ITEMS_BYTES = MAX_PAYLOAD_BYTES - HEADER_BYTES - MAX_ATTRIBUTION_SUFFIX_BYTES;
64
- /**
65
- * Maximum ABI-decoded item bytes accepted by the payload codec.
66
- *
67
- * @example
68
- * ```ts
69
- * import { Payload } from "@morpho-org/midnight-sdk";
70
- *
71
- * console.log(Payload.MAX_DECOMPRESSED_ITEMS_BYTES);
72
- * ```
73
- */
74
- export const MAX_DECOMPRESSED_ITEMS_BYTES = 6_000_000;
75
- const ABI_ARRAY_HEAD_BYTES = 64;
76
- const ABI_LENGTH_LOW_OFFSET = 60;
77
- const MAX_TIMESTAMP_SECONDS = 1000000000000n - 1n;
78
- const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
79
- const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
80
- /**
81
- * Largest `0x`-prefixed hex string that can encode a wire payload.
82
- *
83
- * @example
84
- * ```ts
85
- * import { Payload } from "@morpho-org/midnight-sdk";
86
- *
87
- * console.log(Payload.MAX_PAYLOAD_HEX_LENGTH);
88
- * ```
89
- */
90
- export const MAX_PAYLOAD_HEX_LENGTH = "0x".length + MAX_PAYLOAD_BYTES * 2;
91
- /**
92
- * Largest inbound HTTP request body, in bytes, accepted by the API and
93
- * gatekeeper APIs.
94
- *
95
- * @example
96
- * ```ts
97
- * import { Payload } from "@morpho-org/midnight-sdk";
98
- *
99
- * console.log(Payload.MAX_REQUEST_BODY_BYTES);
100
- * ```
101
- */
102
- export const MAX_REQUEST_BODY_BYTES = MAX_PAYLOAD_HEX_LENGTH + 1_024;
103
- const offerStructAbiComponents = [
104
- {
105
- name: "market",
106
- type: "tuple",
107
- components: [
108
- { name: "loanToken", type: "address" },
109
- {
110
- name: "collateralParams",
111
- type: "tuple[]",
112
- components: [
113
- { name: "token", type: "address" },
114
- { name: "lltv", type: "uint256" },
115
- { name: "maxLif", type: "uint256" },
116
- { name: "oracle", type: "address" },
117
- ],
118
- },
119
- { name: "maturity", type: "uint256" },
120
- { name: "rcfThreshold", type: "uint256" },
121
- { name: "enterGate", type: "address" },
122
- { name: "liquidatorGate", type: "address" },
123
- ],
124
- },
125
- { name: "buy", type: "bool" },
126
- { name: "maker", type: "address" },
127
- { name: "start", type: "uint256" },
128
- { name: "expiry", type: "uint256" },
129
- { name: "tick", type: "uint256" },
130
- { name: "group", type: "bytes32" },
131
- { name: "callback", type: "address" },
132
- { name: "callbackData", type: "bytes" },
133
- { name: "receiverIfMakerIsSeller", type: "address" },
134
- { name: "ratifier", type: "address" },
135
- { name: "reduceOnly", type: "bool" },
136
- { name: "maxUnits", type: "uint256" },
137
- { name: "maxAssets", type: "uint256" },
138
- { name: "continuousFeeCap", type: "uint256" },
139
- ];
140
- const itemsAbi = [
141
- {
142
- name: "items",
143
- type: "tuple[]",
144
- components: [
145
- { name: "offer", type: "tuple", components: offerStructAbiComponents },
146
- { name: "ratifierData", type: "bytes" },
147
- ],
148
- },
149
- ];
150
- /**
151
- * ABI-encode `items` as `(Offer, bytes ratifierData)[]` and gzip the result,
152
- * then emit the wire payload `version || uint32(gzipLen) || gzip(...)`. The
153
- * single-byte version prefix lets future formats coexist on the wire; the
154
- * 4-byte big-endian length delimits the gzip stream so `decode` finds its end
155
- * without scanning. That delimiter is what lets a publisher append a small
156
- * opaque suffix (e.g. an attribution tag) after the gzip stream — `decode`
157
- * ignores up to `MAX_ATTRIBUTION_SUFFIX_BYTES` bytes past `gzipLen` and rejects
158
- * a larger suffix. Item order is preserved verbatim and is the order a consumer
159
- * sees on decode.
160
- *
161
- * Use after ratifier utilities have produced payload-ready items and before an
162
- * onchain mempool submission. Normal SDK maker flows should validate the tree
163
- * with `Tree.mempoolValidate` before ratification; use
164
- * `MidnightApi.validateMempoolPayload` only when validating already encoded
165
- * payload bytes.
166
- *
167
- * @param items - Per-leaf items in the order the maker committed to. Must
168
- * contain at least one item and fit within payload byte-size limits.
169
- * @returns The encoded payload as a `Hex` string, ready to include in onchain
170
- * mempool submission calldata.
171
- * @throws {DecodeError} when the item list is empty, encoding or compression
172
- * exceeds SDK byte-size limits, or a non-empty offer fails payload validation
173
- * for collateral requirements, maturity timing, expiry range, tick bounds, or
174
- * `maxUnits`/`maxAssets` cap shape.
175
- * @example
176
- * ```ts
177
- * import { Offer, Payload } from "@morpho-org/midnight-sdk";
178
- * import { zeroAddress } from "viem";
179
- *
180
- * const offer = Offer.create({
181
- * market: {
182
- * loanToken: "0x0000000000000000000000000000000000006000",
183
- * collateralParams: [
184
- * {
185
- * token: "0x0000000000000000000000000000000000007000",
186
- * lltv: 770000000000000000n,
187
- * maxLif: 1061007957559681697n,
188
- * oracle: "0x0000000000000000000000000000000000008000",
189
- * },
190
- * ],
191
- * maturity: 54_000n,
192
- * rcfThreshold: 0n,
193
- * enterGate: zeroAddress,
194
- * liquidatorGate: zeroAddress,
195
- * },
196
- * buy: true,
197
- * maker: "0x0000000000000000000000000000000000009000",
198
- * tick: 5_000n,
199
- * expiry: 3_600n,
200
- * ratifier: "0x0000000000000000000000000000000000004000",
201
- * maxUnits: 100n,
202
- * });
203
- * const encoded = await Payload.encode([{ offer, ratifierData: "0x" }]);
204
- * console.log(encoded);
205
- * ```
206
- */
207
- export async function encode(items) {
208
- if (CURRENT_VERSION > 0xff) {
209
- throw new DecodeError(`version overflow: ${CURRENT_VERSION} exceeds 255`);
210
- }
211
- assertNonEmptyItemCount(items.length);
212
- const payloadItems = items.map((item) => {
213
- const offer = OfferUtils.toStruct({ offer: item.offer });
214
- assertApiValidOfferStruct(offer);
215
- return {
216
- offer,
217
- ratifierData: item.ratifierData,
218
- };
219
- });
220
- const itemBytes = hexToBytes(encodeAbiParameters(itemsAbi, [payloadItems]));
221
- const compressed = await compressItemsPayload(itemBytes);
222
- const encoded = new Uint8Array(HEADER_BYTES + compressed.length);
223
- encoded[0] = CURRENT_VERSION;
224
- new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength).setUint32(VERSION_PREFIX_BYTES, compressed.length);
225
- encoded.set(compressed, HEADER_BYTES);
226
- return bytesToHex(encoded);
227
- }
228
- /**
229
- * Parse a wire payload back into its `(offer, ratifierData)` items. The version
230
- * byte is validated against `CURRENT_VERSION` and not surfaced — any payload
231
- * that decodes successfully is on the current wire format. The 4-byte length
232
- * prefix bounds the gzip stream; up to `MAX_ATTRIBUTION_SUFFIX_BYTES` bytes
233
- * after it (e.g. an attribution suffix appended by the publishing app) are
234
- * ignored, so a tagged payload decodes identically to an untagged one. A larger
235
- * trailing suffix is rejected so an oversized blob cannot ride a small payload
236
- * past validation and into the indexer.
237
- *
238
- * Use on the take-side, in indexers, or in diagnostics when you need to inspect
239
- * the offer structs and ratifier data that a maker published.
240
- *
241
- * @param payload - Hex payload bytes to decode.
242
- * @param options - Optional decode bounds.
243
- * @returns The decoded items in encoded order.
244
- * @throws {DecodeError} when the payload is malformed or exceeds protocol limits.
245
- * @example
246
- * ```ts
247
- * import { Offer, Payload } from "@morpho-org/midnight-sdk";
248
- * import { zeroAddress } from "viem";
249
- *
250
- * const offer = Offer.create({
251
- * market: {
252
- * loanToken: "0x0000000000000000000000000000000000006000",
253
- * collateralParams: [
254
- * {
255
- * token: "0x0000000000000000000000000000000000007000",
256
- * lltv: 770000000000000000n,
257
- * maxLif: 1061007957559681697n,
258
- * oracle: "0x0000000000000000000000000000000000008000",
259
- * },
260
- * ],
261
- * maturity: 54_000n,
262
- * rcfThreshold: 0n,
263
- * enterGate: zeroAddress,
264
- * liquidatorGate: zeroAddress,
265
- * },
266
- * buy: true,
267
- * maker: "0x0000000000000000000000000000000000009000",
268
- * tick: 5_000n,
269
- * expiry: 3_600n,
270
- * ratifier: "0x0000000000000000000000000000000000004000",
271
- * maxUnits: 100n,
272
- * });
273
- * const encoded = await Payload.encode([{ offer, ratifierData: "0x" }]);
274
- * const items = await Payload.decode(encoded);
275
- * console.log(items.length);
276
- * ```
277
- */
278
- export async function decode(payload, options) {
279
- const maxItems = resolveMaxItems(options?.maxItems);
280
- if (payload.length > MAX_PAYLOAD_HEX_LENGTH) {
281
- throw new DecodeError(`payload hex exceeds ${MAX_PAYLOAD_HEX_LENGTH} characters`);
282
- }
283
- const bytes = hexToBytes(payload);
284
- if (bytes.length < HEADER_BYTES) {
285
- throw new DecodeError("payload too short for header");
286
- }
287
- const version = bytes[0];
288
- if (version !== (CURRENT_VERSION & 0xff)) {
289
- throw new DecodeError(`invalid version: expected ${CURRENT_VERSION}, got ${version}`);
290
- }
291
- const compressedLength = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(VERSION_PREFIX_BYTES);
292
- const compressedEnd = HEADER_BYTES + compressedLength;
293
- if (compressedEnd > bytes.length) {
294
- throw new DecodeError("payload truncated before declared gzip length");
295
- }
296
- const suffixLength = bytes.length - compressedEnd;
297
- if (suffixLength > MAX_ATTRIBUTION_SUFFIX_BYTES) {
298
- throw new DecodeError(`trailing suffix exceeds ${MAX_ATTRIBUTION_SUFFIX_BYTES} bytes: got ${suffixLength}`);
299
- }
300
- const compressed = bytes.subarray(HEADER_BYTES, compressedEnd);
301
- const decoded = await decompressItemsPayload(compressed);
302
- assertAbiItemCountWithinCallerLimit(decoded, maxItems);
303
- const items = decodeItemsBytes(decoded);
304
- assertNonEmptyItemCount(items.length);
305
- assertCallerItemCountLimit(items.length, maxItems);
306
- assertCanonicalItemsBytes(items, decoded);
307
- return items;
308
- }
309
- function decodeItemsBytes(decoded) {
310
- let raw;
311
- try {
312
- [raw] = decodeAbiParameters(itemsAbi, bytesToHex(decoded));
313
- }
314
- catch (error) {
315
- throw new DecodeError("items abi decode failed", error instanceof Error ? error : undefined);
316
- }
317
- try {
318
- return raw.map((entry) => {
319
- assertApiValidOfferStruct(entry.offer);
14
+ export var Payload;
15
+ (function (Payload) {
16
+ /**
17
+ * Current Midnight mempool payload wire version.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { Payload } from "@morpho-org/midnight-sdk";
22
+ *
23
+ * console.log(Payload.CURRENT_VERSION);
24
+ * ```
25
+ */
26
+ Payload.CURRENT_VERSION = 1;
27
+ const VERSION_PREFIX_BYTES = 1;
28
+ const LENGTH_PREFIX_BYTES = 4;
29
+ const HEADER_BYTES = VERSION_PREFIX_BYTES + LENGTH_PREFIX_BYTES;
30
+ /**
31
+ * Upper bound on opaque bytes appended after the gzip stream (e.g. an app
32
+ * attribution tag). The suffix is meant to be small, so `decode` rejects a
33
+ * larger one: this keeps the `MAX_PAYLOAD_BYTES` ceiling meaningful for the
34
+ * whole input. Without it a tiny declared `gzipLen` could smuggle an
35
+ * arbitrarily large suffix past validation and into the indexer while still
36
+ * forcing the API to receive and hex-decode the whole blob.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { Payload } from "@morpho-org/midnight-sdk";
41
+ *
42
+ * console.log(Payload.MAX_ATTRIBUTION_SUFFIX_BYTES);
43
+ * ```
44
+ */
45
+ Payload.MAX_ATTRIBUTION_SUFFIX_BYTES = 256;
46
+ /**
47
+ * Largest fully framed wire payload, in bytes, matching the router/onchain
48
+ * mempool cap.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { Payload } from "@morpho-org/midnight-sdk";
53
+ *
54
+ * console.log(Payload.MAX_PAYLOAD_BYTES);
55
+ * ```
56
+ */
57
+ Payload.MAX_PAYLOAD_BYTES = 1_000_000;
58
+ /**
59
+ * Maximum gzip-compressed item bytes accepted by the payload codec.
60
+ *
61
+ * The budget reserves room for the fixed header and the largest accepted
62
+ * attribution suffix so the full wire payload never exceeds
63
+ * {@link MAX_PAYLOAD_BYTES}.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { Payload } from "@morpho-org/midnight-sdk";
68
+ *
69
+ * console.log(Payload.MAX_COMPRESSED_ITEMS_BYTES);
70
+ * ```
71
+ */
72
+ Payload.MAX_COMPRESSED_ITEMS_BYTES = Payload.MAX_PAYLOAD_BYTES - HEADER_BYTES - Payload.MAX_ATTRIBUTION_SUFFIX_BYTES;
73
+ /**
74
+ * Maximum ABI-decoded item bytes accepted by the payload codec.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { Payload } from "@morpho-org/midnight-sdk";
79
+ *
80
+ * console.log(Payload.MAX_DECOMPRESSED_ITEMS_BYTES);
81
+ * ```
82
+ */
83
+ Payload.MAX_DECOMPRESSED_ITEMS_BYTES = 6_000_000;
84
+ const ABI_ARRAY_HEAD_BYTES = 64;
85
+ const ABI_LENGTH_LOW_OFFSET = 60;
86
+ const MAX_TIMESTAMP_SECONDS = 1000000000000n - 1n;
87
+ /**
88
+ * Largest `0x`-prefixed hex string that can encode a wire payload.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * import { Payload } from "@morpho-org/midnight-sdk";
93
+ *
94
+ * console.log(Payload.MAX_PAYLOAD_HEX_LENGTH);
95
+ * ```
96
+ */
97
+ Payload.MAX_PAYLOAD_HEX_LENGTH = "0x".length + Payload.MAX_PAYLOAD_BYTES * 2;
98
+ /**
99
+ * Largest inbound HTTP request body, in bytes, accepted by the API and
100
+ * gatekeeper APIs.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { Payload } from "@morpho-org/midnight-sdk";
105
+ *
106
+ * console.log(Payload.MAX_REQUEST_BODY_BYTES);
107
+ * ```
108
+ */
109
+ Payload.MAX_REQUEST_BODY_BYTES = Payload.MAX_PAYLOAD_HEX_LENGTH + 1_024;
110
+ const offerStructAbiComponents = [
111
+ {
112
+ name: "market",
113
+ type: "tuple",
114
+ components: [
115
+ { name: "chainId", type: "uint256" },
116
+ { name: "midnight", type: "address" },
117
+ { name: "loanToken", type: "address" },
118
+ {
119
+ name: "collateralParams",
120
+ type: "tuple[]",
121
+ components: [
122
+ { name: "token", type: "address" },
123
+ { name: "lltv", type: "uint256" },
124
+ { name: "liquidationCursor", type: "uint256" },
125
+ { name: "oracle", type: "address" },
126
+ ],
127
+ },
128
+ { name: "maturity", type: "uint256" },
129
+ { name: "rcfThreshold", type: "uint256" },
130
+ { name: "enterGate", type: "address" },
131
+ { name: "liquidatorGate", type: "address" },
132
+ ],
133
+ },
134
+ { name: "buy", type: "bool" },
135
+ { name: "maker", type: "address" },
136
+ { name: "start", type: "uint256" },
137
+ { name: "expiry", type: "uint256" },
138
+ { name: "tick", type: "uint256" },
139
+ { name: "group", type: "bytes32" },
140
+ { name: "callback", type: "address" },
141
+ { name: "callbackData", type: "bytes" },
142
+ { name: "receiverIfMakerIsSeller", type: "address" },
143
+ { name: "ratifier", type: "address" },
144
+ { name: "reduceOnly", type: "bool" },
145
+ { name: "maxUnits", type: "uint256" },
146
+ { name: "maxAssets", type: "uint256" },
147
+ { name: "continuousFeeCap", type: "uint256" },
148
+ ];
149
+ const itemsAbi = [
150
+ {
151
+ name: "items",
152
+ type: "tuple[]",
153
+ components: [
154
+ { name: "offer", type: "tuple", components: offerStructAbiComponents },
155
+ { name: "ratifierData", type: "bytes" },
156
+ ],
157
+ },
158
+ ];
159
+ /**
160
+ * ABI-encode `items` as `(Offer, bytes ratifierData)[]` and gzip the result,
161
+ * then emit the wire payload `version || uint32(gzipLen) || gzip(...)`. The
162
+ * single-byte version prefix lets future formats coexist on the wire; the
163
+ * 4-byte big-endian length delimits the gzip stream so `decode` finds its end
164
+ * without scanning. That delimiter is what lets a publisher append a small
165
+ * opaque suffix (e.g. an attribution tag) after the gzip stream — `decode`
166
+ * ignores up to `MAX_ATTRIBUTION_SUFFIX_BYTES` bytes past `gzipLen` and rejects
167
+ * a larger suffix. Item order is preserved verbatim and is the order a consumer
168
+ * sees on decode.
169
+ *
170
+ * Use after ratifier utilities have produced payload-ready items and before an
171
+ * onchain mempool submission. Normal SDK maker flows should validate the tree
172
+ * with `Tree.mempoolValidate` before ratification; use
173
+ * `MidnightApi.validateMempoolPayload` only when validating already encoded
174
+ * payload bytes.
175
+ *
176
+ * @param items - Per-leaf items in the order the maker committed to. Must
177
+ * contain at least one item and fit within payload byte-size limits.
178
+ * @returns The encoded payload as a `Hex` string, ready to include in onchain
179
+ * mempool submission calldata.
180
+ * @throws {PayloadDecodeError} when the item list is empty, encoding or compression
181
+ * exceeds SDK byte-size limits, or a non-empty offer fails payload validation
182
+ * for collateral requirements, maturity timing, expiry range, tick bounds, or
183
+ * `maxUnits`/`maxAssets` cap shape.
184
+ * @example
185
+ * ```ts
186
+ * import { Offer, Payload } from "@morpho-org/midnight-sdk";
187
+ * import { zeroAddress } from "viem";
188
+ *
189
+ * const offer = Offer.create({
190
+ * market: {
191
+ * chainId: 8453,
192
+ * midnight: "0x0000000000000000000000000000000000001000",
193
+ * loanToken: "0x0000000000000000000000000000000000006000",
194
+ * collateralParams: [
195
+ * {
196
+ * token: "0x0000000000000000000000000000000000007000",
197
+ * lltv: 770000000000000000n,
198
+ * liquidationCursor: 250000000000000000n,
199
+ * oracle: "0x0000000000000000000000000000000000008000",
200
+ * },
201
+ * ],
202
+ * maturity: 54_000n,
203
+ * rcfThreshold: 0n,
204
+ * enterGate: zeroAddress,
205
+ * liquidatorGate: zeroAddress,
206
+ * },
207
+ * buy: true,
208
+ * maker: "0x0000000000000000000000000000000000009000",
209
+ * tick: 5_000n,
210
+ * expiry: 3_600n,
211
+ * ratifier: "0x0000000000000000000000000000000000004000",
212
+ * maxUnits: 100n,
213
+ * });
214
+ * const encoded = await Payload.encode([{ offer, ratifierData: "0x" }]);
215
+ * console.log(encoded);
216
+ * ```
217
+ */
218
+ async function encode(items) {
219
+ if (Payload.CURRENT_VERSION > 0xff) {
220
+ throw new PayloadDecodeError(`version overflow: ${Payload.CURRENT_VERSION} exceeds 255`);
221
+ }
222
+ assertNonEmptyItemCount(items.length);
223
+ const payloadItems = items.map((item) => {
224
+ const offer = OfferUtils.toStruct({ offer: item.offer });
225
+ assertApiValidOfferStruct(offer);
320
226
  return {
321
- offer: Offer.from(entry.offer),
322
- ratifierData: entry.ratifierData,
227
+ offer,
228
+ ratifierData: item.ratifierData,
323
229
  };
324
230
  });
325
- }
326
- catch (error) {
327
- if (error instanceof DecodeError)
231
+ const itemBytes = hexToBytes(encodeAbiParameters(itemsAbi, [payloadItems]));
232
+ const compressed = await compressItemsPayload(itemBytes);
233
+ const encoded = new Uint8Array(HEADER_BYTES + compressed.length);
234
+ encoded[0] = Payload.CURRENT_VERSION;
235
+ new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength).setUint32(VERSION_PREFIX_BYTES, compressed.length);
236
+ encoded.set(compressed, HEADER_BYTES);
237
+ return bytesToHex(encoded);
238
+ }
239
+ Payload.encode = encode;
240
+ /**
241
+ * Parse a wire payload back into its `(offer, ratifierData)` items. The version
242
+ * byte is validated against `CURRENT_VERSION` and not surfaced — any payload
243
+ * that decodes successfully is on the current wire format. The 4-byte length
244
+ * prefix bounds the gzip stream; up to `MAX_ATTRIBUTION_SUFFIX_BYTES` bytes
245
+ * after it (e.g. an attribution suffix appended by the publishing app) are
246
+ * ignored, so a tagged payload decodes identically to an untagged one. A larger
247
+ * trailing suffix is rejected so an oversized blob cannot ride a small payload
248
+ * past validation and into the indexer.
249
+ *
250
+ * Use on the take-side, in indexers, or in diagnostics when you need to inspect
251
+ * the offer structs and ratifier data that a maker published.
252
+ *
253
+ * @param payload - Hex payload bytes to decode.
254
+ * @param options - Optional decode bounds.
255
+ * @returns The decoded items in encoded order.
256
+ * @throws {PayloadDecodeError} when the payload is malformed or exceeds protocol limits.
257
+ * @example
258
+ * ```ts
259
+ * import { Offer, Payload } from "@morpho-org/midnight-sdk";
260
+ * import { zeroAddress } from "viem";
261
+ *
262
+ * const offer = Offer.create({
263
+ * market: {
264
+ * chainId: 8453,
265
+ * midnight: "0x0000000000000000000000000000000000001000",
266
+ * loanToken: "0x0000000000000000000000000000000000006000",
267
+ * collateralParams: [
268
+ * {
269
+ * token: "0x0000000000000000000000000000000000007000",
270
+ * lltv: 770000000000000000n,
271
+ * liquidationCursor: 250000000000000000n,
272
+ * oracle: "0x0000000000000000000000000000000000008000",
273
+ * },
274
+ * ],
275
+ * maturity: 54_000n,
276
+ * rcfThreshold: 0n,
277
+ * enterGate: zeroAddress,
278
+ * liquidatorGate: zeroAddress,
279
+ * },
280
+ * buy: true,
281
+ * maker: "0x0000000000000000000000000000000000009000",
282
+ * tick: 5_000n,
283
+ * expiry: 3_600n,
284
+ * ratifier: "0x0000000000000000000000000000000000004000",
285
+ * maxUnits: 100n,
286
+ * });
287
+ * const encoded = await Payload.encode([{ offer, ratifierData: "0x" }]);
288
+ * const items = await Payload.decode(encoded);
289
+ * console.log(items.length);
290
+ * ```
291
+ */
292
+ async function decode(payload, options) {
293
+ const maxItems = resolveMaxItems(options?.maxItems);
294
+ if (payload.length > Payload.MAX_PAYLOAD_HEX_LENGTH) {
295
+ throw new PayloadDecodeError(`payload hex exceeds ${Payload.MAX_PAYLOAD_HEX_LENGTH} characters`);
296
+ }
297
+ const bytes = hexToBytes(payload);
298
+ if (bytes.length < HEADER_BYTES) {
299
+ throw new PayloadDecodeError("payload too short for header");
300
+ }
301
+ const version = bytes[0];
302
+ if (version !== (Payload.CURRENT_VERSION & 0xff)) {
303
+ throw new PayloadDecodeError(`invalid version: expected ${Payload.CURRENT_VERSION}, got ${version}`);
304
+ }
305
+ const compressedLength = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(VERSION_PREFIX_BYTES);
306
+ const compressedEnd = HEADER_BYTES + compressedLength;
307
+ if (compressedEnd > bytes.length) {
308
+ throw new PayloadDecodeError("payload truncated before declared gzip length");
309
+ }
310
+ const suffixLength = bytes.length - compressedEnd;
311
+ if (suffixLength > Payload.MAX_ATTRIBUTION_SUFFIX_BYTES) {
312
+ throw new PayloadDecodeError(`trailing suffix exceeds ${Payload.MAX_ATTRIBUTION_SUFFIX_BYTES} bytes: got ${suffixLength}`);
313
+ }
314
+ const compressed = bytes.subarray(HEADER_BYTES, compressedEnd);
315
+ const decoded = await decompressItemsPayload(compressed);
316
+ assertAbiItemCountWithinCallerLimit(decoded, maxItems);
317
+ const items = decodeItemsBytes(decoded);
318
+ assertNonEmptyItemCount(items.length);
319
+ assertCallerItemCountLimit(items.length, maxItems);
320
+ assertCanonicalItemsBytes(items, decoded);
321
+ return items;
322
+ }
323
+ Payload.decode = decode;
324
+ function decodeItemsBytes(decoded) {
325
+ let raw;
326
+ try {
327
+ [raw] = decodeAbiParameters(itemsAbi, bytesToHex(decoded));
328
+ }
329
+ catch (error) {
330
+ throw new PayloadDecodeError("items abi decode failed", error instanceof Error ? error : undefined);
331
+ }
332
+ try {
333
+ return raw.map((entry) => {
334
+ assertApiValidOfferStruct(entry.offer);
335
+ return {
336
+ offer: Offer.from(entry.offer),
337
+ ratifierData: entry.ratifierData,
338
+ };
339
+ });
340
+ }
341
+ catch (error) {
342
+ if (error instanceof PayloadDecodeError)
343
+ throw error;
344
+ if (error instanceof Error) {
345
+ throw new PayloadDecodeError(`invalid offer bytes: ${error.message}`, error);
346
+ }
328
347
  throw error;
329
- if (error instanceof Error) {
330
- throw new DecodeError(`invalid offer bytes: ${error.message}`, error);
331
348
  }
332
- throw error;
333
- }
334
- }
335
- function assertCanonicalItemsBytes(items, decoded) {
336
- const canonical = encodeAbiParameters(itemsAbi, [
337
- items.map((item) => ({
338
- offer: OfferUtils.toStruct({ offer: item.offer }),
339
- ratifierData: item.ratifierData,
340
- })),
341
- ]);
342
- if (canonical !== bytesToHex(decoded)) {
343
- throw new DecodeError("items payload has non-canonical ABI bytes");
344
- }
345
- }
346
- function assertApiValidOfferStruct(offer) {
347
- if (isEmptyOfferStruct(offer))
348
- return;
349
- assertCollateralParams(offer);
350
- assertSafeTimestamp("market.maturity", offer.market.maturity);
351
- assertMaturityAt15Utc(offer.market.maturity);
352
- assertSafeTimestamp("start", offer.start);
353
- assertSafeTimestamp("expiry", offer.expiry);
354
- assertApiValidOfferTimeRange(offer);
355
- if (offer.buy && !isZeroAddress(offer.receiverIfMakerIsSeller)) {
356
- throw new DecodeError("invalid offer bytes: receiverIfMakerIsSeller must be zero for buy offers");
357
- }
358
- if (offer.expiry > offer.market.maturity) {
359
- throw new DecodeError("invalid offer bytes: expiry must be before or equal to maturity");
360
- }
361
- if (offer.tick > MAX_TICK) {
362
- throw new DecodeError(`invalid offer bytes: tick exceeds maximum ${MAX_TICK}`);
363
349
  }
364
- if ((offer.maxUnits === 0n) === (offer.maxAssets === 0n)) {
365
- throw new DecodeError("invalid offer bytes: exactly one of maxUnits and maxAssets must be non-zero");
366
- }
367
- if (offer.continuousFeeCap < 0n) {
368
- throw new DecodeError("invalid offer bytes: continuousFeeCap must be non-negative");
369
- }
370
- }
371
- function assertApiValidOfferTimeRange(offer) {
372
- try {
373
- OfferUtils.validateOfferTimeRange(offer);
374
- }
375
- catch (error) {
376
- if (error instanceof InvalidOfferParameterError) {
377
- throw new DecodeError(`invalid offer bytes: ${error.message}`, error);
350
+ function assertCanonicalItemsBytes(items, decoded) {
351
+ const canonical = encodeAbiParameters(itemsAbi, [
352
+ items.map((item) => ({
353
+ offer: OfferUtils.toStruct({ offer: item.offer }),
354
+ ratifierData: item.ratifierData,
355
+ })),
356
+ ]);
357
+ if (canonical !== bytesToHex(decoded)) {
358
+ throw new PayloadDecodeError("items payload has non-canonical ABI bytes");
378
359
  }
379
- throw error;
380
- }
381
- }
382
- function assertCollateralParams(offer) {
383
- const collateralParams = offer.market.collateralParams;
384
- if (collateralParams.length === 0) {
385
- throw new DecodeError("invalid offer bytes: at least one collateral required");
386
360
  }
387
- if (collateralParams.length > Number(MAX_COLLATERALS)) {
388
- throw new DecodeError(`invalid offer bytes: market.collateralParams has ${collateralParams.length} collaterals, exceeding the maximum of ${MAX_COLLATERALS}`);
389
- }
390
- let previousToken;
391
- for (const params of collateralParams) {
392
- if (params.lltv < 0n || params.lltv > MathLib.WAD) {
393
- throw new DecodeError(`invalid offer bytes: collateral lltv must be between 0 and WAD, got ${params.lltv}`);
361
+ function assertApiValidOfferStruct(offer) {
362
+ if (isEmptyOfferStruct(offer))
363
+ return;
364
+ assertCollateralParams(offer);
365
+ assertSafeTimestamp("market.maturity", offer.market.maturity);
366
+ assertMaturityAt15Utc(offer.market.maturity);
367
+ assertSafeTimestamp("start", offer.start);
368
+ assertSafeTimestamp("expiry", offer.expiry);
369
+ assertApiValidOfferTimeRange(offer);
370
+ if (offer.buy && !isZeroAddress(offer.receiverIfMakerIsSeller)) {
371
+ throw new PayloadDecodeError("invalid offer bytes: receiverIfMakerIsSeller must be zero for buy offers");
394
372
  }
395
- const lowMaxLif = MarketUtils.getLiquidationIncentiveFactor(params, LIQUIDATION_CURSOR_LOW);
396
- const highMaxLif = MarketUtils.getLiquidationIncentiveFactor(params, LIQUIDATION_CURSOR_HIGH);
397
- if (params.maxLif !== lowMaxLif && params.maxLif !== highMaxLif) {
398
- throw new DecodeError(`invalid offer bytes: invalid collateral maxLif ${params.maxLif}`);
373
+ if (offer.expiry > offer.market.maturity) {
374
+ throw new PayloadDecodeError("invalid offer bytes: expiry must be before or equal to maturity");
399
375
  }
400
- const token = params.token.toLowerCase();
401
- if (isZeroAddress(token)) {
402
- throw new DecodeError("invalid offer bytes: collateral token must be non-zero");
376
+ if (offer.tick > MAX_TICK) {
377
+ throw new PayloadDecodeError(`invalid offer bytes: tick exceeds maximum ${MAX_TICK}`);
403
378
  }
404
- if (previousToken != null && previousToken >= token) {
405
- throw new DecodeError("invalid offer bytes: collaterals must be sorted and unique");
379
+ if ((offer.maxUnits === 0n) === (offer.maxAssets === 0n)) {
380
+ throw new PayloadDecodeError("invalid offer bytes: exactly one of maxUnits and maxAssets must be non-zero");
381
+ }
382
+ if (offer.maxUnits > MAX_OFFER_CAP) {
383
+ throw new PayloadDecodeError(`invalid offer bytes: maxUnits exceeds maximum ${MAX_OFFER_CAP}`);
384
+ }
385
+ if (offer.maxAssets > MAX_OFFER_CAP) {
386
+ throw new PayloadDecodeError(`invalid offer bytes: maxAssets exceeds maximum ${MAX_OFFER_CAP}`);
387
+ }
388
+ if (offer.continuousFeeCap < 0n) {
389
+ throw new PayloadDecodeError("invalid offer bytes: continuousFeeCap must be non-negative");
406
390
  }
407
- previousToken = token;
408
- }
409
- }
410
- function assertSafeTimestamp(field, value) {
411
- if (value > MAX_TIMESTAMP_SECONDS) {
412
- throw new DecodeError(`invalid offer bytes: ${field} exceeds ${MAX_TIMESTAMP_SECONDS}`);
413
391
  }
414
- }
415
- function assertMaturityAt15Utc(value) {
416
- const date = new Date(Number(value) * 1000);
417
- if (date.getUTCHours() !== 15 ||
418
- date.getUTCMinutes() !== 0 ||
419
- date.getUTCSeconds() !== 0 ||
420
- date.getUTCMilliseconds() !== 0) {
421
- throw new DecodeError("invalid offer bytes: maturity must be at 15:00:00 UTC");
392
+ function assertApiValidOfferTimeRange(offer) {
393
+ try {
394
+ OfferUtils.validateOfferTimeRange(offer);
395
+ }
396
+ catch (error) {
397
+ if (error instanceof InvalidOfferParameterError) {
398
+ throw new PayloadDecodeError(`invalid offer bytes: ${error.message}`, error);
399
+ }
400
+ throw error;
401
+ }
422
402
  }
423
- }
424
- function isEmptyOfferStruct(offer) {
425
- return (isZeroAddress(offer.market.loanToken) &&
426
- offer.market.collateralParams.length === 0 &&
427
- offer.market.maturity === 0n &&
428
- offer.market.rcfThreshold === 0n &&
429
- isZeroAddress(offer.market.enterGate) &&
430
- isZeroAddress(offer.market.liquidatorGate) &&
431
- offer.buy === false &&
432
- isZeroAddress(offer.maker) &&
433
- offer.start === 0n &&
434
- offer.expiry === 0n &&
435
- offer.tick === 0n &&
436
- offer.group === ZERO_BYTES32 &&
437
- isZeroAddress(offer.callback) &&
438
- offer.callbackData === "0x" &&
439
- isZeroAddress(offer.receiverIfMakerIsSeller) &&
440
- isZeroAddress(offer.ratifier) &&
441
- offer.reduceOnly === false &&
442
- offer.maxUnits === 0n &&
443
- offer.maxAssets === 0n &&
444
- offer.continuousFeeCap === 0n);
445
- }
446
- function isZeroAddress(value) {
447
- return value.toLowerCase() === ZERO_ADDRESS;
448
- }
449
- function resolveMaxItems(maxItems) {
450
- if (maxItems === undefined)
451
- return undefined;
452
- if (!Number.isInteger(maxItems) || maxItems < 1) {
453
- throw new DecodeError(`maxItems must be a positive integer: got ${maxItems}`);
403
+ function assertCollateralParams(offer) {
404
+ const collateralParams = offer.market.collateralParams;
405
+ if (collateralParams.length === 0) {
406
+ throw new PayloadDecodeError("invalid offer bytes: at least one collateral required");
407
+ }
408
+ if (collateralParams.length > Number(MAX_COLLATERALS)) {
409
+ throw new PayloadDecodeError(`invalid offer bytes: market.collateralParams has ${collateralParams.length} collaterals, exceeding the maximum of ${MAX_COLLATERALS}`);
410
+ }
411
+ let previousToken;
412
+ for (const params of collateralParams) {
413
+ if (params.lltv < 0n || params.lltv > MathLib.WAD) {
414
+ throw new PayloadDecodeError(`invalid offer bytes: collateral lltv must be between 0 and WAD, got ${params.lltv}`);
415
+ }
416
+ if (params.liquidationCursor < 0n ||
417
+ params.liquidationCursor >= MathLib.WAD) {
418
+ throw new PayloadDecodeError(`invalid offer bytes: collateral liquidationCursor must be between 0 and WAD, exclusive of WAD, got ${params.liquidationCursor}`);
419
+ }
420
+ const maxLif = MathLib.mulDivDown(MathLib.WAD, MathLib.WAD, MathLib.WAD -
421
+ MathLib.mulDivDown(params.liquidationCursor, MathLib.WAD - params.lltv, MathLib.WAD));
422
+ if (maxLif > 2n * MathLib.WAD) {
423
+ throw new PayloadDecodeError(`invalid offer bytes: computed collateral maxLif exceeds 2 WAD, got ${maxLif}`);
424
+ }
425
+ if (params.lltv !== MathLib.WAD &&
426
+ params.lltv * maxLif > 999000000000000000n * MathLib.WAD) {
427
+ throw new PayloadDecodeError(`invalid offer bytes: computed collateral maxLif ${maxLif} is too high for lltv ${params.lltv}`);
428
+ }
429
+ const token = params.token.toLowerCase();
430
+ if (isZeroAddress(token)) {
431
+ throw new PayloadDecodeError("invalid offer bytes: collateral token must be non-zero");
432
+ }
433
+ if (previousToken != null && previousToken >= token) {
434
+ throw new PayloadDecodeError("invalid offer bytes: collaterals must be sorted and unique");
435
+ }
436
+ previousToken = token;
437
+ }
454
438
  }
455
- return maxItems;
456
- }
457
- function assertNonEmptyItemCount(count) {
458
- if (count === 0)
459
- throw new DecodeError("items payload is empty");
460
- }
461
- function assertCallerItemCountLimit(count, maxItems) {
462
- if (maxItems === undefined)
463
- return;
464
- if (count > maxItems) {
465
- throw new DecodeError(`items payload exceeds ${maxItems} items`);
439
+ function assertSafeTimestamp(field, value) {
440
+ if (value > MAX_TIMESTAMP_SECONDS) {
441
+ throw new PayloadDecodeError(`invalid offer bytes: ${field} exceeds ${MAX_TIMESTAMP_SECONDS}`);
442
+ }
466
443
  }
467
- }
468
- async function compressItemsPayload(payload) {
469
- if (payload.length > MAX_DECOMPRESSED_ITEMS_BYTES) {
470
- throw new DecodeError(`decompressed items exceed ${MAX_DECOMPRESSED_ITEMS_BYTES} bytes`);
444
+ function assertMaturityAt15Utc(value) {
445
+ const date = new Date(Number(value) * 1000);
446
+ if (date.getUTCHours() !== 15 ||
447
+ date.getUTCMinutes() !== 0 ||
448
+ date.getUTCSeconds() !== 0 ||
449
+ date.getUTCMilliseconds() !== 0) {
450
+ throw new PayloadDecodeError("invalid offer bytes: maturity must be at 15:00:00 UTC");
451
+ }
471
452
  }
472
- let compressed;
473
- try {
474
- const stream = singleChunkStream(payload).pipeThrough(new CompressionStream("gzip"));
475
- compressed = new Uint8Array(await new Response(stream).arrayBuffer());
453
+ function resolveMaxItems(maxItems) {
454
+ if (maxItems === undefined)
455
+ return undefined;
456
+ if (!Number.isInteger(maxItems) || maxItems < 1) {
457
+ throw new PayloadDecodeError(`maxItems must be a positive integer: got ${maxItems}`);
458
+ }
459
+ return maxItems;
476
460
  }
477
- catch (error) {
478
- throw new DecodeError("compression failed", error instanceof Error ? error : undefined);
461
+ function assertNonEmptyItemCount(count) {
462
+ if (count === 0)
463
+ throw new PayloadDecodeError("items payload is empty");
479
464
  }
480
- if (compressed.length > MAX_COMPRESSED_ITEMS_BYTES) {
481
- throw new DecodeError(`compressed items exceed ${MAX_COMPRESSED_ITEMS_BYTES} bytes`);
465
+ function assertCallerItemCountLimit(count, maxItems) {
466
+ if (maxItems === undefined)
467
+ return;
468
+ if (count > maxItems) {
469
+ throw new PayloadDecodeError(`items payload exceeds ${maxItems} items`);
470
+ }
482
471
  }
483
- return compressed;
484
- }
485
- async function decompressItemsPayload(compressed) {
486
- if (compressed.length === 0)
487
- throw new DecodeError("decompression failed");
488
- if (compressed.length > MAX_COMPRESSED_ITEMS_BYTES) {
489
- throw new DecodeError(`compressed items exceed ${MAX_COMPRESSED_ITEMS_BYTES} bytes`);
472
+ async function compressItemsPayload(payload) {
473
+ if (payload.length > Payload.MAX_DECOMPRESSED_ITEMS_BYTES) {
474
+ throw new PayloadDecodeError(`decompressed items exceed ${Payload.MAX_DECOMPRESSED_ITEMS_BYTES} bytes`);
475
+ }
476
+ let compressed;
477
+ try {
478
+ const stream = singleChunkStream(payload).pipeThrough(new CompressionStream("gzip"));
479
+ compressed = new Uint8Array(await new Response(stream).arrayBuffer());
480
+ }
481
+ catch (error) {
482
+ throw new PayloadDecodeError("compression failed", error instanceof Error ? error : undefined);
483
+ }
484
+ if (compressed.length > Payload.MAX_COMPRESSED_ITEMS_BYTES) {
485
+ throw new PayloadDecodeError(`compressed items exceed ${Payload.MAX_COMPRESSED_ITEMS_BYTES} bytes`);
486
+ }
487
+ return compressed;
490
488
  }
491
- const reader = singleChunkStream(compressed)
492
- .pipeThrough(new DecompressionStream("gzip"))
493
- .getReader();
494
- const chunks = [];
495
- let totalLength = 0;
496
- try {
497
- while (true) {
498
- const { done, value } = await reader.read();
499
- if (done)
500
- break;
501
- totalLength += value.length;
502
- if (totalLength > MAX_DECOMPRESSED_ITEMS_BYTES) {
503
- await reader.cancel();
504
- throw new DecodeError(`decompressed items exceed ${MAX_DECOMPRESSED_ITEMS_BYTES} bytes`);
489
+ async function decompressItemsPayload(compressed) {
490
+ if (compressed.length === 0)
491
+ throw new PayloadDecodeError("decompression failed");
492
+ if (compressed.length > Payload.MAX_COMPRESSED_ITEMS_BYTES) {
493
+ throw new PayloadDecodeError(`compressed items exceed ${Payload.MAX_COMPRESSED_ITEMS_BYTES} bytes`);
494
+ }
495
+ const reader = singleChunkStream(compressed)
496
+ .pipeThrough(new DecompressionStream("gzip"))
497
+ .getReader();
498
+ const chunks = [];
499
+ let totalLength = 0;
500
+ try {
501
+ while (true) {
502
+ const { done, value } = await reader.read();
503
+ if (done)
504
+ break;
505
+ totalLength += value.length;
506
+ if (totalLength > Payload.MAX_DECOMPRESSED_ITEMS_BYTES) {
507
+ await reader.cancel();
508
+ throw new PayloadDecodeError(`decompressed items exceed ${Payload.MAX_DECOMPRESSED_ITEMS_BYTES} bytes`);
509
+ }
510
+ chunks.push(value);
505
511
  }
506
- chunks.push(value);
507
512
  }
508
- }
509
- catch (error) {
510
- if (error instanceof DecodeError)
511
- throw error;
512
- throw new DecodeError("decompression failed", error instanceof Error ? error : undefined);
513
- }
514
- const result = new Uint8Array(totalLength);
515
- let offset = 0;
516
- for (const chunk of chunks) {
517
- result.set(chunk, offset);
518
- offset += chunk.length;
519
- }
520
- return result;
521
- }
522
- function singleChunkStream(bytes) {
523
- return new ReadableStream({
524
- start(controller) {
525
- controller.enqueue(new Uint8Array(bytes));
526
- controller.close();
527
- },
528
- });
529
- }
530
- function assertAbiItemCountWithinCallerLimit(decoded, maxItems) {
531
- if (decoded.length < ABI_ARRAY_HEAD_BYTES) {
532
- throw new DecodeError("items payload truncated before ABI array head");
533
- }
534
- if (maxItems === undefined)
535
- return;
536
- for (let i = 32; i < ABI_LENGTH_LOW_OFFSET; i++) {
537
- if (decoded[i] !== 0) {
538
- throw new DecodeError(`items payload exceeds ${maxItems} items`);
513
+ catch (error) {
514
+ if (error instanceof PayloadDecodeError)
515
+ throw error;
516
+ throw new PayloadDecodeError("decompression failed", error instanceof Error ? error : undefined);
539
517
  }
518
+ const result = new Uint8Array(totalLength);
519
+ let offset = 0;
520
+ for (const chunk of chunks) {
521
+ result.set(chunk, offset);
522
+ offset += chunk.length;
523
+ }
524
+ return result;
540
525
  }
541
- const length = new DataView(decoded.buffer, decoded.byteOffset, decoded.byteLength).getUint32(ABI_LENGTH_LOW_OFFSET);
542
- if (length > maxItems) {
543
- throw new DecodeError(`items payload exceeds ${maxItems} items`);
526
+ function singleChunkStream(bytes) {
527
+ return new ReadableStream({
528
+ start(controller) {
529
+ controller.enqueue(new Uint8Array(bytes));
530
+ controller.close();
531
+ },
532
+ });
544
533
  }
545
- }
546
- /**
547
- * Error thrown when a Midnight mempool payload cannot be decoded.
548
- *
549
- * @example
550
- * ```ts
551
- * import { Payload } from "@morpho-org/midnight-sdk";
552
- *
553
- * throw new Payload.DecodeError("payload too short for header");
554
- * ```
555
- */
556
- export class DecodeError extends Error {
557
- /** Machine-readable decode failure reason without the class message prefix. */
558
- reason;
559
- constructor(reason, cause) {
560
- super(`Failed to decode payload: ${reason}`, cause ? { cause } : undefined);
561
- this.name = "Payload.DecodeError";
562
- this.reason = reason;
534
+ function assertAbiItemCountWithinCallerLimit(decoded, maxItems) {
535
+ if (decoded.length < ABI_ARRAY_HEAD_BYTES) {
536
+ throw new PayloadDecodeError("items payload truncated before ABI array head");
537
+ }
538
+ if (maxItems === undefined)
539
+ return;
540
+ for (let i = 32; i < ABI_LENGTH_LOW_OFFSET; i++) {
541
+ if (decoded[i] !== 0) {
542
+ throw new PayloadDecodeError(`items payload exceeds ${maxItems} items`);
543
+ }
544
+ }
545
+ const length = new DataView(decoded.buffer, decoded.byteOffset, decoded.byteLength).getUint32(ABI_LENGTH_LOW_OFFSET);
546
+ if (length > maxItems) {
547
+ throw new PayloadDecodeError(`items payload exceeds ${maxItems} items`);
548
+ }
563
549
  }
564
- }
550
+ })(Payload || (Payload = {}));