@morpho-dev/router 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8,21 +8,19 @@ import { base, mainnet, anvil } from 'viem/chains';
8
8
  import { getBlock, getLogs } from 'viem/actions';
9
9
  import { spawn } from 'child_process';
10
10
  import 'fs';
11
- import 'tevm';
12
- import 'tevm/common';
13
11
  import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
14
12
  import * as z6 from 'zod';
15
- import { Base64 } from 'js-base64';
16
13
  import { readFile } from 'fs/promises';
17
14
  import dotenv from 'dotenv';
18
15
  import { AsyncLocalStorage } from 'async_hooks';
19
16
  import { serve as serve$1 } from '@hono/node-server';
20
17
  import { Hono } from 'hono';
21
18
  import { cors } from 'hono/cors';
22
- import { asc, desc, and, eq, gt, gte, sql, lte, inArray } from 'drizzle-orm';
23
- import { pgSchema, integer, varchar, bigint, timestamp, text, boolean, numeric, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
24
19
  import { z } from 'zod/v4';
25
20
  import { createDocument } from 'zod-openapi';
21
+ import { Base64 } from 'js-base64';
22
+ import { asc, desc, and, eq, gt, gte, sql, lte, inArray } from 'drizzle-orm';
23
+ import { pgSchema, integer, varchar, bigint, timestamp, text, boolean, numeric, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
26
24
  import { PGlite } from '@electric-sql/pglite';
27
25
  import { drizzle } from 'drizzle-orm/node-postgres';
28
26
  import { migrate } from 'drizzle-orm/node-postgres/migrator';
@@ -43,7 +41,7 @@ var __export = (target, all) => {
43
41
  // package.json
44
42
  var package_default = {
45
43
  name: "@morpho-dev/router",
46
- version: "0.1.15",
44
+ version: "0.1.17",
47
45
  description: "Router package for Morpho protocol"};
48
46
 
49
47
  // src/core/Chain.ts
@@ -729,97 +727,6 @@ var from2 = (parameters) => {
729
727
  };
730
728
  };
731
729
 
732
- // src/core/Cursor.ts
733
- var Cursor_exports = {};
734
- __export(Cursor_exports, {
735
- decode: () => decode,
736
- encode: () => encode,
737
- validate: () => validate
738
- });
739
- function validate(cursor) {
740
- if (!cursor || typeof cursor !== "object") {
741
- throw new Error("Cursor must be an object");
742
- }
743
- const c = cursor;
744
- if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
745
- throw new Error(
746
- `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
747
- );
748
- }
749
- if (!["asc", "desc"].includes(c.dir)) {
750
- throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
751
- }
752
- if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
753
- throw new Error(
754
- `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
755
- );
756
- }
757
- const validations = {
758
- rate: {
759
- field: "rate",
760
- type: "string",
761
- pattern: /^\d+$/,
762
- error: "numeric string"
763
- },
764
- amount: {
765
- field: "assets",
766
- type: "string",
767
- pattern: /^\d+$/,
768
- error: "numeric string"
769
- },
770
- maturity: {
771
- field: "maturity",
772
- type: "number",
773
- validator: (val) => val > 0,
774
- error: "positive number"
775
- },
776
- expiry: {
777
- field: "expiry",
778
- type: "number",
779
- validator: (val) => val > 0,
780
- error: "positive number"
781
- }
782
- };
783
- const validation = validations[c.sort];
784
- if (!validation) {
785
- throw new Error(`Invalid sort field: ${c.sort}`);
786
- }
787
- const fieldValue = c[validation.field];
788
- if (!fieldValue) {
789
- throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
790
- }
791
- if (typeof fieldValue !== validation.type) {
792
- throw new Error(
793
- `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
794
- );
795
- }
796
- if (validation.pattern && !validation.pattern.test(fieldValue)) {
797
- throw new Error(
798
- `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
799
- );
800
- }
801
- if (validation.validator && !validation.validator(fieldValue)) {
802
- throw new Error(
803
- `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
804
- );
805
- }
806
- if (c.page !== void 0) {
807
- if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) {
808
- throw new Error("Invalid page: must be a positive integer");
809
- }
810
- }
811
- return true;
812
- }
813
- function encode(c) {
814
- return Base64.encodeURL(JSON.stringify(c));
815
- }
816
- function decode(token) {
817
- if (!token) return null;
818
- const decoded = JSON.parse(Base64.decode(token));
819
- validate(decoded);
820
- return decoded;
821
- }
822
-
823
730
  // src/core/Liquidity.ts
824
731
  var Liquidity_exports = {};
825
732
  __export(Liquidity_exports, {
@@ -1135,9 +1042,9 @@ __export(Offer_exports, {
1135
1042
  OfferHashSchema: () => OfferHashSchema,
1136
1043
  OfferSchema: () => OfferSchema,
1137
1044
  consumedEvent: () => consumedEvent,
1138
- decode: () => decode2,
1045
+ decode: () => decode,
1139
1046
  domain: () => domain,
1140
- encode: () => encode2,
1047
+ encode: () => encode,
1141
1048
  from: () => from5,
1142
1049
  fromConsumedLog: () => fromConsumedLog,
1143
1050
  fromSnakeCase: () => fromSnakeCase3,
@@ -1462,7 +1369,7 @@ var OfferAbi = [
1462
1369
  },
1463
1370
  { name: "signature", type: "bytes" }
1464
1371
  ];
1465
- function encode2(offer) {
1372
+ function encode(offer) {
1466
1373
  return encodeAbiParameters(OfferAbi, [
1467
1374
  offer.offering,
1468
1375
  offer.assets,
@@ -1479,7 +1386,7 @@ function encode2(offer) {
1479
1386
  offer.signature ?? "0x"
1480
1387
  ]);
1481
1388
  }
1482
- function decode2(data, blockNumber) {
1389
+ function decode(data, blockNumber) {
1483
1390
  let decoded;
1484
1391
  try {
1485
1392
  decoded = decodeAbiParameters(OfferAbi, data);
@@ -1549,6 +1456,57 @@ var AccountNotSetError = class extends BaseError {
1549
1456
  }
1550
1457
  };
1551
1458
 
1459
+ // src/core/Quote.ts
1460
+ var Quote_exports = {};
1461
+ __export(Quote_exports, {
1462
+ InvalidQuoteError: () => InvalidQuoteError,
1463
+ QuoteSchema: () => QuoteSchema,
1464
+ from: () => from6,
1465
+ fromSnakeCase: () => fromSnakeCase4,
1466
+ random: () => random3
1467
+ });
1468
+ var QuoteSchema = z6.object({
1469
+ obligationId: z6.string().transform(transformHex),
1470
+ ask: z6.object({
1471
+ rate: z6.bigint({ coerce: true }).min(0n).max(maxUint256)
1472
+ }),
1473
+ bid: z6.object({
1474
+ rate: z6.bigint({ coerce: true }).min(0n).max(maxUint256)
1475
+ })
1476
+ });
1477
+ function from6(parameters) {
1478
+ try {
1479
+ const parsedQuote = QuoteSchema.parse(parameters);
1480
+ return {
1481
+ obligationId: parsedQuote.obligationId,
1482
+ ask: parsedQuote.ask,
1483
+ bid: parsedQuote.bid
1484
+ };
1485
+ } catch (error2) {
1486
+ throw new InvalidQuoteError(error2);
1487
+ }
1488
+ }
1489
+ function fromSnakeCase4(snake) {
1490
+ return from6(fromSnakeCase(snake));
1491
+ }
1492
+ function random3() {
1493
+ return from6({
1494
+ obligationId: Obligation_exports.id(Obligation_exports.random()),
1495
+ ask: {
1496
+ rate: BigInt(Math.floor(Math.random() * 1e6))
1497
+ },
1498
+ bid: {
1499
+ rate: BigInt(Math.floor(Math.random() * 1e6))
1500
+ }
1501
+ });
1502
+ }
1503
+ var InvalidQuoteError = class extends BaseError {
1504
+ name = "Quote.InvalidQuoteError";
1505
+ constructor(error2) {
1506
+ super("Invalid quote.", { cause: error2 });
1507
+ }
1508
+ };
1509
+
1552
1510
  // src/evm/EVM.ts
1553
1511
  var users = [
1554
1512
  privateKeyToAccount(
@@ -1665,11 +1623,16 @@ function defaultLogger(minLevel, pretty) {
1665
1623
  return;
1666
1624
  }
1667
1625
  const { msg, ...rest } = entry;
1626
+ const stack = typeof rest.stack === "string" ? rest.stack : void 0;
1627
+ if (stack) delete rest.stack;
1668
1628
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
1669
1629
  const level = methodLevel.toUpperCase();
1670
1630
  const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
1671
1631
  const line = extras.length > 0 ? `${timestamp2} [${level}] ${msg} ${extras}` : `${timestamp2} [${level}] ${msg}`;
1672
1632
  console[consoleMethod](line);
1633
+ if (stack) {
1634
+ console[consoleMethod](stack);
1635
+ }
1673
1636
  } : () => {
1674
1637
  };
1675
1638
  return {
@@ -1711,6 +1674,442 @@ function formatValue(value) {
1711
1674
  }
1712
1675
  }
1713
1676
  }
1677
+ var CollectorHealth = z.object({
1678
+ name: z.string(),
1679
+ chain_id: z.number(),
1680
+ block_number: z.number().nullable(),
1681
+ updated_at: z.string().nullable(),
1682
+ lag: z.number().nullable(),
1683
+ status: z.enum(["live", "lagging", "unknown"])
1684
+ });
1685
+ var CollectorsHealthResponse = z.array(CollectorHealth);
1686
+ var ChainHealth = z.object({
1687
+ chain_id: z.number(),
1688
+ block_number: z.number(),
1689
+ updated_at: z.string()
1690
+ });
1691
+ var ChainsHealthResponse = z.array(ChainHealth);
1692
+ var RouterStatusResponse = z.object({
1693
+ status: z.enum(["live", "syncing"])
1694
+ });
1695
+
1696
+ // src/stores/utils/Cursor.ts
1697
+ var Cursor_exports = {};
1698
+ __export(Cursor_exports, {
1699
+ decode: () => decode2,
1700
+ encode: () => encode2,
1701
+ validate: () => validate
1702
+ });
1703
+ function validate(cursor) {
1704
+ if (!cursor || typeof cursor !== "object") {
1705
+ throw new Error("Cursor must be an object");
1706
+ }
1707
+ const c = cursor;
1708
+ if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
1709
+ throw new Error(
1710
+ `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
1711
+ );
1712
+ }
1713
+ if (!["asc", "desc"].includes(c.dir)) {
1714
+ throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
1715
+ }
1716
+ if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
1717
+ throw new Error(
1718
+ `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
1719
+ );
1720
+ }
1721
+ const validations = {
1722
+ rate: {
1723
+ field: "rate",
1724
+ type: "string",
1725
+ pattern: /^\d+$/,
1726
+ error: "numeric string"
1727
+ },
1728
+ amount: {
1729
+ field: "assets",
1730
+ type: "string",
1731
+ pattern: /^\d+$/,
1732
+ error: "numeric string"
1733
+ },
1734
+ maturity: {
1735
+ field: "maturity",
1736
+ type: "number",
1737
+ validator: (val) => val > 0,
1738
+ error: "positive number"
1739
+ },
1740
+ expiry: {
1741
+ field: "expiry",
1742
+ type: "number",
1743
+ validator: (val) => val > 0,
1744
+ error: "positive number"
1745
+ }
1746
+ };
1747
+ const validation = validations[c.sort];
1748
+ if (!validation) {
1749
+ throw new Error(`Invalid sort field: ${c.sort}`);
1750
+ }
1751
+ const fieldValue = c[validation.field];
1752
+ if (!fieldValue) {
1753
+ throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
1754
+ }
1755
+ if (typeof fieldValue !== validation.type) {
1756
+ throw new Error(
1757
+ `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
1758
+ );
1759
+ }
1760
+ if (validation.pattern && !validation.pattern.test(fieldValue)) {
1761
+ throw new Error(
1762
+ `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
1763
+ );
1764
+ }
1765
+ if (validation.validator && !validation.validator(fieldValue)) {
1766
+ throw new Error(
1767
+ `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
1768
+ );
1769
+ }
1770
+ if (c.page !== void 0) {
1771
+ if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) {
1772
+ throw new Error("Invalid page: must be a positive integer");
1773
+ }
1774
+ }
1775
+ return true;
1776
+ }
1777
+ function encode2(c) {
1778
+ return Base64.encodeURL(JSON.stringify(c));
1779
+ }
1780
+ function decode2(token) {
1781
+ if (!token) return null;
1782
+ const decoded = JSON.parse(Base64.decode(token));
1783
+ validate(decoded);
1784
+ return decoded;
1785
+ }
1786
+
1787
+ // src/api/Schema/requests.ts
1788
+ var MAX_LIMIT = 100;
1789
+ var DEFAULT_LIMIT = 20;
1790
+ var PaginationQueryParams = z6.object({
1791
+ cursor: z6.string().optional().refine(
1792
+ (val) => {
1793
+ if (!val) return true;
1794
+ try {
1795
+ const decoded = Cursor_exports.decode(val);
1796
+ return decoded !== null;
1797
+ } catch (_error) {
1798
+ return false;
1799
+ }
1800
+ },
1801
+ {
1802
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
1803
+ }
1804
+ ).meta({
1805
+ description: "Pagination cursor in base64url-encoded format",
1806
+ example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
1807
+ }),
1808
+ limit: z6.string().regex(/^[1-9]\d*$/, {
1809
+ message: "Limit must be a positive integer"
1810
+ }).transform((val) => Number.parseInt(val, 10)).pipe(
1811
+ z6.number().max(MAX_LIMIT, {
1812
+ message: `Limit cannot exceed ${MAX_LIMIT}`
1813
+ })
1814
+ ).optional().default(DEFAULT_LIMIT).meta({
1815
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
1816
+ example: 10
1817
+ })
1818
+ });
1819
+ var GetOffersQueryParams = z6.object({
1820
+ ...PaginationQueryParams.shape,
1821
+ side: z6.enum(["buy", "sell"]).meta({
1822
+ description: "Side of the offer.",
1823
+ example: "buy"
1824
+ }),
1825
+ obligation_id: z6.string({ error: "Obligation id is required and must be a valid 32-byte hex string" }).regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).meta({
1826
+ description: "Offers obligation id",
1827
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
1828
+ })
1829
+ });
1830
+ var GetObligationsQueryParams = z6.object({
1831
+ ...PaginationQueryParams.shape,
1832
+ cursor: z6.string().optional().meta({
1833
+ description: "Obligation id cursor",
1834
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
1835
+ })
1836
+ });
1837
+ var schemas = {
1838
+ get_offers: GetOffersQueryParams,
1839
+ get_obligations: GetObligationsQueryParams
1840
+ };
1841
+ function safeParse(action, query, error2) {
1842
+ return schemas[action].safeParse(query, {
1843
+ error: error2
1844
+ });
1845
+ }
1846
+
1847
+ // src/api/Schema/openapi.ts
1848
+ var timestampExample = "2024-01-01T12:00:00.000Z";
1849
+ var cursorExample = "eyJvZmZzZXQiOjEwMH0";
1850
+ function makeSuccessResponse(parameters) {
1851
+ const { dataSchema, dataDescription, dataExample, cursor } = parameters;
1852
+ const withDataMeta = dataDescription ? dataSchema.meta({ description: dataDescription }) : dataSchema;
1853
+ return z.object({
1854
+ status: z.literal("success"),
1855
+ cursor: z.string().nullable(),
1856
+ data: z.any(),
1857
+ meta: z.object({
1858
+ timestamp: z.string()
1859
+ })
1860
+ }).extend({
1861
+ data: withDataMeta
1862
+ }).meta({
1863
+ example: {
1864
+ status: "success",
1865
+ cursor,
1866
+ data: dataExample,
1867
+ meta: { timestamp: timestampExample }
1868
+ }
1869
+ });
1870
+ }
1871
+ var OffersSuccessResponseSchema = makeSuccessResponse({
1872
+ dataSchema: z.array(z.any()),
1873
+ dataDescription: "Offers matching the provided filters.",
1874
+ dataExample: [toSnakeCase(Offer_exports.random())],
1875
+ cursor: cursorExample
1876
+ });
1877
+ var ObligationsSuccessResponseSchema = makeSuccessResponse({
1878
+ dataSchema: z.array(z.any()),
1879
+ dataDescription: "Obligations known to the router.",
1880
+ dataExample: [toSnakeCase(Obligation_exports.random())],
1881
+ cursor: cursorExample
1882
+ });
1883
+ var RouterStatusSuccessResponseSchema = makeSuccessResponse({
1884
+ dataSchema: RouterStatusResponse,
1885
+ dataDescription: "Aggregated router status.",
1886
+ dataExample: { status: "live" },
1887
+ cursor: null
1888
+ });
1889
+ var CollectorsHealthSuccessResponseSchema = makeSuccessResponse({
1890
+ dataSchema: CollectorsHealthResponse,
1891
+ dataDescription: "Collectors health details and sync status.",
1892
+ dataExample: [
1893
+ {
1894
+ name: "mempool_offers",
1895
+ chain_id: "1",
1896
+ block_number: 21345678,
1897
+ updated_at: "2024-01-01T12:00:00.000Z",
1898
+ lag: 0,
1899
+ status: "live"
1900
+ }
1901
+ ],
1902
+ cursor: null
1903
+ });
1904
+ var ChainsHealthSuccessResponseSchema = makeSuccessResponse({
1905
+ dataSchema: ChainsHealthResponse,
1906
+ dataDescription: "Latest processed block per chain.",
1907
+ dataExample: [
1908
+ {
1909
+ chain_id: "1",
1910
+ block_number: 21345678,
1911
+ updated_at: "2024-01-01T12:00:00.000Z"
1912
+ }
1913
+ ],
1914
+ cursor: null
1915
+ });
1916
+ var errorResponseSchema = z.object({
1917
+ status: z.literal("error"),
1918
+ error: z.object({
1919
+ code: z.string(),
1920
+ message: z.string(),
1921
+ details: z.any().optional()
1922
+ }),
1923
+ meta: z.object({
1924
+ timestamp: z.string()
1925
+ })
1926
+ }).meta({
1927
+ description: "Error response wrapper.",
1928
+ example: {
1929
+ status: "error",
1930
+ error: {
1931
+ code: "VALIDATION_ERROR",
1932
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object",
1933
+ details: [
1934
+ {
1935
+ field: "cursor",
1936
+ issue: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
1937
+ }
1938
+ ]
1939
+ },
1940
+ meta: {
1941
+ timestamp: timestampExample
1942
+ }
1943
+ }
1944
+ });
1945
+ var paths = {
1946
+ "/v1/offers": {
1947
+ get: {
1948
+ summary: "Offers",
1949
+ description: "Find offers that match specific criteria",
1950
+ tags: ["Offers"],
1951
+ requestParams: {
1952
+ query: GetOffersQueryParams
1953
+ },
1954
+ responses: {
1955
+ 200: {
1956
+ description: "Success",
1957
+ content: {
1958
+ "application/json": {
1959
+ schema: OffersSuccessResponseSchema
1960
+ }
1961
+ }
1962
+ },
1963
+ 400: {
1964
+ description: "Bad Request",
1965
+ content: {
1966
+ "application/json": {
1967
+ schema: errorResponseSchema
1968
+ }
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1973
+ },
1974
+ "/v1/obligations": {
1975
+ get: {
1976
+ summary: "Obligations",
1977
+ description: "List obligations with pagination support",
1978
+ tags: ["Obligations"],
1979
+ requestParams: {
1980
+ query: GetObligationsQueryParams
1981
+ },
1982
+ responses: {
1983
+ 200: {
1984
+ description: "Success",
1985
+ content: {
1986
+ "application/json": {
1987
+ schema: ObligationsSuccessResponseSchema
1988
+ }
1989
+ }
1990
+ },
1991
+ 400: {
1992
+ description: "Bad Request",
1993
+ content: {
1994
+ "application/json": {
1995
+ schema: errorResponseSchema
1996
+ }
1997
+ }
1998
+ }
1999
+ }
2000
+ }
2001
+ },
2002
+ "/v1/health": {
2003
+ get: {
2004
+ summary: "Router status",
2005
+ description: "Retrieve the aggregated status of the router.",
2006
+ tags: ["Health"],
2007
+ responses: {
2008
+ 200: {
2009
+ description: "Success",
2010
+ content: {
2011
+ "application/json": {
2012
+ schema: RouterStatusSuccessResponseSchema
2013
+ }
2014
+ }
2015
+ }
2016
+ }
2017
+ }
2018
+ },
2019
+ "/v1/health/collectors": {
2020
+ get: {
2021
+ summary: "Collectors health",
2022
+ description: "Retrieve the block numbers processed by collectors and their sync status.",
2023
+ tags: ["Health"],
2024
+ responses: {
2025
+ 200: {
2026
+ description: "Success",
2027
+ content: {
2028
+ "application/json": {
2029
+ schema: CollectorsHealthSuccessResponseSchema
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2034
+ }
2035
+ },
2036
+ "/v1/health/chains": {
2037
+ get: {
2038
+ summary: "Chains health",
2039
+ description: "Retrieve the latest block processed for each chain.",
2040
+ tags: ["Health"],
2041
+ responses: {
2042
+ 200: {
2043
+ description: "Success",
2044
+ content: {
2045
+ "application/json": {
2046
+ schema: ChainsHealthSuccessResponseSchema
2047
+ }
2048
+ }
2049
+ }
2050
+ }
2051
+ }
2052
+ }
2053
+ };
2054
+ var OpenApi = createDocument({
2055
+ openapi: "3.1.0",
2056
+ info: {
2057
+ title: "Router API",
2058
+ version: "1.0.0",
2059
+ description: "API for the Morpho Router"
2060
+ },
2061
+ tags: [
2062
+ {
2063
+ name: "Offers"
2064
+ },
2065
+ {
2066
+ name: "Obligations"
2067
+ },
2068
+ {
2069
+ name: "Health"
2070
+ }
2071
+ ],
2072
+ servers: [
2073
+ {
2074
+ url: "https://router.morpho.dev",
2075
+ description: "Production server"
2076
+ },
2077
+ {
2078
+ url: "http://localhost:7891",
2079
+ description: "Local development server"
2080
+ }
2081
+ ],
2082
+ paths
2083
+ });
2084
+
2085
+ // src/api/Controllers/getDocs.ts
2086
+ function getSwaggerJson() {
2087
+ return OpenApi;
2088
+ }
2089
+ function getDocsHtml() {
2090
+ const html = `<!DOCTYPE html>
2091
+ <html>
2092
+ <head>
2093
+ <meta charset="UTF-8">
2094
+ <title>Router API Docs (Scalar)</title>
2095
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
2096
+ <style>
2097
+ html, body { margin: 0; height: 100%; }
2098
+ api-reference { height: 100%; width: 100%; }
2099
+ </style>
2100
+ </head>
2101
+ <body>
2102
+ <div id="api-container" style="height:100%;width:100%;"></div>
2103
+ <script>
2104
+ window.addEventListener('load', function () {
2105
+ const spec = ${JSON.stringify(OpenApi)};
2106
+ Scalar.createApiReference('#api-container', { spec: { content: spec } });
2107
+ });
2108
+ </script>
2109
+ </body>
2110
+ </html>`;
2111
+ return html;
2112
+ }
1714
2113
 
1715
2114
  // src/collectors/index.ts
1716
2115
  var collectors_exports = {};
@@ -1993,20 +2392,31 @@ function create2({
1993
2392
  });
1994
2393
  return poll(
1995
2394
  async () => {
1996
- let { blockNumber: lastBlockNumber, epoch } = await collectorStore.getBlockNumber({
1997
- collectorName: name,
1998
- chainId: chain.id
1999
- });
2000
- await admin.syncBlock();
2001
- lastBlockNumber = await collect({
2002
- chain,
2003
- client,
2004
- collector: name,
2005
- epoch,
2006
- lastBlockNumber,
2007
- withTransaction
2008
- });
2009
- emit(lastBlockNumber);
2395
+ try {
2396
+ let { blockNumber: lastBlockNumber, epoch } = await collectorStore.getBlockNumber({
2397
+ collectorName: name,
2398
+ chainId: chain.id
2399
+ });
2400
+ await admin.syncBlock();
2401
+ lastBlockNumber = await collect({
2402
+ chain,
2403
+ client,
2404
+ collector: name,
2405
+ epoch,
2406
+ lastBlockNumber,
2407
+ withTransaction
2408
+ });
2409
+ emit(lastBlockNumber);
2410
+ } catch (err) {
2411
+ const isError = err instanceof Error;
2412
+ logger.error({
2413
+ msg: "Collector error",
2414
+ collector,
2415
+ chain_id: chain.id,
2416
+ error: isError ? err.message : String(err),
2417
+ stack: isError ? err.stack : void 0
2418
+ });
2419
+ }
2010
2420
  },
2011
2421
  { interval: options.interval }
2012
2422
  );
@@ -2026,7 +2436,7 @@ function start(collector) {
2026
2436
  }
2027
2437
  var DEFAULT_BATCH_SIZE2 = 100;
2028
2438
  var DEFAULT_BLOCK_WINDOW2 = 100;
2029
- function from6(parameters) {
2439
+ function from7(parameters) {
2030
2440
  const config = {
2031
2441
  client: parameters.client,
2032
2442
  mempoolAddress: parameters.mempoolAddress,
@@ -2168,7 +2578,7 @@ var ChainIdMismatchError = class extends BaseError {
2168
2578
 
2169
2579
  // src/mempool/MempoolClient.ts
2170
2580
  function connect(parameters) {
2171
- return from6(parameters);
2581
+ return from7(parameters);
2172
2582
  }
2173
2583
 
2174
2584
  // src/stores/CollectorStore.ts
@@ -4008,7 +4418,7 @@ function handleZodError(error2) {
4008
4418
  return new ValidationError("Validation failed", formattedErrors);
4009
4419
  }
4010
4420
 
4011
- // src/api/Api/Controllers/getHealth.ts
4421
+ // src/api/Controllers/getHealth.ts
4012
4422
  async function getHealth(healthService) {
4013
4423
  const logger = Logger_exports.getLogger();
4014
4424
  try {
@@ -4073,266 +4483,31 @@ async function getHealthCollectors(healthService) {
4073
4483
  return error(err);
4074
4484
  }
4075
4485
  }
4076
- var CollectorHealth = z.object({
4077
- name: z.string(),
4078
- chain_id: z.number(),
4079
- block_number: z.number().nullable(),
4080
- updated_at: z.string().nullable(),
4081
- lag: z.number().nullable(),
4082
- status: z.enum(["live", "lagging", "unknown"])
4083
- });
4084
- var CollectorsHealthResponse = z.object({
4085
- collectors: z.array(CollectorHealth)
4086
- });
4087
- var ChainHealth = z.object({
4088
- chain_id: z.number(),
4089
- block_number: z.number(),
4090
- updated_at: z.string()
4091
- });
4092
- var ChainsHealthResponse = z.object({
4093
- chains: z.array(ChainHealth)
4094
- });
4095
- var RouterStatusResponse = z.object({
4096
- status: z.enum(["live", "syncing"])
4097
- });
4098
4486
 
4099
- // src/api/Api/Schema/ObligationResponse.ts
4487
+ // src/api/Schema/ObligationResponse.ts
4100
4488
  var ObligationResponse_exports = {};
4101
4489
  __export(ObligationResponse_exports, {
4102
- from: () => from7
4490
+ from: () => from8
4103
4491
  });
4104
- function from7(obligation) {
4105
- return toSnakeCase({ id: Obligation_exports.id(obligation), ...obligation });
4492
+ function from8(obligation, quote) {
4493
+ return toSnakeCase({
4494
+ id: quote.obligationId,
4495
+ ...obligation,
4496
+ ask: quote.ask,
4497
+ bid: quote.bid
4498
+ });
4106
4499
  }
4107
4500
 
4108
- // src/api/Api/Schema/OfferResponse.ts
4501
+ // src/api/Schema/OfferResponse.ts
4109
4502
  var OfferResponse_exports = {};
4110
4503
  __export(OfferResponse_exports, {
4111
- from: () => from8
4504
+ from: () => from9
4112
4505
  });
4113
- function from8(offer) {
4506
+ function from9(offer) {
4114
4507
  return toSnakeCase(offer);
4115
4508
  }
4116
- var MAX_LIMIT = 100;
4117
- var DEFAULT_LIMIT = 20;
4118
- var PaginationQueryParams = z6.object({
4119
- cursor: z6.string().optional().refine(
4120
- (val) => {
4121
- if (!val) return true;
4122
- try {
4123
- const decoded = Cursor_exports.decode(val);
4124
- return decoded !== null;
4125
- } catch (_error) {
4126
- return false;
4127
- }
4128
- },
4129
- {
4130
- message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
4131
- }
4132
- ).meta({
4133
- description: "Pagination cursor in base64url-encoded format",
4134
- example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
4135
- }),
4136
- limit: z6.string().regex(/^[1-9]\d*$/, {
4137
- message: "Limit must be a positive integer"
4138
- }).transform((val) => Number.parseInt(val, 10)).pipe(
4139
- z6.number().max(MAX_LIMIT, {
4140
- message: `Limit cannot exceed ${MAX_LIMIT}`
4141
- })
4142
- ).optional().default(DEFAULT_LIMIT).meta({
4143
- description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
4144
- example: 10
4145
- })
4146
- });
4147
- var GetOffersQueryParams = z6.object({
4148
- ...PaginationQueryParams.shape,
4149
- side: z6.enum(["buy", "sell"]).meta({
4150
- description: "Side of the offer.",
4151
- example: "buy"
4152
- }),
4153
- obligation_id: z6.string({ error: "Obligation id is required and must be a valid 32-byte hex string" }).regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).meta({
4154
- description: "Offers obligation id",
4155
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
4156
- })
4157
- });
4158
- var GetObligationsQueryParams = z6.object({
4159
- ...PaginationQueryParams.shape,
4160
- cursor: z6.string().optional().meta({
4161
- description: "Obligation id cursor",
4162
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
4163
- })
4164
- });
4165
- var schemas = {
4166
- get_offers: GetOffersQueryParams,
4167
- get_obligations: GetObligationsQueryParams
4168
- };
4169
- function safeParse(action, query, error2) {
4170
- return schemas[action].safeParse(query, {
4171
- error: error2
4172
- });
4173
- }
4174
-
4175
- // src/api/Api/Schema/openapi.ts
4176
- var successResponseSchema = z.object({
4177
- status: z.literal("success"),
4178
- cursor: z.string().nullable(),
4179
- data: z.array(z.any()),
4180
- meta: z.object({
4181
- timestamp: z.string()
4182
- })
4183
- });
4184
- var errorResponseSchema = z.object({
4185
- status: z.literal("error"),
4186
- error: z.object({
4187
- code: z.string(),
4188
- message: z.string(),
4189
- details: z.any().optional()
4190
- }),
4191
- meta: z.object({
4192
- timestamp: z.string()
4193
- })
4194
- });
4195
- var paths = {
4196
- "/v1/offers": {
4197
- get: {
4198
- summary: "Offers",
4199
- description: "Find offers that match specific criteria",
4200
- tags: ["Offers"],
4201
- requestParams: {
4202
- query: GetOffersQueryParams
4203
- },
4204
- responses: {
4205
- 200: {
4206
- description: "Success",
4207
- content: {
4208
- "application/json": {
4209
- schema: successResponseSchema
4210
- }
4211
- }
4212
- },
4213
- 400: {
4214
- description: "Bad Request",
4215
- content: {
4216
- "application/json": {
4217
- schema: errorResponseSchema
4218
- }
4219
- }
4220
- }
4221
- }
4222
- }
4223
- },
4224
- "/v1/obligations": {
4225
- get: {
4226
- summary: "Obligations",
4227
- description: "List obligations with pagination support",
4228
- tags: ["Obligations"],
4229
- requestParams: {
4230
- query: GetObligationsQueryParams
4231
- },
4232
- responses: {
4233
- 200: {
4234
- description: "Success",
4235
- content: {
4236
- "application/json": {
4237
- schema: successResponseSchema
4238
- }
4239
- }
4240
- },
4241
- 400: {
4242
- description: "Bad Request",
4243
- content: {
4244
- "application/json": {
4245
- schema: errorResponseSchema
4246
- }
4247
- }
4248
- }
4249
- }
4250
- }
4251
- },
4252
- "/v1/health": {
4253
- get: {
4254
- summary: "Router status",
4255
- description: "Retrieve the aggregated status of the router.",
4256
- tags: ["Health"],
4257
- responses: {
4258
- 200: {
4259
- description: "Success",
4260
- content: {
4261
- "application/json": {
4262
- schema: RouterStatusResponse
4263
- }
4264
- }
4265
- }
4266
- }
4267
- }
4268
- },
4269
- "/v1/health/collectors": {
4270
- get: {
4271
- summary: "Collectors health",
4272
- description: "Retrieve the block numbers processed by collectors and their sync status.",
4273
- tags: ["Health"],
4274
- responses: {
4275
- 200: {
4276
- description: "Success",
4277
- content: {
4278
- "application/json": {
4279
- schema: CollectorsHealthResponse
4280
- }
4281
- }
4282
- }
4283
- }
4284
- }
4285
- },
4286
- "/v1/health/chains": {
4287
- get: {
4288
- summary: "Chains health",
4289
- description: "Retrieve the latest block processed for each chain.",
4290
- tags: ["Health"],
4291
- responses: {
4292
- 200: {
4293
- description: "Success",
4294
- content: {
4295
- "application/json": {
4296
- schema: ChainsHealthResponse
4297
- }
4298
- }
4299
- }
4300
- }
4301
- }
4302
- }
4303
- };
4304
- createDocument({
4305
- openapi: "3.1.0",
4306
- info: {
4307
- title: "Router API",
4308
- version: "1.0.0",
4309
- description: "API for the Morpho Router"
4310
- },
4311
- tags: [
4312
- {
4313
- name: "Offers"
4314
- },
4315
- {
4316
- name: "Obligations"
4317
- },
4318
- {
4319
- name: "Health"
4320
- }
4321
- ],
4322
- servers: [
4323
- {
4324
- url: "https://router.morpho.dev",
4325
- description: "Production server"
4326
- },
4327
- {
4328
- url: "http://localhost:8081",
4329
- description: "Local development server"
4330
- }
4331
- ],
4332
- paths
4333
- });
4334
4509
 
4335
- // src/api/Api/Controllers/getObligations.ts
4510
+ // src/api/Controllers/getObligations.ts
4336
4511
  async function getObligations(queryParameters, store) {
4337
4512
  const logger = Logger_exports.getLogger();
4338
4513
  const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
@@ -4343,8 +4518,20 @@ async function getObligations(queryParameters, store) {
4343
4518
  cursor: query.cursor,
4344
4519
  limit: query.limit
4345
4520
  });
4521
+ const quotes = await store.getQuotes({
4522
+ obligationIds: obligations2.map((o) => Obligation_exports.id(o))
4523
+ });
4346
4524
  return success({
4347
- data: obligations2.map((o) => ObligationResponse_exports.from(o)),
4525
+ data: obligations2.map(
4526
+ (o) => ObligationResponse_exports.from(
4527
+ o,
4528
+ quotes.find((q) => q.obligationId === Obligation_exports.id(o)) ?? {
4529
+ obligationId: Obligation_exports.id(o),
4530
+ ask: { rate: 0n },
4531
+ bid: { rate: 0n }
4532
+ }
4533
+ )
4534
+ ),
4348
4535
  cursor: nextCursor ?? null
4349
4536
  });
4350
4537
  } catch (err) {
@@ -4358,7 +4545,7 @@ async function getObligations(queryParameters, store) {
4358
4545
  }
4359
4546
  }
4360
4547
 
4361
- // src/api/Api/Controllers/getOffers.ts
4548
+ // src/api/Controllers/getOffers.ts
4362
4549
  async function getOffers(queryParameters, store) {
4363
4550
  const logger = Logger_exports.getLogger();
4364
4551
  const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
@@ -4386,7 +4573,7 @@ async function getOffers(queryParameters, store) {
4386
4573
  }
4387
4574
  }
4388
4575
 
4389
- // src/api/Api/Api.ts
4576
+ // src/api/Api.ts
4390
4577
  function create5(config) {
4391
4578
  return {
4392
4579
  serve: ({ port }) => {
@@ -4418,6 +4605,13 @@ function serve2(parameters) {
4418
4605
  const { statusCode, body } = await getHealthChains(healthService);
4419
4606
  return c.json(body, statusCode);
4420
4607
  });
4608
+ app.get(
4609
+ "/docs/swagger.json",
4610
+ (c) => c.text(JSON.stringify(getSwaggerJson()), 200, {
4611
+ "Content-Type": "application/json; charset=utf-8"
4612
+ })
4613
+ );
4614
+ app.get("/docs", (c) => c.html(getDocsHtml(), 200));
4421
4615
  serve$1({
4422
4616
  fetch: app.fetch,
4423
4617
  port: parameters.port
@@ -5038,6 +5232,64 @@ function create8(config) {
5038
5232
  const returnedItems = Array.from(items.values());
5039
5233
  const nextCursor = returnedItems.length === limit && returnedItems.length > 0 ? result[result.length - 1].obligationId : null;
5040
5234
  return { obligations: returnedItems, nextCursor };
5235
+ },
5236
+ getQuotes: async (parameters) => {
5237
+ const { obligationIds } = parameters;
5238
+ if (obligationIds.length === 0) return [];
5239
+ const now2 = time_exports.now();
5240
+ const sumConsumed = db.select({
5241
+ consumed: sql`COALESCE(SUM(${consumed.consumed}), 0)`.as("consumed")
5242
+ }).from(consumed).where(
5243
+ and(
5244
+ eq(consumed.offering, offers.offering),
5245
+ eq(consumed.nonce, offers.nonce),
5246
+ eq(consumed.chainId, offers.chainId)
5247
+ )
5248
+ ).as("sum_consumed");
5249
+ const remaining = sql`(${offers.assets} - COALESCE(${sumConsumed.consumed}, 0))`;
5250
+ const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
5251
+ obligationId: offers.obligationId,
5252
+ rate: offers.rate
5253
+ }).from(offers).leftJoinLateral(sumConsumed, sql`true`).where(
5254
+ and(
5255
+ inArray(offers.obligationId, obligationIds),
5256
+ gte(offers.expiry, now2),
5257
+ gte(offers.maturity, now2),
5258
+ eq(offers.buy, side === "buy"),
5259
+ sql`${remaining} > 0`
5260
+ )
5261
+ ).orderBy(
5262
+ offers.obligationId,
5263
+ // lower rates first for buy, higher rates first for sell
5264
+ side === "buy" ? asc(offers.rate) : desc(offers.rate)
5265
+ );
5266
+ const [bestBuys, bestSells] = await Promise.all([
5267
+ query({ side: "buy" }),
5268
+ query({ side: "sell" })
5269
+ ]);
5270
+ const quotes = /* @__PURE__ */ new Map();
5271
+ for (const row of bestSells) {
5272
+ quotes.set(row.obligationId, {
5273
+ ask: { rate: row.rate },
5274
+ bid: { rate: 0n }
5275
+ });
5276
+ }
5277
+ for (const row of bestBuys) {
5278
+ const quote = quotes.get(row.obligationId);
5279
+ if (!quote) {
5280
+ quotes.set(row.obligationId, {
5281
+ ask: { rate: 0n },
5282
+ bid: { rate: row.rate }
5283
+ });
5284
+ continue;
5285
+ }
5286
+ quote.bid = { rate: row.rate };
5287
+ }
5288
+ return Array.from(quotes.entries()).map(([id2, quote]) => {
5289
+ return Quote_exports.from({ obligationId: id2, ask: quote.ask, bid: quote.bid });
5290
+ }).sort((a, b) => {
5291
+ return a.obligationId.localeCompare(b.obligationId);
5292
+ });
5041
5293
  }
5042
5294
  };
5043
5295
  }
@@ -5779,7 +6031,7 @@ function create9(params) {
5779
6031
  }
5780
6032
 
5781
6033
  // src/services/Services.ts
5782
- function from9(config) {
6034
+ function from10(config) {
5783
6035
  const {
5784
6036
  chain,
5785
6037
  rpcUrl,
@@ -5907,7 +6159,7 @@ var RouterCmd = class _RouterCmd extends Command {
5907
6159
  "RPC URL is required, please set the ROUTER_RPC_URL environment variable or use --rpc-url <url> option."
5908
6160
  );
5909
6161
  }
5910
- const routerServices = from9({
6162
+ const routerServices = from10({
5911
6163
  chain,
5912
6164
  rpcUrl,
5913
6165
  dbConfig: {