@morpho-dev/router 0.1.16 → 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
@@ -10,17 +10,17 @@ import { spawn } from 'child_process';
10
10
  import 'fs';
11
11
  import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
12
12
  import * as z6 from 'zod';
13
- import { Base64 } from 'js-base64';
14
13
  import { readFile } from 'fs/promises';
15
14
  import dotenv from 'dotenv';
16
15
  import { AsyncLocalStorage } from 'async_hooks';
17
16
  import { serve as serve$1 } from '@hono/node-server';
18
17
  import { Hono } from 'hono';
19
18
  import { cors } from 'hono/cors';
20
- import { asc, desc, and, eq, gt, gte, sql, lte, inArray } from 'drizzle-orm';
21
- import { pgSchema, integer, varchar, bigint, timestamp, text, boolean, numeric, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
22
19
  import { z } from 'zod/v4';
23
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';
24
24
  import { PGlite } from '@electric-sql/pglite';
25
25
  import { drizzle } from 'drizzle-orm/node-postgres';
26
26
  import { migrate } from 'drizzle-orm/node-postgres/migrator';
@@ -41,7 +41,7 @@ var __export = (target, all) => {
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@morpho-dev/router",
44
- version: "0.1.16",
44
+ version: "0.1.17",
45
45
  description: "Router package for Morpho protocol"};
46
46
 
47
47
  // src/core/Chain.ts
@@ -727,97 +727,6 @@ var from2 = (parameters) => {
727
727
  };
728
728
  };
729
729
 
