@morpho-dev/router 0.0.18 → 0.0.19

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.
@@ -1,9 +1,9 @@
1
- import { Errors, Offer, Format, Time, Maturity, LLTV } from '@morpho-dev/mempool';
1
+ import { Errors, LLTV, Maturity, Offer, Format, Time } from '@morpho-dev/mempool';
2
2
  export * from '@morpho-dev/mempool';
3
3
  import { base, mainnet } from 'viem/chains';
4
+ import { parseUnits, maxUint256, formatUnits, parseAbi } from 'viem';
4
5
  import { z } from 'zod/v4';
5
6
  import { createDocument } from 'zod-openapi';
6
- import { maxUint256, parseUnits, parseAbi } from 'viem';
7
7
 
8
8
  var __defProp = Object.defineProperty;
9
9
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -344,19 +344,33 @@ var GetOffersQueryParams = z.object({
344
344
  example: "1500000000000000000"
345
345
  }),
346
346
  // Time range
347
- min_maturity: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
348
- description: "Minimum maturity timestamp (Unix timestamp in seconds)",
349
- example: "1700000000"
350
- }),
351
- max_maturity: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
352
- description: "Maximum maturity timestamp (Unix timestamp in seconds)",
353
- example: "1800000000"
354
- }),
355
- min_expiry: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
347
+ min_maturity: z.coerce.number().int().positive().transform((maturity, ctx) => {
348
+ try {
349
+ return Maturity.from(maturity);
350
+ } catch (e) {
351
+ ctx.addIssue({
352
+ code: "custom",
353
+ message: e.message
354
+ });
355
+ return z.NEVER;
356
+ }
357
+ }).optional(),
358
+ max_maturity: z.coerce.number().int().positive().transform((maturity, ctx) => {
359
+ try {
360
+ return Maturity.from(maturity);
361
+ } catch (e) {
362
+ ctx.addIssue({
363
+ code: "custom",
364
+ message: e.message
365
+ });
366
+ return z.NEVER;
367
+ }
368
+ }).optional(),
369
+ min_expiry: z.coerce.number().int().optional().meta({
356
370
  description: "Minimum expiry timestamp (Unix timestamp in seconds)",
357
371
  example: "1700000000"
358
372
  }),
359
- max_expiry: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
373
+ max_expiry: z.coerce.number().int().optional().meta({
360
374
  description: "Maximum expiry timestamp (Unix timestamp in seconds)",
361
375
  example: "1800000000"
362
376
  }),
