@morpho-dev/router 0.1.16 → 0.1.18

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.18",
45
45
  description: "Router package for Morpho protocol"};
46
46
 
47
47
  // src/core/Chain.ts
@@ -142,8 +142,12 @@ var chains = {
142
142
  [
143
143
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
144
144
  // USDC
145
- "0x6B175474E89094C44Da98b954EedeAC495271d0F"
145
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
146
146
  // DAI
147
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
148
+ // WETH
149
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
150
+ // WBTC
147
151
  ].map((address) => address.toLowerCase())
148
152
  ),
149
153
  morpho: "0x0000000000000000000000000000000000000000",
@@ -166,8 +170,12 @@ var chains = {
166
170
  [
167
171
  "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
168
172
  // USDC
169
- "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb"
173
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
170
174
  // DAI
175
+ "0x4200000000000000000000000000000000000006",
176
+ // WETH
177
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
178
+ // WBTC
171
179
  ].map((address) => address.toLowerCase())
172
180
  ),
173
181
  morpho: "0x0000000000000000000000000000000000000000",
@@ -190,8 +198,12 @@ var chains = {
190
198
  [
191
199
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
192
200
  // USDC
193
- "0x6B175474E89094C44Da98b954EedeAC495271d0F"
201
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
194
202
  // DAI
203
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
204
+ // WETH
205
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
206
+ // WBTC
195
207
  ].map((address) => address.toLowerCase())
196
208
  ),
197
209
  morpho: "0x11a002d45db720ed47a80d2f3489cba5b833eaf5",
@@ -215,8 +227,12 @@ var chains = {
215
227
  [
216
228
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
217
229
  // USDC
218
- "0x6B175474E89094C44Da98b954EedeAC495271d0F"
230
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
219
231
  // DAI
232
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
233
+ // WETH
234
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
235
+ // WBTC
220
236
  ].map((address) => address.toLowerCase())
221
237
  ),
222
238
  morpho: "0x23DFBc4B8B80C14CC5e25011B8491f268395BAd6",
@@ -727,97 +743,6 @@ var from2 = (parameters) => {
727
743
  };
728
744
  };
729
745
 
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
746
  // src/core/Liquidity.ts
822
747
  var Liquidity_exports = {};