730
- // src/core/Cursor.ts
731
- var Cursor_exports = {};
732
- __export(Cursor_exports, {
733
- decode: () => decode,
734
- encode: () => encode,
735
- validate: () => validate
736
- });
737
- function validate(cursor) {
738
- if (!cursor || typeof cursor !== "object") {
739
- throw new Error("Cursor must be an object");
740
- }
741
- const c = cursor;
742
- if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
743
- throw new Error(
744
- `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
745
- );
746
- }
747
- if (!["asc", "desc"].includes(c.dir)) {
748
- throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
749
- }
750
- if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
751
- throw new Error(
752
- `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
753
- );
754
- }
755
- const validations = {
756
- rate: {
757
- field: "rate",
758
- type: "string",
759
- pattern: /^\d+$/,
760
- error: "numeric string"
761
- },
762
- amount: {
763
- field: "assets",
764
- type: "string",
765
- pattern: /^\d+$/,
766
- error: "numeric string"
767
- },
768
- maturity: {
769
- field: "maturity",
770
- type: "number",
771
- validator: (val) => val > 0,
772
- error: "positive number"
773
- },
774
- expiry: {
775
- field: "expiry",
776
- type: "number",
777
- validator: (val) => val > 0,
778
- error: "positive number"
779
- }
780
- };
781
- const validation = validations[c.sort];
782
- if (!validation) {
783
- throw new Error(`Invalid sort field: ${c.sort}`);
784
- }
785
- const fieldValue = c[validation.field];
786
- if (!fieldValue) {
787
- throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
788
- }
789
- if (typeof fieldValue !== validation.type) {
790
- throw new Error(
791
- `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
792
- );
793
- }
794
- if (validation.pattern && !validation.pattern.test(fieldValue)) {
795
- throw new Error(
796
- `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
797
- );
798
- }
799
- if (validation.validator && !validation.validator(fieldValue)) {
800
- throw new Error(
801
- `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
802
- );
803
- }
804
- if (c.page !== void 0) {
805
- if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) {
806
- throw new Error("Invalid page: must be a positive integer");
807
- }
808
- }
809
- return true;
810
- }
811
- function encode(c) {
812
- return Base64.encodeURL(JSON.stringify(c));
813
- }
814
- function decode(token) {
815
- if (!token) return null;
816
- const decoded = JSON.parse(Base64.decode(token));
817
- validate(decoded);
818
- return decoded;
819
- }
820
-
821
730
  // src/core/Liquidity.ts
822
731
  var Liquidity_exports = {};
823
732
  __export(Liquidity_exports, {
@@ -1133,9 +1042,9 @@ __export(Offer_exports, {
1133
1042
  OfferHashSchema: () => OfferHashSchema,
1134
1043
  OfferSchema: () => OfferSchema,
1135
1044
  consumedEvent: () => consumedEvent,
1136
- decode: () => decode2,
1045
+ decode: () => decode,
1137
1046
  domain: () => domain,
1138
- encode: () => encode2,
1047
+ encode: () => encode,
1139
1048
  from: () => from5,
1140
1049
  fromConsumedLog: () => fromConsumedLog,
1141
1050
  fromSnakeCase: () => fromSnakeCase3,
@@ -1460,7 +1369,7 @@ var OfferAbi = [
1460
1369
  },
1461
1370
  { name: "signature", type: "bytes" }
1462
1371
  ];
1463
- function encode2(offer) {
1372
+ function encode(offer) {
1464
1373
  return encodeAbiParameters(OfferAbi, [
1465
1374
  offer.offering,
1466
1375
  offer.assets,
@@ -1477,7 +1386,7 @@ function encode2(offer) {
1477
1386
  offer.signature ?? "0x"
1478
1387
  ]);
1479
1388
  }
1480
- function decode2(data, blockNumber) {
1389
+ function decode(data, blockNumber) {
1481
1390
  let decoded;
1482
1391
  try {
1483
1392
  decoded = decodeAbiParameters(OfferAbi, data);
@@ -1547,6 +1456,57 @@ var AccountNotSetError = class extends BaseError {
1547
1456
  }
1548
1457
  };
1549
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
+
1550
1510
  // src/evm/EVM.ts
1551
1511
  var users = [
1552
1512
  privateKeyToAccount(
@@ -1663,11 +1623,16 @@ function defaultLogger(minLevel, pretty) {
1663
1623
  return;
1664
1624
  }
1665
1625
  const { msg, ...rest } = entry;
1626
+ const stack = typeof rest.stack === "string" ? rest.stack : void 0;
1627
+ if (stack) delete rest.stack;
1666
1628
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
1667
1629
  const level = methodLevel.toUpperCase();
1668
1630
  const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
1669
1631
  const line = extras.length > 0 ? `${timestamp2} [${level}] ${msg} ${extras}` : `${timestamp2} [${level}] ${msg}`;
1670
1632
  console[consoleMethod](line);
1633
+ if (stack) {
1634
+ console[consoleMethod](stack);
1635
+ }
1671
1636
  } : () => {
1672
1637
  };
1673
1638
  return {
@@ -1709,6 +1674,442 @@ function formatValue(value) {
1709
1674
  }
1710
1675
  }
1711
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
+ }
1712
2113
 
1713
2114
  // src/collectors/index.ts
1714
2115
  var collectors_exports = {};
@@ -1991,20 +2392,31 @@ function create2({
1991
2392
  });
1992
2393
  return poll(
1993
2394
  async () => {
1994
- let { blockNumber: lastBlockNumber, epoch } = await collectorStore.getBlockNumber({
1995
- collectorName: name,
1996
- chainId: chain.id
1997
- });
1998
- await admin.syncBlock();
1999
- lastBlockNumber = await collect({
2000
- chain,
2001
- client,
2002
- collector: name,
2003
- epoch,
2004
- lastBlockNumber,
2005
- withTransaction
2006
- });
2007
- 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
+ }
2008
2420
  },
2009
2421
  { interval: options.interval }
2010
2422
  );
@@ -2024,7 +2436,7 @@ function start(collector) {
2024
2436
  }
2025
2437
  var DEFAULT_BATCH_SIZE2 = 100;
2026
2438
  var DEFAULT_BLOCK_WINDOW2 = 100;
2027
- function from6(parameters) {
2439
+ function from7(parameters) {
2028
2440
  const config = {
2029
2441
  client: parameters.client,
2030
2442
  mempoolAddress: parameters.mempoolAddress,
@@ -2166,7 +2578,7 @@ var ChainIdMismatchError = class extends BaseError {
2166
2578
 
2167
2579
  // src/mempool/MempoolClient.ts
2168
2580
  function connect(parameters) {
2169
- return from6(parameters);
2581
+ return from7(parameters);
2170
2582
  }
2171
2583
 
2172
2584
  // src/stores/CollectorStore.ts
@@ -4006,7 +4418,7 @@ function handleZodError(error2) {
4006
4418
  return new ValidationError("Validation failed", formattedErrors);
4007
4419
  }
4008
4420
 
4009
- // src/api/Api/Controllers/getHealth.ts
4421
+ // src/api/Controllers/getHealth.ts
4010
4422
  async function getHealth(healthService) {
4011
4423
  const logger = Logger_exports.getLogger();
4012
4424
  try {
@@ -4071,266 +4483,31 @@ async function getHealthCollectors(healthService) {
4071
4483
  return error(err);
4072
4484
  }
4073
4485
  }
4074
- var CollectorHealth = z.object({
4075
- name: z.string(),
4076
- chain_id: z.number(),
4077
- block_number: z.number().nullable(),
4078
- updated_at: z.string().nullable(),
4079
- lag: z.number().nullable(),
4080
- status: z.enum(["live", "lagging", "unknown"])
4081
- });
4082
- var CollectorsHealthResponse = z.object({
4083
- collectors: z.array(CollectorHealth)
4084
- });
4085
- var ChainHealth = z.object({
4086
- chain_id: z.number(),
4087
- block_number: z.number(),
4088
- updated_at: z.string()
4089
- });
4090
- var ChainsHealthResponse = z.object({
4091
- chains: z.array(ChainHealth)
4092
- });
4093
- var RouterStatusResponse = z.object({
4094
- status: z.enum(["live", "syncing"])
4095
- });
4096
4486
 
4097
- // src/api/Api/Schema/ObligationResponse.ts
4487
+ // src/api/Schema/ObligationResponse.ts
4098
4488
  var ObligationResponse_exports = {};
4099
4489
  __export(ObligationResponse_exports, {
4100
- from: () => from7
4490
+ from: () => from8
4101
4491
  });
4102
- function from7(obligation) {
4103
- 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
+ });
4104
4499
  }
4105
4500
 
4106
- // src/api/Api/Schema/OfferResponse.ts
4501
+ // src/api/Schema/OfferResponse.ts
4107
4502
  var OfferResponse_exports = {};
4108
4503
  __export(OfferResponse_exports, {
4109
- from: () => from8
4504
+ from: () => from9
4110
4505
  });
4111
- function from8(offer) {
4506
+ function from9(offer) {
4112
4507
  return toSnakeCase(offer);
4113
4508
  }
4114
- var MAX_LIMIT = 100;
4115
- var DEFAULT_LIMIT = 20;
4116
- var PaginationQueryParams = z6.object({
4117
- cursor: z6.string().optional().refine(
4118
- (val) => {
4119
- if (!val) return true;
4120
- try {
4121
- const decoded = Cursor_exports.decode(val);
4122
- return decoded !== null;
4123
- } catch (_error) {
4124
- return false;
4125
- }
4126
- },
4127
- {
4128
- message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
4129
- }
4130
- ).meta({
4131
- description: "Pagination cursor in base64url-encoded format",
4132
- example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
4133
- }),
4134
- limit: z6.string().regex(/^[1-9]\d*$/, {
4135
- message: "Limit must be a positive integer"
4136
- }).transform((val) => Number.parseInt(val, 10)).pipe(
4137
- z6.number().max(MAX_LIMIT, {
4138
- message: `Limit cannot exceed ${MAX_LIMIT}`
4139
- })
4140
- ).optional().default(DEFAULT_LIMIT).meta({
4141
- description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
4142
- example: 10
4143
- })
4144
- });
4145
- var GetOffersQueryParams = z6.object({
4146
- ...PaginationQueryParams.shape,
4147
- side: z6.enum(["buy", "sell"]).meta({
4148
- description: "Side of the offer.",
4149
- example: "buy"
4150
- }),
4151
- 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({
4152
- description: "Offers obligation id",
4153
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
4154
- })
4155
- });
4156
- var GetObligationsQueryParams = z6.object({
4157
- ...PaginationQueryParams.shape,
4158
- cursor: z6.string().optional().meta({
4159
- description: "Obligation id cursor",
4160
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
4161
- })
4162
- });
4163
- var schemas = {
4164
- get_offers: GetOffersQueryParams,
4165
- get_obligations: GetObligationsQueryParams
4166
- };
4167
- function safeParse(action, query, error2) {
4168
- return schemas[action].safeParse(query, {
4169
- error: error2
4170
- });
4171
- }
4172
-
4173
- // src/api/Api/Schema/openapi.ts
4174
- var successResponseSchema = z.object({
4175
- status: z.literal("success"),
4176
- cursor: z.string().nullable(),
4177
- data: z.array(z.any()),
4178
- meta: z.object({
4179
- timestamp: z.string()
4180
- })
4181
- });
4182
- var errorResponseSchema = z.object({
4183
- status: z.literal("error"),
4184
- error: z.object({
4185
- code: z.string(),
4186
- message: z.string(),
4187
- details: z.any().optional()
4188
- }),
4189
- meta: z.object({
4190
- timestamp: z.string()
4191
- })
4192
- });
4193
- var paths = {
4194
- "/v1/offers": {
4195
- get: {
4196
- summary: "Offers",
4197
- description: "Find offers that match specific criteria",
4198
- tags: ["Offers"],
4199
- requestParams: {
4200
- query: GetOffersQueryParams
4201
- },
4202
- responses: {
4203
- 200: {
4204
- description: "Success",
4205
- content: {
4206
- "application/json": {
4207
- schema: successResponseSchema
4208
- }
4209
- }
4210
- },
4211
- 400: {
4212
- description: "Bad Request",
4213
- content: {
4214
- "application/json": {
4215
- schema: errorResponseSchema
4216
- }
4217
- }
4218
- }
4219
- }
4220
- }
4221
- },
4222
- "/v1/obligations": {
4223
- get: {
4224
- summary: "Obligations",
4225
- description: "List obligations with pagination support",
4226
- tags: ["Obligations"],
4227
- requestParams: {
4228
- query: GetObligationsQueryParams
4229
- },
4230
- responses: {
4231
- 200: {
4232
- description: "Success",
4233
- content: {
4234
- "application/json": {
4235
- schema: successResponseSchema
4236
- }
4237
- }
4238
- },
4239
- 400: {
4240
- description: "Bad Request",
4241
- content: {
4242
- "application/json": {
4243
- schema: errorResponseSchema
4244
- }
4245
- }
4246
- }
4247
- }
4248
- }
4249
- },
4250
- "/v1/health": {
4251
- get: {
4252
- summary: "Router status",
4253
- description: "Retrieve the aggregated status of the router.",
4254
- tags: ["Health"],
4255
- responses: {
4256
- 200: {
4257
- description: "Success",
4258
- content: {
4259
- "application/json": {
4260
- schema: RouterStatusResponse
4261
- }
4262
- }
4263
- }
4264
- }
4265
- }
4266
- },
4267
- "/v1/health/collectors": {
4268
- get: {
4269
- summary: "Collectors health",
4270
- description: "Retrieve the block numbers processed by collectors and their sync status.",
4271
- tags: ["Health"],
4272
- responses: {
4273
- 200: {
4274
- description: "Success",
4275
- content: {
4276
- "application/json": {
4277
- schema: CollectorsHealthResponse
4278
- }
4279
- }
4280
- }
4281
- }
4282
- }
4283
- },
4284
- "/v1/health/chains": {
4285
- get: {
4286
- summary: "Chains health",
4287
- description: "Retrieve the latest block processed for each chain.",
4288
- tags: ["Health"],
4289
- responses: {
4290
- 200: {
4291
- description: "Success",
4292
- content: {
4293
- "application/json": {
4294
- schema: ChainsHealthResponse
4295
- }
4296
- }
4297
- }
4298
- }
4299
- }
4300
- }
4301
- };
4302
- createDocument({
4303
- openapi: "3.1.0",
4304
- info: {
4305
- title: "Router API",
4306
- version: "1.0.0",
4307
- description: "API for the Morpho Router"
4308
- },
4309
- tags: [
4310
- {
4311
- name: "Offers"
4312
- },
4313
- {
4314
- name: "Obligations"
4315
- },
4316
- {
4317
- name: "Health"
4318
- }
4319
- ],
4320
- servers: [
4321
- {
4322
- url: "https://router.morpho.dev",
4323
- description: "Production server"
4324
- },
4325
- {
4326
- url: "http://localhost:8081",
4327
- description: "Local development server"
4328
- }
4329
- ],
4330
- paths
4331
- });
4332
4509
 
4333
- // src/api/Api/Controllers/getObligations.ts
4510
+ // src/api/Controllers/getObligations.ts
4334
4511
  async function getObligations(queryParameters, store) {
4335
4512
  const logger = Logger_exports.getLogger();
4336
4513
  const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
@@ -4341,8 +4518,20 @@ async function getObligations(queryParameters, store) {
4341
4518
  cursor: query.cursor,
4342
4519
  limit: query.limit
4343
4520
  });
4521
+ const quotes = await store.getQuotes({
4522
+ obligationIds: obligations2.map((o) => Obligation_exports.id(o))
4523
+ });
4344
4524
  return success({
4345
- 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
+ ),
4346
4535
  cursor: nextCursor ?? null
4347
4536
  });
4348
4537
  } catch (err) {
@@ -4356,7 +4545,7 @@ async function getObligations(queryParameters, store) {
4356
4545
  }
4357
4546
  }
4358
4547
 
4359
- // src/api/Api/Controllers/getOffers.ts
4548
+ // src/api/Controllers/getOffers.ts
4360
4549
  async function getOffers(queryParameters, store) {
4361
4550
  const logger = Logger_exports.getLogger();
4362
4551
  const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
@@ -4384,7 +4573,7 @@ async function getOffers(queryParameters, store) {
4384
4573
  }
4385
4574
  }
4386
4575
 
4387
- // src/api/Api/Api.ts
4576
+ // src/api/Api.ts
4388
4577
  function create5(config) {
4389
4578
  return {
4390
4579
  serve: ({ port }) => {
@@ -4416,6 +4605,13 @@ function serve2(parameters) {
4416
4605
  const { statusCode, body } = await getHealthChains(healthService);
4417
4606
  return c.json(body, statusCode);
4418
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));
4419
4615
  serve$1({
4420
4616
  fetch: app.fetch,
4421
4617
  port: parameters.port
@@ -5036,6 +5232,64 @@ function create8(config) {
5036
5232
  const returnedItems = Array.from(items.values());
5037
5233
  const nextCursor = returnedItems.length === limit && returnedItems.length > 0 ? result[result.length - 1].obligationId : null;
5038
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
+ });
5039
5293
  }
5040
5294
  };
5041
5295
  }
@@ -5777,7 +6031,7 @@ function create9(params) {
5777
6031
  }
5778
6032
 
5779
6033
  // src/services/Services.ts
5780
- function from9(config) {
6034
+ function from10(config) {
5781
6035
  const {
5782
6036
  chain,
5783
6037
  rpcUrl,
@@ -5905,7 +6159,7 @@ var RouterCmd = class _RouterCmd extends Command {
5905
6159
  "RPC URL is required, please set the ROUTER_RPC_URL environment variable or use --rpc-url <url> option."
5906
6160
  );
5907
6161
  }
5908
- const routerServices = from9({
6162
+ const routerServices = from10({
5909
6163
  chain,
5910
6164
  rpcUrl,
5911
6165
  dbConfig: {