@@ -378,51 +392,81 @@ var GetOffersQueryParams = z.object({
378
392
  {
379
393
  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)."
380
394
  }
381
- ).transform((val) => {
395
+ ).transform((val, ctx) => {
382
396
  return val.split("#").map((tuple) => {
383
397
  const parts = tuple.split(":");
384
398
  if (parts.length === 0 || !parts[0]) {
385
- throw new z.ZodError([
386
- {
387
- code: "custom",
388
- message: "Asset address is required for each collateral tuple",
389
- path: ["collateral_tuple"],
390
- input: val
391
- }
392
- ]);
399
+ ctx.addIssue({
400
+ code: "custom",
401
+ message: "Asset address is required for each collateral tuple",
402
+ path: ["asset"],
403
+ input: val
404
+ });
405
+ return z.NEVER;
393
406
  }
394
407
  const asset = parts[0]?.toLowerCase();
395
408
  const oracle = parts[1]?.toLowerCase();
396
409
  const lltv = parts[2] ? parseFloat(parts[2]) : void 0;
397
410
  if (lltv !== void 0 && (lltv < MIN_LLTV || lltv > MAX_LLTV)) {
398
- throw new z.ZodError([
399
- {
411
+ ctx.addIssue({
412
+ code: "custom",
413
+ message: `LLTV must be between ${MIN_LLTV} and ${MAX_LLTV} (0-100%)`,
414
+ path: ["lltv"],
415
+ input: val
416
+ });
417
+ return z.NEVER;
418
+ }
419
+ let lltvValue;
420
+ if (lltv !== void 0) {
421
+ try {
422
+ lltvValue = LLTV.from(parseUnits(lltv.toString(), 16));
423
+ } catch (e) {
424
+ ctx.issues.push({
400
425
  code: "custom",
401
- message: `LLTV must be between ${MIN_LLTV} and ${MAX_LLTV} (0-100%)`,
402
- path: ["collateral_tuple"],
403
- input: val
404
- }
405
- ]);
426
+ message: e instanceof LLTV.InvalidLLTVError || e instanceof LLTV.InvalidOptionError ? e.message : "Invalid LLTV.",
427
+ input: lltv,
428
+ path: ["lltv"]
429
+ });
430
+ return z.NEVER;
431
+ }
406
432
  }
407
433
  return {
408
434
  asset,
409
435
  oracle,
410
- lltv
436
+ lltv: lltvValue
411
437
  };
412
438
  });
413
439
  }).optional().meta({
414
440
  description: "Filter by collateral combinations in format: asset:oracle:lltv#asset2:oracle2:lltv2. Oracle and lltv are optional. Use # to separate multiple combinations.",
415
- example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:8000#0x9876543210987654321098765432109876543210::8000"
441
+ example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:86#0x9876543210987654321098765432109876543210:94.5"
416
442
  }),
417
- min_lltv: z.string().regex(/^\d+(\.\d+)?$/, {
418
- message: "Min LLTV must be a valid number"
419
- }).transform((val) => parseFloat(val)).pipe(z.number().min(0).max(100)).optional().meta({
443
+ min_lltv: z.coerce.number().min(0, { message: "LLTV must be above 0" }).max(100, { message: "LLTV must be below 100" }).transform((lltv, ctx) => {
444
+ try {
445
+ return LLTV.from(parseUnits(lltv.toString(), 16));
446
+ } catch (e) {
447
+ ctx.addIssue({
448
+ code: "custom",
449
+ message: e.message,
450
+ input: lltv
451
+ });
452
+ return z.NEVER;
453
+ }
454
+ }).optional().meta({
420
455
  description: "Minimum Loan-to-Value ratio (LLTV) for collateral (percentage as decimal, e.g., 80.5 = 80.5%)",
421
456
  example: "80.5"
422
457
  }),
423
- max_lltv: z.string().regex(/^\d+(\.\d+)?$/, {
424
- message: "Max LLTV must be a valid number"
425
- }).transform((val) => parseFloat(val)).pipe(z.number().min(0).max(100)).optional().meta({
458
+ max_lltv: z.coerce.number().min(0, { message: "LLTV must be above 0" }).max(100, { message: "LLTV must be below 100" }).transform((lltv, ctx) => {
459
+ try {
460
+ return LLTV.from(parseUnits(lltv.toString(), 16));
461
+ } catch (e) {
462
+ ctx.addIssue({
463
+ code: "custom",
464
+ message: e.message,
465
+ input: lltv
466
+ });
467
+ return z.NEVER;
468
+ }
469
+ }).optional().meta({
426
470
  description: "Maximum Loan-to-Value ratio (LLTV) for collateral (percentage as decimal, e.g., 95.5 = 95.5%)",
427
471
  example: "95.5"
428
472
  }),
@@ -515,68 +559,89 @@ var MatchOffersQueryParams = z.object({
515
559
  }),
516
560
  // Collateral filtering
517
561
  collaterals: z.string().regex(
518
- /^(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+)*$/,
562
+ /^(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]+)?)*$/,
519
563
  {
520
564
  message: "Collaterals must be in format: asset:oracle:lltv#asset2:oracle2:lltv2. All fields are required for each collateral."
521
565
  }
522
- ).transform((val) => {
566
+ ).transform((val, ctx) => {
523
567
  return val.split("#").map((collateral) => {
524
568
  const parts = collateral.split(":");
525
569
  if (parts.length !== 3) {
526
- throw new z.ZodError([
527
- {
528
- code: "custom",
529
- message: "Each collateral must have exactly 3 parts: asset:oracle:lltv",
530
- path: ["collaterals"],
531
- input: val
532
- }
533
- ]);
570
+ ctx.addIssue({
571
+ code: "custom",
572
+ message: "Each collateral must have exactly 3 parts: asset:oracle:lltv",
573
+ path: ["collaterals"],
574
+ input: val
575
+ });
576
+ return z.NEVER;
534
577
  }
535
578
  const [asset, oracle, lltvStr] = parts;
536
579
  if (!asset || !oracle || !lltvStr) {
537
- throw new z.ZodError([
538
- {
539
- code: "custom",
540
- message: "Asset, oracle, and lltv are all required for each collateral",
541
- path: ["collaterals"],
542
- input: val
543
- }
544
- ]);
580
+ ctx.addIssue({
581
+ code: "custom",
582
+ message: "Asset, oracle, and lltv are all required for each collateral",
583
+ path: ["collaterals"],
584
+ input: val
585
+ });
545
586
  }
546
- const lltv = BigInt(lltvStr);
547
- if (lltv <= 0n) {
548
- throw new z.ZodError([
549
- {
587
+ let lltvValue;
588
+ if (lltvStr !== void 0) {
589
+ try {
590
+ lltvValue = LLTV.from(parseUnits(lltvStr, 16));
591
+ } catch (e) {
592
+ ctx.issues.push({
550
593
  code: "custom",
551
- message: "LLTV must be a positive number",
552
- path: ["collaterals"],
553
- input: val
554
- }
555
- ]);
594
+ message: e instanceof LLTV.InvalidLLTVError || e instanceof LLTV.InvalidOptionError ? e.message : "Invalid LLTV.",
595
+ input: lltvStr,
596
+ path: ["lltv"]
597
+ });
598
+ return z.NEVER;
599
+ }
556
600
  }
557
601
  return {
558
602
  asset: asset.toLowerCase(),
559
603
  oracle: oracle.toLowerCase(),
560
- lltv
604
+ lltv: lltvValue
561
605
  };
562
606
  });
563
607
  }).optional().meta({
564
608
  description: "Collateral requirements in format: asset:oracle:lltv#asset2:oracle2:lltv2. Use # to separate multiple collaterals.",
565
- example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:800000000000000000#0x9876543210987654321098765432109876543210:0xfedcbafedcbafedcbafedcbafedcbafedcbafedc:900000000000000000"
609
+ example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:86#0x9876543210987654321098765432109876543210:0xfedcbafedcbafedcbafedcbafedcbafedcbafedc:94.5"
566
610
  }),
567
611
  // Maturity filtering
568
- maturity: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
569
- description: "Exact maturity timestamp (Unix timestamp in seconds)",
570
- example: "1700000000"
571
- }),
572
- min_maturity: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
573
- description: "Minimum maturity timestamp (Unix timestamp in seconds, inclusive)",
574
- example: "1700000000"
575
- }),
576
- max_maturity: z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
577
- description: "Maximum maturity timestamp (Unix timestamp in seconds, inclusive)",
578
- example: "1800000000"
579
- }),
612
+ maturity: z.coerce.number().int().positive().transform((maturity, ctx) => {
613
+ try {
614
+ return Maturity.from(maturity);
615
+ } catch (e) {
616
+ ctx.addIssue({
617
+ code: "custom",
618
+ message: e.message
619
+ });
620
+ return z.NEVER;
621
+ }
622
+ }).optional(),
623
+ min_maturity: z.coerce.number().int().positive().transform((maturity, ctx) => {
624
+ try {
625
+ return Maturity.from(maturity);
626
+ } catch (e) {
627
+ ctx.addIssue({
628
+ code: "custom",
629
+ message: e.message
630
+ });
631
+ return z.NEVER;
632
+ }
633
+ }).optional(),
634
+ max_maturity: z.coerce.number().int().positive().transform((maturity, ctx) => {
635
+ try {
636
+ return Maturity.from(maturity);
637
+ } catch (e) {
638
+ ctx.addIssue({
639
+ code: "custom",
640
+ message: e.message
641
+ });
642
+ return z.NEVER;
643
+ }
644
+ }).optional(),
580
645
  // Asset and creator filtering
581
646
  loan_token: z.string().regex(/^0x[a-fA-F0-9]{40}$/, {
582
647
  message: "Loan asset must be a valid Ethereum address"
@@ -638,8 +703,10 @@ var schemas = {
638
703
  get_offers: GetOffersQueryParams,
639
704
  match_offers: MatchOffersQueryParams
640
705
  };
641
- function safeParse(action, query) {
642
- return schemas[action].safeParse(query);
706
+ function safeParse(action, query, error) {
707
+ return schemas[action].safeParse(query, {
708
+ error
709
+ });
643
710
  }
644
711
 
645
712
  // src/core/apiSchema/openapi.ts
@@ -691,7 +758,7 @@ var paths = {
691
758
  }
692
759
  }
693
760
  },
694
- "/v1/match-offers": {
761
+ "/v1/offers/match": {
695
762
  get: {
696
763
  summary: "Match offers",
697
764
  description: "Find offers that match specific criteria",
@@ -842,16 +909,16 @@ async function get(config, parameters) {
842
909
  } else if (lltv !== void 0) {
843
910
  result += `:`;
844
911
  }
845
- if (lltv !== void 0) result += `:${lltv}`;
912
+ if (lltv !== void 0) result += `:${formatUnits(lltv, 16)}`;
846
913
  return result;
847
914
  }).join("#");
848
915
  url.searchParams.set("collateral_tuple", tupleStr);
849
916
  }
850
917
  if (parameters.minLltv !== void 0) {
851
- url.searchParams.set("min_lltv", parameters.minLltv.toString());
918
+ url.searchParams.set("min_lltv", formatUnits(parameters.minLltv, 16));
852
919
  }
853
920
  if (parameters.maxLltv !== void 0) {
854
- url.searchParams.set("max_lltv", parameters.maxLltv.toString());
921
+ url.searchParams.set("max_lltv", formatUnits(parameters.maxLltv, 16));
855
922
  }
856
923
  if (parameters.sortBy) {
857
924
  url.searchParams.set("sort_by", parameters.sortBy);
@@ -873,14 +940,14 @@ async function get(config, parameters) {
873
940
  };
874
941
  }
875
942
  async function match(config, parameters) {
876
- const url = new URL(`${config.url.toString()}v1/match-offers`);
943
+ const url = new URL(`${config.url.toString()}v1/offers/match`);
877
944
  url.searchParams.set("side", parameters.side);
878
945
  url.searchParams.set("chain_id", parameters.chainId.toString());
879
946
  if (parameters.rate !== void 0) {
880
947
  url.searchParams.set("rate", parameters.rate.toString());
881
948
  }
882
949
  if (parameters.collaterals?.length) {
883
- const collateralsStr = parameters.collaterals.map(({ asset, oracle, lltv }) => `${asset}:${oracle}:${lltv}`).join("#");
950
+ const collateralsStr = parameters.collaterals.map(({ asset, oracle, lltv }) => `${asset}:${oracle}:${formatUnits(lltv, 16)}`).join("#");
884
951
  url.searchParams.set("collaterals", collateralsStr);
885
952
  }
886
953
  if (parameters.maturity !== void 0) {
@@ -921,7 +988,7 @@ async function getApi(config, url) {
921
988
  case pathname.includes("/v1/offers"):
922
989
  action = "get_offers";
923
990
  break;
924
- case pathname.includes("/v1/match-offers"):
991
+ case pathname.includes("/v1/offers/match"):
925
992
  action = "match_offers";
926
993
  break;
927
994
  default:
@@ -1134,6 +1201,7 @@ function memory(parameters) {
1134
1201
  }
1135
1202
  creators && (creators = creators.map((c) => c.toLowerCase()));
1136
1203
  loanTokens && (loanTokens = loanTokens.map((lt) => lt.toLowerCase()));
1204
+ callbackAddresses && (callbackAddresses = callbackAddresses.map((ca) => ca.toLowerCase()));
1137
1205
  collateralAssets && (collateralAssets = collateralAssets.map((ca) => ca.toLowerCase()));
1138
1206
  collateralOracles && (collateralOracles = collateralOracles.map((co) => co.toLowerCase()));
1139
1207
  collateralTuple && (collateralTuple = collateralTuple.map((ct) => ({
@@ -1170,12 +1238,8 @@ function memory(parameters) {
1170
1238
  )
1171
1239
  )
1172
1240
  ));
1173
- minLltv && (offers = offers.filter(
1174
- (o) => o.collaterals.every((c) => c.lltv >= LLTV.from(parseUnits(minLltv.toString(), 16)))
1175
- ));
1176
- maxLltv && (offers = offers.filter(
1177
- (o) => o.collaterals.every((c) => c.lltv <= LLTV.from(parseUnits(maxLltv.toString(), 16)))
1178
- ));
1241
+ minLltv && (offers = offers.filter((o) => o.collaterals.every((c) => c.lltv >= minLltv)));
1242
+ maxLltv && (offers = offers.filter((o) => o.collaterals.every((c) => c.lltv <= maxLltv)));
1179
1243
  offers = offers.sort((a, b) => sort(sortBy, sortOrder, a, b));
1180
1244
  let nextCursor = null;
1181
1245
  if (offers.length > limit) {
@@ -1222,8 +1286,8 @@ function memory(parameters) {
1222
1286
  limit = 20
1223
1287
  } = params;
1224
1288
  const now = Time.now();
1225
- const buy = side !== "buy";
1226
- const sortOrder = buy ? "asc" : "desc";
1289
+ const isBuying = side === "buy";
1290
+ const sortOrder = isBuying ? "desc" : "asc";
1227
1291
  let offers = Array.from(map.values()).map((o) => ({
1228
1292
  ...o,
1229
1293
  consumed: filled.get(o.chainId)?.get(o.offering.toLowerCase())?.get(o.nonce) || 0n
@@ -1236,22 +1300,27 @@ function memory(parameters) {
1236
1300
  offers = offers.filter(
1237
1301
  (o) => sortOrder === "asc" ? o.rate >= BigInt(cursor.rate) : o.rate <= BigInt(cursor.rate)
1238
1302
  );
1239
- offers = offers.filter((o) => o.hash !== cursor.hash);
1240
1303
  }
1241
- offers = offers.filter((o) => o.buy === buy);
1304
+ offers = offers.filter((o) => o.buy === !isBuying);
1242
1305
  offers = offers.filter((o) => o.chainId === BigInt(chainId));
1243
1306
  offers = offers.filter((o) => o.expiry >= now);
1244
- rate && (offers = offers.filter((o) => buy ? o.rate >= rate : o.rate <= rate));
1307
+ rate && (offers = offers.filter((o) => isBuying ? o.rate >= rate : o.rate <= rate));
1245
1308
  collaterals.length > 0 && (offers = offers.filter(
1246
- (o) => buy ? collaterals.every((c) => {
1247
- return o.collaterals.some(
1248
- (oc) => oc.asset.toLowerCase() === c.asset.toLowerCase() && oc.oracle.toLowerCase() === c.oracle.toLowerCase() && oc.lltv === c.lltv
1249
- );
1250
- }) : o.collaterals.every((oc) => {
1251
- return collaterals.some(
1252
- (c) => oc.asset.toLowerCase() === c.asset.toLowerCase() && oc.oracle.toLowerCase() === c.oracle.toLowerCase() && oc.lltv === c.lltv
1253
- );
1254
- })
1309
+ (o) => isBuying ? (
1310
+ // when wanting to buy, sell offer collaterals ⊆ user buy collaterals
1311
+ o.collaterals.every((oc) => {
1312
+ return collaterals.some(
1313
+ (c) => oc.asset.toLowerCase() === c.asset.toLowerCase() && oc.oracle.toLowerCase() === c.oracle.toLowerCase() && oc.lltv === c.lltv
1314
+ );
1315
+ })
1316
+ ) : (
1317
+ // when wanting to sell, user sell collaterals ⊆ buy offer collaterals
1318
+ collaterals.every((c) => {
1319
+ return o.collaterals.some(
1320
+ (oc) => oc.asset.toLowerCase() === c.asset.toLowerCase() && oc.oracle.toLowerCase() === c.oracle.toLowerCase() && oc.lltv === c.lltv
1321
+ );
1322
+ })
1323
+ )
1255
1324
  ));
1256
1325
  maturity && (offers = offers.filter((o) => o.maturity === maturity));
1257
1326
  minMaturity && (offers = offers.filter((o) => o.maturity >= minMaturity));
@@ -1289,6 +1358,7 @@ function memory(parameters) {
1289
1358
  }
1290
1359
  offers = Array.from(byGroup.values());
1291
1360
  offers = offers.sort((a, b) => sort("rate", sortOrder, a, b));
1361
+ cursor && (offers = offers.filter((o) => o.hash !== cursor.hash));
1292
1362
  let nextCursor = null;
1293
1363
  if (offers.length > limit) {
1294
1364
  const last = offers[limit - 1];
@@ -1321,9 +1391,12 @@ function memory(parameters) {
1321
1391
  return deleted;
1322
1392
  },
1323
1393
  updateStatus: async (parameters2) => {
1324
- if (!map.has(parameters2.offerHash.toLowerCase())) return;
1325
- map.set(parameters2.offerHash.toLowerCase(), {
1326
- ...map.get(parameters2.offerHash.toLowerCase()),
1394
+ const key = parameters2.offerHash.toLowerCase();
1395
+ const existing = map.get(key);
1396
+ if (!existing) return;
1397
+ if (existing.status === parameters2.status) return;
1398
+ map.set(key, {
1399
+ ...existing,
1327
1400
  status: parameters2.status,
1328
1401
  metadata: parameters2.metadata
1329
1402
  });