@morpho-dev/router 0.0.12 → 0.0.14

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 (33) hide show
  1. package/README.md +15 -0
  2. package/dist/drizzle/0000_add-offers-table.sql +37 -0
  3. package/dist/drizzle/0001_create_offer_status_relation.sql +10 -0
  4. package/dist/drizzle/0002_add_created_at_in_offer_status_relation.sql +3 -0
  5. package/dist/drizzle/0003_add-cursor-indices-to-offers.sql +6 -0
  6. package/dist/drizzle/0004_offer-start.sql +1 -0
  7. package/dist/drizzle/0005_rename-price-token-buy.sql +8 -0
  8. package/dist/drizzle/0006_rename-buy.sql +3 -0
  9. package/dist/drizzle/0007_rename-offering.sql +3 -0
  10. package/dist/drizzle/0008_add-consumed-relation.sql +10 -0
  11. package/dist/drizzle/meta/0000_snapshot.json +344 -0
  12. package/dist/drizzle/meta/0001_snapshot.json +426 -0
  13. package/dist/drizzle/meta/0002_snapshot.json +439 -0
  14. package/dist/drizzle/meta/0003_snapshot.json +553 -0
  15. package/dist/drizzle/meta/0004_snapshot.json +559 -0
  16. package/dist/drizzle/meta/0005_snapshot.json +559 -0
  17. package/dist/drizzle/meta/0006_snapshot.json +559 -0
  18. package/dist/drizzle/meta/0007_snapshot.json +559 -0
  19. package/dist/drizzle/meta/0008_snapshot.json +635 -0
  20. package/dist/drizzle/meta/_journal.json +69 -0
  21. package/dist/index.browser.d.cts +116 -25
  22. package/dist/index.browser.d.ts +116 -25
  23. package/dist/index.browser.js +671 -70
  24. package/dist/index.browser.js.map +1 -1
  25. package/dist/index.browser.mjs +670 -72
  26. package/dist/index.browser.mjs.map +1 -1
  27. package/dist/index.node.d.cts +934 -7
  28. package/dist/index.node.d.ts +934 -7
  29. package/dist/index.node.js +2328 -4819
  30. package/dist/index.node.js.map +1 -1
  31. package/dist/index.node.mjs +2319 -4820
  32. package/dist/index.node.mjs.map +1 -1
  33. package/package.json +17 -3
@@ -3,6 +3,8 @@
3
3
  var mempool = require('@morpho-dev/mempool');
4
4
  var chains$1 = require('viem/chains');
5
5
  var viem = require('viem');
6
+ var v4 = require('zod/v4');
7
+ var zodOpenapi = require('zod-openapi');
6
8
 
7
9
  var __defProp = Object.defineProperty;
8
10
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -66,9 +68,9 @@ var chains = {
66
68
  }
67
69
  };
68
70
 