823
748
  __export(Liquidity_exports, {
@@ -1133,9 +1058,9 @@ __export(Offer_exports, {
1133
1058
  OfferHashSchema: () => OfferHashSchema,
1134
1059
  OfferSchema: () => OfferSchema,
1135
1060
  consumedEvent: () => consumedEvent,
1136
- decode: () => decode2,
1061
+ decode: () => decode,
1137
1062
  domain: () => domain,
1138
- encode: () => encode2,
1063
+ encode: () => encode,
1139
1064
  from: () => from5,
1140
1065
  fromConsumedLog: () => fromConsumedLog,
1141
1066
  fromSnakeCase: () => fromSnakeCase3,
@@ -1306,39 +1231,92 @@ function fromSnakeCase3(input) {
1306
1231
  function toSnakeCase2(offer) {
1307
1232
  return toSnakeCase(offer);
1308
1233
  }
1309
- function random2() {
1310
- const loanToken = privateKeyToAccount(generatePrivateKey()).address;
1311
- const maturity = from3("end_of_month");
1312
- const expiry = from3("end_of_week") - 1;
1313
- const lltv = from(0.965);
1234
+ function random2(config) {
1235
+ const chain = config?.chains ? config.chains[Math.floor(Math.random() * config.chains.length)] : chains.ethereum;
1236
+ const loanToken = config?.loanTokens ? config.loanTokens[Math.floor(Math.random() * config.loanTokens.length)] : privateKeyToAccount(generatePrivateKey()).address;
1237
+ const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [privateKeyToAccount(generatePrivateKey()).address];
1238
+ const collateralAsset = collateralCandidates[Math.floor(Math.random() * collateralCandidates.length)];
1239
+ const maturityOption = weightedChoice([
1240
+ ["end_of_month", 1],
1241
+ ["end_of_next_month", 1]
1242
+ ]);
1243
+ const maturity = config?.maturity ?? from3(maturityOption);
1244
+ const lltv = from(
1245
+ weightedChoice([
1246
+ [0.385, 1],
1247
+ [0.5, 1],
1248
+ [0.625, 2],
1249
+ [0.77, 8],
1250
+ [0.86, 10],
1251
+ [0.915, 8],
1252
+ [0.945, 6],
1253
+ [0.965, 4],
1254
+ [0.98, 2]
1255
+ ])
1256
+ );
1257
+ const buy = config?.buy !== void 0 ? config.buy : Math.random() > 0.5;
1258
+ const ONE = 1000000000000000000n;
1259
+ const qMin = buy ? 16 : 4;
1260
+ const qMax = buy ? 32 : 16;
1261
+ const len = qMax - qMin + 1;
1262
+ const ratePairs = Array.from(
1263
+ { length: len },
1264
+ (_, idx) => {
1265
+ const q = qMin + idx;
1266
+ const scaledRate = BigInt(q) * (ONE / 4n);
1267
+ const weight = buy ? 1 + idx : 1 + (len - 1 - idx);
1268
+ return [scaledRate, weight];
1269
+ }
1270
+ );
1271
+ const rate = config?.rate ?? weightedChoice(ratePairs);
1272
+ const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
1273
+ const unit = BigInt(10) ** BigInt(loanTokenDecimals);
1274
+ const amountBase = BigInt(100 + Math.floor(Math.random() * (1e6 - 100 + 1)));
1275
+ const assetsScaled = config?.assets ?? amountBase * unit;
1276
+ const consumed2 = config?.consumed !== void 0 ? config.consumed : Math.random() < 0.8 ? 0n : assetsScaled * BigInt(1 + Math.floor(Math.random() * 900)) / 1000n;
1277
+ const callbackBySide = (() => {
1278
+ if (buy) return { address: zeroAddress, data: "0x", gasLimit: 0n };
1279
+ const sellCallbackAddress = WhitelistedCallbackAddresses["sell_erc20_callback" /* SellERC20Callback */][0].toLowerCase();
1280
+ const amount = assetsScaled * 1000000000000000000000n;
1281
+ const data = encodeSellERC20Callback({
1282
+ collaterals: [collateralAsset],
1283
+ amounts: [amount]
1284
+ });
1285
+ return { address: sellCallbackAddress, data, gasLimit: 500000n };
1286
+ })();
1314
1287
  const offer = from5({
1315
- offering: privateKeyToAccount(generatePrivateKey()).address,
1316
- assets: BigInt(Math.floor(Math.random() * 1e6)),
1317
- rate: BigInt(Math.floor(Math.random() * 1e6)),
1288
+ offering: config?.offering ?? privateKeyToAccount(generatePrivateKey()).address,
1289
+ assets: assetsScaled,
1290
+ rate,
1318
1291
  maturity,
1319
- expiry,
1320
- start: expiry - 10,
1292
+ expiry: config?.expiry ?? maturity - 1,
1293
+ start: config?.start ?? maturity - 10,
1321
1294
  nonce: BigInt(Math.floor(Math.random() * 1e6)),
1322
- buy: Math.random() > 0.5,
1323
- chainId: 1n,
1295
+ buy,
1296
+ chainId: chain.id,
1324
1297
  loanToken,
1325
- collaterals: [
1298
+ collaterals: config?.collaterals ?? [
1326
1299
  from2({
1327
- asset: zeroAddress,
1300
+ asset: collateralAsset,
1328
1301
  oracle: zeroAddress,
1329
1302
  lltv
1330
1303
  })
1331
1304
  ],
1332
- callback: {
1333
- address: zeroAddress,
1334
- data: "0x",
1335
- gasLimit: 0n
1336
- },
1337
- consumed: 0n,
1305
+ callback: config?.callback ?? callbackBySide,
1306
+ consumed: consumed2,
1338
1307
  blockNumber: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
1339
1308
  });
1340
1309
  return offer;
1341
1310
  }
1311
+ var weightedChoice = (pairs) => {
1312
+ const total = pairs.reduce((sum, [, weight]) => sum + weight, 0);
1313
+ let roll = Math.random() * total;
1314
+ for (const [value, weight] of pairs) {
1315
+ roll -= weight;
1316
+ if (roll < 0) return value;
1317
+ }
1318
+ return pairs[0][0];
1319
+ };
1342
1320
  var domain = (chainId) => ({
1343
1321
  chainId,
1344
1322
  verifyingContract: zeroAddress
@@ -1460,7 +1438,7 @@ var OfferAbi = [
1460
1438
  },
1461
1439
  { name: "signature", type: "bytes" }
1462
1440
  ];
1463
- function encode2(offer) {
1441
+ function encode(offer) {
1464
1442
  return encodeAbiParameters(OfferAbi, [
1465
1443
  offer.offering,
1466
1444
  offer.assets,
@@ -1477,7 +1455,7 @@ function encode2(offer) {
1477
1455
  offer.signature ?? "0x"
1478
1456
  ]);
1479
1457
  }
1480
- function decode2(data, blockNumber) {
1458
+ function decode(data, blockNumber) {
1481
1459
  let decoded;
1482
1460
  try {
1483
1461
  decoded = decodeAbiParameters(OfferAbi, data);
@@ -1547,6 +1525,57 @@ var AccountNotSetError = class extends BaseError {
1547
1525
  }
1548
1526
  };
1549
1527
 
1528
+ // src/core/Quote.ts
1529
+ var Quote_exports = {};
1530
+ __export(Quote_exports, {
1531
+ InvalidQuoteError: () => InvalidQuoteError,
1532
+ QuoteSchema: () => QuoteSchema,
1533
+ from: () => from6,
1534
+ fromSnakeCase: () => fromSnakeCase4,
1535
+ random: () => random3
1536
+ });
1537
+ var QuoteSchema = z6.object({
1538
+ obligationId: z6.string().transform(transformHex),
1539
+ ask: z6.object({
1540
+ rate: z6.bigint({ coerce: true }).min(0n).max(maxUint256)
1541
+ }),
1542
+ bid: z6.object({
1543
+ rate: z6.bigint({ coerce: true }).min(0n).max(maxUint256)
1544
+ })
1545
+ });
1546
+ function from6(parameters) {
1547
+ try {
1548
+ const parsedQuote = QuoteSchema.parse(parameters);
1549
+ return {
1550
+ obligationId: parsedQuote.obligationId,
1551
+ ask: parsedQuote.ask,
1552
+ bid: parsedQuote.bid
1553
+ };
1554
+ } catch (error2) {
1555
+ throw new InvalidQuoteError(error2);
1556
+ }
1557
+ }
1558
+ function fromSnakeCase4(snake) {
1559
+ return from6(fromSnakeCase(snake));
1560
+ }
1561
+ function random3() {
1562
+ return from6({
1563
+ obligationId: Obligation_exports.id(Obligation_exports.random()),
1564
+ ask: {
1565
+ rate: BigInt(Math.floor(Math.random() * 1e6))
1566
+ },
1567
+ bid: {
1568
+ rate: BigInt(Math.floor(Math.random() * 1e6))
1569
+ }
1570
+ });
1571
+ }
1572
+ var InvalidQuoteError = class extends BaseError {
1573
+ name = "Quote.InvalidQuoteError";
1574
+ constructor(error2) {
1575
+ super("Invalid quote.", { cause: error2 });
1576
+ }
1577
+ };
1578
+
1550
1579
  // src/evm/EVM.ts
1551
1580
  var users = [
1552
1581
  privateKeyToAccount(
@@ -1663,11 +1692,16 @@ function defaultLogger(minLevel, pretty) {
1663
1692
  return;
1664
1693
  }
1665
1694
  const { msg, ...rest } = entry;
1695
+ const stack = typeof rest.stack === "string" ? rest.stack : void 0;
1696
+ if (stack) delete rest.stack;
1666
1697
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
1667
1698
  const level = methodLevel.toUpperCase();
1668
1699
  const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
1669
1700
  const line = extras.length > 0 ? `${timestamp2} [${level}] ${msg} ${extras}` : `${timestamp2} [${level}] ${msg}`;
1670
1701
  console[consoleMethod](line);
1702
+ if (stack) {
1703
+ console[consoleMethod](stack);
1704
+ }
1671
1705
  } : () => {
1672
1706
  };
1673
1707
  return {
@@ -1709,30 +1743,466 @@ function formatValue(value) {
1709
1743
  }
1710
1744
  }
1711
1745
  }
1746
+ var CollectorHealth = z.object({
1747
+ name: z.string(),
1748
+ chain_id: z.number(),
1749
+ block_number: z.number().nullable(),
1750
+ updated_at: z.string().nullable(),
1751
+ lag: z.number().nullable(),
1752
+ status: z.enum(["live", "lagging", "unknown"])
1753
+ });
1754
+ var CollectorsHealthResponse = z.array(CollectorHealth);
1755
+ var ChainHealth = z.object({
1756
+ chain_id: z.number(),
1757
+ block_number: z.number(),
1758
+ updated_at: z.string()
1759
+ });
1760
+ var ChainsHealthResponse = z.array(ChainHealth);
1761
+ var RouterStatusResponse = z.object({
1762
+ status: z.enum(["live", "syncing"])
1763
+ });
1712
1764
 
1713
- // src/collectors/index.ts
1714
- var collectors_exports = {};
1715
- __export(collectors_exports, {
1716
- batch: () => batch2,
1717
- create: () => create2,
1718
- createBuilder: () => createBuilder,
1719
- fetchBalancesAndAllowances: () => fetchBalancesAndAllowances,
1720
- fetchCollateralAndDebt: () => fetchCollateralAndDebt,
1721
- fetchOraclePrices: () => fetchOraclePrices,
1722
- fetchUserVaultMarketLiquidity: () => fetchUserVaultMarketLiquidity,
1723
- morpho: () => morpho,
1724
- names: () => names,
1725
- run: () => run,
1726
- single: () => single,
1727
- start: () => start
1765
+ // src/stores/utils/Cursor.ts
1766
+ var Cursor_exports = {};
1767
+ __export(Cursor_exports, {
1768
+ decode: () => decode2,
1769
+ encode: () => encode2,
1770
+ validate: () => validate
1728
1771
  });
1772
+ function validate(cursor) {
1773
+ if (!cursor || typeof cursor !== "object") {
1774
+ throw new Error("Cursor must be an object");
1775
+ }
1776
+ const c = cursor;
1777
+ if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
1778
+ throw new Error(
1779
+ `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
1780
+ );
1781
+ }
1782
+ if (!["asc", "desc"].includes(c.dir)) {
1783
+ throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
1784
+ }
1785
+ if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
1786
+ throw new Error(
1787
+ `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
1788
+ );
1789
+ }
1790
+ const validations = {
1791
+ rate: {
1792
+ field: "rate",
1793
+ type: "string",
1794
+ pattern: /^\d+$/,
1795
+ error: "numeric string"
1796
+ },
1797
+ amount: {
1798
+ field: "assets",
1799
+ type: "string",
1800
+ pattern: /^\d+$/,
1801
+ error: "numeric string"
1802
+ },
1803
+ maturity: {
1804
+ field: "maturity",
1805
+ type: "number",
1806
+ validator: (val) => val > 0,
1807
+ error: "positive number"
1808
+ },
1809
+ expiry: {
1810
+ field: "expiry",
1811
+ type: "number",
1812
+ validator: (val) => val > 0,
1813
+ error: "positive number"
1814
+ }
1815
+ };
1816
+ const validation = validations[c.sort];
1817
+ if (!validation) {
1818
+ throw new Error(`Invalid sort field: ${c.sort}`);
1819
+ }
1820
+ const fieldValue = c[validation.field];
1821
+ if (!fieldValue) {
1822
+ throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
1823
+ }
1824
+ if (typeof fieldValue !== validation.type) {
1825
+ throw new Error(
1826
+ `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
1827
+ );
1828
+ }
1829
+ if (validation.pattern && !validation.pattern.test(fieldValue)) {
1830
+ throw new Error(
1831
+ `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
1832
+ );
1833
+ }
1834
+ if (validation.validator && !validation.validator(fieldValue)) {
1835
+ throw new Error(
1836
+ `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
1837
+ );
1838
+ }
1839
+ if (c.page !== void 0) {
1840
+ if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) {
1841
+ throw new Error("Invalid page: must be a positive integer");
1842
+ }
1843
+ }
1844
+ return true;
1845
+ }
1846
+ function encode2(c) {
1847
+ return Base64.encodeURL(JSON.stringify(c));
1848
+ }
1849
+ function decode2(token) {
1850
+ if (!token) return null;
1851
+ const decoded = JSON.parse(Base64.decode(token));
1852
+ validate(decoded);
1853
+ return decoded;
1854
+ }
1729
1855
 
1730
- // src/services/RouterAdmin.ts
1731
- function create(parameters) {
1732
- const collector = "admin";
1733
- const {
1734
- client,
1735
- chain,
1856
+ // src/api/Schema/requests.ts
1857
+ var MAX_LIMIT = 100;
1858
+ var DEFAULT_LIMIT = 20;
1859
+ var PaginationQueryParams = z6.object({
1860
+ cursor: z6.string().optional().refine(
1861
+ (val) => {
1862
+ if (!val) return true;
1863
+ try {
1864
+ const decoded = Cursor_exports.decode(val);
1865
+ return decoded !== null;
1866
+ } catch (_error) {
1867
+ return false;
1868
+ }
1869
+ },
1870
+ {
1871
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
1872
+ }
1873
+ ).meta({
1874
+ description: "Pagination cursor in base64url-encoded format",
1875
+ example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
1876
+ }),
1877
+ limit: z6.string().regex(/^[1-9]\d*$/, {
1878
+ message: "Limit must be a positive integer"
1879
+ }).transform((val) => Number.parseInt(val, 10)).pipe(
1880
+ z6.number().max(MAX_LIMIT, {
1881
+ message: `Limit cannot exceed ${MAX_LIMIT}`
1882
+ })
1883
+ ).optional().default(DEFAULT_LIMIT).meta({
1884
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
1885
+ example: 10
1886
+ })
1887
+ });
1888
+ var GetOffersQueryParams = z6.object({
1889
+ ...PaginationQueryParams.shape,
1890
+ side: z6.enum(["buy", "sell"]).meta({
1891
+ description: "Side of the offer.",
1892
+ example: "buy"
1893
+ }),
1894
+ 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({
1895
+ description: "Offers obligation id",
1896
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
1897
+ })
1898
+ });
1899
+ var GetObligationsQueryParams = z6.object({
1900
+ ...PaginationQueryParams.shape,
1901
+ cursor: z6.string().optional().meta({
1902
+ description: "Obligation id cursor",
1903
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
1904
+ })
1905
+ });
1906
+ var schemas = {
1907
+ get_offers: GetOffersQueryParams,
1908
+ get_obligations: GetObligationsQueryParams
1909
+ };
1910
+ function safeParse(action, query, error2) {
1911
+ return schemas[action].safeParse(query, {
1912
+ error: error2
1913
+ });
1914
+ }
1915
+
1916
+ // src/api/Schema/openapi.ts
1917
+ var timestampExample = "2024-01-01T12:00:00.000Z";
1918
+ var cursorExample = "eyJvZmZzZXQiOjEwMH0";
1919
+ function makeSuccessResponse(parameters) {
1920
+ const { dataSchema, dataDescription, dataExample, cursor } = parameters;
1921
+ const withDataMeta = dataDescription ? dataSchema.meta({ description: dataDescription }) : dataSchema;
1922
+ return z.object({
1923
+ status: z.literal("success"),
1924
+ cursor: z.string().nullable(),
1925
+ data: z.any(),
1926
+ meta: z.object({
1927
+ timestamp: z.string()
1928
+ })
1929
+ }).extend({
1930
+ data: withDataMeta
1931
+ }).meta({
1932
+ example: {
1933
+ status: "success",
1934
+ cursor,
1935
+ data: dataExample,
1936
+ meta: { timestamp: timestampExample }
1937
+ }
1938
+ });
1939
+ }
1940
+ var OffersSuccessResponseSchema = makeSuccessResponse({
1941
+ dataSchema: z.array(z.any()),
1942
+ dataDescription: "Offers matching the provided filters.",
1943
+ dataExample: [toSnakeCase(Offer_exports.random())],
1944
+ cursor: cursorExample
1945
+ });
1946
+ var ObligationsSuccessResponseSchema = makeSuccessResponse({
1947
+ dataSchema: z.array(z.any()),
1948
+ dataDescription: "Obligations known to the router.",
1949
+ dataExample: [toSnakeCase(Obligation_exports.random())],
1950
+ cursor: cursorExample
1951
+ });
1952
+ var RouterStatusSuccessResponseSchema = makeSuccessResponse({
1953
+ dataSchema: RouterStatusResponse,
1954
+ dataDescription: "Aggregated router status.",
1955
+ dataExample: { status: "live" },
1956
+ cursor: null
1957
+ });
1958
+ var CollectorsHealthSuccessResponseSchema = makeSuccessResponse({
1959
+ dataSchema: CollectorsHealthResponse,
1960
+ dataDescription: "Collectors health details and sync status.",
1961
+ dataExample: [
1962
+ {
1963
+ name: "mempool_offers",
1964
+ chain_id: "1",
1965
+ block_number: 21345678,
1966
+ updated_at: "2024-01-01T12:00:00.000Z",
1967
+ lag: 0,
1968
+ status: "live"
1969
+ }
1970
+ ],
1971
+ cursor: null
1972
+ });
1973
+ var ChainsHealthSuccessResponseSchema = makeSuccessResponse({
1974
+ dataSchema: ChainsHealthResponse,
1975
+ dataDescription: "Latest processed block per chain.",
1976
+ dataExample: [
1977
+ {
1978
+ chain_id: "1",
1979
+ block_number: 21345678,
1980
+ updated_at: "2024-01-01T12:00:00.000Z"
1981
+ }
1982
+ ],
1983
+ cursor: null
1984
+ });
1985
+ var errorResponseSchema = z.object({
1986
+ status: z.literal("error"),
1987
+ error: z.object({
1988
+ code: z.string(),
1989
+ message: z.string(),
1990
+ details: z.any().optional()
1991
+ }),
1992
+ meta: z.object({
1993
+ timestamp: z.string()
1994
+ })
1995
+ }).meta({
1996
+ description: "Error response wrapper.",
1997
+ example: {
1998
+ status: "error",
1999
+ error: {
2000
+ code: "VALIDATION_ERROR",
2001
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object",
2002
+ details: [
2003
+ {
2004
+ field: "cursor",
2005
+ issue: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
2006
+ }
2007
+ ]
2008
+ },
2009
+ meta: {
2010
+ timestamp: timestampExample
2011
+ }
2012
+ }
2013
+ });
2014
+ var paths = {
2015
+ "/v1/offers": {
2016
+ get: {
2017
+ summary: "Offers",
2018
+ description: "Find offers that match specific criteria",
2019
+ tags: ["Offers"],
2020
+ requestParams: {
2021
+ query: GetOffersQueryParams
2022
+ },
2023
+ responses: {
2024
+ 200: {
2025
+ description: "Success",
2026
+ content: {
2027
+ "application/json": {
2028
+ schema: OffersSuccessResponseSchema
2029
+ }
2030
+ }
2031
+ },
2032
+ 400: {
2033
+ description: "Bad Request",
2034
+ content: {
2035
+ "application/json": {
2036
+ schema: errorResponseSchema
2037
+ }
2038
+ }
2039
+ }
2040
+ }
2041
+ }
2042
+ },
2043
+ "/v1/obligations": {
2044
+ get: {
2045
+ summary: "Obligations",
2046
+ description: "List obligations with pagination support",
2047
+ tags: ["Obligations"],
2048
+ requestParams: {
2049
+ query: GetObligationsQueryParams
2050
+ },
2051
+ responses: {
2052
+ 200: {
2053
+ description: "Success",
2054
+ content: {
2055
+ "application/json": {
2056
+ schema: ObligationsSuccessResponseSchema
2057
+ }
2058
+ }
2059
+ },
2060
+ 400: {
2061
+ description: "Bad Request",
2062
+ content: {
2063
+ "application/json": {
2064
+ schema: errorResponseSchema
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2069
+ }
2070
+ },
2071
+ "/v1/health": {
2072
+ get: {
2073
+ summary: "Router status",
2074
+ description: "Retrieve the aggregated status of the router.",
2075
+ tags: ["Health"],
2076
+ responses: {
2077
+ 200: {
2078
+ description: "Success",
2079
+ content: {
2080
+ "application/json": {
2081
+ schema: RouterStatusSuccessResponseSchema
2082
+ }
2083
+ }
2084
+ }
2085
+ }
2086
+ }
2087
+ },
2088
+ "/v1/health/collectors": {
2089
+ get: {
2090
+ summary: "Collectors health",
2091
+ description: "Retrieve the block numbers processed by collectors and their sync status.",
2092
+ tags: ["Health"],
2093
+ responses: {
2094
+ 200: {
2095
+ description: "Success",
2096
+ content: {
2097
+ "application/json": {
2098
+ schema: CollectorsHealthSuccessResponseSchema
2099
+ }
2100
+ }
2101
+ }
2102
+ }
2103
+ }
2104
+ },
2105
+ "/v1/health/chains": {
2106
+ get: {
2107
+ summary: "Chains health",
2108
+ description: "Retrieve the latest block processed for each chain.",
2109
+ tags: ["Health"],
2110
+ responses: {
2111
+ 200: {
2112
+ description: "Success",
2113
+ content: {
2114
+ "application/json": {
2115
+ schema: ChainsHealthSuccessResponseSchema
2116
+ }
2117
+ }
2118
+ }
2119
+ }
2120
+ }
2121
+ }
2122
+ };
2123
+ var OpenApi = createDocument({
2124
+ openapi: "3.1.0",
2125
+ info: {
2126
+ title: "Router API",
2127
+ version: "1.0.0",
2128
+ description: "API for the Morpho Router"
2129
+ },
2130
+ tags: [
2131
+ {
2132
+ name: "Offers"
2133
+ },
2134
+ {
2135
+ name: "Obligations"
2136
+ },
2137
+ {
2138
+ name: "Health"
2139
+ }
2140
+ ],
2141
+ servers: [
2142
+ {
2143
+ url: "https://router.morpho.dev",
2144
+ description: "Production server"
2145
+ },
2146
+ {
2147
+ url: "http://localhost:7891",
2148
+ description: "Local development server"
2149
+ }
2150
+ ],
2151
+ paths
2152
+ });
2153
+
2154
+ // src/api/Controllers/getDocs.ts
2155
+ function getSwaggerJson() {
2156
+ return OpenApi;
2157
+ }
2158
+ function getDocsHtml() {
2159
+ const html = `<!DOCTYPE html>
2160
+ <html>
2161
+ <head>
2162
+ <meta charset="UTF-8">
2163
+ <title>Router API Docs (Scalar)</title>
2164
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
2165
+ <style>
2166
+ html, body { margin: 0; height: 100%; }
2167
+ api-reference { height: 100%; width: 100%; }
2168
+ </style>
2169
+ </head>
2170
+ <body>
2171
+ <div id="api-container" style="height:100%;width:100%;"></div>
2172
+ <script>
2173
+ window.addEventListener('load', function () {
2174
+ const spec = ${JSON.stringify(OpenApi)};
2175
+ Scalar.createApiReference('#api-container', { spec: { content: spec } });
2176
+ });
2177
+ </script>
2178
+ </body>
2179
+ </html>`;
2180
+ return html;
2181
+ }
2182
+
2183
+ // src/collectors/index.ts
2184
+ var collectors_exports = {};
2185
+ __export(collectors_exports, {
2186
+ batch: () => batch2,
2187
+ create: () => create2,
2188
+ createBuilder: () => createBuilder,
2189
+ fetchBalancesAndAllowances: () => fetchBalancesAndAllowances,
2190
+ fetchCollateralAndDebt: () => fetchCollateralAndDebt,
2191
+ fetchOraclePrices: () => fetchOraclePrices,
2192
+ fetchUserVaultMarketLiquidity: () => fetchUserVaultMarketLiquidity,
2193
+ morpho: () => morpho,
2194
+ names: () => names,
2195
+ run: () => run,
2196
+ single: () => single,
2197
+ start: () => start
2198
+ });
2199
+
2200
+ // src/services/RouterAdmin.ts
2201
+ function create(parameters) {
2202
+ const collector = "admin";
2203
+ const {
2204
+ client,
2205
+ chain,
1736
2206
  withTransaction,
1737
2207
  options: { maxBatchSize = 25, maxBlockNumber } = {}
1738
2208
  } = parameters;
@@ -1991,20 +2461,31 @@ function create2({
1991
2461
  });
1992
2462
  return poll(
1993
2463
  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);
2464
+ try {
2465
+ let { blockNumber: lastBlockNumber, epoch } = await collectorStore.getBlockNumber({
2466
+ collectorName: name,
2467
+ chainId: chain.id
2468
+ });
2469
+ await admin.syncBlock();
2470
+ lastBlockNumber = await collect({
2471
+ chain,
2472
+ client,
2473
+ collector: name,
2474
+ epoch,
2475
+ lastBlockNumber,
2476
+ withTransaction
2477
+ });
2478
+ emit(lastBlockNumber);
2479
+ } catch (err) {
2480
+ const isError = err instanceof Error;
2481
+ logger.error({
2482
+ msg: "Collector error",
2483
+ collector,
2484
+ chain_id: chain.id,
2485
+ error: isError ? err.message : String(err),
2486
+ stack: isError ? err.stack : void 0
2487
+ });
2488
+ }
2008
2489
  },
2009
2490
  { interval: options.interval }
2010
2491
  );
@@ -2024,7 +2505,7 @@ function start(collector) {
2024
2505
  }
2025
2506
  var DEFAULT_BATCH_SIZE2 = 100;
2026
2507
  var DEFAULT_BLOCK_WINDOW2 = 100;
2027
- function from6(parameters) {
2508
+ function from7(parameters) {
2028
2509
  const config = {
2029
2510
  client: parameters.client,
2030
2511
  mempoolAddress: parameters.mempoolAddress,
@@ -2166,7 +2647,7 @@ var ChainIdMismatchError = class extends BaseError {
2166
2647
 
2167
2648
  // src/mempool/MempoolClient.ts
2168
2649
  function connect(parameters) {
2169
- return from6(parameters);
2650
+ return from7(parameters);
2170
2651
  }
2171
2652
 
2172
2653
  // src/stores/CollectorStore.ts
@@ -3952,385 +4433,150 @@ function handleAPIError(error2) {
3952
4433
  code: error2.code,
3953
4434
  message: error2.message,
3954
4435
  ...error2.details && typeof error2.details === "object" ? { details: error2.details } : {}
3955
- },
3956
- meta: {
3957
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3958
- }
3959
- }
3960
- };
3961
- }
3962
- var APIError = class extends Error {
3963
- constructor(statusCode, message, code, details) {
3964
- super(message);
3965
- this.statusCode = statusCode;
3966
- this.code = code;
3967
- this.details = details;
3968
- this.name = "APIError";
3969
- }
3970
- };
3971
- var ValidationError = class extends APIError {
3972
- constructor(message, details) {
3973
- super(400 /* BAD_REQUEST */, message, "VALIDATION_ERROR", details);
3974
- }
3975
- };
3976
- var InternalServerError = class extends APIError {
3977
- constructor(message = "Internal server error") {
3978
- super(500 /* INTERNAL_SERVER_ERROR */, message, "INTERNAL_SERVER_ERROR");
3979
- }
3980
- };
3981
- var BadRequestError = class extends APIError {
3982
- constructor(message = "Invalid JSON format", details) {
3983
- super(400 /* BAD_REQUEST */, message, "BAD_REQUEST", details);
3984
- }
3985
- };
3986
- function handleZodError(error2) {
3987
- const formattedErrors = error2.issues.map((err) => {
3988
- const field = err.path.join(".");
3989
- let issue = err.message;
3990
- if (err.code === "invalid_type") {
3991
- if (err.message.includes("received undefined")) {
3992
- issue = `${field} is required`;
3993
- } else {
3994
- issue = err.message;
3995
- }
3996
- } else if (err.code === "invalid_format" && err.format === "regex") {
3997
- issue = err.message;
3998
- } else if (err.code === "invalid_format") {
3999
- issue = `${field} has an invalid format`;
4000
- }
4001
- return {
4002
- field,
4003
- issue
4004
- };
4005
- });
4006
- return new ValidationError("Validation failed", formattedErrors);
4007
- }
4008
-
4009
- // src/api/Api/Controllers/getHealth.ts
4010
- async function getHealth(healthService) {
4011
- const logger = Logger_exports.getLogger();
4012
- try {
4013
- const status = await healthService.getStatus();
4014
- return success({ data: toSnakeCase({ status }) });
4015
- } catch (err) {
4016
- logger.error({
4017
- err,
4018
- msg: "Error getting health status",
4019
- errorMessage: err instanceof Error ? err.message : String(err),
4020
- errorStack: err instanceof Error ? err.stack : void 0
4021
- });
4022
- return error(err);
4023
- }
4024
- }
4025
- async function getHealthChains(healthService) {
4026
- const logger = Logger_exports.getLogger();
4027
- try {
4028
- const chains3 = await healthService.getChains();
4029
- return success({
4030
- data: chains3.map(
4031
- ({ chainId, blockNumber, updatedAt }) => toSnakeCase({
4032
- chainId,
4033
- blockNumber,
4034
- updatedAt
4035
- })
4036
- )
4037
- });
4038
- } catch (err) {
4039
- logger.error({
4040
- err,
4041
- msg: "Error getting health status for chains",
4042
- errorMessage: err instanceof Error ? err.message : String(err),
4043
- errorStack: err instanceof Error ? err.stack : void 0
4044
- });
4045
- return error(err);
4046
- }
4047
- }
4048
- async function getHealthCollectors(healthService) {
4049
- const logger = Logger_exports.getLogger();
4050
- try {
4051
- const collectors2 = await healthService.getCollectors();
4052
- return success({
4053
- data: collectors2.map(
4054
- ({ name, chainId, blockNumber, updatedAt, lag, status }) => toSnakeCase({
4055
- name,
4056
- chainId,
4057
- blockNumber,
4058
- updatedAt,
4059
- lag,
4060
- status
4061
- })
4062
- )
4063
- });
4064
- } catch (err) {
4065
- logger.error({
4066
- err,
4067
- msg: "Error getting health status for collectors",
4068
- errorMessage: err instanceof Error ? err.message : String(err),
4069
- errorStack: err instanceof Error ? err.stack : void 0
4070
- });
4071
- return error(err);
4072
- }
4073
- }
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
-
4097
- // src/api/Api/Schema/ObligationResponse.ts
4098
- var ObligationResponse_exports = {};
4099
- __export(ObligationResponse_exports, {
4100
- from: () => from7
4101
- });
4102
- function from7(obligation) {
4103
- return toSnakeCase({ id: Obligation_exports.id(obligation), ...obligation });
4104
- }
4105
-
4106
- // src/api/Api/Schema/OfferResponse.ts
4107
- var OfferResponse_exports = {};
4108
- __export(OfferResponse_exports, {
4109
- from: () => from8
4110
- });
4111
- function from8(offer) {
4112
- return toSnakeCase(offer);
4113
- }
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
- }
4436
+ },
4437
+ meta: {
4438
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4298
4439
  }
4299
4440
  }
4441
+ };
4442
+ }
4443
+ var APIError = class extends Error {
4444
+ constructor(statusCode, message, code, details) {
4445
+ super(message);
4446
+ this.statusCode = statusCode;
4447
+ this.code = code;
4448
+ this.details = details;
4449
+ this.name = "APIError";
4300
4450
  }
4301
4451
  };
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"
4452
+ var ValidationError = class extends APIError {
4453
+ constructor(message, details) {
4454
+ super(400 /* BAD_REQUEST */, message, "VALIDATION_ERROR", details);
4455
+ }
4456
+ };
4457
+ var InternalServerError = class extends APIError {
4458
+ constructor(message = "Internal server error") {
4459
+ super(500 /* INTERNAL_SERVER_ERROR */, message, "INTERNAL_SERVER_ERROR");
4460
+ }
4461
+ };
4462
+ var BadRequestError = class extends APIError {
4463
+ constructor(message = "Invalid JSON format", details) {
4464
+ super(400 /* BAD_REQUEST */, message, "BAD_REQUEST", details);
4465
+ }
4466
+ };
4467
+ function handleZodError(error2) {
4468
+ const formattedErrors = error2.issues.map((err) => {
4469
+ const field = err.path.join(".");
4470
+ let issue = err.message;
4471
+ if (err.code === "invalid_type") {
4472
+ if (err.message.includes("received undefined")) {
4473
+ issue = `${field} is required`;
4474
+ } else {
4475
+ issue = err.message;
4476
+ }
4477
+ } else if (err.code === "invalid_format" && err.format === "regex") {
4478
+ issue = err.message;
4479
+ } else if (err.code === "invalid_format") {
4480
+ issue = `${field} has an invalid format`;
4328
4481
  }
4329
- ],
4330
- paths
4482
+ return {
4483
+ field,
4484
+ issue
4485
+ };
4486
+ });
4487
+ return new ValidationError("Validation failed", formattedErrors);
4488
+ }
4489
+
4490
+ // src/api/Controllers/getHealth.ts
4491
+ async function getHealth(healthService) {
4492
+ const logger = Logger_exports.getLogger();
4493
+ try {
4494
+ const status = await healthService.getStatus();
4495
+ return success({ data: toSnakeCase({ status }) });
4496
+ } catch (err) {
4497
+ logger.error({
4498
+ err,
4499
+ msg: "Error getting health status",
4500
+ errorMessage: err instanceof Error ? err.message : String(err),
4501
+ errorStack: err instanceof Error ? err.stack : void 0
4502
+ });
4503
+ return error(err);
4504
+ }
4505
+ }
4506
+ async function getHealthChains(healthService) {
4507
+ const logger = Logger_exports.getLogger();
4508
+ try {
4509
+ const chains3 = await healthService.getChains();
4510
+ return success({
4511
+ data: chains3.map(
4512
+ ({ chainId, blockNumber, updatedAt }) => toSnakeCase({
4513
+ chainId,
4514
+ blockNumber,
4515
+ updatedAt
4516
+ })
4517
+ )
4518
+ });
4519
+ } catch (err) {
4520
+ logger.error({
4521
+ err,
4522
+ msg: "Error getting health status for chains",
4523
+ errorMessage: err instanceof Error ? err.message : String(err),
4524
+ errorStack: err instanceof Error ? err.stack : void 0
4525
+ });
4526
+ return error(err);
4527
+ }
4528
+ }
4529
+ async function getHealthCollectors(healthService) {
4530
+ const logger = Logger_exports.getLogger();
4531
+ try {
4532
+ const collectors2 = await healthService.getCollectors();
4533
+ return success({
4534
+ data: collectors2.map(
4535
+ ({ name, chainId, blockNumber, updatedAt, lag, status }) => toSnakeCase({
4536
+ name,
4537
+ chainId,
4538
+ blockNumber,
4539
+ updatedAt,
4540
+ lag,
4541
+ status
4542
+ })
4543
+ )
4544
+ });
4545
+ } catch (err) {
4546
+ logger.error({
4547
+ err,
4548
+ msg: "Error getting health status for collectors",
4549
+ errorMessage: err instanceof Error ? err.message : String(err),
4550
+ errorStack: err instanceof Error ? err.stack : void 0
4551
+ });
4552
+ return error(err);
4553
+ }
4554
+ }
4555
+
4556
+ // src/api/Schema/ObligationResponse.ts
4557
+ var ObligationResponse_exports = {};
4558
+ __export(ObligationResponse_exports, {
4559
+ from: () => from8
4560
+ });
4561
+ function from8(obligation, quote) {
4562
+ return toSnakeCase({
4563
+ id: quote.obligationId,
4564
+ ...obligation,
4565
+ ask: quote.ask,
4566
+ bid: quote.bid
4567
+ });
4568
+ }
4569
+
4570
+ // src/api/Schema/OfferResponse.ts
4571
+ var OfferResponse_exports = {};
4572
+ __export(OfferResponse_exports, {
4573
+ from: () => from9
4331
4574
  });
4575
+ function from9(offer) {
4576
+ return toSnakeCase(offer);
4577
+ }
4332
4578
 
4333
- // src/api/Api/Controllers/getObligations.ts
4579
+ // src/api/Controllers/getObligations.ts
4334
4580
  async function getObligations(queryParameters, store) {
4335
4581
  const logger = Logger_exports.getLogger();
4336
4582
  const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
@@ -4341,8 +4587,20 @@ async function getObligations(queryParameters, store) {
4341
4587
  cursor: query.cursor,
4342
4588
  limit: query.limit
4343
4589
  });
4590
+ const quotes = await store.getQuotes({
4591
+ obligationIds: obligations2.map((o) => Obligation_exports.id(o))
4592
+ });
4344
4593
  return success({
4345
- data: obligations2.map((o) => ObligationResponse_exports.from(o)),
4594
+ data: obligations2.map(
4595
+ (o) => ObligationResponse_exports.from(
4596
+ o,
4597
+ quotes.find((q) => q.obligationId === Obligation_exports.id(o)) ?? {
4598
+ obligationId: Obligation_exports.id(o),
4599
+ ask: { rate: 0n },
4600
+ bid: { rate: 0n }
4601
+ }
4602
+ )
4603
+ ),
4346
4604
  cursor: nextCursor ?? null
4347
4605
  });
4348
4606
  } catch (err) {
@@ -4356,7 +4614,7 @@ async function getObligations(queryParameters, store) {
4356
4614
  }
4357
4615
  }
4358
4616
 
4359
- // src/api/Api/Controllers/getOffers.ts
4617
+ // src/api/Controllers/getOffers.ts
4360
4618
  async function getOffers(queryParameters, store) {
4361
4619
  const logger = Logger_exports.getLogger();
4362
4620
  const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
@@ -4384,7 +4642,7 @@ async function getOffers(queryParameters, store) {
4384
4642
  }
4385
4643
  }
4386
4644
 
4387
- // src/api/Api/Api.ts
4645
+ // src/api/Api.ts
4388
4646
  function create5(config) {
4389
4647
  return {
4390
4648
  serve: ({ port }) => {
@@ -4416,6 +4674,13 @@ function serve2(parameters) {
4416
4674
  const { statusCode, body } = await getHealthChains(healthService);
4417
4675
  return c.json(body, statusCode);
4418
4676
  });
4677
+ app.get(
4678
+ "/docs/swagger.json",
4679
+ (c) => c.text(JSON.stringify(getSwaggerJson()), 200, {
4680
+ "Content-Type": "application/json; charset=utf-8"
4681
+ })
4682
+ );
4683
+ app.get("/docs", (c) => c.html(getDocsHtml(), 200));
4419
4684
  serve$1({
4420
4685
  fetch: app.fetch,
4421
4686
  port: parameters.port
@@ -5036,6 +5301,64 @@ function create8(config) {
5036
5301
  const returnedItems = Array.from(items.values());
5037
5302
  const nextCursor = returnedItems.length === limit && returnedItems.length > 0 ? result[result.length - 1].obligationId : null;
5038
5303
  return { obligations: returnedItems, nextCursor };
5304
+ },
5305
+ getQuotes: async (parameters) => {
5306
+ const { obligationIds } = parameters;
5307
+ if (obligationIds.length === 0) return [];
5308
+ const now2 = time_exports.now();
5309
+ const sumConsumed = db.select({
5310
+ consumed: sql`COALESCE(SUM(${consumed.consumed}), 0)`.as("consumed")
5311
+ }).from(consumed).where(
5312
+ and(
5313
+ eq(consumed.offering, offers.offering),
5314
+ eq(consumed.nonce, offers.nonce),
5315
+ eq(consumed.chainId, offers.chainId)
5316
+ )
5317
+ ).as("sum_consumed");
5318
+ const remaining = sql`(${offers.assets} - COALESCE(${sumConsumed.consumed}, 0))`;
5319
+ const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
5320
+ obligationId: offers.obligationId,
5321
+ rate: offers.rate
5322
+ }).from(offers).leftJoinLateral(sumConsumed, sql`true`).where(
5323
+ and(
5324
+ inArray(offers.obligationId, obligationIds),
5325
+ gte(offers.expiry, now2),
5326
+ gte(offers.maturity, now2),
5327
+ eq(offers.buy, side === "buy"),
5328
+ sql`${remaining} > 0`
5329
+ )
5330
+ ).orderBy(
5331
+ offers.obligationId,
5332
+ // lower rates first for buy, higher rates first for sell
5333
+ side === "buy" ? asc(offers.rate) : desc(offers.rate)
5334
+ );
5335
+ const [bestBuys, bestSells] = await Promise.all([
5336
+ query({ side: "buy" }),
5337
+ query({ side: "sell" })
5338
+ ]);
5339
+ const quotes = /* @__PURE__ */ new Map();
5340
+ for (const row of bestSells) {
5341
+ quotes.set(row.obligationId, {
5342
+ ask: { rate: row.rate },
5343
+ bid: { rate: 0n }
5344
+ });
5345
+ }
5346
+ for (const row of bestBuys) {
5347
+ const quote = quotes.get(row.obligationId);
5348
+ if (!quote) {
5349
+ quotes.set(row.obligationId, {
5350
+ ask: { rate: 0n },
5351
+ bid: { rate: row.rate }
5352
+ });
5353
+ continue;
5354
+ }
5355
+ quote.bid = { rate: row.rate };
5356
+ }
5357
+ return Array.from(quotes.entries()).map(([id2, quote]) => {
5358
+ return Quote_exports.from({ obligationId: id2, ask: quote.ask, bid: quote.bid });
5359
+ }).sort((a, b) => {
5360
+ return a.obligationId.localeCompare(b.obligationId);
5361
+ });
5039
5362
  }
5040
5363
  };
5041
5364
  }
@@ -5777,7 +6100,7 @@ function create9(params) {
5777
6100
  }
5778
6101
 
5779
6102
  // src/services/Services.ts
5780
- function from9(config) {
6103
+ function from10(config) {
5781
6104
  const {
5782
6105
  chain,
5783
6106
  rpcUrl,
@@ -5905,7 +6228,7 @@ var RouterCmd = class _RouterCmd extends Command {
5905
6228
  "RPC URL is required, please set the ROUTER_RPC_URL environment variable or use --rpc-url <url> option."
5906
6229
  );
5907
6230
  }
5908
- const routerServices = from9({
6231
+ const routerServices = from10({
5909
6232
  chain,
5910
6233
  rpcUrl,
5911
6234
  dbConfig: {
@@ -5919,6 +6242,7 @@ var RouterCmd = class _RouterCmd extends Command {
5919
6242
  command.setOptionValue("rpcUrl", rpcUrl);
5920
6243
  command.setOptionValue("routerServices", routerServices);
5921
6244
  command.setOptionValue("logger", logger);
6245
+ command.setOptionValue("chain", chain);
5922
6246
  return { routerServices };
5923
6247
  }
5924
6248
  // allows to use the logger defined in the setup function for the action of the command
@@ -5933,12 +6257,116 @@ var RouterCmd = class _RouterCmd extends Command {
5933
6257
  }
5934
6258
  };
5935
6259
 
5936
- // src/cli/commands/start.ts
6260
+ // src/cli/commands/mock.ts
6261
+ var mockCmd = new RouterCmd("mock");
6262
+ mockCmd.description("Start Router mock.").addOption(
6263
+ new Option("--seed <n>", "Seed random offers to router").argParser((v) => Number(v)).default(0)
6264
+ ).addOption(new Option("--file <path>", "Seed offers from a JSON file")).action(
6265
+ async (opts) => {
6266
+ const {
6267
+ port,
6268
+ routerServices: { liquidityStore, offerStore, routerApi },
6269
+ logger,
6270
+ chain
6271
+ } = opts;
6272
+ const stops = [];
6273
+ if (opts.seed > 0) {
6274
+ const offers2 = Array.from(Array(opts.seed ?? 0).keys()).map((_i) => ({
6275
+ offer: Offer_exports.random({
6276
+ chains: [chain],
6277
+ loanTokens: Array.from(chain.whitelistedAssets.values()),
6278
+ collateralTokens: Array.from(chain.whitelistedAssets.values()),
6279
+ assetsDecimals: {
6280
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": 6,
6281
+ // USDC
6282
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F": 18,
6283
+ // DAI
6284
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": 18,
6285
+ // WETH
6286
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": 8,
6287
+ // WBTC
6288
+ "0x4200000000000000000000000000000000000006": 18,
6289
+ // WETH
6290
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 6,
6291
+ // USDC
6292
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": 18
6293
+ // DAI
6294
+ }
6295
+ }),
6296
+ status: "valid"
6297
+ }));
6298
+ await offerStore.createMany(offers2);
6299
+ for (const offer of offers2) {
6300
+ await liquidityStore.save({
6301
+ pools: [
6302
+ {
6303
+ id: offer.offer.hash,
6304
+ amount: offer.offer.assets
6305
+ }
6306
+ ],
6307
+ offerPools: [
6308
+ {
6309
+ offerHash: offer.offer.hash,
6310
+ poolId: offer.offer.hash,
6311
+ amount: offer.offer.assets
6312
+ }
6313
+ ],
6314
+ links: []
6315
+ });
6316
+ }
6317
+ logger.info({ msg: `Offers seeded`, count: opts.seed });
6318
+ }
6319
+ if (opts.file) {
6320
+ const offers2 = await getOffersFromFile(opts.file);
6321
+ await offerStore.createMany(offers2.map((ro) => ({ offer: ro })));
6322
+ for (const offer of offers2) {
6323
+ await liquidityStore.save({
6324
+ pools: [
6325
+ {
6326
+ id: offer.hash,
6327
+ amount: offer.assets
6328
+ }
6329
+ ],
6330
+ offerPools: [
6331
+ {
6332
+ offerHash: offer.hash,
6333
+ poolId: offer.hash,
6334
+ amount: offer.assets
6335
+ }
6336
+ ],
6337
+ links: []
6338
+ });
6339
+ }
6340
+ logger.info({ msg: `Offers seeded from file`, count: offers2.length, file: opts.file });
6341
+ }
6342
+ await routerApi.serve({ port });
6343
+ logger.info({ msg: `API started`, url: `http://localhost:${port}` });
6344
+ const shutdown = async (signal) => {
6345
+ logger.warn({ msg: `Shutdown signal`, signal });
6346
+ try {
6347
+ stops.forEach((stop) => stop());
6348
+ } catch {
6349
+ }
6350
+ process.exit(0);
6351
+ };
6352
+ process.once("SIGINT", () => void shutdown("SIGINT"));
6353
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
6354
+ process.stdin.resume();
6355
+ }
6356
+ );
6357
+ async function getOffersFromFile(file) {
6358
+ const raw = await readFile(file, "utf8");
6359
+ const json = JSON.parse(raw);
6360
+ const items = Array.isArray(json) ? json : json.offers ?? [];
6361
+ if (!Array.isArray(items) || items.length === 0) {
6362
+ console.log("No offers found in file");
6363
+ return [];
6364
+ }
6365
+ return items.map((o) => Offer_exports.from(o));
6366
+ }
5937
6367
  var startCmd = new RouterCmd("start");
5938
6368
  startCmd.description("Start Router services.").addOption(
5939
6369
  new Option("--block-window <n>", "Block window to get logs from").argParser((v) => Number(v)).default(100)
5940
- ).addOption(
5941
- new Option("--seed <n>", "Seed random offers to router").argParser((v) => Number(v)).default(0)
5942
6370
  ).addOption(new Option("--file <path>", "Seed offers from a JSON file")).addOption(
5943
6371
  new Option("--max-block-number <n>", "Block number at which to stop indexing").argParser(
5944
6372
  (v) => Number(v)
@@ -5951,24 +6379,10 @@ startCmd.description("Start Router services.").addOption(
5951
6379
  async (opts) => {
5952
6380
  const {
5953
6381
  port,
5954
- routerServices: { offerStore, indexer, routerApi },
6382
+ routerServices: { indexer, routerApi },
5955
6383
  logger
5956
6384
  } = opts;
5957
6385
  const stops = [];
5958
- if (opts.seed > 0) {
5959
- await offerStore.createMany(
5960
- Array.from(Array(opts.seed ?? 0).keys()).map((_i) => ({
5961
- offer: Offer_exports.random(),
5962
- status: "valid"
5963
- }))
5964
- );
5965
- logger.info({ msg: `Offers seeded`, count: opts.seed });
5966
- }
5967
- if (opts.file) {
5968
- const offers2 = await getOffersFromFile(opts.file);
5969
- await offerStore.createMany(offers2.map((ro) => ({ offer: ro })));
5970
- logger.info({ msg: `Offers seeded from file`, count: offers2.length, file: opts.file });
5971
- }
5972
6386
  const selectedServices = new Set(opts.services);
5973
6387
  if (selectedServices.has("indexer")) {
5974
6388
  stops.push(await indexer.start());
@@ -5991,16 +6405,6 @@ startCmd.description("Start Router services.").addOption(
5991
6405
  process.stdin.resume();
5992
6406
  }
5993
6407
  );
5994
- async function getOffersFromFile(file) {
5995
- const raw = await readFile(file, "utf8");
5996
- const json = JSON.parse(raw);
5997
- const items = Array.isArray(json) ? json : json.offers ?? [];
5998
- if (!Array.isArray(items) || items.length === 0) {
5999
- console.log("No offers found in file");
6000
- return [];
6001
- }
6002
- return items.map((o) => Offer_exports.from(o));
6003
- }
6004
6408
  var tunnelCmd = new Command("tunnel");
6005
6409
  tunnelCmd.description("Expose the local Router via ngrok").addOption(
6006
6410
  new Option("--port <n>").env("ROUTER_PORT").default(7891).argParser((v) => Number(v))
@@ -6032,6 +6436,7 @@ cli.name(package_default.name.split("/")[1] || "cli").description(package_defaul
6032
6436
  cli.addCommand(startCmd);
6033
6437
  cli.addCommand(tunnelCmd);
6034
6438
  cli.addCommand(mempoolCmd);
6439
+ cli.addCommand(mockCmd);
6035
6440
  cli.command("__commands", { hidden: true }).description("internal: list commands").action(() => {
6036
6441
  const names2 = cli.commands.map((c) => c.name());
6037
6442
  console.log(JSON.stringify(names2));