69
- // src/core/index.ts
70
- var core_exports = {};
71
- __export(core_exports, {
71
+ // src/core/Client.ts
72
+ var Client_exports = {};
73
+ __export(Client_exports, {
72
74
  HttpForbiddenError: () => HttpForbiddenError,
73
75
  HttpGetOffersFailedError: () => HttpGetOffersFailedError,
74
76
  HttpRateLimitError: () => HttpRateLimitError,
@@ -78,7 +80,647 @@ __export(core_exports, {
78
80
  get: () => get,
79
81
  match: () => match
80
82
  });
81
- var MAX_BATCH_SIZE = 100;
83
+
84
+ // src/RouterOffer.ts
85
+ var RouterOffer_exports = {};
86
+ __export(RouterOffer_exports, {
87
+ InvalidRouterOfferError: () => InvalidRouterOfferError,
88
+ OfferStatusValues: () => OfferStatusValues,
89
+ RouterOfferSchema: () => RouterOfferSchema,
90
+ from: () => from,
91
+ fromSnakeCase: () => fromSnakeCase,
92
+ random: () => random,
93
+ toSnakeCase: () => toSnakeCase
94
+ });
95
+ var OfferStatusValues = [
96
+ "valid",
97
+ "callback_not_supported",
98
+ "callback_error",
99
+ "unverified"
100
+ ];
101
+ var RouterOfferSchema = (parameters) => mempool.Offer.OfferSchema(parameters).extend({
102
+ consumed: v4.z.bigint({ coerce: true }).min(0n).max(viem.maxUint256),
103
+ status: v4.z.enum(OfferStatusValues),
104
+ metadata: v4.z.object({
105
+ issue: v4.z.string()
106
+ }).optional()
107
+ });
108
+ function from(input) {
109
+ try {
110
+ const parsedOffer = RouterOfferSchema({ omitHash: true }).parse(input);
111
+ const parsedHash = mempool.Offer.OfferHashSchema.parse(mempool.Offer.hash(parsedOffer));
112
+ return {
113
+ ...parsedOffer,
114
+ hash: parsedHash
115
+ };
116
+ } catch (error) {
117
+ throw new InvalidRouterOfferError(error);
118
+ }
119
+ }
120
+ function fromSnakeCase(input) {
121
+ return from(mempool.Format.fromSnakeCase(input));
122
+ }
123
+ function toSnakeCase(offer) {
124
+ return mempool.Format.toSnakeCase(offer);
125
+ }
126
+ function random() {
127
+ const baseOffer = mempool.Offer.random();
128
+ return from({
129
+ ...baseOffer,
130
+ status: "valid",
131
+ metadata: void 0,
132
+ consumed: 0n
133
+ });
134
+ }
135
+ var InvalidRouterOfferError = class extends mempool.Errors.BaseError {
136
+ constructor(error) {
137
+ super("Invalid router offer.", { cause: error });
138
+ __publicField(this, "name", "RouterOffer.InvalidRouterOfferError");
139
+ }
140
+ };
141
+
142
+ // src/utils/batch.ts
143
+ function* batch(array, batchSize) {
144
+ for (let i = 0; i < array.length; i += batchSize) {
145
+ yield array.slice(i, i + batchSize);
146
+ }
147
+ }
148
+
149
+ // src/utils/cursor.ts
150
+ function validateCursor(cursor) {
151
+ if (!cursor || typeof cursor !== "object") {
152
+ throw new Error("Cursor must be an object");
153
+ }
154
+ const c = cursor;
155
+ if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
156
+ throw new Error(
157
+ `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
158
+ );
159
+ }
160
+ if (!["asc", "desc"].includes(c.dir)) {
161
+ throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
162
+ }
163
+ if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
164
+ throw new Error(
165
+ `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
166
+ );
167
+ }
168
+ const validations = {
169
+ rate: {
170
+ field: "rate",
171
+ type: "string",
172
+ pattern: /^\d+$/,
173
+ error: "numeric string"
174
+ },
175
+ amount: {
176
+ field: "assets",
177
+ type: "string",
178
+ pattern: /^\d+$/,
179
+ error: "numeric string"
180
+ },
181
+ maturity: {
182
+ field: "maturity",
183
+ type: "number",
184
+ validator: (val) => val > 0,
185
+ error: "positive number"
186
+ },
187
+ expiry: {
188
+ field: "expiry",
189
+ type: "number",
190
+ validator: (val) => val > 0,
191
+ error: "positive number"
192
+ }
193
+ };
194
+ const validation = validations[c.sort];
195
+ if (!validation) {
196
+ throw new Error(`Invalid sort field: ${c.sort}`);
197
+ }
198
+ const fieldValue = c[validation.field];
199
+ if (!fieldValue) {
200
+ throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
201
+ }
202
+ if (typeof fieldValue !== validation.type) {
203
+ throw new Error(
204
+ `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
205
+ );
206
+ }
207
+ if (validation.pattern && !validation.pattern.test(fieldValue)) {
208
+ throw new Error(
209
+ `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
210
+ );
211
+ }
212
+ if (validation.validator && !validation.validator(fieldValue)) {
213
+ throw new Error(
214
+ `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
215
+ );
216
+ }
217
+ return true;
218
+ }
219
+ function encodeCursor(c) {
220
+ return Buffer.from(JSON.stringify(c)).toString("base64url");
221
+ }
222
+ function decodeCursor(token) {
223
+ if (!token) return null;
224
+ const decoded = JSON.parse(Buffer.from(token, "base64url").toString());
225
+ validateCursor(decoded);
226
+ return decoded;
227
+ }
228
+
229
+ // src/utils/wait.ts
230
+ async function wait(time) {
231
+ return new Promise((res) => setTimeout(res, time));
232
+ }
233
+
234
+ // src/utils/poll.ts
235
+ function poll(fn, { interval }) {
236
+ let active = true;
237
+ const unwatch = () => active = false;
238
+ const watch = async () => {
239
+ await wait(interval);
240
+ const poll2 = async () => {
241
+ if (!active) return;
242
+ await fn({ unpoll: unwatch });
243
+ await wait(interval);
244
+ poll2();
245
+ };
246
+ poll2();
247
+ };
248
+ watch();
249
+ return unwatch;
250
+ }
251
+
252
+ // src/core/apiSchema/requests.ts
253
+ var MAX_LIMIT = 100;
254
+ var DEFAULT_LIMIT = 20;
255
+ var MAX_LLTV = 100;
256
+ var MIN_LLTV = 0;
257
+ var GetOffersQueryParams = v4.z.object({
258
+ // Core filtering parameters
259
+ creators: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
260
+ message: "Creators must be comma-separated Ethereum addresses"
261
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
262
+ description: "Filter by multiple creator addresses (comma-separated)",
263
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
264
+ }),
265
+ side: v4.z.enum(["buy", "sell"]).optional().meta({
266
+ description: "Filter by offer type: buy offers or sell offers",
267
+ example: "buy"
268
+ }),
269
+ chains: v4.z.string().regex(/^\d+(,\d+)*$/, {
270
+ message: "Chains must be comma-separated chain IDs"
271
+ }).transform((val) => val.split(",").map(Number)).optional().meta({
272
+ description: "Filter by multiple blockchain networks (comma-separated chain IDs)",
273
+ example: "1,137,10"
274
+ }),
275
+ loan_tokens: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
276
+ message: "Loan assets must be comma-separated Ethereum addresses"
277
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
278
+ description: "Filter by multiple loan assets (comma-separated)",
279
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
280
+ }),
281
+ status: v4.z.string().regex(/^[a-zA-Z_]+(,[a-zA-Z_]+)*$/, {
282
+ message: "Status must be comma-separated status values"
283
+ }).transform((val) => val.split(",")).refine((statuses) => statuses.every((status) => OfferStatusValues.includes(status)), {
284
+ message: `Invalid status value. Must be one of: ${OfferStatusValues.join(", ")}`
285
+ }).optional().meta({
286
+ description: `Filter by multiple statuses (comma-separated). Valid values: ${OfferStatusValues.join(", ")}. By default, only offers with 'valid' status are returned.`,
287
+ example: "valid,callback_error"
288
+ }),
289
+ callback_addresses: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
290
+ message: "Callback addresses must be comma-separated Ethereum addresses"
291
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
292
+ description: "Filter by multiple callback addresses (comma-separated)",
293
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
294
+ }),
295
+ // Asset range
296
+ min_amount: v4.z.bigint({ coerce: true }).positive({
297
+ message: "Min amount must be a positive number"
298
+ }).optional().meta({
299
+ description: "Minimum amount of assets in the offer",
300
+ example: "1000"
301
+ }),
302
+ max_amount: v4.z.bigint({ coerce: true }).positive({
303
+ message: "Max amount must be a positive number"
304
+ }).optional().meta({
305
+ description: "Maximum amount of assets in the offer",
306
+ example: "10000"
307
+ }),
308
+ // Rate range
309
+ min_rate: v4.z.bigint({ coerce: true }).positive({
310
+ message: "Min rate must be a positive number"
311
+ }).optional().meta({
312
+ description: "Minimum rate per asset (in wei)",
313
+ example: "500000000000000000"
314
+ }),
315
+ max_rate: v4.z.bigint({ coerce: true }).positive({
316
+ message: "Max rate must be a positive number"
317
+ }).optional().meta({
318
+ description: "Maximum rate per asset (in wei)",
319
+ example: "1500000000000000000"
320
+ }),
321
+ // Time range
322
+ min_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
323
+ description: "Minimum maturity timestamp (Unix timestamp in seconds)",
324
+ example: "1700000000"
325
+ }),
326
+ max_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
327
+ description: "Maximum maturity timestamp (Unix timestamp in seconds)",
328
+ example: "1800000000"
329
+ }),
330
+ min_expiry: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
331
+ description: "Minimum expiry timestamp (Unix timestamp in seconds)",
332
+ example: "1700000000"
333
+ }),
334
+ max_expiry: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
335
+ description: "Maximum expiry timestamp (Unix timestamp in seconds)",
336
+ example: "1800000000"
337
+ }),
338
+ // Collateral filtering
339
+ collateral_assets: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
340
+ message: "Collateral assets must be comma-separated Ethereum addresses"
341
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
342
+ description: "Filter by multiple collateral assets (comma-separated)",
343
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
344
+ }),
345
+ collateral_oracles: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
346
+ message: "Collateral oracles must be comma-separated Ethereum addresses"
347
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
348
+ description: "Filter by multiple rate oracles (comma-separated)",
349
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
350
+ }),
351
+ collateral_tuple: v4.z.string().regex(
352
+ /^(0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)(#0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)*$/,
353
+ {
354
+ message: "Collateral tuple must be in format: asset:oracle:lltv#asset2:oracle2:lltv2. Oracle and lltv are optional. Asset must be 0x + 40 hex chars, oracle must be 0x + 40 hex chars, lltv must be a number (e.g., 80.5)."
355
+ }
356
+ ).transform((val) => {
357
+ return val.split("#").map((tuple) => {
358
+ const parts = tuple.split(":");
359
+ if (parts.length === 0 || !parts[0]) {
360
+ throw new v4.z.ZodError([
361
+ {
362
+ code: "custom",
363
+ message: "Asset address is required for each collateral tuple",
364
+ path: ["collateral_tuple"],
365
+ input: val
366
+ }
367
+ ]);
368
+ }
369
+ const asset = parts[0]?.toLowerCase();
370
+ const oracle = parts[1]?.toLowerCase();
371
+ const lltv = parts[2] ? parseFloat(parts[2]) : void 0;
372
+ if (lltv !== void 0 && (lltv < MIN_LLTV || lltv > MAX_LLTV)) {
373
+ throw new v4.z.ZodError([
374
+ {
375
+ code: "custom",
376
+ message: `LLTV must be between ${MIN_LLTV} and ${MAX_LLTV} (0-100%)`,
377
+ path: ["collateral_tuple"],
378
+ input: val
379
+ }
380
+ ]);
381
+ }
382
+ return {
383
+ asset,
384
+ oracle,
385
+ lltv
386
+ };
387
+ });
388
+ }).optional().meta({
389
+ description: "Filter by collateral combinations in format: asset:oracle:lltv#asset2:oracle2:lltv2. Oracle and lltv are optional. Use # to separate multiple combinations.",
390
+ example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:8000#0x9876543210987654321098765432109876543210::8000"
391
+ }),
392
+ min_lltv: v4.z.string().regex(/^\d+(\.\d+)?$/, {
393
+ message: "Min LLTV must be a valid number"
394
+ }).transform((val) => parseFloat(val)).pipe(v4.z.number().min(0).max(100)).optional().meta({
395
+ description: "Minimum Loan-to-Value ratio (LLTV) for collateral (percentage as decimal, e.g., 80.5 = 80.5%)",
396
+ example: "80.5"
397
+ }),
398
+ max_lltv: v4.z.string().regex(/^\d+(\.\d+)?$/, {
399
+ message: "Max LLTV must be a valid number"
400
+ }).transform((val) => parseFloat(val)).pipe(v4.z.number().min(0).max(100)).optional().meta({
401
+ description: "Maximum Loan-to-Value ratio (LLTV) for collateral (percentage as decimal, e.g., 95.5 = 95.5%)",
402
+ example: "95.5"
403
+ }),
404
+ // Sorting parameters
405
+ sort_by: v4.z.enum(["rate", "maturity", "expiry", "amount"]).optional().meta({
406
+ description: "Field to sort results by",
407
+ example: "rate"
408
+ }),
409
+ sort_order: v4.z.enum(["asc", "desc"]).optional().default("desc").meta({
410
+ description: "Sort direction: asc (ascending) or desc (descending, default)",
411
+ example: "desc"
412
+ }),
413
+ // Pagination
414
+ cursor: v4.z.string().optional().refine(
415
+ (val) => {
416
+ if (!val) return true;
417
+ try {
418
+ const decoded = decodeCursor(val);
419
+ return decoded !== null;
420
+ } catch (_error) {
421
+ return false;
422
+ }
423
+ },
424
+ {
425
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
426
+ }
427
+ ).meta({
428
+ description: "Pagination cursor in base64url-encoded format",
429
+ example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
430
+ }),
431
+ limit: v4.z.string().regex(/^[1-9]\d*$/, {
432
+ message: "Limit must be a positive integer"
433
+ }).transform((val) => Number.parseInt(val, 10)).pipe(
434
+ v4.z.number().max(MAX_LIMIT, {
435
+ message: `Limit cannot exceed ${MAX_LIMIT}`
436
+ })
437
+ ).optional().default(DEFAULT_LIMIT).meta({
438
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
439
+ example: 10
440
+ })
441
+ }).refine(
442
+ (data) => data.min_maturity === void 0 || data.max_maturity === void 0 || data.max_maturity >= data.min_maturity,
443
+ {
444
+ message: "max_maturity must be greater than or equal to min_maturity",
445
+ path: ["max_maturity"]
446
+ }
447
+ ).refine(
448
+ (data) => data.min_expiry === void 0 || data.max_expiry === void 0 || data.max_expiry >= data.min_expiry,
449
+ {
450
+ message: "max_expiry must be greater than or equal to min_expiry",
451
+ path: ["max_expiry"]
452
+ }
453
+ ).refine(
454
+ (data) => data.min_amount === void 0 || data.max_amount === void 0 || data.max_amount >= data.min_amount,
455
+ {
456
+ message: "max_amount must be greater than or equal to min_amount",
457
+ path: ["max_amount"]
458
+ }
459
+ ).refine(
460
+ (data) => data.min_rate === void 0 || data.max_rate === void 0 || data.max_rate >= data.min_rate,
461
+ {
462
+ message: "max_rate must be greater than or equal to min_rate",
463
+ path: ["max_rate"]
464
+ }
465
+ ).refine(
466
+ (data) => data.min_lltv === void 0 || data.max_lltv === void 0 || data.max_lltv >= data.min_lltv,
467
+ {
468
+ message: "max_lltv must be greater than or equal to min_lltv",
469
+ path: ["max_lltv"]
470
+ }
471
+ );
472
+ var MatchOffersQueryParams = v4.z.object({
473
+ // Required parameters
474
+ side: v4.z.enum(["buy", "sell"]).meta({
475
+ description: "The desired side of the match: 'buy' if you want to buy, 'sell' if you want to sell. If your intent is to sell, buy offers will be returned, and vice versa.",
476
+ example: "buy"
477
+ }),
478
+ chain_id: v4.z.string().regex(/^\d+$/, {
479
+ message: "Chain ID must be a positive integer"
480
+ }).transform((val) => Number.parseInt(val, 10)).pipe(v4.z.number().positive()).meta({
481
+ description: "The blockchain network chain ID",
482
+ example: "1"
483
+ }),
484
+ // Optional parameters
485
+ rate: v4.z.bigint({ coerce: true }).positive({
486
+ message: "Rate must be a positive number"
487
+ }).optional().meta({
488
+ description: "Rate per asset (in wei) for matching offers",
489
+ example: "1000000000000000000"
490
+ }),
491
+ // Collateral filtering
492
+ collaterals: v4.z.string().regex(
493
+ /^(0x[a-fA-F0-9]{40}:0x[a-fA-F0-9]{40}:\d+)(#0x[a-fA-F0-9]{40}:0x[a-fA-F0-9]{40}:\d+)*$/,
494
+ {
495
+ message: "Collaterals must be in format: asset:oracle:lltv#asset2:oracle2:lltv2. All fields are required for each collateral."
496
+ }
497
+ ).transform((val) => {
498
+ return val.split("#").map((collateral) => {
499
+ const parts = collateral.split(":");
500
+ if (parts.length !== 3) {
501
+ throw new v4.z.ZodError([
502
+ {
503
+ code: "custom",
504
+ message: "Each collateral must have exactly 3 parts: asset:oracle:lltv",
505
+ path: ["collaterals"],
506
+ input: val
507
+ }
508
+ ]);
509
+ }
510
+ const [asset, oracle, lltvStr] = parts;
511
+ if (!asset || !oracle || !lltvStr) {
512
+ throw new v4.z.ZodError([
513
+ {
514
+ code: "custom",
515
+ message: "Asset, oracle, and lltv are all required for each collateral",
516
+ path: ["collaterals"],
517
+ input: val
518
+ }
519
+ ]);
520
+ }
521
+ const lltv = BigInt(lltvStr);
522
+ if (lltv <= 0n) {
523
+ throw new v4.z.ZodError([
524
+ {
525
+ code: "custom",
526
+ message: "LLTV must be a positive number",
527
+ path: ["collaterals"],
528
+ input: val
529
+ }
530
+ ]);
531
+ }
532
+ return {
533
+ asset: asset.toLowerCase(),
534
+ oracle: oracle.toLowerCase(),
535
+ lltv
536
+ };
537
+ });
538
+ }).optional().meta({
539
+ description: "Collateral requirements in format: asset:oracle:lltv#asset2:oracle2:lltv2. Use # to separate multiple collaterals.",
540
+ example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:800000000000000000#0x9876543210987654321098765432109876543210:0xfedcbafedcbafedcbafedcbafedcbafedcbafedc:900000000000000000"
541
+ }),
542
+ // Maturity filtering
543
+ maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
544
+ description: "Exact maturity timestamp (Unix timestamp in seconds)",
545
+ example: "1700000000"
546
+ }),
547
+ min_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
548
+ description: "Minimum maturity timestamp (Unix timestamp in seconds, inclusive)",
549
+ example: "1700000000"
550
+ }),
551
+ max_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
552
+ description: "Maximum maturity timestamp (Unix timestamp in seconds, inclusive)",
553
+ example: "1800000000"
554
+ }),
555
+ // Asset and creator filtering
556
+ loan_token: v4.z.string().regex(/^0x[a-fA-F0-9]{40}$/, {
557
+ message: "Loan asset must be a valid Ethereum address"
558
+ }).transform((val) => val.toLowerCase()).optional().meta({
559
+ description: "The loan asset address to match against",
560
+ example: "0x1234567890123456789012345678901234567890"
561
+ }),
562
+ creator: v4.z.string().regex(/^0x[a-fA-F0-9]{40}$/, {
563
+ message: "Creator must be a valid Ethereum address"
564
+ }).transform((val) => val.toLowerCase()).optional().meta({
565
+ description: "Filter by a specific offer creator address",
566
+ example: "0x1234567890123456789012345678901234567890"
567
+ }),
568
+ // Status filtering
569
+ status: v4.z.string().regex(/^[a-zA-Z_]+(,[a-zA-Z_]+)*$/, {
570
+ message: "Status must be comma-separated status values"
571
+ }).transform((val) => val.split(",")).refine((statuses) => statuses.every((status) => OfferStatusValues.includes(status)), {
572
+ message: `Invalid status value. Must be one of: ${OfferStatusValues.join(", ")}`
573
+ }).optional().meta({
574
+ description: `Filter by multiple statuses (comma-separated). Valid values: ${OfferStatusValues.join(", ")}. By default, only offers with 'valid' status are returned.`,
575
+ example: "valid,callback_error"
576
+ }),
577
+ // Pagination
578
+ cursor: v4.z.string().optional().refine(
579
+ (val) => {
580
+ if (!val) return true;
581
+ try {
582
+ const decoded = decodeCursor(val);
583
+ return decoded !== null;
584
+ } catch (_error) {
585
+ return false;
586
+ }
587
+ },
588
+ {
589
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
590
+ }
591
+ ).meta({
592
+ description: "Pagination cursor in base64url-encoded format",
593
+ example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
594
+ }),
595
+ limit: v4.z.string().regex(/^[1-9]\d*$/, {
596
+ message: "Limit must be a positive integer"
597
+ }).transform((val) => Number.parseInt(val, 10)).pipe(
598
+ v4.z.number().max(MAX_LIMIT, {
599
+ message: `Limit cannot exceed ${MAX_LIMIT}`
600
+ })
601
+ ).optional().default(DEFAULT_LIMIT).meta({
602
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
603
+ example: 10
604
+ })
605
+ }).refine(
606
+ (data) => data.min_maturity === void 0 || data.max_maturity === void 0 || data.max_maturity >= data.min_maturity,
607
+ {
608
+ message: "max_maturity must be greater than or equal to min_maturity",
609
+ path: ["max_maturity"]
610
+ }
611
+ );
612
+ var schemas = {
613
+ get_offers: GetOffersQueryParams,
614
+ match_offers: MatchOffersQueryParams
615
+ };
616
+ function safeParse(action, query) {
617
+ return schemas[action].safeParse(query);
618
+ }
619
+
620
+ // src/core/apiSchema/openapi.ts
621
+ var successResponseSchema = v4.z.object({
622
+ status: v4.z.literal("success"),
623
+ cursor: v4.z.string().nullable(),
624
+ data: v4.z.array(v4.z.any()),
625
+ meta: v4.z.object({
626
+ timestamp: v4.z.string()
627
+ })
628
+ });
629
+ var errorResponseSchema = v4.z.object({
630
+ status: v4.z.literal("error"),
631
+ error: v4.z.object({
632
+ code: v4.z.string(),
633
+ message: v4.z.string(),
634
+ details: v4.z.any().optional()
635
+ }),
636
+ meta: v4.z.object({
637
+ timestamp: v4.z.string()
638
+ })
639
+ });
640
+ var paths = {
641
+ "/v1/offers": {
642
+ get: {
643
+ summary: "Get offers",
644
+ description: "Get all offers with optional filtering and pagination",
645
+ tags: ["Offers"],
646
+ requestParams: {
647
+ query: GetOffersQueryParams
648
+ },
649
+ responses: {
650
+ 200: {
651
+ description: "Success",
652
+ content: {
653
+ "application/json": {
654
+ schema: successResponseSchema
655
+ }
656
+ }
657
+ },
658
+ 400: {
659
+ description: "Bad Request",
660
+ content: {
661
+ "application/json": {
662
+ schema: errorResponseSchema
663
+ }
664
+ }
665
+ }
666
+ }
667
+ }
668
+ },
669
+ "/v1/match-offers": {
670
+ get: {
671
+ summary: "Match offers",
672
+ description: "Find offers that match specific criteria",
673
+ tags: ["Offers"],
674
+ requestParams: {
675
+ query: MatchOffersQueryParams
676
+ },
677
+ responses: {
678
+ 200: {
679
+ description: "Success",
680
+ content: {
681
+ "application/json": {
682
+ schema: successResponseSchema
683
+ }
684
+ }
685
+ },
686
+ 400: {
687
+ description: "Bad Request",
688
+ content: {
689
+ "application/json": {
690
+ schema: errorResponseSchema
691
+ }
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ };
698
+ zodOpenapi.createDocument({
699
+ openapi: "3.1.0",
700
+ info: {
701
+ title: "Router API",
702
+ version: "1.0.0",
703
+ description: "API for the Morpho Router"
704
+ },
705
+ tags: [
706
+ {
707
+ name: "Offers"
708
+ }
709
+ ],
710
+ servers: [
711
+ {
712
+ url: "https://router.morpho.dev",
713
+ description: "Production server"
714
+ },
715
+ {
716
+ url: "http://localhost:8081",
717
+ description: "Local development server"
718
+ }
719
+ ],
720
+ paths
721
+ });
722
+
723
+ // src/core/Client.ts
82
724
  function connect(opts) {
83
725
  const u = new URL(opts?.url || "https://router.morpho.dev");
84
726
  if (u.protocol !== "http:" && u.protocol !== "https:") {
@@ -181,14 +823,7 @@ async function get(config, parameters) {
181
823
  const { cursor: returnedCursor, data: offers } = await getApi(config, url);
182
824
  return {
183
825
  cursor: returnedCursor,
184
- offers: offers.map((offer) => {
185
- const baseOffer = mempool.Offer.fromSnakeCase(offer);
186
- return {
187
- ...baseOffer,
188
- status: offer.status,
189
- metadata: offer.metadata
190
- };
191
- })
826
+ offers: offers.map(fromSnakeCase)
192
827
  };
193
828
  }
194
829
  async function match(config, parameters) {
@@ -229,24 +864,29 @@ async function match(config, parameters) {
229
864
  const { cursor: returnedCursor, data: offers } = await getApi(config, url);
230
865
  return {
231
866
  cursor: returnedCursor,
232
- offers: offers.map((offer) => {
233
- const baseOffer = mempool.Offer.fromSnakeCase(offer);
234
- return {
235
- ...baseOffer,
236
- status: offer.status,
237
- metadata: offer.metadata
238
- };
239
- })
867
+ offers: offers.map(fromSnakeCase)
240
868
  };
241
869
  }
242
870
  async function getApi(config, url) {
243
- if (url.searchParams.get("limit") && Number(url.searchParams.get("limit")) > MAX_BATCH_SIZE) {
244
- throw new HttpGetOffersFailedError(
245
- `The maximum number of offers that can be returned is ${MAX_BATCH_SIZE}.`,
246
- {
247
- details: `The limit is set to ${url.searchParams.get("limit")}. Maximum is ${MAX_BATCH_SIZE}.`
248
- }
249
- );
871
+ const pathname = url.pathname;
872
+ let action;
873
+ switch (true) {
874
+ case pathname.includes("/v1/offers"):
875
+ action = "get_offers";
876
+ break;
877
+ case pathname.includes("/v1/match-offers"):
878
+ action = "match_offers";
879
+ break;
880
+ default:
881
+ throw new HttpGetOffersFailedError("Unknown endpoint", {
882
+ details: `Unsupported path: ${pathname}`
883
+ });
884
+ }
885
+ const schemaParseResult = safeParse(action, Object.fromEntries(url.searchParams));
886
+ if (!schemaParseResult.success) {
887
+ throw new HttpGetOffersFailedError(`Invalid URL parameters`, {
888
+ details: schemaParseResult.error.issues[0]?.message
889
+ });
250
890
  }
251
891
  const response = await fetch(url.toString(), {
252
892
  method: "GET",
@@ -329,48 +969,6 @@ function buildId(event) {
329
969
  }
330
970
  }
331
971
 
332
- // src/RouterOffer.ts
333
- var RouterOffer_exports = {};
334
- __export(RouterOffer_exports, {
335
- OfferStatusValues: () => OfferStatusValues
336
- });
337
- var OfferStatusValues = [
338
- "valid",
339
- "callback_not_supported",
340
- "callback_error",
341
- "unverified"
342
- ];
343
-
344
- // src/utils/batch.ts
345
- function* batch(array, batchSize) {
346
- for (let i = 0; i < array.length; i += batchSize) {
347
- yield array.slice(i, i + batchSize);
348
- }
349
- }
350
-
351
- // src/utils/wait.ts
352
- async function wait(time) {
353
- return new Promise((res) => setTimeout(res, time));
354
- }
355
-
356
- // src/utils/poll.ts
357
- function poll(fn, { interval }) {
358
- let active = true;
359
- const unwatch = () => active = false;
360
- const watch = async () => {
361
- await wait(interval);
362
- const poll2 = async () => {
363
- if (!active) return;
364
- await fn({ unpoll: unwatch });
365
- await wait(interval);
366
- poll2();
367
- };
368
- poll2();
369
- };
370
- watch();
371
- return unwatch;
372
- }
373
-
374
972
  // src/Validation.ts
375
973
  var Validation_exports = {};
376
974
  __export(Validation_exports, {
@@ -585,13 +1183,16 @@ function morpho(parameters) {
585
1183
  }
586
1184
 
587
1185
  exports.Chain = Chain_exports;
588
- exports.Router = core_exports;
1186
+ exports.Router = Client_exports;
589
1187
  exports.RouterEvent = RouterEvent_exports;
590
1188
  exports.RouterOffer = RouterOffer_exports;
591
1189
  exports.Validation = Validation_exports;
592
1190
  exports.ValidationRule = ValidationRule_exports;
593
1191
  exports.batch = batch;
1192
+ exports.decodeCursor = decodeCursor;
1193
+ exports.encodeCursor = encodeCursor;
594
1194
  exports.poll = poll;
1195
+ exports.validateCursor = validateCursor;
595
1196
  exports.wait = wait;
596
1197
  Object.keys(mempool).forEach(function (k) {
597
1198
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {