@morpho-dev/router 0.6.0 → 0.7.1

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.
@@ -12,10 +12,12 @@ import "@opentelemetry/propagator-aws-xray";
12
12
  import "@opentelemetry/resources";
13
13
  import "@opentelemetry/sdk-trace-node";
14
14
  import "@opentelemetry/semantic-conventions";
15
+ import { and, asc, eq, gt, gte, inArray, lte, ne, sql } from "drizzle-orm";
15
16
  import { anvil, base, mainnet } from "viem/chains";
16
17
  import * as z$1 from "zod";
17
18
  import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
18
19
  import { gzip, ungzip } from "pako";
20
+ import { bigint, boolean, foreignKey, index, integer, numeric, pgSchema, primaryKey, serial, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
19
21
  import { serve } from "@hono/node-server";
20
22
  import { Hono } from "hono";
21
23
  import { cors } from "hono/cors";
@@ -29,8 +31,6 @@ import { readFile } from "node:fs/promises";
29
31
  import { dirname, resolve } from "node:path";
30
32
  import { fileURLToPath } from "node:url";
31
33
  import { marked } from "marked";
32
- import { and, asc, eq, gt, gte, inArray, lte, ne, sql } from "drizzle-orm";
33
- import { bigint, boolean, foreignKey, index, integer, numeric, pgSchema, primaryKey, serial, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
34
34
  import createOpenApiFetchClient from "openapi-fetch";
35
35
  import { PGlite } from "@electric-sql/pglite";
36
36
  import { drizzle } from "drizzle-orm/node-postgres";
@@ -1104,107 +1104,14 @@ const Morpho = [
1104
1104
  //#region src/core/Callback.ts
1105
1105
  var Callback_exports = /* @__PURE__ */ __exportAll({
1106
1106
  Type: () => Type$1,
1107
- decode: () => decode$2,
1108
- decodeBuyERC20: () => decodeBuyERC20,
1109
- decodeBuyVaultV1Callback: () => decodeBuyVaultV1Callback,
1110
- decodeSellERC20Callback: () => decodeSellERC20Callback,
1111
- encode: () => encode$2,
1112
- encodeBuyERC20: () => encodeBuyERC20,
1113
- encodeBuyVaultV1Callback: () => encodeBuyVaultV1Callback,
1114
- encodeSellERC20Callback: () => encodeSellERC20Callback,
1115
1107
  isEmptyCallback: () => isEmptyCallback
1116
1108
  });
1117
1109
  let Type$1 = /* @__PURE__ */ function(Type) {
1118
1110
  Type["BuyWithEmptyCallback"] = "buy_with_empty_callback";
1119
- Type["BuyERC20"] = "buy_erc20";
1120
- Type["BuyVaultV1Callback"] = "buy_vault_v1_callback";
1121
- Type["SellERC20Callback"] = "sell_erc20_callback";
1111
+ Type["SellWithEmptyCallback"] = "sell_with_empty_callback";
1122
1112
  return Type;
1123
1113
  }({});
1124
1114
  const isEmptyCallback = (offer) => offer.callback.data === "0x";
1125
- function decode$2(type, data) {
1126
- switch (type) {
1127
- case Type$1.BuyERC20: return decodeBuyERC20(data);
1128
- case Type$1.BuyVaultV1Callback: return decodeBuyVaultV1Callback(data);
1129
- case Type$1.SellERC20Callback: return decodeSellERC20Callback(data);
1130
- default: throw new Error("Invalid callback type");
1131
- }
1132
- }
1133
- function encode$2(type, data) {
1134
- switch (type) {
1135
- case Type$1.BuyERC20:
1136
- if (!("tokens" in data)) throw new Error("Invalid callback data");
1137
- return encodeBuyERC20(data);
1138
- case Type$1.BuyVaultV1Callback:
1139
- if (!("vaults" in data)) throw new Error("Invalid callback data");
1140
- return encodeBuyVaultV1Callback(data);
1141
- case Type$1.SellERC20Callback:
1142
- if (!("collaterals" in data)) throw new Error("Invalid callback data");
1143
- return encodeSellERC20Callback(data);
1144
- default: throw new Error("Invalid callback type");
1145
- }
1146
- }
1147
- /**
1148
- * Decodes BuyERC20 callback data into positions.
1149
- * @param data - The ABI-encoded callback data containing token addresses and amounts.
1150
- * @returns Array of positions with contract address and amount.
1151
- * @throws If data is empty, malformed, or arrays have mismatched lengths.
1152
- */
1153
- function decodeBuyERC20(data) {
1154
- if (!data || data === "0x") throw new Error("Empty callback data");
1155
- let tokens;
1156
- let amounts;
1157
- try {
1158
- [tokens, amounts] = decodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], data);
1159
- } catch (_) {
1160
- throw new Error("Invalid BuyERC20 callback data");
1161
- }
1162
- if (tokens.length !== amounts.length) throw new Error("Mismatched array lengths");
1163
- return tokens.map((token, index) => ({
1164
- contract: token,
1165
- amount: amounts[index]
1166
- }));
1167
- }
1168
- /**
1169
- * Encodes BuyERC20 callback parameters into ABI-encoded data.
1170
- * @param parameters - The tokens and amounts to encode.
1171
- * @returns ABI-encoded hex string.
1172
- */
1173
- function encodeBuyERC20(parameters) {
1174
- return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.tokens, parameters.amounts]);
1175
- }
1176
- function decodeBuyVaultV1Callback(data) {
1177
- if (!data || data === "0x") throw new Error("Empty callback data");
1178
- try {
1179
- const [vaults, amounts] = decodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], data);
1180
- if (vaults.length !== amounts.length) throw new Error("Mismatched array lengths");
1181
- return vaults.map((v, i) => ({
1182
- contract: v,
1183
- amount: amounts[i]
1184
- }));
1185
- } catch (_) {
1186
- throw new Error("Invalid BuyVaultV1Callback callback data");
1187
- }
1188
- }
1189
- function decodeSellERC20Callback(data) {
1190
- if (!data || data === "0x") throw new Error("Empty callback data");
1191
- try {
1192
- const [collaterals, amounts] = decodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], data);
1193
- if (collaterals.length !== amounts.length) throw new Error("Mismatched array lengths");
1194
- return collaterals.map((c, i) => ({
1195
- contract: c,
1196
- amount: amounts[i]
1197
- }));
1198
- } catch (_) {
1199
- throw new Error("Invalid SellERC20Callback callback data");
1200
- }
1201
- }
1202
- function encodeBuyVaultV1Callback(parameters) {
1203
- return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.vaults, parameters.amounts]);
1204
- }
1205
- function encodeSellERC20Callback(parameters) {
1206
- return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.collaterals, parameters.amounts]);
1207
- }
1208
1115
 
1209
1116
  //#endregion
1210
1117
  //#region src/core/Chain.ts
@@ -1726,11 +1633,9 @@ var Liquidity_exports = /* @__PURE__ */ __exportAll({
1726
1633
  calculateMaxDebt: () => calculateMaxDebt,
1727
1634
  generateAllowancePoolId: () => generateAllowancePoolId,
1728
1635
  generateBalancePoolId: () => generateBalancePoolId,
1729
- generateBuyVaultCallbackPoolId: () => generateBuyVaultCallbackPoolId,
1730
1636
  generateDebtPoolId: () => generateDebtPoolId,
1731
1637
  generateMarketLiquidityPoolId: () => generateMarketLiquidityPoolId,
1732
1638
  generateObligationCollateralPoolId: () => generateObligationCollateralPoolId,
1733
- generateSellERC20CallbackPoolId: () => generateSellERC20CallbackPoolId,
1734
1639
  generateUserVaultPositionPoolId: () => generateUserVaultPositionPoolId,
1735
1640
  generateVaultPositionPoolId: () => generateVaultPositionPoolId
1736
1641
  });
@@ -1759,14 +1664,6 @@ function generateAllowancePoolId(parameters) {
1759
1664
  return `${user}-${chainId.toString()}-${token}-allowance`.toLowerCase();
1760
1665
  }
1761
1666
  /**
1762
- * Generate pool ID for sell ERC20 callback pools.
1763
- * Each offer has its own callback pool to prevent liquidity conflicts.
1764
- */
1765
- function generateSellERC20CallbackPoolId(parameters) {
1766
- const { user, chainId, obligationId, token, offerHash } = parameters;
1767
- return `${user}-${chainId.toString()}-${obligationId}-${token}-${offerHash}-sell_erc20_callback`.toLowerCase();
1768
- }
1769
- /**
1770
1667
  * Generate pool ID for obligation collateral pools.
1771
1668
  * Obligation collateral pools represent collateral already deposited in the obligation.
1772
1669
  * These pools are shared across all offers with the same obligation.
@@ -1776,13 +1673,6 @@ function generateObligationCollateralPoolId(parameters) {
1776
1673
  return `${user}-${chainId.toString()}-${obligationId}-${token}-obligation-collateral`.toLowerCase();
1777
1674
  }
1778
1675
  /**
1779
- * Generate pool ID for buy vault callback pools.
1780
- */
1781
- function generateBuyVaultCallbackPoolId(parameters) {
1782
- const { user, chainId, vault, offerHash } = parameters;
1783
- return `${user}-${chainId.toString()}-${vault}-${offerHash}-${Type$1.BuyVaultV1Callback}`.toLowerCase();
1784
- }
1785
- /**
1786
1676
  * Generate pool ID for debt pools.
1787
1677
  */
1788
1678
  function generateDebtPoolId(parameters) {
@@ -2117,6 +2007,7 @@ var Offer_exports = /* @__PURE__ */ __exportAll({
2117
2007
  obligationId: () => obligationId,
2118
2008
  random: () => random$1,
2119
2009
  serialize: () => serialize,
2010
+ takeEvent: () => takeEvent,
2120
2011
  toSnakeCase: () => toSnakeCase,
2121
2012
  types: () => types
2122
2013
  });
@@ -2235,7 +2126,7 @@ function random$1(config) {
2235
2126
  const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
2236
2127
  const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
2237
2128
  const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
2238
- const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
2129
+ collateralCandidates[int(collateralCandidates.length)];
2239
2130
  const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
2240
2131
  const maturity = config?.maturity ?? from$16(maturityOption);
2241
2132
  const lltv = from$18(weightedChoice([
@@ -2262,21 +2153,10 @@ function random$1(config) {
2262
2153
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2263
2154
  const amountBase = BigInt(100 + int(999901));
2264
2155
  const assetsScaled = config?.assets ?? amountBase * unit;
2265
- const callbackBySide = (() => {
2266
- if (buy) return {
2267
- address: zeroAddress,
2268
- data: "0x"
2269
- };
2270
- const sellCallbackAddress = "0x3333333333333333333333333333333333333333";
2271
- const amount = assetsScaled * 1000000000000000000000n;
2272
- return {
2273
- address: sellCallbackAddress,
2274
- data: encodeSellERC20Callback({
2275
- collaterals: [collateralAsset],
2276
- amounts: [amount]
2277
- })
2278
- };
2279
- })();
2156
+ const emptyCallback = {
2157
+ address: zeroAddress,
2158
+ data: "0x"
2159
+ };
2280
2160
  return from$14({
2281
2161
  maker: config?.maker ?? address(),
2282
2162
  assets: assetsScaled,
@@ -2295,7 +2175,7 @@ function random$1(config) {
2295
2175
  ...random$3(),
2296
2176
  lltv
2297
2177
  })).sort((a, b) => a.asset.localeCompare(b.asset)),
2298
- callback: config?.callback ?? callbackBySide
2178
+ callback: config?.callback ?? emptyCallback
2299
2179
  });
2300
2180
  }
2301
2181
  const weightedChoice = (pairs) => {
@@ -2585,6 +2465,94 @@ function decode$1(data) {
2585
2465
  });
2586
2466
  }
2587
2467
  /**
2468
+ * ABI for the Take event emitted by the Morpho V2 contract.
2469
+ */
2470
+ const takeEvent = {
2471
+ type: "event",
2472
+ name: "Take",
2473
+ inputs: [
2474
+ {
2475
+ name: "caller",
2476
+ type: "address",
2477
+ indexed: false,
2478
+ internalType: "address"
2479
+ },
2480
+ {
2481
+ name: "id",
2482
+ type: "bytes32",
2483
+ indexed: true,
2484
+ internalType: "bytes32"
2485
+ },
2486
+ {
2487
+ name: "maker",
2488
+ type: "address",
2489
+ indexed: true,
2490
+ internalType: "address"
2491
+ },
2492
+ {
2493
+ name: "taker",
2494
+ type: "address",
2495
+ indexed: true,
2496
+ internalType: "address"
2497
+ },
2498
+ {
2499
+ name: "offerIsBuy",
2500
+ type: "bool",
2501
+ indexed: false,
2502
+ internalType: "bool"
2503
+ },
2504
+ {
2505
+ name: "buyerAssets",
2506
+ type: "uint256",
2507
+ indexed: false,
2508
+ internalType: "uint256"
2509
+ },
2510
+ {
2511
+ name: "sellerAssets",
2512
+ type: "uint256",
2513
+ indexed: false,
2514
+ internalType: "uint256"
2515
+ },
2516
+ {
2517
+ name: "obligationUnits",
2518
+ type: "uint256",
2519
+ indexed: false,
2520
+ internalType: "uint256"
2521
+ },
2522
+ {
2523
+ name: "obligationShares",
2524
+ type: "uint256",
2525
+ indexed: false,
2526
+ internalType: "uint256"
2527
+ },
2528
+ {
2529
+ name: "buyerIsLender",
2530
+ type: "bool",
2531
+ indexed: false,
2532
+ internalType: "bool"
2533
+ },
2534
+ {
2535
+ name: "sellerIsBorrower",
2536
+ type: "bool",
2537
+ indexed: false,
2538
+ internalType: "bool"
2539
+ },
2540
+ {
2541
+ name: "group",
2542
+ type: "bytes32",
2543
+ indexed: false,
2544
+ internalType: "bytes32"
2545
+ },
2546
+ {
2547
+ name: "consumed",
2548
+ type: "uint256",
2549
+ indexed: false,
2550
+ internalType: "uint256"
2551
+ }
2552
+ ],
2553
+ anonymous: false
2554
+ };
2555
+ /**
2588
2556
  * ABI for the Consume event emitted by the Obligation contract.
2589
2557
  */
2590
2558
  const consumedEvent = {
@@ -3336,160 +3304,701 @@ var SignatureDomainError = class extends BaseError {
3336
3304
  const BrandTypeId = Symbol.for("mempool/Brand");
3337
3305
 
3338
3306
  //#endregion
3339
- //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
3340
- async function* collectConsumedEvents(parameters) {
3341
- let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3342
- const logger = getLogger();
3343
- let startBlock = blockNumber;
3344
- let reorgDetected = false;
3345
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3346
- const stream = streamLogs({
3347
- client,
3348
- contractAddress: client.chain.custom.morpho.address,
3349
- event: consumedEvent,
3350
- blockNumberGte: blockNumber,
3351
- blockNumberLte: latestBlockNumberChain,
3352
- order: "asc",
3353
- options: {
3354
- maxBatchSize,
3355
- blockWindow
3356
- }
3357
- });
3358
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3359
- const parsedLogs = parseEventLogs({
3360
- abi: [consumedEvent],
3361
- logs
3362
- });
3363
- const events = [];
3364
- for (const log of parsedLogs) {
3365
- if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
3366
- logger.debug({
3367
- collector,
3368
- chainId: client.chain.id,
3369
- msg: "Skipping log because it is missing required fields"
3370
- });
3371
- continue;
3372
- }
3373
- events.push({
3374
- id: `${log.blockNumber.toString()}-${log.logIndex.toString()}-${client.chain.id}-${log.transactionHash}`,
3375
- chainId: client.chain.id,
3376
- maker: log.args.user,
3377
- group: log.args.group,
3378
- amount: log.args.amount,
3379
- blockNumber: Number(log.blockNumber)
3380
- });
3381
- }
3382
- await db.transaction(async (dbTx) => {
3383
- try {
3384
- await dbTx.consumed.create(events);
3385
- if (events.length > 0) logger.info({
3386
- msg: `Events indexed`,
3387
- collector,
3388
- count: events.length,
3389
- chain_id: client.chain.id,
3390
- block_range: [startBlock, lastStreamBlockNumber]
3391
- });
3392
- } catch (err) {
3393
- logger.error({
3394
- err,
3395
- msg: "Failed to process offer_consumed events"
3396
- });
3397
- }
3398
- blockNumber = lastStreamBlockNumber;
3399
- try {
3400
- await dbTx.blocks.advanceCollector({
3401
- collectorName: collector,
3402
- chainId: client.chain.id,
3403
- blockNumber,
3404
- epoch
3405
- });
3406
- } catch (_) {
3407
- try {
3408
- const ancestor = await dbTx.blocks.getCollector({
3409
- collectorName: collector,
3410
- chainId: client.chain.id
3411
- });
3412
- blockNumber = ancestor.blockNumber;
3413
- const deleted = await dbTx.consumed.delete({
3414
- chainId: client.chain.id,
3415
- blockNumberGte: blockNumber + 1
3416
- });
3417
- logger.info({
3418
- collector,
3419
- chain_id: client.chain.id,
3420
- msg: `Reorg detected, events deleted`,
3421
- count: deleted,
3422
- block_number: blockNumber
3423
- });
3424
- await dbTx.blocks.advanceCollector({
3425
- collectorName: collector,
3426
- chainId: client.chain.id,
3427
- blockNumber,
3428
- epoch: ancestor.epoch
3429
- });
3430
- reorgDetected = true;
3431
- } catch (err) {
3432
- const msg = "Failed to delete consumed events when handling reorg.";
3433
- logger.error({
3434
- collector,
3435
- chainId: client.chain.id,
3436
- msg,
3437
- err
3438
- });
3439
- throw new Error(msg);
3440
- }
3441
- }
3442
- });
3443
- if (reorgDetected) return;
3444
- yield blockNumber;
3445
- startBlock = blockNumber;
3446
- }
3447
- }
3307
+ //#region src/database/drizzle/VERSION.ts
3308
+ const VERSION = "router_v1.6";
3448
3309
 
3449
3310
  //#endregion
3450
- //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
3451
- async function* collectOffersV2(parameters) {
3452
- let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3453
- const logger = getLogger();
3454
- let startBlock = blockNumber;
3455
- let reorgDetected = false;
3456
- if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
3457
- const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
3458
- logger.error({
3459
- msg,
3460
- chain_id: client.chain.id
3461
- });
3462
- throw new Error(msg);
3463
- }
3464
- const signatureDomain = {
3465
- chainId: client.chain.id,
3466
- verifyingContract: client.chain.custom.morpho.address
3467
- };
3468
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3469
- const stream = streamLogs({
3470
- client,
3471
- contractAddress: client.chain.custom.mempool.address,
3472
- event: {
3473
- type: "event",
3474
- name: "Event",
3475
- inputs: [{
3476
- name: "data",
3477
- type: "bytes",
3478
- indexed: false,
3479
- internalType: "bytes"
3480
- }],
3481
- anonymous: false
3482
- },
3483
- blockNumberGte: blockNumber,
3484
- blockNumberLte: latestBlockNumberChain,
3485
- order: "asc",
3486
- options: {
3487
- maxBatchSize,
3488
- blockWindow
3489
- }
3490
- });
3491
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3492
- blockNumber = lastStreamBlockNumber;
3311
+ //#region src/database/drizzle/schema.ts
3312
+ var schema_exports = /* @__PURE__ */ __exportAll({
3313
+ PositionTypes: () => PositionTypes,
3314
+ StatusCode: () => StatusCode,
3315
+ TABLE_NAMES: () => TABLE_NAMES,
3316
+ VERSIONED_TABLE_NAMES: () => VERSIONED_TABLE_NAMES,
3317
+ callbacks: () => callbacks,
3318
+ chains: () => chains$1,
3319
+ collectors: () => collectors,
3320
+ consumedEvents: () => consumedEvents,
3321
+ groups: () => groups,
3322
+ lots: () => lots,
3323
+ merklePaths: () => merklePaths,
3324
+ obligationCollateralsV2: () => obligationCollateralsV2,
3325
+ obligations: () => obligations,
3326
+ offers: () => offers,
3327
+ offersCallbacks: () => offersCallbacks,
3328
+ offsets: () => offsets,
3329
+ oracles: () => oracles$1,
3330
+ positionTypes: () => positionTypes,
3331
+ positions: () => positions,
3332
+ status: () => status,
3333
+ transfers: () => transfers,
3334
+ trees: () => trees,
3335
+ validations: () => validations
3336
+ });
3337
+ const s = pgSchema(VERSION);
3338
+ var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
3339
+ EnumTableName["OBLIGATIONS"] = "obligations";
3340
+ EnumTableName["GROUPS"] = "groups";
3341
+ EnumTableName["CONSUMED_EVENTS"] = "consumed_events";
3342
+ EnumTableName["OBLIGATION_COLLATERALS_V2"] = "obligation_collaterals_v2";
3343
+ EnumTableName["ORACLES"] = "oracles";
3344
+ EnumTableName["OFFERS"] = "offers";
3345
+ EnumTableName["OFFERS_CALLBACKS"] = "offers_callbacks";
3346
+ EnumTableName["CALLBACKS"] = "callbacks";
3347
+ EnumTableName["POSITIONS"] = "positions";
3348
+ EnumTableName["TRANSFERS"] = "transfers";
3349
+ EnumTableName["VALIDATIONS"] = "validations";
3350
+ EnumTableName["COLLECTORS"] = "collectors";
3351
+ EnumTableName["CHAINS"] = "chains";
3352
+ EnumTableName["LOTS"] = "lots";
3353
+ EnumTableName["OFFSETS"] = "offsets";
3354
+ EnumTableName["TREES"] = "trees";
3355
+ EnumTableName["MERKLE_PATHS"] = "merkle_paths";
3356
+ return EnumTableName;
3357
+ }(EnumTableName || {});
3358
+ const TABLE_NAMES = Object.values(EnumTableName);
3359
+ const VERSIONED_TABLE_NAMES = TABLE_NAMES.map((table) => `"${VERSION}"."${table}"`);
3360
+ const obligations = s.table(EnumTableName.OBLIGATIONS, {
3361
+ obligationId: varchar("obligation_id", { length: 66 }).primaryKey(),
3362
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3363
+ loanToken: varchar("loan_token", { length: 42 }).notNull(),
3364
+ maturity: integer("maturity").notNull()
3365
+ });
3366
+ const groups = s.table(EnumTableName.GROUPS, {
3367
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3368
+ maker: varchar("maker", { length: 42 }).notNull(),
3369
+ group: varchar("group", { length: 66 }).notNull(),
3370
+ consumed: numeric("consumed", {
3371
+ precision: 78,
3372
+ scale: 0
3373
+ }).notNull(),
3374
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3375
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3376
+ }, (table) => [primaryKey({
3377
+ columns: [
3378
+ table.chainId,
3379
+ table.maker,
3380
+ table.group
3381
+ ],
3382
+ name: "groups_pk"
3383
+ }), index("groups_chain_id_maker_group_consumed_idx").on(table.chainId, table.maker, table.group, table.consumed)]);
3384
+ const consumedEvents = s.table(EnumTableName.CONSUMED_EVENTS, {
3385
+ eventId: varchar("event_id", { length: 128 }).primaryKey(),
3386
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3387
+ maker: varchar("maker", { length: 42 }).notNull(),
3388
+ group: varchar("group", { length: 66 }).notNull(),
3389
+ amount: numeric("amount", {
3390
+ precision: 78,
3391
+ scale: 0
3392
+ }).notNull(),
3393
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3394
+ createdAt: timestamp("created_at").defaultNow().notNull()
3395
+ }, (t) => [
3396
+ foreignKey({
3397
+ columns: [
3398
+ t.chainId,
3399
+ t.maker,
3400
+ t.group
3401
+ ],
3402
+ foreignColumns: [
3403
+ groups.chainId,
3404
+ groups.maker,
3405
+ groups.group
3406
+ ],
3407
+ name: "consumed_events_groups_fk"
3408
+ }).onDelete("cascade"),
3409
+ index("consumed_events_group_idx").on(t.chainId, t.maker, t.group),
3410
+ index("consumed_events_block_number_idx").on(t.blockNumber)
3411
+ ]);
3412
+ const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2, {
3413
+ obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
3414
+ asset: varchar("asset", { length: 42 }).notNull(),
3415
+ oracleChainId: bigint("oracle_chain_id", { mode: "number" }).$type().notNull(),
3416
+ oracleAddress: varchar("oracle_address", { length: 42 }).notNull(),
3417
+ lltv: bigint("lltv", { mode: "bigint" }).notNull(),
3418
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3419
+ }, (table) => [
3420
+ primaryKey({
3421
+ columns: [table.obligationId, table.asset],
3422
+ name: "obligation_collaterals_v2_pk"
3423
+ }),
3424
+ foreignKey({
3425
+ columns: [table.oracleChainId, table.oracleAddress],
3426
+ foreignColumns: [oracles$1.chainId, oracles$1.address],
3427
+ name: "obligation_collaterals_v2_oracles_fk"
3428
+ }),
3429
+ index("obligation_collaterals_v2_obligation_id_idx").on(table.obligationId),
3430
+ index("obligation_collaterals_v2_oracle_fk_idx").on(table.oracleChainId, table.oracleAddress)
3431
+ ]);
3432
+ const oracles$1 = s.table(EnumTableName.ORACLES, {
3433
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3434
+ address: varchar("address", { length: 42 }).notNull(),
3435
+ price: numeric("price", {
3436
+ precision: 78,
3437
+ scale: 0
3438
+ }),
3439
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3440
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3441
+ }, (table) => [primaryKey({
3442
+ columns: [table.chainId, table.address],
3443
+ name: "oracles_pk"
3444
+ })]);
3445
+ const offers = s.table(EnumTableName.OFFERS, {
3446
+ hash: varchar("hash", { length: 66 }).primaryKey(),
3447
+ obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
3448
+ assets: numeric("assets", {
3449
+ precision: 78,
3450
+ scale: 0
3451
+ }).notNull(),
3452
+ obligationUnits: numeric("obligation_units", {
3453
+ precision: 78,
3454
+ scale: 0
3455
+ }).notNull().default("0"),
3456
+ obligationShares: numeric("obligation_shares", {
3457
+ precision: 78,
3458
+ scale: 0
3459
+ }).notNull().default("0"),
3460
+ price: numeric("price", {
3461
+ precision: 78,
3462
+ scale: 0
3463
+ }).notNull(),
3464
+ maturity: integer("maturity").notNull(),
3465
+ expiry: integer("expiry").notNull(),
3466
+ start: integer("start").notNull(),
3467
+ groupChainId: bigint("group_chain_id", { mode: "number" }).$type().notNull(),
3468
+ groupMaker: varchar("group_maker", { length: 42 }).notNull(),
3469
+ group: varchar("group_group", { length: 66 }).notNull(),
3470
+ session: varchar("session", { length: 66 }).notNull(),
3471
+ buy: boolean("buy").notNull(),
3472
+ callbackAddress: varchar("callback_address", { length: 42 }).notNull(),
3473
+ callbackData: text("callback_data").notNull(),
3474
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3475
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3476
+ }, (table) => [
3477
+ foreignKey({
3478
+ columns: [
3479
+ table.groupChainId,
3480
+ table.groupMaker,
3481
+ table.group
3482
+ ],
3483
+ foreignColumns: [
3484
+ groups.chainId,
3485
+ groups.maker,
3486
+ groups.group
3487
+ ],
3488
+ name: "offers_groups_fk"
3489
+ }).onDelete("cascade"),
3490
+ index("offers_group_fk_idx").on(table.groupChainId, table.groupMaker, table.group),
3491
+ index("offers_group_and_hash_idx").on(table.groupChainId, table.groupMaker, table.group, table.hash),
3492
+ index("offers_obligation_id_side_idx").on(table.obligationId, table.buy)
3493
+ ]);
3494
+ const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
3495
+ offerHash: varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
3496
+ callbackId: varchar("callback_id", { length: 66 })
3497
+ }, (table) => [primaryKey({
3498
+ columns: [table.offerHash, table.callbackId],
3499
+ name: "offers_callbacks_pk"
3500
+ })]);
3501
+ const callbacks = s.table(EnumTableName.CALLBACKS, {
3502
+ id: varchar("id", { length: 66 }).primaryKey(),
3503
+ positionChainId: bigint("position_chain_id", { mode: "number" }).$type().notNull(),
3504
+ positionContract: varchar("position_contract", { length: 42 }).notNull(),
3505
+ positionUser: varchar("position_user", { length: 42 }).notNull(),
3506
+ amount: numeric("amount", {
3507
+ precision: 78,
3508
+ scale: 0
3509
+ })
3510
+ }, (table) => [foreignKey({
3511
+ columns: [
3512
+ table.positionChainId,
3513
+ table.positionContract,
3514
+ table.positionUser
3515
+ ],
3516
+ foreignColumns: [
3517
+ positions.chainId,
3518
+ positions.contract,
3519
+ positions.user
3520
+ ],
3521
+ name: "callbacks_positions_fk"
3522
+ }).onDelete("cascade")]);
3523
+ const lots = s.table(EnumTableName.LOTS, {
3524
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3525
+ user: varchar("user", { length: 42 }).notNull(),
3526
+ contract: varchar("contract", { length: 42 }).notNull(),
3527
+ group: varchar("group", { length: 66 }).notNull(),
3528
+ lower: numeric("lower", {
3529
+ precision: 78,
3530
+ scale: 0
3531
+ }).notNull(),
3532
+ upper: numeric("upper", {
3533
+ precision: 78,
3534
+ scale: 0
3535
+ }).notNull()
3536
+ }, (table) => [
3537
+ primaryKey({
3538
+ columns: [
3539
+ table.chainId,
3540
+ table.user,
3541
+ table.contract,
3542
+ table.group
3543
+ ],
3544
+ name: "lots_pk"
3545
+ }),
3546
+ foreignKey({
3547
+ columns: [
3548
+ table.chainId,
3549
+ table.contract,
3550
+ table.user
3551
+ ],
3552
+ foreignColumns: [
3553
+ positions.chainId,
3554
+ positions.contract,
3555
+ positions.user
3556
+ ],
3557
+ name: "lots_positions_fk"
3558
+ }).onDelete("cascade"),
3559
+ foreignKey({
3560
+ columns: [
3561
+ table.chainId,
3562
+ table.user,
3563
+ table.group
3564
+ ],
3565
+ foreignColumns: [
3566
+ groups.chainId,
3567
+ groups.maker,
3568
+ groups.group
3569
+ ],
3570
+ name: "lots_groups_fk"
3571
+ }).onDelete("cascade")
3572
+ ]);
3573
+ const offsets = s.table(EnumTableName.OFFSETS, {
3574
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3575
+ user: varchar("user", { length: 42 }).notNull(),
3576
+ contract: varchar("contract", { length: 42 }).notNull(),
3577
+ group: varchar("group", { length: 66 }).notNull(),
3578
+ value: numeric("value", {
3579
+ precision: 78,
3580
+ scale: 0
3581
+ }).notNull()
3582
+ }, (table) => [primaryKey({
3583
+ columns: [
3584
+ table.chainId,
3585
+ table.user,
3586
+ table.contract,
3587
+ table.group
3588
+ ],
3589
+ name: "offsets_pk"
3590
+ }), foreignKey({
3591
+ columns: [
3592
+ table.chainId,
3593
+ table.contract,
3594
+ table.user
3595
+ ],
3596
+ foreignColumns: [
3597
+ positions.chainId,
3598
+ positions.contract,
3599
+ positions.user
3600
+ ],
3601
+ name: "offsets_positions_fk"
3602
+ }).onDelete("cascade")]);
3603
+ const PositionTypes = s.enum("position_type", Object.values(Type));
3604
+ const positionTypes = s.table("position_types", {
3605
+ id: serial("id").primaryKey(),
3606
+ type: PositionTypes("type").notNull()
3607
+ });
3608
+ const positions = s.table(EnumTableName.POSITIONS, {
3609
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3610
+ contract: varchar("contract", { length: 42 }).notNull(),
3611
+ user: varchar("user", { length: 42 }).notNull(),
3612
+ positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
3613
+ balance: numeric("balance", {
3614
+ precision: 78,
3615
+ scale: 0
3616
+ }),
3617
+ asset: varchar("asset", { length: 42 }),
3618
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3619
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3620
+ }, (table) => [primaryKey({
3621
+ columns: [
3622
+ table.chainId,
3623
+ table.contract,
3624
+ table.user
3625
+ ],
3626
+ name: "positions_pk"
3627
+ })]);
3628
+ const transfers = s.table(EnumTableName.TRANSFERS, {
3629
+ eventId: varchar("event_id", { length: 128 }).primaryKey(),
3630
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3631
+ contract: varchar("contract", { length: 42 }).notNull(),
3632
+ from: varchar("from", { length: 42 }).notNull(),
3633
+ to: varchar("to", { length: 42 }).notNull(),
3634
+ value: numeric("value", {
3635
+ precision: 78,
3636
+ scale: 0
3637
+ }).notNull(),
3638
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3639
+ createdAt: timestamp("created_at").defaultNow().notNull()
3640
+ }, (table) => [
3641
+ foreignKey({
3642
+ columns: [
3643
+ table.chainId,
3644
+ table.contract,
3645
+ table.from
3646
+ ],
3647
+ foreignColumns: [
3648
+ positions.chainId,
3649
+ positions.contract,
3650
+ positions.user
3651
+ ],
3652
+ name: "transfers_positions_from_fk"
3653
+ }).onDelete("cascade"),
3654
+ foreignKey({
3655
+ columns: [
3656
+ table.chainId,
3657
+ table.contract,
3658
+ table.to
3659
+ ],
3660
+ foreignColumns: [
3661
+ positions.chainId,
3662
+ positions.contract,
3663
+ positions.user
3664
+ ],
3665
+ name: "transfers_positions_to_fk"
3666
+ }).onDelete("cascade"),
3667
+ index("transfers_chain_contract_user_idx").on(table.chainId, table.contract, table.from, table.to, table.blockNumber)
3668
+ ]);
3669
+ const StatusCode = s.enum("status_code", Object.values(Status));
3670
+ const status = s.table("status", {
3671
+ id: serial("id").primaryKey(),
3672
+ code: StatusCode("code").unique()
3673
+ });
3674
+ const validations = s.table("validations", {
3675
+ offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
3676
+ statusId: integer("status_id").notNull().references(() => status.id, { onDelete: "no action" }),
3677
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3678
+ });
3679
+ const collectors = s.table(EnumTableName.COLLECTORS, {
3680
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull().references(() => chains$1.chainId, { onDelete: "no action" }),
3681
+ name: text("name").$type().notNull(),
3682
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3683
+ epoch: numeric("epoch", {
3684
+ precision: 78,
3685
+ scale: 0
3686
+ }).default("0").notNull(),
3687
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3688
+ }, (table) => [uniqueIndex("collectors_chain_name_idx").on(table.chainId, table.name)]);
3689
+ const chains$1 = s.table(EnumTableName.CHAINS, {
3690
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3691
+ blockNumber: bigint("block_number", { mode: "number" }).notNull(),
3692
+ epoch: numeric("epoch", {
3693
+ precision: 78,
3694
+ scale: 0
3695
+ }).default("0").notNull(),
3696
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
3697
+ }, (table) => [uniqueIndex("chains_chain_id_idx").on(table.chainId)]);
3698
+ const trees = s.table(EnumTableName.TREES, {
3699
+ root: varchar("root", { length: 66 }).primaryKey(),
3700
+ rootSignature: varchar("root_signature", { length: 132 }).notNull(),
3701
+ createdAt: timestamp("created_at").defaultNow().notNull()
3702
+ });
3703
+ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
3704
+ offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
3705
+ treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
3706
+ proofNodes: text("proof_nodes").notNull(),
3707
+ createdAt: timestamp("created_at").defaultNow().notNull()
3708
+ }, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
3709
+
3710
+ //#endregion
3711
+ //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
3712
+ const buildGroupKey = (parameters) => {
3713
+ return `${parameters.chainId}-${parameters.maker.toLowerCase()}-${parameters.group.toLowerCase()}`;
3714
+ };
3715
+ async function* collectConsumedEvents(parameters) {
3716
+ let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3717
+ const logger = getLogger();
3718
+ let startBlock = blockNumber;
3719
+ let reorgDetected = false;
3720
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3721
+ const stream = streamLogs({
3722
+ client,
3723
+ contractAddress: client.chain.custom.morpho.address,
3724
+ blockNumberGte: blockNumber,
3725
+ blockNumberLte: latestBlockNumberChain,
3726
+ order: "asc",
3727
+ options: {
3728
+ maxBatchSize,
3729
+ blockWindow
3730
+ }
3731
+ });
3732
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3733
+ const parsedLogs = parseEventLogs({
3734
+ abi: [consumedEvent, takeEvent],
3735
+ logs,
3736
+ strict: false
3737
+ });
3738
+ const normalizedLogs = [];
3739
+ const groups$3 = /* @__PURE__ */ new Map();
3740
+ const eventIds = /* @__PURE__ */ new Set();
3741
+ const recordLog = (log) => {
3742
+ normalizedLogs.push(log);
3743
+ eventIds.add(log.id);
3744
+ const groupKey = buildGroupKey({
3745
+ chainId: log.chainId,
3746
+ maker: log.maker,
3747
+ group: log.group
3748
+ });
3749
+ if (!groups$3.has(groupKey)) groups$3.set(groupKey, {
3750
+ chainId: log.chainId,
3751
+ maker: log.maker.toLowerCase(),
3752
+ group: log.group.toLowerCase()
3753
+ });
3754
+ };
3755
+ for (const rawLog of parsedLogs) {
3756
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
3757
+ logger.debug({
3758
+ collector,
3759
+ chainId: client.chain.id,
3760
+ msg: "Skipping log because it is missing required fields"
3761
+ });
3762
+ continue;
3763
+ }
3764
+ if (rawLog.eventName === consumedEvent.name) {
3765
+ const consumeArgs = rawLog.args;
3766
+ if (consumeArgs.user === void 0 || consumeArgs.group === void 0 || consumeArgs.amount === void 0) {
3767
+ logger.debug({
3768
+ collector,
3769
+ chainId: client.chain.id,
3770
+ msg: "Skipping Consume log because it is missing required args"
3771
+ });
3772
+ continue;
3773
+ }
3774
+ recordLog({
3775
+ kind: "consume",
3776
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
3777
+ chainId: client.chain.id,
3778
+ maker: consumeArgs.user,
3779
+ group: consumeArgs.group,
3780
+ amount: consumeArgs.amount,
3781
+ blockNumber: Number(rawLog.blockNumber)
3782
+ });
3783
+ continue;
3784
+ }
3785
+ if (rawLog.eventName === takeEvent.name) {
3786
+ const takeArgs = rawLog.args;
3787
+ if (takeArgs.maker === void 0 || takeArgs.group === void 0 || takeArgs.consumed === void 0) {
3788
+ logger.debug({
3789
+ collector,
3790
+ chainId: client.chain.id,
3791
+ msg: "Skipping Take log because it is missing required args"
3792
+ });
3793
+ continue;
3794
+ }
3795
+ recordLog({
3796
+ kind: "take",
3797
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
3798
+ chainId: client.chain.id,
3799
+ maker: takeArgs.maker,
3800
+ group: takeArgs.group,
3801
+ consumed: takeArgs.consumed,
3802
+ blockNumber: Number(rawLog.blockNumber)
3803
+ });
3804
+ }
3805
+ }
3806
+ await db.transaction(async (dbTx) => {
3807
+ const existingEventIds = /* @__PURE__ */ new Set();
3808
+ if (eventIds.size > 0) {
3809
+ const ids = Array.from(eventIds);
3810
+ for (let index = 0; index < ids.length; index += 500) {
3811
+ const slice = ids.slice(index, index + 500);
3812
+ const { rows } = await dbTx.execute(sql`
3813
+ SELECT event_id
3814
+ FROM ${consumedEvents}
3815
+ WHERE event_id IN (${sql.join(slice.map((id) => sql`${id}`), sql`,`)});
3816
+ `);
3817
+ for (const row of rows) existingEventIds.add(row.event_id);
3818
+ }
3819
+ }
3820
+ const consumedByGroup = /* @__PURE__ */ new Map();
3821
+ if (groups$3.size > 0) {
3822
+ const groupList = Array.from(groups$3.values());
3823
+ for (let index = 0; index < groupList.length; index += 500) {
3824
+ const slice = groupList.slice(index, index + 500);
3825
+ const { rows } = await dbTx.execute(sql`
3826
+ WITH targets(chain_id, maker, "group") AS (
3827
+ VALUES ${sql.join(slice.map((group) => sql`(${group.chainId}::bigint, ${group.maker.toLowerCase()}::varchar(42), ${group.group.toLowerCase()}::varchar(66))`), sql`,`)}
3828
+ )
3829
+ SELECT
3830
+ targets.chain_id,
3831
+ targets.maker,
3832
+ targets."group",
3833
+ COALESCE(g.consumed, 0)::numeric AS consumed
3834
+ FROM targets
3835
+ LEFT JOIN ${groups} g
3836
+ ON g.chain_id = targets.chain_id
3837
+ AND g.maker = targets.maker
3838
+ AND g."group" = targets."group";
3839
+ `);
3840
+ for (const row of rows) {
3841
+ const groupKey = buildGroupKey({
3842
+ chainId: Number(row.chain_id),
3843
+ maker: row.maker,
3844
+ group: row.group
3845
+ });
3846
+ consumedByGroup.set(groupKey, BigInt(row.consumed ?? "0"));
3847
+ }
3848
+ }
3849
+ }
3850
+ const events = [];
3851
+ for (const log of normalizedLogs) {
3852
+ if (existingEventIds.has(log.id)) continue;
3853
+ const groupKey = buildGroupKey({
3854
+ chainId: log.chainId,
3855
+ maker: log.maker,
3856
+ group: log.group
3857
+ });
3858
+ const previousConsumed = consumedByGroup.get(groupKey) ?? 0n;
3859
+ if (log.kind === "consume") {
3860
+ events.push({
3861
+ id: log.id,
3862
+ chainId: log.chainId,
3863
+ maker: log.maker,
3864
+ group: log.group,
3865
+ amount: log.amount,
3866
+ blockNumber: log.blockNumber
3867
+ });
3868
+ consumedByGroup.set(groupKey, previousConsumed + log.amount);
3869
+ continue;
3870
+ }
3871
+ const delta = log.consumed - previousConsumed;
3872
+ if (delta <= 0n) {
3873
+ logger.debug({
3874
+ collector,
3875
+ chainId: client.chain.id,
3876
+ msg: "Skipping Take log because consumed did not increase",
3877
+ previous_consumed: previousConsumed.toString(),
3878
+ consumed: log.consumed.toString()
3879
+ });
3880
+ continue;
3881
+ }
3882
+ events.push({
3883
+ id: log.id,
3884
+ chainId: log.chainId,
3885
+ maker: log.maker,
3886
+ group: log.group,
3887
+ amount: delta,
3888
+ blockNumber: log.blockNumber
3889
+ });
3890
+ consumedByGroup.set(groupKey, log.consumed);
3891
+ }
3892
+ try {
3893
+ await dbTx.consumed.create(events);
3894
+ if (events.length > 0) logger.info({
3895
+ msg: `Events indexed`,
3896
+ collector,
3897
+ count: events.length,
3898
+ chain_id: client.chain.id,
3899
+ block_range: [startBlock, lastStreamBlockNumber]
3900
+ });
3901
+ } catch (err) {
3902
+ logger.error({
3903
+ err,
3904
+ msg: "Failed to process consumed events"
3905
+ });
3906
+ }
3907
+ blockNumber = lastStreamBlockNumber;
3908
+ try {
3909
+ await dbTx.blocks.advanceCollector({
3910
+ collectorName: collector,
3911
+ chainId: client.chain.id,
3912
+ blockNumber,
3913
+ epoch
3914
+ });
3915
+ } catch (_) {
3916
+ try {
3917
+ const ancestor = await dbTx.blocks.getCollector({
3918
+ collectorName: collector,
3919
+ chainId: client.chain.id
3920
+ });
3921
+ blockNumber = ancestor.blockNumber;
3922
+ const deleted = await dbTx.consumed.delete({
3923
+ chainId: client.chain.id,
3924
+ blockNumberGte: blockNumber + 1
3925
+ });
3926
+ logger.info({
3927
+ collector,
3928
+ chain_id: client.chain.id,
3929
+ msg: `Reorg detected, events deleted`,
3930
+ count: deleted,
3931
+ block_number: blockNumber
3932
+ });
3933
+ await dbTx.blocks.advanceCollector({
3934
+ collectorName: collector,
3935
+ chainId: client.chain.id,
3936
+ blockNumber,
3937
+ epoch: ancestor.epoch
3938
+ });
3939
+ reorgDetected = true;
3940
+ } catch (err) {
3941
+ const msg = "Failed to delete consumed events when handling reorg.";
3942
+ logger.error({
3943
+ collector,
3944
+ chainId: client.chain.id,
3945
+ msg,
3946
+ err
3947
+ });
3948
+ throw new Error(msg, { cause: err });
3949
+ }
3950
+ }
3951
+ });
3952
+ if (reorgDetected) return;
3953
+ yield blockNumber;
3954
+ startBlock = blockNumber;
3955
+ }
3956
+ }
3957
+
3958
+ //#endregion
3959
+ //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
3960
+ async function* collectOffersV2(parameters) {
3961
+ let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3962
+ const logger = getLogger();
3963
+ let startBlock = blockNumber;
3964
+ let reorgDetected = false;
3965
+ if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
3966
+ const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
3967
+ logger.error({
3968
+ msg,
3969
+ chain_id: client.chain.id
3970
+ });
3971
+ throw new Error(msg);
3972
+ }
3973
+ const signatureDomain = {
3974
+ chainId: client.chain.id,
3975
+ verifyingContract: client.chain.custom.morpho.address
3976
+ };
3977
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3978
+ const stream = streamLogs({
3979
+ client,
3980
+ contractAddress: client.chain.custom.mempool.address,
3981
+ event: {
3982
+ type: "event",
3983
+ name: "Event",
3984
+ inputs: [{
3985
+ name: "data",
3986
+ type: "bytes",
3987
+ indexed: false,
3988
+ internalType: "bytes"
3989
+ }],
3990
+ anonymous: false
3991
+ },
3992
+ blockNumberGte: blockNumber,
3993
+ blockNumberLte: latestBlockNumberChain,
3994
+ order: "asc",
3995
+ options: {
3996
+ maxBatchSize,
3997
+ blockWindow
3998
+ }
3999
+ });
4000
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
4001
+ blockNumber = lastStreamBlockNumber;
3493
4002
  const decodedTrees = [];
3494
4003
  for (const log of logs) {
3495
4004
  if (!log) continue;
@@ -3575,9 +4084,8 @@ async function* collectOffersV2(parameters) {
3575
4084
  offers: offersWithBlock,
3576
4085
  hashes: insertedHashes
3577
4086
  });
3578
- const { callbacks, positions, lots } = await decodeCallbacks({
4087
+ const { callbacks, positions, lots } = decodeCallbacks({
3579
4088
  chainId: client.chain.id,
3580
- gatekeeper,
3581
4089
  offers: insertedOffers
3582
4090
  });
3583
4091
  if (positions.length > 0) await dbTx.positions.upsert(positions);
@@ -3640,83 +4148,44 @@ async function* collectOffersV2(parameters) {
3640
4148
  startBlock = blockNumber;
3641
4149
  }
3642
4150
  }
3643
- async function decodeCallbacks(parameters) {
3644
- const { chainId, gatekeeper, offers } = parameters;
4151
+ function decodeCallbacks(parameters) {
4152
+ const { offers } = parameters;
3645
4153
  if (offers.length === 0) return {
3646
4154
  callbacks: [],
3647
4155
  positions: [],
3648
4156
  lots: []
3649
4157
  };
3650
- const addresses = offers.filter((entry) => entry.offer.callback.data !== "0x").map((entry) => entry.offer.callback.address);
3651
- if (addresses.length === 0) return {
3652
- callbacks: [],
3653
- positions: [],
3654
- lots: []
3655
- };
3656
- let response;
3657
- try {
3658
- response = await gatekeeper.getCallbackTypes({ callbacks: [{
3659
- chain_id: chainId,
3660
- addresses
3661
- }] });
3662
- } catch (err) {
3663
- const error = err instanceof Error ? err : new Error(String(err));
3664
- throw new Error("Failed to resolve callback types", { cause: error });
3665
- }
3666
- const entry = response.find((item) => item.chain_id === chainId);
3667
- const typeByAddress = /* @__PURE__ */ new Map();
3668
- if (entry) for (const [key, list] of Object.entries(entry)) {
3669
- if (key === "chain_id" || key === "not_supported") continue;
3670
- if (!Array.isArray(list)) continue;
3671
- for (const address of list) typeByAddress.set(address.toLowerCase(), key);
3672
- }
3673
4158
  const callbacks = [];
3674
4159
  const positions = [];
3675
4160
  const lots = [];
3676
4161
  for (const { offer, blockNumber: offerBlockNumber } of offers) {
3677
- if (offer.callback.data === "0x") continue;
3678
- const callbackType = typeByAddress.get(offer.callback.address.toLowerCase());
3679
- if (!callbackType) continue;
3680
- let decoded;
3681
- try {
3682
- decoded = decode$2(callbackType, offer.callback.data);
3683
- } catch (err) {
3684
- const error = err instanceof Error ? err : new Error(String(err));
3685
- throw new Error("Failed to decode callback data", { cause: error });
3686
- }
3687
- if (decoded.length === 0) continue;
3688
- const offerHash = hash(offer);
3689
- const callbackInputs = decoded.map((callback) => ({
4162
+ if (!offer.buy) continue;
4163
+ if (!isEmptyCallback(offer)) continue;
4164
+ const loanToken = offer.loanToken.toLowerCase();
4165
+ positions.push(from$12({
3690
4166
  chainId: offer.chainId,
3691
- contract: callback.contract,
4167
+ contract: loanToken,
3692
4168
  user: offer.maker,
3693
- amount: callback.amount
4169
+ type: Type.ERC20,
4170
+ asset: loanToken,
4171
+ blockNumber: offerBlockNumber
3694
4172
  }));
3695
- callbacks.push({
3696
- offerHash,
3697
- callbacks: callbackInputs
4173
+ lots.push({
4174
+ positionChainId: offer.chainId,
4175
+ positionContract: loanToken,
4176
+ positionUser: offer.maker,
4177
+ group: offer.group,
4178
+ size: offer.assets
3698
4179
  });
3699
- for (const callback of decoded) {
3700
- const contract = callback.contract;
3701
- const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
3702
- const asset = callbackType === Type$1.BuyVaultV1Callback ? void 0 : contract;
3703
- positions.push(from$12({
4180
+ callbacks.push({
4181
+ offerHash: hash(offer),
4182
+ callbacks: [{
3704
4183
  chainId: offer.chainId,
3705
- contract,
4184
+ contract: loanToken,
3706
4185
  user: offer.maker,
3707
- type: positionType,
3708
- asset,
3709
- blockNumber: offerBlockNumber
3710
- }));
3711
- const isLoanPosition = offer.loanToken.toLowerCase() === asset?.toLowerCase();
3712
- lots.push({
3713
- positionChainId: offer.chainId,
3714
- positionContract: contract,
3715
- positionUser: offer.maker,
3716
- group: offer.group,
3717
- size: isLoanPosition ? offer.assets : callback.amount
3718
- });
3719
- }
4186
+ amount: offer.assets
4187
+ }]
4188
+ });
3720
4189
  }
3721
4190
  return {
3722
4191
  callbacks,
@@ -4903,8 +5372,8 @@ const offerExample = {
4903
5372
  price: "2750000000000000000",
4904
5373
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
4905
5374
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
4906
- callback: "0x1111111111111111111111111111111111111111",
4907
- callback_data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
5375
+ callback: "0x0000000000000000000000000000000000000000",
5376
+ callback_data: "0x"
4908
5377
  },
4909
5378
  offer_hash: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427",
4910
5379
  obligation_id: "0x25690ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9abc",
@@ -4956,25 +5425,10 @@ const validateOfferExample = {
4956
5425
  lltv: "860000000000000000"
4957
5426
  }],
4958
5427
  callback: {
4959
- address: "0x1111111111111111111111111111111111111111",
4960
- data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
5428
+ address: "0x0000000000000000000000000000000000000000",
5429
+ data: "0x"
4961
5430
  }
4962
5431
  };
4963
- const callbackTypesRequestExample = { callbacks: [{
4964
- chain_id: 1,
4965
- addresses: [
4966
- "0x1111111111111111111111111111111111111111",
4967
- "0x3333333333333333333333333333333333333333",
4968
- "0x9999999999999999999999999999999999999999"
4969
- ]
4970
- }] };
4971
- const callbackTypesResponseExample = [{
4972
- chain_id: 1,
4973
- sell_erc20_callback: ["0x1111111111111111111111111111111111111111"],
4974
- buy_erc20: ["0x5555555555555555555555555555555555555555"],
4975
- buy_vault_v1_callback: ["0x3333333333333333333333333333333333333333"],
4976
- not_supported: ["0x9999999999999999999999999999999999999999"]
4977
- }];
4978
5432
  const routerStatusExample = {
4979
5433
  status: "live",
4980
5434
  initialized: true,
@@ -5043,55 +5497,6 @@ __decorate([ApiProperty({
5043
5497
  type: "string",
5044
5498
  example: validateOfferExample.callback.data
5045
5499
  })], ValidateCallbackRequest.prototype, "data", void 0);
5046
- var CallbackTypesChainRequest = class {};
5047
- __decorate([ApiProperty({
5048
- type: "number",
5049
- example: callbackTypesRequestExample.callbacks[0].chain_id
5050
- })], CallbackTypesChainRequest.prototype, "chain_id", void 0);
5051
- __decorate([ApiProperty({
5052
- type: () => [String],
5053
- example: callbackTypesRequestExample.callbacks[0].addresses
5054
- })], CallbackTypesChainRequest.prototype, "addresses", void 0);
5055
- var CallbackTypesRequest = class {};
5056
- __decorate([ApiProperty({
5057
- type: () => [CallbackTypesChainRequest],
5058
- example: callbackTypesRequestExample.callbacks
5059
- })], CallbackTypesRequest.prototype, "callbacks", void 0);
5060
- var CallbackTypesChainResponse = class {};
5061
- __decorate([ApiProperty({
5062
- type: "number",
5063
- example: callbackTypesResponseExample[0].chain_id
5064
- })], CallbackTypesChainResponse.prototype, "chain_id", void 0);
5065
- __decorate([ApiProperty({
5066
- type: () => [String],
5067
- required: false,
5068
- example: callbackTypesResponseExample[0].buy_vault_v1_callback
5069
- })], CallbackTypesChainResponse.prototype, "buy_vault_v1_callback", void 0);
5070
- __decorate([ApiProperty({
5071
- type: () => [String],
5072
- required: false,
5073
- example: callbackTypesResponseExample[0].sell_erc20_callback
5074
- })], CallbackTypesChainResponse.prototype, "sell_erc20_callback", void 0);
5075
- __decorate([ApiProperty({
5076
- type: () => [String],
5077
- required: false,
5078
- example: callbackTypesResponseExample[0].buy_erc20
5079
- })], CallbackTypesChainResponse.prototype, "buy_erc20", void 0);
5080
- __decorate([ApiProperty({
5081
- type: () => [String],
5082
- example: callbackTypesResponseExample[0].not_supported
5083
- })], CallbackTypesChainResponse.prototype, "not_supported", void 0);
5084
- var CallbackTypesSuccessResponse = class extends SuccessResponse {};
5085
- __decorate([ApiProperty({
5086
- type: "string",
5087
- nullable: true,
5088
- example: "maturity:1:1730415600:end_of_next_month"
5089
- })], CallbackTypesSuccessResponse.prototype, "cursor", void 0);
5090
- __decorate([ApiProperty({
5091
- type: () => [CallbackTypesChainResponse],
5092
- description: "Callback types grouped by chain.",
5093
- example: callbackTypesResponseExample
5094
- })], CallbackTypesSuccessResponse.prototype, "data", void 0);
5095
5500
  var AskResponse = class {};
5096
5501
  __decorate([ApiProperty({
5097
5502
  type: "string",
@@ -5256,7 +5661,8 @@ var OfferListResponse = class extends SuccessResponse {};
5256
5661
  __decorate([ApiProperty({
5257
5662
  type: "string",
5258
5663
  nullable: true,
5259
- example: offerCursorExample
5664
+ example: offerCursorExample,
5665
+ description: "Pagination cursor. Offer hash (0x...) for maker queries; base64url-encoded cursor for obligation queries."
5260
5666
  })], OfferListResponse.prototype, "cursor", void 0);
5261
5667
  __decorate([ApiProperty({
5262
5668
  type: () => [OfferListItemResponse],
@@ -5608,7 +6014,7 @@ __decorate([
5608
6014
  methods: ["post"],
5609
6015
  path: "/v1/validate",
5610
6016
  summary: "Validate offers",
5611
- description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
6017
+ description: "Validates offers against router validation rules. Only empty callbacks (zero address, 0x data) are accepted. Returns unsigned payload + root on success, or issues only on validation failure."
5612
6018
  }),
5613
6019
  ApiBody({ type: ValidateOffersRequest }),
5614
6020
  ApiResponse({
@@ -5627,28 +6033,6 @@ ValidateController = __decorate([ApiTags("Make"), ApiResponse({
5627
6033
  description: "Bad Request",
5628
6034
  type: BadRequestResponse
5629
6035
  })], ValidateController);
5630
- let CallbacksController = class CallbacksController {
5631
- async resolveCallbackTypes() {}
5632
- };
5633
- __decorate([
5634
- ApiOperation({
5635
- methods: ["post"],
5636
- path: "/v1/callbacks",
5637
- summary: "Resolve callback types",
5638
- description: "Returns callback types for callback addresses grouped by chain."
5639
- }),
5640
- ApiBody({ type: CallbackTypesRequest }),
5641
- ApiResponse({
5642
- status: 200,
5643
- description: "Success",
5644
- type: CallbackTypesSuccessResponse
5645
- })
5646
- ], CallbacksController.prototype, "resolveCallbackTypes", null);
5647
- CallbacksController = __decorate([ApiTags("Make"), ApiResponse({
5648
- status: 400,
5649
- description: "Bad Request",
5650
- type: BadRequestResponse
5651
- })], CallbacksController);
5652
6036
  let OffersController = class OffersController {
5653
6037
  async getOffers() {}
5654
6038
  };
@@ -5798,22 +6182,21 @@ const configRulesMaturityExample = {
5798
6182
  name: "end_of_next_month",
5799
6183
  timestamp: 1730415600
5800
6184
  };
5801
- const configRulesCallbackExample = {
5802
- type: "callback",
5803
- chain_id: 1,
5804
- address: "0x1111111111111111111111111111111111111111",
5805
- callback_type: "sell_erc20_callback"
5806
- };
5807
6185
  const configRulesLoanTokenExample = {
5808
6186
  type: "loan_token",
5809
6187
  chain_id: 1,
5810
6188
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
5811
6189
  };
6190
+ const configRulesOracleExample = {
6191
+ type: "oracle",
6192
+ chain_id: 1,
6193
+ address: "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83"
6194
+ };
5812
6195
  const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
5813
6196
  const configRulesPayloadExample = [
5814
6197
  configRulesMaturityExample,
5815
- configRulesCallbackExample,
5816
- configRulesLoanTokenExample
6198
+ configRulesLoanTokenExample,
6199
+ configRulesOracleExample
5817
6200
  ];
5818
6201
  const configContractNames = [
5819
6202
  "mempool",
@@ -5876,14 +6259,9 @@ __decorate([ApiProperty({
5876
6259
  })], ConfigRulesRuleResponse.prototype, "timestamp", void 0);
5877
6260
  __decorate([ApiProperty({
5878
6261
  type: "string",
5879
- example: configRulesCallbackExample.address,
6262
+ example: configRulesLoanTokenExample.address,
5880
6263
  required: false
5881
6264
  })], ConfigRulesRuleResponse.prototype, "address", void 0);
5882
- __decorate([ApiProperty({
5883
- type: "string",
5884
- example: configRulesCallbackExample.callback_type,
5885
- required: false
5886
- })], ConfigRulesRuleResponse.prototype, "callback_type", void 0);
5887
6265
  var ConfigRulesSuccessResponse = class {};
5888
6266
  __decorate([ApiProperty({ type: () => ConfigRulesMeta })], ConfigRulesSuccessResponse.prototype, "meta", void 0);
5889
6267
  __decorate([ApiProperty({
@@ -5944,7 +6322,7 @@ __decorate([
5944
6322
  methods: ["get"],
5945
6323
  path: "/v1/config/rules",
5946
6324
  summary: "Get config rules",
5947
- description: "Returns configured rules for supported chains."
6325
+ description: "Returns configured rules (maturities, loan tokens, oracles) for supported chains."
5948
6326
  }),
5949
6327
  ApiQuery({
5950
6328
  name: "cursor",
@@ -5964,7 +6342,7 @@ __decorate([
5964
6342
  name: "types",
5965
6343
  type: ["string"],
5966
6344
  required: false,
5967
- example: "maturity,loan_token",
6345
+ example: "maturity,loan_token,oracle",
5968
6346
  description: "Filter by rule types (comma-separated).",
5969
6347
  style: "form",
5970
6348
  explode: false
@@ -5979,1062 +6357,456 @@ __decorate([
5979
6357
  explode: false
5980
6358
  }),
5981
6359
  ApiResponse({
5982
- status: 200,
5983
- description: "Success",
5984
- type: ConfigRulesSuccessResponse
5985
- })
5986
- ], ConfigRulesController.prototype, "getConfigRules", null);
5987
- ConfigRulesController = __decorate([ApiTags("System")], ConfigRulesController);
5988
- let ObligationsController = class ObligationsController {
5989
- async getObligations() {}
5990
- async getObligation() {}
5991
- };
5992
- __decorate([
5993
- ApiOperation({
5994
- methods: ["get"],
5995
- path: "/v1/obligations",
5996
- summary: "List all obligations",
5997
- description: "Returns a list of obligations with their current best ask and bid. Obligations are sorted by their id in ascending order by default."
5998
- }),
5999
- ApiQuery({
6000
- name: "cursor",
6001
- type: "string",
6002
- example: obligationCursorExample,
6003
- description: "Obligation id cursor for pagination."
6004
- }),
6005
- ApiQuery({
6006
- name: "limit",
6007
- type: "number",
6008
- example: 10,
6009
- description: "Maximum number of obligations to return."
6010
- }),
6011
- ApiQuery({
6012
- name: "chains",
6013
- type: ["number"],
6014
- required: false,
6015
- example: "1,8453",
6016
- description: "Filter by chain IDs (comma-separated).",
6017
- style: "form",
6018
- explode: false
6019
- }),
6020
- ApiQuery({
6021
- name: "loan_tokens",
6022
- type: ["string"],
6023
- required: false,
6024
- example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751",
6025
- description: "Filter by loan token addresses (comma-separated).",
6026
- style: "form",
6027
- explode: false
6028
- }),
6029
- ApiQuery({
6030
- name: "collateral_tokens",
6031
- type: ["string"],
6032
- required: false,
6033
- example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
6034
- description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6035
- style: "form",
6036
- explode: false
6037
- }),
6038
- ApiQuery({
6039
- name: "maturities",
6040
- type: ["number"],
6041
- required: false,
6042
- example: "1761922800,1764524800",
6043
- description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6044
- style: "form",
6045
- explode: false
6046
- }),
6047
- ApiResponse({
6048
- status: 200,
6049
- description: "Success",
6050
- type: ObligationListResponse
6051
- })
6052
- ], ObligationsController.prototype, "getObligations", null);
6053
- __decorate([
6054
- ApiOperation({
6055
- methods: ["get"],
6056
- path: "/v1/obligations/{obligationId}",
6057
- summary: "Get an obligation",
6058
- description: "Returns an obligation by its id."
6059
- }),
6060
- ApiParam({
6061
- name: "obligationId",
6062
- type: "string",
6063
- example: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
6064
- description: "Obligation id."
6065
- }),
6066
- ApiResponse({
6067
- status: 200,
6068
- description: "Success",
6069
- type: ObligationSingleSuccessResponse
6070
- })
6071
- ], ObligationsController.prototype, "getObligation", null);
6072
- ObligationsController = __decorate([ApiTags("Markets"), ApiResponse({
6073
- status: 400,
6074
- description: "Bad Request",
6075
- type: BadRequestResponse
6076
- })], ObligationsController);
6077
- let UsersController = class UsersController {
6078
- async getUserPositions() {}
6360
+ status: 200,
6361
+ description: "Success",
6362
+ type: ConfigRulesSuccessResponse
6363
+ })
6364
+ ], ConfigRulesController.prototype, "getConfigRules", null);
6365
+ ConfigRulesController = __decorate([ApiTags("System")], ConfigRulesController);
6366
+ let ObligationsController = class ObligationsController {
6367
+ async getObligations() {}
6368
+ async getObligation() {}
6079
6369
  };
6080
6370
  __decorate([
6081
6371
  ApiOperation({
6082
6372
  methods: ["get"],
6083
- path: "/v1/users/{userAddress}/positions",
6084
- summary: "Get user positions",
6085
- description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
6086
- }),
6087
- ApiParam({
6088
- name: "userAddress",
6089
- type: "string",
6090
- example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6091
- description: "User address to get positions for."
6373
+ path: "/v1/obligations",
6374
+ summary: "List all obligations",
6375
+ description: "Returns a list of obligations with their current best ask and bid. Obligations are sorted by their id in ascending order by default."
6092
6376
  }),
6093
6377
  ApiQuery({
6094
6378
  name: "cursor",
6095
6379
  type: "string",
6096
- example: offerCursorExample,
6097
- description: "Pagination cursor in base64url-encoded format."
6380
+ example: obligationCursorExample,
6381
+ description: "Obligation id cursor for pagination."
6098
6382
  }),
6099
6383
  ApiQuery({
6100
6384
  name: "limit",
6101
6385
  type: "number",
6102
6386
  example: 10,
6103
- description: "Maximum number of positions to return."
6104
- }),
6105
- ApiResponse({
6106
- status: 200,
6107
- description: "Success",
6108
- type: PositionListResponse
6109
- })
6110
- ], UsersController.prototype, "getUserPositions", null);
6111
- UsersController = __decorate([ApiTags("Make"), ApiResponse({
6112
- status: 400,
6113
- description: "Bad Request",
6114
- type: BadRequestResponse
6115
- })], UsersController);
6116
- const OpenApi = async () => {
6117
- return await generateDocument({
6118
- controllers: [
6119
- BooksController,
6120
- ConfigContractsController,
6121
- ConfigRulesController,
6122
- OffersController,
6123
- ObligationsController,
6124
- HealthController,
6125
- UsersController,
6126
- ValidateController,
6127
- CallbacksController
6128
- ],
6129
- document: {
6130
- openapi: "3.1.0",
6131
- info: {
6132
- title: "Router API",
6133
- version: "1.0.0",
6134
- description: "API for the Morpho Router"
6135
- },
6136
- servers: [{
6137
- url: "https://router.morpho.dev",
6138
- description: "Production server"
6139
- }, {
6140
- url: "http://localhost:7891",
6141
- description: "Local development server"
6142
- }],
6143
- tags: [
6144
- {
6145
- name: "Markets",
6146
- description: "Read-only endpoints to discover markets, order books and fetch current offers."
6147
- },
6148
- {
6149
- name: "Make",
6150
- description: "Utilities to ease making offers."
6151
- },
6152
- {
6153
- name: "System",
6154
- description: "Router configuration and health monitoring."
6155
- }
6156
- ]
6157
- }
6158
- });
6159
- };
6160
-
6161
- //#endregion
6162
- //#region src/api/Schema/PositionResponse.ts
6163
- var PositionResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$2 });
6164
- /**
6165
- * Creates a `PositionResponse` from a `PositionWithReserved`.
6166
- * @param position - {@link PositionWithReserved}
6167
- * @returns The created `PositionResponse`. {@link PositionResponse}
6168
- */
6169
- function from$2(position) {
6170
- return {
6171
- chain_id: position.chainId,
6172
- contract: position.contract,
6173
- user: position.user,
6174
- reserved: position.reserved.toString(),
6175
- block_number: position.blockNumber
6176
- };
6177
- }
6178
-
6179
- //#endregion
6180
- //#region src/api/Schema/requests.ts
6181
- const MAX_LIMIT = 100;
6182
- const DEFAULT_LIMIT$4 = 20;
6183
- const CONFIG_RULES_MAX_LIMIT = 1e3;
6184
- const CONFIG_RULES_DEFAULT_LIMIT = 100;
6185
- const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
6186
- const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
6187
- /** Validate cursor is a valid base64url-encoded JSON object.
6188
- * Domain layer handles semantic validation of cursor fields. */
6189
- function isValidBase64urlJson(val) {
6190
- try {
6191
- const decoded = Buffer.from(val, "base64url").toString("utf8");
6192
- JSON.parse(decoded);
6193
- return true;
6194
- } catch {
6195
- return false;
6196
- }
6197
- }
6198
- const csvArray = (schema) => z$1.preprocess((value) => {
6199
- if (value === void 0) return void 0;
6200
- if (Array.isArray(value)) {
6201
- if (value.some((item) => typeof item !== "string")) return value;
6202
- return value.flatMap((item) => item.split(",")).map((item) => item.trim()).filter((item) => item.length > 0);
6203
- }
6204
- if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
6205
- return value;
6206
- }, z$1.array(schema)).optional();
6207
- const PaginationQueryParams = z$1.object({
6208
- cursor: z$1.string().optional().refine((val) => {
6209
- if (!val) return true;
6210
- return isValidBase64urlJson(val);
6211
- }, { message: "Invalid cursor format. Must be a valid base64url-encoded cursor object" }).meta({
6212
- description: "Pagination cursor in base64url-encoded format",
6213
- example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
6214
- }),
6215
- limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6216
- description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6217
- example: 10
6218
- })
6219
- });
6220
- const ConfigRuleTypes = z$1.enum([
6221
- "maturity",
6222
- "callback",
6223
- "loan_token"
6224
- ]);
6225
- const GetConfigRulesQueryParams = z$1.object({
6226
- cursor: z$1.string().regex(/^(maturity|callback|loan_token):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6227
- description: "Pagination cursor in type:chain_id:<value> format",
6228
- example: "maturity:1:1730415600:end_of_next_month"
6229
- }),
6230
- limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(CONFIG_RULES_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_RULES_MAX_LIMIT}` })).optional().default(CONFIG_RULES_DEFAULT_LIMIT).meta({
6231
- description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
6232
- example: 100
6233
- }),
6234
- types: csvArray(ConfigRuleTypes).meta({
6235
- description: "Filter by rule types (comma-separated).",
6236
- example: "maturity,loan_token"
6237
- }),
6238
- chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6239
- description: "Filter by chain IDs (comma-separated).",
6240
- example: "1,8453"
6241
- })
6242
- });
6243
- const GetConfigContractsQueryParams = z$1.object({
6244
- cursor: z$1.string().regex(/^[1-9]\d*:0x[a-fA-F0-9]{40}$/, { message: "Cursor must be in the format chain_id:0x..." }).optional().meta({
6245
- description: "Pagination cursor in chain_id:address format (lowercase address).",
6246
- example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
6247
- }),
6248
- limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(CONFIG_CONTRACTS_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_CONTRACTS_MAX_LIMIT}` })).optional().default(CONFIG_CONTRACTS_DEFAULT_LIMIT).meta({
6249
- description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
6250
- example: 1e3
6251
- }),
6252
- chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6253
- description: "Filter by chain IDs (comma-separated).",
6254
- example: "1,8453"
6255
- })
6256
- });
6257
- const GetOffersQueryParams = z$1.object({
6258
- ...PaginationQueryParams.shape,
6259
- side: z$1.enum(["buy", "sell"]).optional().meta({
6260
- description: "Side of the offer. Required when using obligation_id.",
6261
- example: "buy"
6262
- }),
6263
- obligation_id: z$1.string().regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).optional().meta({
6264
- description: "Offers obligation id. Required when not using maker.",
6265
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6266
- }),
6267
- maker: z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Maker must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).optional().meta({
6268
- description: "Maker address to filter offers by. Alternative to obligation_id + side.",
6269
- example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6270
- })
6271
- }).superRefine((val, ctx) => {
6272
- const hasObligation = val.obligation_id !== void 0;
6273
- const hasSide = val.side !== void 0;
6274
- const hasMaker = val.maker !== void 0;
6275
- if (hasMaker && (hasObligation || hasSide)) {
6276
- ctx.addIssue({
6277
- code: "custom",
6278
- message: "Cannot use both maker and obligation_id/side parameters"
6279
- });
6280
- return;
6281
- }
6282
- if (hasMaker) return;
6283
- if (!hasObligation || !hasSide) ctx.addIssue({
6284
- code: "custom",
6285
- message: "Must provide either maker or both obligation_id and side"
6286
- });
6287
- });
6288
- const GetObligationsQueryParams = z$1.object({
6289
- ...PaginationQueryParams.shape,
6290
- cursor: z$1.string().optional().meta({
6291
- description: "Obligation id cursor",
6292
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6387
+ description: "Maximum number of obligations to return."
6293
6388
  }),
6294
- chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6389
+ ApiQuery({
6390
+ name: "chains",
6391
+ type: ["number"],
6392
+ required: false,
6393
+ example: "1,8453",
6295
6394
  description: "Filter by chain IDs (comma-separated).",
6296
- example: "1,8453"
6395
+ style: "form",
6396
+ explode: false
6297
6397
  }),
6298
- loan_tokens: csvArray(z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Loan token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6398
+ ApiQuery({
6399
+ name: "loan_tokens",
6400
+ type: ["string"],
6401
+ required: false,
6402
+ example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751",
6299
6403
  description: "Filter by loan token addresses (comma-separated).",
6300
- example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751"
6301
- }),
6302
- collateral_tokens: csvArray(z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Collateral token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6303
- description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6304
- example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078"
6305
- }),
6306
- maturities: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Maturity must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6307
- description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6308
- example: "1761922800,1764524800"
6309
- })
6310
- });
6311
- const GetObligationParams = z$1.object({ obligation_id: z$1.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({
6312
- description: "Obligation id",
6313
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6314
- }) });
6315
- /** Validate a book cursor format: {side, lastPrice, offersCursor} */
6316
- function isValidBookCursor(cursorString) {
6317
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
6318
- try {
6319
- const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
6320
- return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6321
- } catch {
6322
- return false;
6323
- }
6324
- }
6325
- const BookPaginationQueryParams = z$1.object({
6326
- cursor: z$1.string().optional().refine((value) => {
6327
- if (!value) return true;
6328
- return isValidBookCursor(value);
6329
- }, { message: "Invalid cursor format. Must be a valid base64url-encoded book cursor object" }).meta({
6330
- description: "Pagination cursor in base64url-encoded format for book levels",
6331
- example: "eyJzaWRlIjoiYnV5IiwibGFzdFJhdGUiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwib2ZmZXJzQ3Vyc29yIjpudWxsfQ"
6404
+ style: "form",
6405
+ explode: false
6332
6406
  }),
6333
- limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6334
- description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6335
- example: 10
6336
- })
6337
- });
6338
- const HealthQueryParams = z$1.object({ strict: z$1.enum([
6339
- "true",
6340
- "false",
6341
- "1",
6342
- "0"
6343
- ]).transform((value) => value === "true" || value === "1").optional().meta({
6344
- description: "Enable strict mode to fail health checks when initialization is incomplete.",
6345
- example: "true"
6346
- }) });
6347
- const GetBookParams = z$1.object({
6348
- ...BookPaginationQueryParams.shape,
6349
- obligation_id: z$1.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({
6350
- description: "Obligation id",
6351
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6407
+ ApiQuery({
6408
+ name: "collateral_tokens",
6409
+ type: ["string"],
6410
+ required: false,
6411
+ example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
6412
+ description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6413
+ style: "form",
6414
+ explode: false
6352
6415
  }),
6353
- side: z$1.enum(["buy", "sell"]).meta({
6354
- description: "Side of the book (buy or sell).",
6355
- example: "buy"
6356
- })
6357
- });
6358
- const ValidateOffersBody = z$1.object({ offers: z$1.array(z$1.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
6359
- const CallbackTypesBody = z$1.object({ callbacks: z$1.array(z$1.object({
6360
- chain_id: z$1.number().int().positive().meta({
6361
- description: "Chain id.",
6362
- example: 1
6416
+ ApiQuery({
6417
+ name: "maturities",
6418
+ type: ["number"],
6419
+ required: false,
6420
+ example: "1761922800,1764524800",
6421
+ description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6422
+ style: "form",
6423
+ explode: false
6363
6424
  }),
6364
- addresses: z$1.array(z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Callback address must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6365
- description: "Callback contract addresses.",
6366
- example: ["0x1111111111111111111111111111111111111111", "0x3333333333333333333333333333333333333333"]
6367
- })
6368
- }).strict()) }).strict();
6369
- const GetUserPositionsParams = z$1.object({
6370
- ...PaginationQueryParams.shape,
6371
- user_address: z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
6372
- description: "User address to get positions for",
6373
- example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6425
+ ApiResponse({
6426
+ status: 200,
6427
+ description: "Success",
6428
+ type: ObligationListResponse
6374
6429
  })
6375
- });
6376
- const schemas = {
6377
- get_health: HealthQueryParams,
6378
- get_health_collectors: HealthQueryParams,
6379
- get_health_chains: HealthQueryParams,
6380
- get_config_contracts: GetConfigContractsQueryParams,
6381
- get_config_rules: GetConfigRulesQueryParams,
6382
- get_offers: GetOffersQueryParams,
6383
- get_obligations: GetObligationsQueryParams,
6384
- get_obligation: GetObligationParams,
6385
- get_book: GetBookParams,
6386
- validate_offers: ValidateOffersBody,
6387
- callback_types: CallbackTypesBody,
6388
- get_user_positions: GetUserPositionsParams
6389
- };
6390
- function parse(action, query) {
6391
- return schemas[action].parse(query);
6392
- }
6393
- function safeParse(action, query, error) {
6394
- return schemas[action].safeParse(query, { error });
6395
- }
6396
-
6397
- //#endregion
6398
- //#region src/api/Controllers/getBook.ts
6399
- async function getBook(params, db) {
6400
- const logger = getLogger();
6401
- const result = safeParse("get_book", params, (issue) => issue.message);
6402
- if (!result.success) return failure(result.error);
6403
- const query = result.data;
6404
- try {
6405
- const { levels, nextCursor } = await db.book.get({
6406
- side: query.side,
6407
- obligationId: query.obligation_id,
6408
- cursor: query.cursor,
6409
- limit: query.limit
6410
- });
6411
- return success({
6412
- data: levels.map(from$5),
6413
- cursor: nextCursor
6414
- });
6415
- } catch (err) {
6416
- logger.error({
6417
- err,
6418
- msg: "Error get book",
6419
- errorMessage: err instanceof Error ? err.message : String(err),
6420
- errorStack: err instanceof Error ? err.stack : void 0
6421
- });
6422
- return failure(err);
6423
- }
6424
- }
6425
-
6426
- //#endregion
6427
- //#region src/api/Controllers/getConfigContracts.ts
6428
- const CONFIG_CONTRACT_NAMES = [
6429
- "mempool",
6430
- "multicall",
6431
- "v2"
6432
- ];
6433
- /**
6434
- * Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
6435
- * @param query - Raw query parameters containing optional chain filters.
6436
- * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
6437
- * @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
6438
- */
6439
- async function getConfigContracts(query, chainRegistry) {
6440
- const parsed = safeParse("get_config_contracts", query ?? {});
6441
- if (!parsed.success) return failure(parsed.error);
6442
- const { chains: chainsFilter, cursor, limit } = parsed.data;
6443
- const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
6444
- const contracts = [];
6445
- const seenAddresses = /* @__PURE__ */ new Set();
6446
- for (const chain of chainRegistry.list()) {
6447
- if (chainFilter && !chainFilter.has(chain.id)) continue;
6448
- const mempool = chain.custom?.mempool?.address;
6449
- if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
6450
- const multicall = chain.contracts?.multicall3?.address;
6451
- if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
6452
- const v2 = chain.custom?.morpho?.address;
6453
- if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
6454
- const chainContracts = [
6455
- {
6456
- chain_id: chain.id,
6457
- name: "mempool",
6458
- address: mempool
6459
- },
6460
- {
6461
- chain_id: chain.id,
6462
- name: "multicall",
6463
- address: multicall
6464
- },
6465
- {
6466
- chain_id: chain.id,
6467
- name: "v2",
6468
- address: v2
6469
- }
6470
- ];
6471
- for (const contract of chainContracts) {
6472
- const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
6473
- if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
6474
- seenAddresses.add(cursorKey);
6475
- contracts.push(contract);
6476
- }
6477
- }
6478
- contracts.sort((a, b) => {
6479
- if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
6480
- const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
6481
- if (addressCompare !== 0) return addressCompare;
6482
- return a.name.localeCompare(b.name);
6483
- });
6484
- let cursorContract = null;
6485
- if (cursor) try {
6486
- cursorContract = parseCursor$1(cursor);
6487
- } catch (err) {
6488
- return failure(err);
6489
- }
6490
- const startIndex = cursorContract ? findStartIndex$1(contracts, cursorContract) : 0;
6491
- const page = contracts.slice(startIndex, startIndex + limit);
6492
- const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
6493
- return success({
6494
- data: page,
6495
- cursor: nextCursor
6496
- });
6497
- }
6498
- function parseCursor$1(cursor) {
6499
- const [chain, address] = cursor.split(":", 2);
6500
- if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
6501
- return {
6502
- chain_id: Number.parseInt(chain, 10),
6503
- address: address.toLowerCase()
6504
- };
6505
- }
6506
- function formatCursor$1(contract) {
6507
- return `${contract.chain_id}:${contract.address.toLowerCase()}`;
6508
- }
6509
- function findStartIndex$1(contracts, cursor) {
6510
- let low = 0;
6511
- let high = contracts.length;
6512
- while (low < high) {
6513
- const mid = Math.floor((low + high) / 2);
6514
- const current = contracts[mid];
6515
- if (compareContract(current, cursor) <= 0) low = mid + 1;
6516
- else high = mid;
6517
- }
6518
- return low;
6519
- }
6520
- function compareContract(contract, cursor) {
6521
- if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
6522
- return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
6523
- }
6524
-
6525
- //#endregion
6526
- //#region src/gatekeeper/GateConfig.ts
6527
- /**
6528
- * Returns the callback configuration for a given chain and callback type, if it exists.
6529
- *
6530
- * @param chain - Chain name for which to read the validation configuration
6531
- * @param type - Callback type to retrieve
6532
- * @returns The matching callback configuration or undefined if not configured
6533
- */
6534
- function getCallback(chain, type) {
6535
- return configs[chain].callbacks?.find((c) => c.type === type);
6536
- }
6537
- /**
6538
- * Attempts to infer the configured callback type from a callback address on a chain.
6539
- * Skips the empty callback type as it does not carry addresses.
6540
- *
6541
- * @param chain - Chain name for which to infer the callback type
6542
- * @param address - Callback contract address
6543
- * @returns The callback type when found, otherwise undefined
6544
- */
6545
- function getCallbackType(chain, address) {
6546
- return configs[chain].callbacks?.find((c) => c.type !== Type$1.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
6547
- }
6548
- /**
6549
- * Returns the list of allowed non-empty callback addresses for a chain.
6550
- *
6551
- * @param chain - Chain name
6552
- * @returns Array of allowed callback addresses (lowercased). Empty when none configured
6553
- */
6554
- const getCallbackAddresses = (chain) => {
6555
- return configs[chain].callbacks?.filter((c) => c.type !== Type$1.BuyWithEmptyCallback).flatMap((c) => c.addresses) ?? [];
6556
- };
6557
- const assets = {
6558
- [ChainId.ETHEREUM.toString()]: [
6559
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6560
- "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6561
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6562
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6563
- ],
6564
- [ChainId.BASE.toString()]: [
6565
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
6566
- "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
6567
- "0x4200000000000000000000000000000000000006",
6568
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6569
- ],
6570
- [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
6571
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6572
- "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6573
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6574
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6575
- "0xce79ddb3152d52ff8fe65a4c7e058b035fcb560a"
6576
- ],
6577
- [ChainId.ANVIL.toString()]: [
6578
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6579
- "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6580
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6581
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6582
- ]
6430
+ ], ObligationsController.prototype, "getObligations", null);
6431
+ __decorate([
6432
+ ApiOperation({
6433
+ methods: ["get"],
6434
+ path: "/v1/obligations/{obligationId}",
6435
+ summary: "Get an obligation",
6436
+ description: "Returns an obligation by its id."
6437
+ }),
6438
+ ApiParam({
6439
+ name: "obligationId",
6440
+ type: "string",
6441
+ example: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
6442
+ description: "Obligation id."
6443
+ }),
6444
+ ApiResponse({
6445
+ status: 200,
6446
+ description: "Success",
6447
+ type: ObligationSingleSuccessResponse
6448
+ })
6449
+ ], ObligationsController.prototype, "getObligation", null);
6450
+ ObligationsController = __decorate([ApiTags("Markets"), ApiResponse({
6451
+ status: 400,
6452
+ description: "Bad Request",
6453
+ type: BadRequestResponse
6454
+ })], ObligationsController);
6455
+ let UsersController = class UsersController {
6456
+ async getUserPositions() {}
6583
6457
  };
6584
- const configs = {
6585
- ethereum: {
6586
- callbacks: [
6587
- {
6588
- type: Type$1.BuyVaultV1Callback,
6589
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6590
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
6591
- },
6592
- {
6593
- type: Type$1.SellERC20Callback,
6594
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6595
- },
6596
- { type: Type$1.BuyWithEmptyCallback }
6597
- ],
6598
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6599
- },
6600
- base: {
6601
- callbacks: [
6602
- {
6603
- type: Type$1.BuyVaultV1Callback,
6604
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6605
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0xFf62A7c278C62eD665133147129245053Bbf5918"]
6606
- },
6607
- {
6608
- type: Type$1.SellERC20Callback,
6609
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6610
- },
6611
- { type: Type$1.BuyWithEmptyCallback }
6612
- ],
6613
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6614
- },
6615
- "ethereum-virtual-testnet": {
6616
- callbacks: [
6617
- {
6618
- type: Type$1.BuyVaultV1Callback,
6619
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6620
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
6621
- },
6622
- {
6623
- type: Type$1.SellERC20Callback,
6624
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6625
- },
6626
- { type: Type$1.BuyWithEmptyCallback }
6458
+ __decorate([
6459
+ ApiOperation({
6460
+ methods: ["get"],
6461
+ path: "/v1/users/{userAddress}/positions",
6462
+ summary: "Get user positions",
6463
+ description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
6464
+ }),
6465
+ ApiParam({
6466
+ name: "userAddress",
6467
+ type: "string",
6468
+ example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6469
+ description: "User address to get positions for."
6470
+ }),
6471
+ ApiQuery({
6472
+ name: "cursor",
6473
+ type: "string",
6474
+ example: offerCursorExample,
6475
+ description: "Pagination cursor in base64url-encoded format."
6476
+ }),
6477
+ ApiQuery({
6478
+ name: "limit",
6479
+ type: "number",
6480
+ example: 10,
6481
+ description: "Maximum number of positions to return."
6482
+ }),
6483
+ ApiResponse({
6484
+ status: 200,
6485
+ description: "Success",
6486
+ type: PositionListResponse
6487
+ })
6488
+ ], UsersController.prototype, "getUserPositions", null);
6489
+ UsersController = __decorate([ApiTags("Make"), ApiResponse({
6490
+ status: 400,
6491
+ description: "Bad Request",
6492
+ type: BadRequestResponse
6493
+ })], UsersController);
6494
+ const OpenApi = async () => {
6495
+ return await generateDocument({
6496
+ controllers: [
6497
+ BooksController,
6498
+ ConfigContractsController,
6499
+ ConfigRulesController,
6500
+ OffersController,
6501
+ ObligationsController,
6502
+ HealthController,
6503
+ UsersController,
6504
+ ValidateController
6627
6505
  ],
6628
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6629
- },
6630
- anvil: {
6631
- callbacks: [
6632
- {
6633
- type: Type$1.BuyVaultV1Callback,
6634
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6635
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
6636
- },
6637
- {
6638
- type: Type$1.SellERC20Callback,
6639
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6506
+ document: {
6507
+ openapi: "3.1.0",
6508
+ info: {
6509
+ title: "Router API",
6510
+ version: "1.0.0",
6511
+ description: "API for the Morpho Router"
6640
6512
  },
6641
- { type: Type$1.BuyWithEmptyCallback }
6642
- ],
6643
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6644
- }
6645
- };
6646
-
6647
- //#endregion
6648
- //#region src/gatekeeper/ConfigRules.ts
6649
- /**
6650
- * Build the configured rules (maturities + callback addresses + loan tokens) for the provided chains.
6651
- * @param chains - Chains to include in the configured rules.
6652
- * @returns Sorted list of config rules.
6653
- */
6654
- function buildConfigRules(chains) {
6655
- const rules = [];
6656
- for (const chain of chains) {
6657
- const config = configs[chain.name];
6658
- const maturities = config.maturities ?? [];
6659
- for (const maturityName of maturities) rules.push({
6660
- type: "maturity",
6661
- chain_id: chain.id,
6662
- name: maturityName,
6663
- timestamp: from$16(maturityName)
6664
- });
6665
- const callbacks = config.callbacks ?? [];
6666
- for (const callback of callbacks) {
6667
- if (callback.type === Type$1.BuyWithEmptyCallback) continue;
6668
- if (!("addresses" in callback)) continue;
6669
- for (const address of callback.addresses) rules.push({
6670
- type: "callback",
6671
- chain_id: chain.id,
6672
- address: normalizeAddress(address),
6673
- callback_type: callback.type
6674
- });
6675
- }
6676
- const loanTokens = assets[chain.id.toString()] ?? [];
6677
- for (const address of loanTokens) rules.push({
6678
- type: "loan_token",
6679
- chain_id: chain.id,
6680
- address: normalizeAddress(address)
6681
- });
6682
- }
6683
- rules.sort(compareConfigRules);
6684
- return rules;
6685
- }
6686
- /**
6687
- * Compute a stable checksum for the provided configured rules.
6688
- * @param rules - Configured rules to checksum.
6689
- * @returns MD5 checksum.
6690
- */
6691
- function buildConfigRulesChecksum(rules) {
6692
- const hash = createHash("md5");
6693
- const orderedRules = [...rules].sort(compareConfigRules);
6694
- for (const rule of orderedRules) {
6695
- if (rule.type === "maturity") {
6696
- hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
6697
- continue;
6698
- }
6699
- if (rule.type === "callback") {
6700
- hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
6701
- continue;
6513
+ servers: [{
6514
+ url: "https://router.morpho.dev",
6515
+ description: "Production server"
6516
+ }, {
6517
+ url: "http://localhost:7891",
6518
+ description: "Local development server"
6519
+ }],
6520
+ tags: [
6521
+ {
6522
+ name: "Markets",
6523
+ description: "Read-only endpoints to discover markets, order books and fetch current offers."
6524
+ },
6525
+ {
6526
+ name: "Make",
6527
+ description: "Utilities to ease making offers."
6528
+ },
6529
+ {
6530
+ name: "System",
6531
+ description: "Router configuration and health monitoring."
6532
+ }
6533
+ ]
6702
6534
  }
6703
- hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
6704
- }
6705
- return hash.digest("hex");
6706
- }
6707
- function normalizeAddress(address) {
6708
- return address.toLowerCase();
6709
- }
6710
- function compareConfigRules(left, right) {
6711
- if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
6712
- if (left.type !== right.type) return left.type.localeCompare(right.type);
6713
- if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
6714
- if (left.type === "callback" && right.type === "callback") {
6715
- if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
6716
- return left.address.localeCompare(right.address);
6717
- }
6718
- if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
6719
- return 0;
6720
- }
6535
+ });
6536
+ };
6721
6537
 
6722
6538
  //#endregion
6723
- //#region src/api/Controllers/getConfigRules.ts
6539
+ //#region src/api/Schema/PositionResponse.ts
6540
+ var PositionResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$2 });
6724
6541
  /**
6725
- * Returns configured rules for the configured chains.
6726
- * @param query - Raw query parameters containing filters/cursor/limit.
6727
- * @param chains - Chains to include in the configured rules.
6728
- * @returns Config rules response payload. {@link ApiPayload.Payload}
6729
- */
6730
- async function getConfigRules(query, chains) {
6731
- const parsed = safeParse("get_config_rules", query ?? {});
6732
- if (!parsed.success) return failure(parsed.error);
6733
- const { cursor, limit, types, chains: chainIds } = parsed.data;
6734
- const typeFilter = types?.length ? new Set(types) : null;
6735
- const chainFilter = chainIds?.length ? new Set(chainIds) : null;
6736
- const filteredRules = buildConfigRules(chains).filter((rule) => {
6737
- if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
6738
- if (typeFilter && !typeFilter.has(rule.type)) return false;
6739
- return true;
6740
- });
6741
- const checksum = buildConfigRulesChecksum(filteredRules);
6742
- let cursorRule = null;
6743
- if (cursor) try {
6744
- cursorRule = parseCursor(cursor);
6745
- } catch (err) {
6746
- return failure(err);
6747
- }
6748
- if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
6749
- if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
6750
- const startIndex = cursorRule ? findStartIndex(filteredRules, cursorRule) : 0;
6751
- const page = filteredRules.slice(startIndex, startIndex + limit);
6752
- const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
6753
- const response = success({
6754
- data: page,
6755
- cursor: nextCursor
6756
- });
6757
- response.body.meta.checksum = checksum;
6758
- return response;
6759
- }
6760
- function formatCursor(rule) {
6761
- if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
6762
- if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
6763
- return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
6764
- }
6765
- function parseCursor(cursor) {
6766
- const [type, chain, ...rest] = cursor.split(":");
6767
- if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
6768
- if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
6769
- const chain_id = Number.parseInt(chain, 10);
6770
- if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
6771
- if (type === "maturity") {
6772
- const timestampValue = Number.parseInt(rest[0] ?? "", 10);
6773
- const nameValue = rest.slice(1).join(":");
6774
- if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
6775
- if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
6776
- return {
6777
- type,
6778
- chain_id,
6779
- timestamp: parseMaturity(timestampValue),
6780
- name: nameValue
6781
- };
6782
- }
6783
- if (type === "callback") {
6784
- const callbackTypeValue = rest[0] ?? "";
6785
- const addressValue = rest.slice(1).join(":");
6786
- if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
6787
- if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
6788
- return {
6789
- type,
6790
- chain_id,
6791
- callback_type: callbackTypeValue,
6792
- address: parseAddress(addressValue, "Cursor address")
6793
- };
6794
- }
6795
- const addressValue = rest.join(":");
6796
- if (!addressValue) throw new BadRequestError("Cursor must be in the format loan_token:chain_id:address");
6542
+ * Creates a `PositionResponse` from a `PositionWithReserved`.
6543
+ * @param position - {@link PositionWithReserved}
6544
+ * @returns The created `PositionResponse`. {@link PositionResponse}
6545
+ */
6546
+ function from$2(position) {
6797
6547
  return {
6798
- type,
6799
- chain_id,
6800
- address: parseAddress(addressValue, "Cursor address")
6548
+ chain_id: position.chainId,
6549
+ contract: position.contract,
6550
+ user: position.user,
6551
+ reserved: position.reserved.toString(),
6552
+ block_number: position.blockNumber
6801
6553
  };
6802
6554
  }
6803
- function findStartIndex(rules, cursor) {
6804
- let low = 0;
6805
- let high = rules.length;
6806
- while (low < high) {
6807
- const mid = Math.floor((low + high) / 2);
6808
- const current = rules[mid];
6809
- if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
6810
- else high = mid;
6811
- }
6812
- return low;
6813
- }
6814
- function parseAddress(address, label) {
6815
- if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
6816
- return address.toLowerCase();
6817
- }
6818
- function isConfigRuleType(value) {
6819
- return value === "maturity" || value === "callback" || value === "loan_token";
6820
- }
6821
- function isMaturityType(value) {
6822
- return Object.values(MaturityType).includes(value);
6823
- }
6824
- function parseMaturity(value) {
6825
- try {
6826
- return from$16(value);
6827
- } catch (err) {
6828
- throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
6829
- }
6830
- }
6831
- function isCallbackType(value) {
6832
- if (value === Type$1.BuyWithEmptyCallback) return false;
6833
- return Object.values(Type$1).includes(value);
6834
- }
6835
6555
 
6836
6556
  //#endregion
6837
- //#region src/api/Controllers/getDocs.ts
6838
- const __dirname = (() => {
6557
+ //#region src/api/Schema/requests.ts
6558
+ const MAX_LIMIT = 100;
6559
+ const DEFAULT_LIMIT$4 = 20;
6560
+ const CONFIG_RULES_MAX_LIMIT = 1e3;
6561
+ const CONFIG_RULES_DEFAULT_LIMIT = 100;
6562
+ const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
6563
+ const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
6564
+ /** Validate cursor is a valid base64url-encoded JSON object.
6565
+ * Domain layer handles semantic validation of cursor fields. */
6566
+ function isValidBase64urlJson(val) {
6839
6567
  try {
6840
- return dirname(fileURLToPath(import.meta.url));
6568
+ const decoded = Buffer.from(val, "base64url").toString("utf8");
6569
+ JSON.parse(decoded);
6570
+ return true;
6841
6571
  } catch {
6842
- return process.cwd();
6572
+ return false;
6843
6573
  }
6844
- })();
6845
- /**
6846
- * Build the OpenAPI document for the router.
6847
- * @returns OpenAPI document. {@link OpenAPIDocument}
6848
- */
6849
- async function getSwaggerJson() {
6850
- return OpenApi();
6851
- }
6852
- /**
6853
- * Render the API documentation HTML page.
6854
- * @returns HTML page as string.
6855
- */
6856
- async function getDocsHtml() {
6857
- const spec = await OpenApi();
6858
- return `<!DOCTYPE html>
6859
- <html>
6860
- <head>
6861
- <meta charset="UTF-8">
6862
- <title>Router API Docs (Scalar)</title>
6863
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
6864
- <style>
6865
- html, body { margin: 0; height: 100%; }
6866
- api-reference { height: 100%; width: 100%; }
6867
- </style>
6868
- </head>
6869
- <body>
6870
- <div id="api-container" style="height:100%;width:100%;"></div>
6871
- <script>
6872
- window.addEventListener('load', function () {
6873
- const spec = ${JSON.stringify(spec)};
6874
- Scalar.createApiReference('#api-container', { spec: { content: spec, hideModels: true } });
6875
- });
6876
- <\/script>
6877
- </body>
6878
- </html>`;
6879
- }
6880
- /**
6881
- * Finds the integrator.md file.
6882
- * Handles source, bundled CLI, and Lambda scenarios.
6883
- */
6884
- function findIntegratorMd() {
6885
- const candidates = [
6886
- resolve(__dirname, "../../../docs/integrator.md"),
6887
- resolve(__dirname, "../docs/integrator.md"),
6888
- resolve(process.cwd(), "docs/integrator.md")
6889
- ];
6890
- for (const candidate of candidates) if (existsSync(candidate)) return candidate;
6891
- throw new Error(`integrator.md not found. Tried: ${candidates.join(", ")}`);
6892
6574
  }
6893
- /**
6894
- * Renders the integrator documentation as HTML.
6895
- * @returns HTML page with the rendered markdown documentation.
6896
- */
6897
- async function getIntegratorDocsHtml() {
6898
- return `<!DOCTYPE html>
6899
- <html>
6900
- <head>
6901
- <meta charset="UTF-8">
6902
- <title>Documentation</title>
6903
- <style>
6904
- body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
6905
- pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; }
6906
- code { background: #f4f4f4; padding: 0.2rem 0.4rem; }
6907
- a { color: #0066cc; }
6908
- </style>
6909
- </head>
6910
- <body>
6911
- <nav><a href="/docs/api">API Reference &rarr;</a></nav>
6912
- ${await marked(await readFile(findIntegratorMd(), "utf-8"))}
6913
- </body>
6914
- </html>`;
6575
+ function isValidOfferHashCursor(val) {
6576
+ return /^0x[a-f0-9]{64}$/i.test(val);
6915
6577
  }
6916
-
6917
- //#endregion
6918
- //#region src/api/Controllers/getHealth.ts
6919
- async function getHealth(query, db, chainRegistry) {
6920
- const logger = getLogger();
6921
- try {
6922
- const parsed = safeParse("get_health", query);
6923
- if (!parsed.success) return failure(parsed.error);
6924
- const snapshot = await create$16({
6925
- db,
6926
- chainRegistry
6927
- }).getSnapshot();
6928
- if (parsed.data.strict && !snapshot.initialized) return failure(new APIError(STATUS_CODE.INTERNAL_SERVER_ERROR, "Indexer block state is not initialized", "INTERNAL_SERVER_ERROR", toSnakeCase$1({
6929
- missingChains: snapshot.missingChains,
6930
- missingCollectors: snapshot.missingCollectors
6931
- })));
6932
- return success({ data: toSnakeCase$1({
6933
- status: snapshot.status,
6934
- initialized: snapshot.initialized,
6935
- missingChains: snapshot.missingChains,
6936
- missingCollectors: snapshot.missingCollectors
6937
- }) });
6938
- } catch (err) {
6939
- logger.error({
6940
- err,
6941
- msg: "Error getting health status",
6942
- errorMessage: err instanceof Error ? err.message : String(err),
6943
- errorStack: err instanceof Error ? err.stack : void 0
6944
- });
6945
- return failure(err);
6578
+ const csvArray = (schema) => z$1.preprocess((value) => {
6579
+ if (value === void 0) return void 0;
6580
+ if (Array.isArray(value)) {
6581
+ if (value.some((item) => typeof item !== "string")) return value;
6582
+ return value.flatMap((item) => item.split(",")).map((item) => item.trim()).filter((item) => item.length > 0);
6946
6583
  }
6947
- }
6948
- async function getHealthChains(query, db, healthClients, chainRegistry) {
6949
- const logger = getLogger();
6950
- try {
6951
- const parsed = safeParse("get_health_chains", query);
6952
- if (!parsed.success) return failure(parsed.error);
6953
- const snapshot = await create$16({
6954
- db,
6955
- healthClients,
6956
- chainRegistry
6957
- }).getSnapshot();
6958
- if (parsed.data.strict && !snapshot.initialized) return failure(new APIError(STATUS_CODE.INTERNAL_SERVER_ERROR, "Indexer block state is not initialized", "INTERNAL_SERVER_ERROR", toSnakeCase$1({
6959
- missingChains: snapshot.missingChains,
6960
- missingCollectors: snapshot.missingCollectors
6961
- })));
6962
- const chains = snapshot.chains;
6963
- return success({ data: chains.map(({ chainId, localBlockNumber, remoteBlockNumber, updatedAt, initialized }) => toSnakeCase$1({
6964
- chainId,
6965
- localBlockNumber,
6966
- remoteBlockNumber,
6967
- updatedAt,
6968
- initialized
6969
- })) });
6970
- } catch (err) {
6971
- logger.error({
6972
- err,
6973
- msg: "Error getting health status for chains",
6974
- errorMessage: err instanceof Error ? err.message : String(err),
6975
- errorStack: err instanceof Error ? err.stack : void 0
6584
+ if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
6585
+ return value;
6586
+ }, z$1.array(schema)).optional();
6587
+ const PaginationQueryParams = z$1.object({
6588
+ cursor: z$1.string().optional().refine((val) => {
6589
+ if (!val) return true;
6590
+ return isValidBase64urlJson(val);
6591
+ }, { message: "Invalid cursor format. Must be a valid base64url-encoded cursor object" }).meta({
6592
+ description: "Pagination cursor in base64url-encoded format",
6593
+ example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
6594
+ }),
6595
+ limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6596
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6597
+ example: 10
6598
+ })
6599
+ });
6600
+ const ConfigRuleTypes = z$1.enum([
6601
+ "maturity",
6602
+ "callback",
6603
+ "loan_token",
6604
+ "oracle"
6605
+ ]);
6606
+ const GetConfigRulesQueryParams = z$1.object({
6607
+ cursor: z$1.string().regex(/^(maturity|callback|loan_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6608
+ description: "Pagination cursor in type:chain_id:<value> format",
6609
+ example: "maturity:1:1730415600:end_of_next_month"
6610
+ }),
6611
+ limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(CONFIG_RULES_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_RULES_MAX_LIMIT}` })).optional().default(CONFIG_RULES_DEFAULT_LIMIT).meta({
6612
+ description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
6613
+ example: 100
6614
+ }),
6615
+ types: csvArray(ConfigRuleTypes).meta({
6616
+ description: "Filter by rule types (comma-separated).",
6617
+ example: "maturity,loan_token,oracle"
6618
+ }),
6619
+ chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6620
+ description: "Filter by chain IDs (comma-separated).",
6621
+ example: "1,8453"
6622
+ })
6623
+ });
6624
+ const GetConfigContractsQueryParams = z$1.object({
6625
+ cursor: z$1.string().regex(/^[1-9]\d*:0x[a-fA-F0-9]{40}$/, { message: "Cursor must be in the format chain_id:0x..." }).optional().meta({
6626
+ description: "Pagination cursor in chain_id:address format (lowercase address).",
6627
+ example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
6628
+ }),
6629
+ limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(CONFIG_CONTRACTS_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_CONTRACTS_MAX_LIMIT}` })).optional().default(CONFIG_CONTRACTS_DEFAULT_LIMIT).meta({
6630
+ description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
6631
+ example: 1e3
6632
+ }),
6633
+ chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6634
+ description: "Filter by chain IDs (comma-separated).",
6635
+ example: "1,8453"
6636
+ })
6637
+ });
6638
+ const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend({
6639
+ cursor: z$1.string().optional().meta({
6640
+ description: "Pagination cursor. Use offer hash (0x...) for maker queries, base64url for obligation queries.",
6641
+ example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
6642
+ }),
6643
+ side: z$1.enum(["buy", "sell"]).optional().meta({
6644
+ description: "Side of the offer. Required when using obligation_id.",
6645
+ example: "buy"
6646
+ }),
6647
+ obligation_id: z$1.string().regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).optional().meta({
6648
+ description: "Offers obligation id. Required when not using maker.",
6649
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6650
+ }),
6651
+ maker: z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Maker must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).optional().meta({
6652
+ description: "Maker address to filter offers by. Alternative to obligation_id + side.",
6653
+ example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6654
+ })
6655
+ }).superRefine((val, ctx) => {
6656
+ const hasObligation = val.obligation_id !== void 0;
6657
+ const hasSide = val.side !== void 0;
6658
+ const hasMaker = val.maker !== void 0;
6659
+ if (hasMaker && (hasObligation || hasSide)) {
6660
+ ctx.addIssue({
6661
+ code: "custom",
6662
+ message: "Cannot use both maker and obligation_id/side parameters"
6976
6663
  });
6977
- return failure(err);
6664
+ return;
6978
6665
  }
6979
- }
6980
- async function getHealthCollectors(query, db, chainRegistry) {
6981
- const logger = getLogger();
6982
- try {
6983
- const parsed = safeParse("get_health_collectors", query);
6984
- if (!parsed.success) return failure(parsed.error);
6985
- const snapshot = await create$16({
6986
- db,
6987
- chainRegistry
6988
- }).getSnapshot();
6989
- if (parsed.data.strict && !snapshot.initialized) return failure(new APIError(STATUS_CODE.INTERNAL_SERVER_ERROR, "Indexer block state is not initialized", "INTERNAL_SERVER_ERROR", toSnakeCase$1({
6990
- missingChains: snapshot.missingChains,
6991
- missingCollectors: snapshot.missingCollectors
6992
- })));
6993
- const collectors = snapshot.collectors;
6994
- return success({ data: collectors.map(({ name, chainId, blockNumber, updatedAt, lag, status, initialized }) => toSnakeCase$1({
6995
- name,
6996
- chainId,
6997
- blockNumber,
6998
- updatedAt,
6999
- lag,
7000
- status,
7001
- initialized
7002
- })) });
7003
- } catch (err) {
7004
- logger.error({
7005
- err,
7006
- msg: "Error getting health status for collectors",
7007
- errorMessage: err instanceof Error ? err.message : String(err),
7008
- errorStack: err instanceof Error ? err.stack : void 0
6666
+ if (hasMaker) {
6667
+ if (val.cursor !== void 0 && !isValidOfferHashCursor(val.cursor)) ctx.addIssue({
6668
+ code: "custom",
6669
+ path: ["cursor"],
6670
+ message: "Cursor must be a 32-byte hex offer hash when filtering by maker"
7009
6671
  });
7010
- return failure(err);
6672
+ return;
6673
+ }
6674
+ if (!hasObligation || !hasSide) ctx.addIssue({
6675
+ code: "custom",
6676
+ message: "Must provide either maker or both obligation_id and side"
6677
+ });
6678
+ if (val.cursor !== void 0 && !isValidBase64urlJson(val.cursor)) ctx.addIssue({
6679
+ code: "custom",
6680
+ path: ["cursor"],
6681
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
6682
+ });
6683
+ }).transform((val) => {
6684
+ if (val.maker && val.cursor) return {
6685
+ ...val,
6686
+ cursor: val.cursor.toLowerCase()
6687
+ };
6688
+ return val;
6689
+ });
6690
+ const GetObligationsQueryParams = z$1.object({
6691
+ ...PaginationQueryParams.shape,
6692
+ cursor: z$1.string().optional().meta({
6693
+ description: "Obligation id cursor",
6694
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6695
+ }),
6696
+ chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6697
+ description: "Filter by chain IDs (comma-separated).",
6698
+ example: "1,8453"
6699
+ }),
6700
+ loan_tokens: csvArray(z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Loan token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6701
+ description: "Filter by loan token addresses (comma-separated).",
6702
+ example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751"
6703
+ }),
6704
+ collateral_tokens: csvArray(z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Collateral token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6705
+ description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6706
+ example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078"
6707
+ }),
6708
+ maturities: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Maturity must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6709
+ description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6710
+ example: "1761922800,1764524800"
6711
+ })
6712
+ });
6713
+ const GetObligationParams = z$1.object({ obligation_id: z$1.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({
6714
+ description: "Obligation id",
6715
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6716
+ }) });
6717
+ /** Validate a book cursor format: {side, lastPrice, offersCursor} */
6718
+ function isValidBookCursor(cursorString) {
6719
+ const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
6720
+ try {
6721
+ const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
6722
+ return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6723
+ } catch {
6724
+ return false;
7011
6725
  }
7012
6726
  }
6727
+ const BookPaginationQueryParams = z$1.object({
6728
+ cursor: z$1.string().optional().refine((value) => {
6729
+ if (!value) return true;
6730
+ return isValidBookCursor(value);
6731
+ }, { message: "Invalid cursor format. Must be a valid base64url-encoded book cursor object" }).meta({
6732
+ description: "Pagination cursor in base64url-encoded format for book levels",
6733
+ example: "eyJzaWRlIjoiYnV5IiwibGFzdFJhdGUiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwib2ZmZXJzQ3Vyc29yIjpudWxsfQ"
6734
+ }),
6735
+ limit: z$1.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$1.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6736
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6737
+ example: 10
6738
+ })
6739
+ });
6740
+ const HealthQueryParams = z$1.object({ strict: z$1.enum([
6741
+ "true",
6742
+ "false",
6743
+ "1",
6744
+ "0"
6745
+ ]).transform((value) => value === "true" || value === "1").optional().meta({
6746
+ description: "Enable strict mode to fail health checks when initialization is incomplete.",
6747
+ example: "true"
6748
+ }) });
6749
+ const GetBookParams = z$1.object({
6750
+ ...BookPaginationQueryParams.shape,
6751
+ obligation_id: z$1.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({
6752
+ description: "Obligation id",
6753
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6754
+ }),
6755
+ side: z$1.enum(["buy", "sell"]).meta({
6756
+ description: "Side of the book (buy or sell).",
6757
+ example: "buy"
6758
+ })
6759
+ });
6760
+ const ValidateOffersBody = z$1.object({ offers: z$1.array(z$1.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
6761
+ const GetUserPositionsParams = z$1.object({
6762
+ ...PaginationQueryParams.shape,
6763
+ user_address: z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
6764
+ description: "User address to get positions for",
6765
+ example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6766
+ })
6767
+ });
6768
+ const schemas = {
6769
+ get_health: HealthQueryParams,
6770
+ get_health_collectors: HealthQueryParams,
6771
+ get_health_chains: HealthQueryParams,
6772
+ get_config_contracts: GetConfigContractsQueryParams,
6773
+ get_config_rules: GetConfigRulesQueryParams,
6774
+ get_offers: GetOffersQueryParams,
6775
+ get_obligations: GetObligationsQueryParams,
6776
+ get_obligation: GetObligationParams,
6777
+ get_book: GetBookParams,
6778
+ validate_offers: ValidateOffersBody,
6779
+ get_user_positions: GetUserPositionsParams
6780
+ };
6781
+ function parse(action, query) {
6782
+ return schemas[action].parse(query);
6783
+ }
6784
+ function safeParse(action, query, error) {
6785
+ return schemas[action].safeParse(query, { error });
6786
+ }
7013
6787
 
7014
6788
  //#endregion
7015
- //#region src/api/Controllers/getObligation.ts
7016
- async function getObligation(params, db) {
6789
+ //#region src/api/Controllers/getBook.ts
6790
+ async function getBook(params, db) {
7017
6791
  const logger = getLogger();
7018
- const result = safeParse("get_obligation", params, (issue) => issue.message);
6792
+ const result = safeParse("get_book", params, (issue) => issue.message);
7019
6793
  if (!result.success) return failure(result.error);
7020
6794
  const query = result.data;
7021
6795
  try {
7022
- const { obligations } = await db.offers.getObligations({ ids: [query.obligation_id] });
7023
- if (obligations.length === 0) return failure(new NotFoundError("Obligation not found"));
7024
- const obligation = obligations[0];
7025
- const [quote] = await db.offers.getQuotes({ obligationIds: [id(obligation)] });
6796
+ const { levels, nextCursor } = await db.book.get({
6797
+ side: query.side,
6798
+ obligationId: query.obligation_id,
6799
+ cursor: query.cursor,
6800
+ limit: query.limit
6801
+ });
7026
6802
  return success({
7027
- data: from$4(obligation, quote ?? {
7028
- obligationId: id(obligation),
7029
- ask: { price: 0n },
7030
- bid: { price: 0n }
7031
- }),
7032
- cursor: null
6803
+ data: levels.map(from$5),
6804
+ cursor: nextCursor
7033
6805
  });
7034
6806
  } catch (err) {
7035
6807
  logger.error({
7036
6808
  err,
7037
- msg: "Error get obligation",
6809
+ msg: "Error get book",
7038
6810
  errorMessage: err instanceof Error ? err.message : String(err),
7039
6811
  errorStack: err instanceof Error ? err.stack : void 0
7040
6812
  });
@@ -7043,463 +6815,649 @@ async function getObligation(params, db) {
7043
6815
  }
7044
6816
 
7045
6817
  //#endregion
7046
- //#region src/api/Controllers/getObligations.ts
7047
- async function getObligations$1(queryParameters, db) {
7048
- const logger = getLogger();
7049
- const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
7050
- if (!result.success) return failure(result.error);
7051
- const query = result.data;
7052
- try {
7053
- const chainIds = query.chains?.length ? query.chains : void 0;
7054
- const loanTokens = query.loan_tokens?.length ? query.loan_tokens : void 0;
7055
- const collateralTokens = query.collateral_tokens?.length ? query.collateral_tokens : void 0;
7056
- const maturities = query.maturities?.length ? query.maturities : void 0;
7057
- const { obligations, nextCursor } = await db.offers.getObligations({
7058
- cursor: query.cursor,
7059
- limit: query.limit,
7060
- chainId: chainIds,
7061
- loanToken: loanTokens,
7062
- collateralToken: collateralTokens,
7063
- maturity: maturities
7064
- });
7065
- const quotes = await db.offers.getQuotes({ obligationIds: obligations.map((o) => id(o)) });
7066
- return success({
7067
- data: obligations.map((o) => from$4(o, quotes.find((q) => q.obligationId === id(o)) ?? {
7068
- obligationId: id(o),
7069
- ask: { price: 0n },
7070
- bid: { price: 0n }
7071
- })),
7072
- cursor: nextCursor ?? null
7073
- });
6818
+ //#region src/api/Controllers/getConfigContracts.ts
6819
+ const CONFIG_CONTRACT_NAMES = [
6820
+ "mempool",
6821
+ "multicall",
6822
+ "v2"
6823
+ ];
6824
+ /**
6825
+ * Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
6826
+ * @param query - Raw query parameters containing optional chain filters.
6827
+ * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
6828
+ * @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
6829
+ */
6830
+ async function getConfigContracts(query, chainRegistry) {
6831
+ const parsed = safeParse("get_config_contracts", query ?? {});
6832
+ if (!parsed.success) return failure(parsed.error);
6833
+ const { chains: chainsFilter, cursor, limit } = parsed.data;
6834
+ const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
6835
+ const contracts = [];
6836
+ const seenAddresses = /* @__PURE__ */ new Set();
6837
+ for (const chain of chainRegistry.list()) {
6838
+ if (chainFilter && !chainFilter.has(chain.id)) continue;
6839
+ const mempool = chain.custom?.mempool?.address;
6840
+ if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
6841
+ const multicall = chain.contracts?.multicall3?.address;
6842
+ if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
6843
+ const v2 = chain.custom?.morpho?.address;
6844
+ if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
6845
+ const chainContracts = [
6846
+ {
6847
+ chain_id: chain.id,
6848
+ name: "mempool",
6849
+ address: mempool
6850
+ },
6851
+ {
6852
+ chain_id: chain.id,
6853
+ name: "multicall",
6854
+ address: multicall
6855
+ },
6856
+ {
6857
+ chain_id: chain.id,
6858
+ name: "v2",
6859
+ address: v2
6860
+ }
6861
+ ];
6862
+ for (const contract of chainContracts) {
6863
+ const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
6864
+ if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
6865
+ seenAddresses.add(cursorKey);
6866
+ contracts.push(contract);
6867
+ }
6868
+ }
6869
+ contracts.sort((a, b) => {
6870
+ if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
6871
+ const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
6872
+ if (addressCompare !== 0) return addressCompare;
6873
+ return a.name.localeCompare(b.name);
6874
+ });
6875
+ let cursorContract = null;
6876
+ if (cursor) try {
6877
+ cursorContract = parseCursor$1(cursor);
7074
6878
  } catch (err) {
7075
- logger.error({
7076
- err,
7077
- msg: "Error get obligations",
7078
- errorMessage: err instanceof Error ? err.message : String(err),
7079
- errorStack: err instanceof Error ? err.stack : void 0
7080
- });
7081
6879
  return failure(err);
7082
6880
  }
6881
+ const startIndex = cursorContract ? findStartIndex$1(contracts, cursorContract) : 0;
6882
+ const page = contracts.slice(startIndex, startIndex + limit);
6883
+ const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
6884
+ return success({
6885
+ data: page,
6886
+ cursor: nextCursor
6887
+ });
7083
6888
  }
7084
-
7085
- //#endregion
7086
- //#region src/database/constants.ts
7087
- /**
7088
- * Default batch size for bulk database inserts.
7089
- *
7090
- * PostgreSQL limits a single query to at most 65,535 parameters
7091
- * (e.g. $1, $2, ...). In bulk inserts, each row consumes one
7092
- * parameter per column, so inserting too many rows at once can
7093
- * exceed this limit.
7094
- *
7095
- * Our largest batched insert is into the `offers` table with 15 columns.
7096
- * 15 cols × 4,000 rows = 60,000 parameters, safely under 65,535.
7097
- */
7098
- const DEFAULT_BATCH_SIZE$1 = 4e3;
7099
-
7100
- //#endregion
7101
- //#region src/database/drizzle/VERSION.ts
7102
- const VERSION = "router_v1.6";
7103
-
7104
- //#endregion
7105
- //#region src/database/drizzle/schema.ts
7106
- var schema_exports = /* @__PURE__ */ __exportAll({
7107
- PositionTypes: () => PositionTypes,
7108
- StatusCode: () => StatusCode,
7109
- TABLE_NAMES: () => TABLE_NAMES,
7110
- VERSIONED_TABLE_NAMES: () => VERSIONED_TABLE_NAMES,
7111
- callbacks: () => callbacks,
7112
- chains: () => chains$1,
7113
- collectors: () => collectors,
7114
- consumedEvents: () => consumedEvents,
7115
- groups: () => groups,
7116
- lots: () => lots,
7117
- merklePaths: () => merklePaths,
7118
- obligationCollateralsV2: () => obligationCollateralsV2,
7119
- obligations: () => obligations,
7120
- offers: () => offers,
7121
- offersCallbacks: () => offersCallbacks,
7122
- offsets: () => offsets,
7123
- oracles: () => oracles,
7124
- positionTypes: () => positionTypes,
7125
- positions: () => positions,
7126
- status: () => status,
7127
- transfers: () => transfers,
7128
- trees: () => trees,
7129
- validations: () => validations
7130
- });
7131
- const s = pgSchema(VERSION);
7132
- var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
7133
- EnumTableName["OBLIGATIONS"] = "obligations";
7134
- EnumTableName["GROUPS"] = "groups";
7135
- EnumTableName["CONSUMED_EVENTS"] = "consumed_events";
7136
- EnumTableName["OBLIGATION_COLLATERALS_V2"] = "obligation_collaterals_v2";
7137
- EnumTableName["ORACLES"] = "oracles";
7138
- EnumTableName["OFFERS"] = "offers";
7139
- EnumTableName["OFFERS_CALLBACKS"] = "offers_callbacks";
7140
- EnumTableName["CALLBACKS"] = "callbacks";
7141
- EnumTableName["POSITIONS"] = "positions";
7142
- EnumTableName["TRANSFERS"] = "transfers";
7143
- EnumTableName["VALIDATIONS"] = "validations";
7144
- EnumTableName["COLLECTORS"] = "collectors";
7145
- EnumTableName["CHAINS"] = "chains";
7146
- EnumTableName["LOTS"] = "lots";
7147
- EnumTableName["OFFSETS"] = "offsets";
7148
- EnumTableName["TREES"] = "trees";
7149
- EnumTableName["MERKLE_PATHS"] = "merkle_paths";
7150
- return EnumTableName;
7151
- }(EnumTableName || {});
7152
- const TABLE_NAMES = Object.values(EnumTableName);
7153
- const VERSIONED_TABLE_NAMES = TABLE_NAMES.map((table) => `"${VERSION}"."${table}"`);
7154
- const obligations = s.table(EnumTableName.OBLIGATIONS, {
7155
- obligationId: varchar("obligation_id", { length: 66 }).primaryKey(),
7156
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7157
- loanToken: varchar("loan_token", { length: 42 }).notNull(),
7158
- maturity: integer("maturity").notNull()
7159
- });
7160
- const groups = s.table(EnumTableName.GROUPS, {
7161
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7162
- maker: varchar("maker", { length: 42 }).notNull(),
7163
- group: varchar("group", { length: 66 }).notNull(),
7164
- consumed: numeric("consumed", {
7165
- precision: 78,
7166
- scale: 0
7167
- }).notNull(),
7168
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7169
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7170
- }, (table) => [primaryKey({
7171
- columns: [
7172
- table.chainId,
7173
- table.maker,
7174
- table.group
7175
- ],
7176
- name: "groups_pk"
7177
- }), index("groups_chain_id_maker_group_consumed_idx").on(table.chainId, table.maker, table.group, table.consumed)]);
7178
- const consumedEvents = s.table(EnumTableName.CONSUMED_EVENTS, {
7179
- eventId: varchar("event_id", { length: 128 }).primaryKey(),
7180
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7181
- maker: varchar("maker", { length: 42 }).notNull(),
7182
- group: varchar("group", { length: 66 }).notNull(),
7183
- amount: numeric("amount", {
7184
- precision: 78,
7185
- scale: 0
7186
- }).notNull(),
7187
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7188
- createdAt: timestamp("created_at").defaultNow().notNull()
7189
- }, (t) => [
7190
- foreignKey({
7191
- columns: [
7192
- t.chainId,
7193
- t.maker,
7194
- t.group
7195
- ],
7196
- foreignColumns: [
7197
- groups.chainId,
7198
- groups.maker,
7199
- groups.group
7200
- ],
7201
- name: "consumed_events_groups_fk"
7202
- }).onDelete("cascade"),
7203
- index("consumed_events_group_idx").on(t.chainId, t.maker, t.group),
7204
- index("consumed_events_block_number_idx").on(t.blockNumber)
7205
- ]);
7206
- const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2, {
7207
- obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
7208
- asset: varchar("asset", { length: 42 }).notNull(),
7209
- oracleChainId: bigint("oracle_chain_id", { mode: "number" }).$type().notNull(),
7210
- oracleAddress: varchar("oracle_address", { length: 42 }).notNull(),
7211
- lltv: bigint("lltv", { mode: "bigint" }).notNull(),
7212
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7213
- }, (table) => [
7214
- primaryKey({
7215
- columns: [table.obligationId, table.asset],
7216
- name: "obligation_collaterals_v2_pk"
7217
- }),
7218
- foreignKey({
7219
- columns: [table.oracleChainId, table.oracleAddress],
7220
- foreignColumns: [oracles.chainId, oracles.address],
7221
- name: "obligation_collaterals_v2_oracles_fk"
7222
- }),
7223
- index("obligation_collaterals_v2_obligation_id_idx").on(table.obligationId),
7224
- index("obligation_collaterals_v2_oracle_fk_idx").on(table.oracleChainId, table.oracleAddress)
7225
- ]);
7226
- const oracles = s.table(EnumTableName.ORACLES, {
7227
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7228
- address: varchar("address", { length: 42 }).notNull(),
7229
- price: numeric("price", {
7230
- precision: 78,
7231
- scale: 0
7232
- }),
7233
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7234
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7235
- }, (table) => [primaryKey({
7236
- columns: [table.chainId, table.address],
7237
- name: "oracles_pk"
7238
- })]);
7239
- const offers = s.table(EnumTableName.OFFERS, {
7240
- hash: varchar("hash", { length: 66 }).primaryKey(),
7241
- obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
7242
- assets: numeric("assets", {
7243
- precision: 78,
7244
- scale: 0
7245
- }).notNull(),
7246
- obligationUnits: numeric("obligation_units", {
7247
- precision: 78,
7248
- scale: 0
7249
- }).notNull().default("0"),
7250
- obligationShares: numeric("obligation_shares", {
7251
- precision: 78,
7252
- scale: 0
7253
- }).notNull().default("0"),
7254
- price: numeric("price", {
7255
- precision: 78,
7256
- scale: 0
7257
- }).notNull(),
7258
- maturity: integer("maturity").notNull(),
7259
- expiry: integer("expiry").notNull(),
7260
- start: integer("start").notNull(),
7261
- groupChainId: bigint("group_chain_id", { mode: "number" }).$type().notNull(),
7262
- groupMaker: varchar("group_maker", { length: 42 }).notNull(),
7263
- group: varchar("group_group", { length: 66 }).notNull(),
7264
- session: varchar("session", { length: 66 }).notNull(),
7265
- buy: boolean("buy").notNull(),
7266
- callbackAddress: varchar("callback_address", { length: 42 }).notNull(),
7267
- callbackData: text("callback_data").notNull(),
7268
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7269
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7270
- }, (table) => [
7271
- foreignKey({
7272
- columns: [
7273
- table.groupChainId,
7274
- table.groupMaker,
7275
- table.group
7276
- ],
7277
- foreignColumns: [
7278
- groups.chainId,
7279
- groups.maker,
7280
- groups.group
7281
- ],
7282
- name: "offers_groups_fk"
7283
- }).onDelete("cascade"),
7284
- index("offers_group_fk_idx").on(table.groupChainId, table.groupMaker, table.group),
7285
- index("offers_group_and_hash_idx").on(table.groupChainId, table.groupMaker, table.group, table.hash),
7286
- index("offers_obligation_id_side_idx").on(table.obligationId, table.buy)
7287
- ]);
7288
- const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
7289
- offerHash: varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
7290
- callbackId: varchar("callback_id", { length: 66 })
7291
- }, (table) => [primaryKey({
7292
- columns: [table.offerHash, table.callbackId],
7293
- name: "offers_callbacks_pk"
7294
- })]);
7295
- const callbacks = s.table(EnumTableName.CALLBACKS, {
7296
- id: varchar("id", { length: 66 }).primaryKey(),
7297
- positionChainId: bigint("position_chain_id", { mode: "number" }).$type().notNull(),
7298
- positionContract: varchar("position_contract", { length: 42 }).notNull(),
7299
- positionUser: varchar("position_user", { length: 42 }).notNull(),
7300
- amount: numeric("amount", {
7301
- precision: 78,
7302
- scale: 0
7303
- })
7304
- }, (table) => [foreignKey({
7305
- columns: [
7306
- table.positionChainId,
7307
- table.positionContract,
7308
- table.positionUser
6889
+ function parseCursor$1(cursor) {
6890
+ const [chain, address] = cursor.split(":", 2);
6891
+ if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
6892
+ return {
6893
+ chain_id: Number.parseInt(chain, 10),
6894
+ address: address.toLowerCase()
6895
+ };
6896
+ }
6897
+ function formatCursor$1(contract) {
6898
+ return `${contract.chain_id}:${contract.address.toLowerCase()}`;
6899
+ }
6900
+ function findStartIndex$1(contracts, cursor) {
6901
+ let low = 0;
6902
+ let high = contracts.length;
6903
+ while (low < high) {
6904
+ const mid = Math.floor((low + high) / 2);
6905
+ const current = contracts[mid];
6906
+ if (compareContract(current, cursor) <= 0) low = mid + 1;
6907
+ else high = mid;
6908
+ }
6909
+ return low;
6910
+ }
6911
+ function compareContract(contract, cursor) {
6912
+ if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
6913
+ return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
6914
+ }
6915
+
6916
+ //#endregion
6917
+ //#region src/gatekeeper/GateConfig.ts
6918
+ const assets = {
6919
+ [ChainId.ETHEREUM.toString()]: [
6920
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6921
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6922
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6923
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6924
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
6925
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7309
6926
  ],
7310
- foreignColumns: [
7311
- positions.chainId,
7312
- positions.contract,
7313
- positions.user
6927
+ [ChainId.BASE.toString()]: [
6928
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
6929
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
6930
+ "0x4200000000000000000000000000000000000006",
6931
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6932
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
6933
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
6934
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
7314
6935
  ],
7315
- name: "callbacks_positions_fk"
7316
- }).onDelete("cascade")]);
7317
- const lots = s.table(EnumTableName.LOTS, {
7318
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7319
- user: varchar("user", { length: 42 }).notNull(),
7320
- contract: varchar("contract", { length: 42 }).notNull(),
7321
- group: varchar("group", { length: 66 }).notNull(),
7322
- lower: numeric("lower", {
7323
- precision: 78,
7324
- scale: 0
7325
- }).notNull(),
7326
- upper: numeric("upper", {
7327
- precision: 78,
7328
- scale: 0
7329
- }).notNull()
7330
- }, (table) => [
7331
- primaryKey({
7332
- columns: [
7333
- table.chainId,
7334
- table.user,
7335
- table.contract,
7336
- table.group
7337
- ],
7338
- name: "lots_pk"
7339
- }),
7340
- foreignKey({
7341
- columns: [
7342
- table.chainId,
7343
- table.contract,
7344
- table.user
7345
- ],
7346
- foreignColumns: [
7347
- positions.chainId,
7348
- positions.contract,
7349
- positions.user
7350
- ],
7351
- name: "lots_positions_fk"
7352
- }).onDelete("cascade"),
7353
- foreignKey({
7354
- columns: [
7355
- table.chainId,
7356
- table.user,
7357
- table.group
7358
- ],
7359
- foreignColumns: [
7360
- groups.chainId,
7361
- groups.maker,
7362
- groups.group
7363
- ],
7364
- name: "lots_groups_fk"
7365
- }).onDelete("cascade")
7366
- ]);
7367
- const offsets = s.table(EnumTableName.OFFSETS, {
7368
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7369
- user: varchar("user", { length: 42 }).notNull(),
7370
- contract: varchar("contract", { length: 42 }).notNull(),
7371
- group: varchar("group", { length: 66 }).notNull(),
7372
- value: numeric("value", {
7373
- precision: 78,
7374
- scale: 0
7375
- }).notNull()
7376
- }, (table) => [primaryKey({
7377
- columns: [
7378
- table.chainId,
7379
- table.user,
7380
- table.contract,
7381
- table.group
6936
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
6937
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6938
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6939
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6940
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6941
+ "0xce79ddb3152d52ff8fe65a4c7e058b035fcb560a"
7382
6942
  ],
7383
- name: "offsets_pk"
7384
- }), foreignKey({
7385
- columns: [
7386
- table.chainId,
7387
- table.contract,
7388
- table.user
6943
+ [ChainId.ANVIL.toString()]: [
6944
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6945
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6946
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6947
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6948
+ ]
6949
+ };
6950
+ const oracles = {
6951
+ [ChainId.ETHEREUM.toString()]: [
6952
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
6953
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
6954
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
6955
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
6956
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
6957
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
7389
6958
  ],
7390
- foreignColumns: [
7391
- positions.chainId,
7392
- positions.contract,
7393
- positions.user
6959
+ [ChainId.BASE.toString()]: [
6960
+ "0xD09048c8B568Dbf5f189302beA26c9edABFC4858",
6961
+ "0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4",
6962
+ "0x05D2618404668D725B66c0f32B39e4EC15B393dC",
6963
+ "0xE1bb8E5b4930eC9FeC7f7943FCF6227649F14B37",
6964
+ "0x663BECd10daE6C4A3Dcd89F1d76c1174199639B9",
6965
+ "0x10b95702a0ce895972C91e432C4f7E19811D320E",
6966
+ "0x8C87DbD7A0c647A4291592Bc2994dbF95880fE2F",
6967
+ "0x4A11590e5326138B514E08A9B52202D42077Ca65",
6968
+ "0xa54122f0E0766258377Ffe732e454A3248f454F4"
7394
6969
  ],
7395
- name: "offsets_positions_fk"
7396
- }).onDelete("cascade")]);
7397
- const PositionTypes = s.enum("position_type", Object.values(Type));
7398
- const positionTypes = s.table("position_types", {
7399
- id: serial("id").primaryKey(),
7400
- type: PositionTypes("type").notNull()
7401
- });
7402
- const positions = s.table(EnumTableName.POSITIONS, {
7403
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7404
- contract: varchar("contract", { length: 42 }).notNull(),
7405
- user: varchar("user", { length: 42 }).notNull(),
7406
- positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
7407
- balance: numeric("balance", {
7408
- precision: 78,
7409
- scale: 0
7410
- }),
7411
- asset: varchar("asset", { length: 42 }),
7412
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7413
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7414
- }, (table) => [primaryKey({
7415
- columns: [
7416
- table.chainId,
7417
- table.contract,
7418
- table.user
6970
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
6971
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
6972
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
6973
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
6974
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
6975
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
6976
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
7419
6977
  ],
7420
- name: "positions_pk"
7421
- })]);
7422
- const transfers = s.table(EnumTableName.TRANSFERS, {
7423
- eventId: varchar("event_id", { length: 128 }).primaryKey(),
7424
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7425
- contract: varchar("contract", { length: 42 }).notNull(),
7426
- from: varchar("from", { length: 42 }).notNull(),
7427
- to: varchar("to", { length: 42 }).notNull(),
7428
- value: numeric("value", {
7429
- precision: 78,
7430
- scale: 0
7431
- }).notNull(),
7432
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7433
- createdAt: timestamp("created_at").defaultNow().notNull()
7434
- }, (table) => [
7435
- foreignKey({
7436
- columns: [
7437
- table.chainId,
7438
- table.contract,
7439
- table.from
7440
- ],
7441
- foreignColumns: [
7442
- positions.chainId,
7443
- positions.contract,
7444
- positions.user
7445
- ],
7446
- name: "transfers_positions_from_fk"
7447
- }).onDelete("cascade"),
7448
- foreignKey({
7449
- columns: [
7450
- table.chainId,
7451
- table.contract,
7452
- table.to
7453
- ],
7454
- foreignColumns: [
7455
- positions.chainId,
7456
- positions.contract,
7457
- positions.user
7458
- ],
7459
- name: "transfers_positions_to_fk"
7460
- }).onDelete("cascade"),
7461
- index("transfers_chain_contract_user_idx").on(table.chainId, table.contract, table.from, table.to, table.blockNumber)
7462
- ]);
7463
- const StatusCode = s.enum("status_code", Object.values(Status));
7464
- const status = s.table("status", {
7465
- id: serial("id").primaryKey(),
7466
- code: StatusCode("code").unique()
7467
- });
7468
- const validations = s.table("validations", {
7469
- offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
7470
- statusId: integer("status_id").notNull().references(() => status.id, { onDelete: "no action" }),
7471
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7472
- });
7473
- const collectors = s.table(EnumTableName.COLLECTORS, {
7474
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull().references(() => chains$1.chainId, { onDelete: "no action" }),
7475
- name: text("name").$type().notNull(),
7476
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7477
- epoch: numeric("epoch", {
7478
- precision: 78,
7479
- scale: 0
7480
- }).default("0").notNull(),
7481
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7482
- }, (table) => [uniqueIndex("collectors_chain_name_idx").on(table.chainId, table.name)]);
7483
- const chains$1 = s.table(EnumTableName.CHAINS, {
7484
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
7485
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
7486
- epoch: numeric("epoch", {
7487
- precision: 78,
7488
- scale: 0
7489
- }).default("0").notNull(),
7490
- updatedAt: timestamp("updated_at").defaultNow().notNull()
7491
- }, (table) => [uniqueIndex("chains_chain_id_idx").on(table.chainId)]);
7492
- const trees = s.table(EnumTableName.TREES, {
7493
- root: varchar("root", { length: 66 }).primaryKey(),
7494
- rootSignature: varchar("root_signature", { length: 132 }).notNull(),
7495
- createdAt: timestamp("created_at").defaultNow().notNull()
7496
- });
7497
- const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
7498
- offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
7499
- treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
7500
- proofNodes: text("proof_nodes").notNull(),
7501
- createdAt: timestamp("created_at").defaultNow().notNull()
7502
- }, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
6978
+ [ChainId.ANVIL.toString()]: [
6979
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
6980
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
6981
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
6982
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
6983
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
6984
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
6985
+ ]
6986
+ };
6987
+ const configs = {
6988
+ ethereum: {
6989
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
6990
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6991
+ },
6992
+ base: {
6993
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
6994
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6995
+ },
6996
+ "ethereum-virtual-testnet": {
6997
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
6998
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6999
+ },
7000
+ anvil: {
7001
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7002
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
7003
+ }
7004
+ };
7005
+
7006
+ //#endregion
7007
+ //#region src/gatekeeper/ConfigRules.ts
7008
+ /**
7009
+ * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
7010
+ * @param chains - Chains to include in the configured rules.
7011
+ * @returns Sorted list of config rules.
7012
+ */
7013
+ function buildConfigRules(chains) {
7014
+ const rules = [];
7015
+ for (const chain of chains) {
7016
+ const maturities = configs[chain.name].maturities ?? [];
7017
+ for (const maturityName of maturities) rules.push({
7018
+ type: "maturity",
7019
+ chain_id: chain.id,
7020
+ name: maturityName,
7021
+ timestamp: from$16(maturityName)
7022
+ });
7023
+ const loanTokens = assets[chain.id.toString()] ?? [];
7024
+ for (const address of loanTokens) rules.push({
7025
+ type: "loan_token",
7026
+ chain_id: chain.id,
7027
+ address: normalizeAddress(address)
7028
+ });
7029
+ const oracles$2 = oracles[chain.id.toString()] ?? [];
7030
+ for (const address of oracles$2) rules.push({
7031
+ type: "oracle",
7032
+ chain_id: chain.id,
7033
+ address: normalizeAddress(address)
7034
+ });
7035
+ }
7036
+ rules.sort(compareConfigRules);
7037
+ return rules;
7038
+ }
7039
+ /**
7040
+ * Compute a stable checksum for the provided configured rules.
7041
+ * @param rules - Configured rules to checksum.
7042
+ * @returns MD5 checksum.
7043
+ */
7044
+ function buildConfigRulesChecksum(rules) {
7045
+ const hash = createHash("md5");
7046
+ const orderedRules = [...rules].sort(compareConfigRules);
7047
+ for (const rule of orderedRules) {
7048
+ if (rule.type === "maturity") {
7049
+ hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
7050
+ continue;
7051
+ }
7052
+ if (rule.type === "callback") {
7053
+ hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
7054
+ continue;
7055
+ }
7056
+ if (rule.type === "oracle") {
7057
+ hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
7058
+ continue;
7059
+ }
7060
+ hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
7061
+ }
7062
+ return hash.digest("hex");
7063
+ }
7064
+ function normalizeAddress(address) {
7065
+ return address.toLowerCase();
7066
+ }
7067
+ function compareConfigRules(left, right) {
7068
+ if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
7069
+ if (left.type !== right.type) return left.type.localeCompare(right.type);
7070
+ if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
7071
+ if (left.type === "callback" && right.type === "callback") {
7072
+ if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
7073
+ return left.address.localeCompare(right.address);
7074
+ }
7075
+ if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
7076
+ if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
7077
+ return 0;
7078
+ }
7079
+
7080
+ //#endregion
7081
+ //#region src/api/Controllers/getConfigRules.ts
7082
+ /**
7083
+ * Returns configured rules for the configured chains.
7084
+ * @param query - Raw query parameters containing filters/cursor/limit.
7085
+ * @param chains - Chains to include in the configured rules.
7086
+ * @returns Config rules response payload. {@link ApiPayload.Payload}
7087
+ */
7088
+ async function getConfigRules(query, chains) {
7089
+ const parsed = safeParse("get_config_rules", query ?? {});
7090
+ if (!parsed.success) return failure(parsed.error);
7091
+ const { cursor, limit, types, chains: chainIds } = parsed.data;
7092
+ const typeFilter = types?.length ? new Set(types) : null;
7093
+ const chainFilter = chainIds?.length ? new Set(chainIds) : null;
7094
+ const filteredRules = buildConfigRules(chains).filter((rule) => {
7095
+ if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
7096
+ if (typeFilter && !typeFilter.has(rule.type)) return false;
7097
+ return true;
7098
+ });
7099
+ const checksum = buildConfigRulesChecksum(filteredRules);
7100
+ let cursorRule = null;
7101
+ if (cursor) try {
7102
+ cursorRule = parseCursor(cursor);
7103
+ } catch (err) {
7104
+ return failure(err);
7105
+ }
7106
+ if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
7107
+ if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
7108
+ const startIndex = cursorRule ? findStartIndex(filteredRules, cursorRule) : 0;
7109
+ const page = filteredRules.slice(startIndex, startIndex + limit);
7110
+ const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
7111
+ const response = success({
7112
+ data: page,
7113
+ cursor: nextCursor
7114
+ });
7115
+ response.body.meta.checksum = checksum;
7116
+ return response;
7117
+ }
7118
+ function formatCursor(rule) {
7119
+ if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
7120
+ if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
7121
+ if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
7122
+ return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
7123
+ }
7124
+ function parseCursor(cursor) {
7125
+ const [type, chain, ...rest] = cursor.split(":");
7126
+ if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
7127
+ if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
7128
+ const chain_id = Number.parseInt(chain, 10);
7129
+ if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
7130
+ if (type === "maturity") {
7131
+ const timestampValue = Number.parseInt(rest[0] ?? "", 10);
7132
+ const nameValue = rest.slice(1).join(":");
7133
+ if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
7134
+ if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
7135
+ return {
7136
+ type,
7137
+ chain_id,
7138
+ timestamp: parseMaturity(timestampValue),
7139
+ name: nameValue
7140
+ };
7141
+ }
7142
+ if (type === "callback") {
7143
+ const callbackTypeValue = rest[0] ?? "";
7144
+ const addressValue = rest.slice(1).join(":");
7145
+ if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
7146
+ if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
7147
+ return {
7148
+ type,
7149
+ chain_id,
7150
+ callback_type: callbackTypeValue,
7151
+ address: parseAddress(addressValue, "Cursor address")
7152
+ };
7153
+ }
7154
+ if (type === "loan_token" || type === "oracle") {
7155
+ const addressValue = rest.join(":");
7156
+ if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
7157
+ return {
7158
+ type,
7159
+ chain_id,
7160
+ address: parseAddress(addressValue, "Cursor address")
7161
+ };
7162
+ }
7163
+ throw new BadRequestError("Cursor has an invalid rule type");
7164
+ }
7165
+ function findStartIndex(rules, cursor) {
7166
+ let low = 0;
7167
+ let high = rules.length;
7168
+ while (low < high) {
7169
+ const mid = Math.floor((low + high) / 2);
7170
+ const current = rules[mid];
7171
+ if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
7172
+ else high = mid;
7173
+ }
7174
+ return low;
7175
+ }
7176
+ function parseAddress(address, label) {
7177
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
7178
+ return address.toLowerCase();
7179
+ }
7180
+ function isConfigRuleType(value) {
7181
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
7182
+ }
7183
+ function isMaturityType(value) {
7184
+ return Object.values(MaturityType).includes(value);
7185
+ }
7186
+ function parseMaturity(value) {
7187
+ try {
7188
+ return from$16(value);
7189
+ } catch (err) {
7190
+ throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
7191
+ }
7192
+ }
7193
+ function isCallbackType(value) {
7194
+ if (value === Type$1.BuyWithEmptyCallback) return false;
7195
+ return Object.values(Type$1).includes(value);
7196
+ }
7197
+
7198
+ //#endregion
7199
+ //#region src/api/Controllers/getDocs.ts
7200
+ const __dirname = (() => {
7201
+ try {
7202
+ return dirname(fileURLToPath(import.meta.url));
7203
+ } catch {
7204
+ return process.cwd();
7205
+ }
7206
+ })();
7207
+ /**
7208
+ * Build the OpenAPI document for the router.
7209
+ * @returns OpenAPI document. {@link OpenAPIDocument}
7210
+ */
7211
+ async function getSwaggerJson() {
7212
+ return OpenApi();
7213
+ }
7214
+ /**
7215
+ * Render the API documentation HTML page.
7216
+ * @returns HTML page as string.
7217
+ */
7218
+ async function getDocsHtml() {
7219
+ const spec = await OpenApi();
7220
+ return `<!DOCTYPE html>
7221
+ <html>
7222
+ <head>
7223
+ <meta charset="UTF-8">
7224
+ <title>Router API Docs (Scalar)</title>
7225
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
7226
+ <style>
7227
+ html, body { margin: 0; height: 100%; }
7228
+ api-reference { height: 100%; width: 100%; }
7229
+ </style>
7230
+ </head>
7231
+ <body>
7232
+ <div id="api-container" style="height:100%;width:100%;"></div>
7233
+ <script>
7234
+ window.addEventListener('load', function () {
7235
+ const spec = ${JSON.stringify(spec)};
7236
+ Scalar.createApiReference('#api-container', { spec: { content: spec, hideModels: true } });
7237
+ });
7238
+ <\/script>
7239
+ </body>
7240
+ </html>`;
7241
+ }
7242
+ /**
7243
+ * Finds the integrator.md file.
7244
+ * Handles source, bundled CLI, and Lambda scenarios.
7245
+ */
7246
+ function findIntegratorMd() {
7247
+ const candidates = [
7248
+ resolve(__dirname, "../../../docs/integrator.md"),
7249
+ resolve(__dirname, "../docs/integrator.md"),
7250
+ resolve(process.cwd(), "docs/integrator.md")
7251
+ ];
7252
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
7253
+ throw new Error(`integrator.md not found. Tried: ${candidates.join(", ")}`);
7254
+ }
7255
+ /**
7256
+ * Renders the integrator documentation as HTML.
7257
+ * @returns HTML page with the rendered markdown documentation.
7258
+ */
7259
+ async function getIntegratorDocsHtml() {
7260
+ return `<!DOCTYPE html>
7261
+ <html>
7262
+ <head>
7263
+ <meta charset="UTF-8">
7264
+ <title>Documentation</title>
7265
+ <style>
7266
+ body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
7267
+ pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; }
7268
+ code { background: #f4f4f4; padding: 0.2rem 0.4rem; }
7269
+ a { color: #0066cc; }
7270
+ </style>
7271
+ </head>
7272
+ <body>
7273
+ <nav><a href="/docs/api">API Reference &rarr;</a></nav>
7274
+ ${await marked(await readFile(findIntegratorMd(), "utf-8"))}
7275
+ </body>
7276
+ </html>`;
7277
+ }
7278
+
7279
+ //#endregion
7280
+ //#region src/api/Controllers/getHealth.ts
7281
+ async function getHealth(query, db, chainRegistry) {
7282
+ const logger = getLogger();
7283
+ try {
7284
+ const parsed = safeParse("get_health", query);
7285
+ if (!parsed.success) return failure(parsed.error);
7286
+ const snapshot = await create$16({
7287
+ db,
7288
+ chainRegistry
7289
+ }).getSnapshot();
7290
+ if (parsed.data.strict && !snapshot.initialized) return failure(new APIError(STATUS_CODE.INTERNAL_SERVER_ERROR, "Indexer block state is not initialized", "INTERNAL_SERVER_ERROR", toSnakeCase$1({
7291
+ missingChains: snapshot.missingChains,
7292
+ missingCollectors: snapshot.missingCollectors
7293
+ })));
7294
+ return success({ data: toSnakeCase$1({
7295
+ status: snapshot.status,
7296
+ initialized: snapshot.initialized,
7297
+ missingChains: snapshot.missingChains,
7298
+ missingCollectors: snapshot.missingCollectors
7299
+ }) });
7300
+ } catch (err) {
7301
+ logger.error({
7302
+ err,
7303
+ msg: "Error getting health status",
7304
+ errorMessage: err instanceof Error ? err.message : String(err),
7305
+ errorStack: err instanceof Error ? err.stack : void 0
7306
+ });
7307
+ return failure(err);
7308
+ }
7309
+ }
7310
+ async function getHealthChains(query, db, healthClients, chainRegistry) {
7311
+ const logger = getLogger();
7312
+ try {
7313
+ const parsed = safeParse("get_health_chains", query);
7314
+ if (!parsed.success) return failure(parsed.error);
7315
+ const snapshot = await create$16({
7316
+ db,
7317
+ healthClients,
7318
+ chainRegistry
7319
+ }).getSnapshot();
7320
+ if (parsed.data.strict && !snapshot.initialized) return failure(new APIError(STATUS_CODE.INTERNAL_SERVER_ERROR, "Indexer block state is not initialized", "INTERNAL_SERVER_ERROR", toSnakeCase$1({
7321
+ missingChains: snapshot.missingChains,
7322
+ missingCollectors: snapshot.missingCollectors
7323
+ })));
7324
+ const chains = snapshot.chains;
7325
+ return success({ data: chains.map(({ chainId, localBlockNumber, remoteBlockNumber, updatedAt, initialized }) => toSnakeCase$1({
7326
+ chainId,
7327
+ localBlockNumber,
7328
+ remoteBlockNumber,
7329
+ updatedAt,
7330
+ initialized
7331
+ })) });
7332
+ } catch (err) {
7333
+ logger.error({
7334
+ err,
7335
+ msg: "Error getting health status for chains",
7336
+ errorMessage: err instanceof Error ? err.message : String(err),
7337
+ errorStack: err instanceof Error ? err.stack : void 0
7338
+ });
7339
+ return failure(err);
7340
+ }
7341
+ }
7342
+ async function getHealthCollectors(query, db, chainRegistry) {
7343
+ const logger = getLogger();
7344
+ try {
7345
+ const parsed = safeParse("get_health_collectors", query);
7346
+ if (!parsed.success) return failure(parsed.error);
7347
+ const snapshot = await create$16({
7348
+ db,
7349
+ chainRegistry
7350
+ }).getSnapshot();
7351
+ if (parsed.data.strict && !snapshot.initialized) return failure(new APIError(STATUS_CODE.INTERNAL_SERVER_ERROR, "Indexer block state is not initialized", "INTERNAL_SERVER_ERROR", toSnakeCase$1({
7352
+ missingChains: snapshot.missingChains,
7353
+ missingCollectors: snapshot.missingCollectors
7354
+ })));
7355
+ const collectors = snapshot.collectors;
7356
+ return success({ data: collectors.map(({ name, chainId, blockNumber, updatedAt, lag, status, initialized }) => toSnakeCase$1({
7357
+ name,
7358
+ chainId,
7359
+ blockNumber,
7360
+ updatedAt,
7361
+ lag,
7362
+ status,
7363
+ initialized
7364
+ })) });
7365
+ } catch (err) {
7366
+ logger.error({
7367
+ err,
7368
+ msg: "Error getting health status for collectors",
7369
+ errorMessage: err instanceof Error ? err.message : String(err),
7370
+ errorStack: err instanceof Error ? err.stack : void 0
7371
+ });
7372
+ return failure(err);
7373
+ }
7374
+ }
7375
+
7376
+ //#endregion
7377
+ //#region src/api/Controllers/getObligation.ts
7378
+ async function getObligation(params, db) {
7379
+ const logger = getLogger();
7380
+ const result = safeParse("get_obligation", params, (issue) => issue.message);
7381
+ if (!result.success) return failure(result.error);
7382
+ const query = result.data;
7383
+ try {
7384
+ const { obligations } = await db.offers.getObligations({ ids: [query.obligation_id] });
7385
+ if (obligations.length === 0) return failure(new NotFoundError("Obligation not found"));
7386
+ const obligation = obligations[0];
7387
+ const [quote] = await db.offers.getQuotes({ obligationIds: [id(obligation)] });
7388
+ return success({
7389
+ data: from$4(obligation, quote ?? {
7390
+ obligationId: id(obligation),
7391
+ ask: { price: 0n },
7392
+ bid: { price: 0n }
7393
+ }),
7394
+ cursor: null
7395
+ });
7396
+ } catch (err) {
7397
+ logger.error({
7398
+ err,
7399
+ msg: "Error get obligation",
7400
+ errorMessage: err instanceof Error ? err.message : String(err),
7401
+ errorStack: err instanceof Error ? err.stack : void 0
7402
+ });
7403
+ return failure(err);
7404
+ }
7405
+ }
7406
+
7407
+ //#endregion
7408
+ //#region src/api/Controllers/getObligations.ts
7409
+ async function getObligations$1(queryParameters, db) {
7410
+ const logger = getLogger();
7411
+ const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
7412
+ if (!result.success) return failure(result.error);
7413
+ const query = result.data;
7414
+ try {
7415
+ const chainIds = query.chains?.length ? query.chains : void 0;
7416
+ const loanTokens = query.loan_tokens?.length ? query.loan_tokens : void 0;
7417
+ const collateralTokens = query.collateral_tokens?.length ? query.collateral_tokens : void 0;
7418
+ const maturities = query.maturities?.length ? query.maturities : void 0;
7419
+ const { obligations, nextCursor } = await db.offers.getObligations({
7420
+ cursor: query.cursor,
7421
+ limit: query.limit,
7422
+ chainId: chainIds,
7423
+ loanToken: loanTokens,
7424
+ collateralToken: collateralTokens,
7425
+ maturity: maturities
7426
+ });
7427
+ const quotes = await db.offers.getQuotes({ obligationIds: obligations.map((o) => id(o)) });
7428
+ return success({
7429
+ data: obligations.map((o) => from$4(o, quotes.find((q) => q.obligationId === id(o)) ?? {
7430
+ obligationId: id(o),
7431
+ ask: { price: 0n },
7432
+ bid: { price: 0n }
7433
+ })),
7434
+ cursor: nextCursor ?? null
7435
+ });
7436
+ } catch (err) {
7437
+ logger.error({
7438
+ err,
7439
+ msg: "Error get obligations",
7440
+ errorMessage: err instanceof Error ? err.message : String(err),
7441
+ errorStack: err instanceof Error ? err.stack : void 0
7442
+ });
7443
+ return failure(err);
7444
+ }
7445
+ }
7446
+
7447
+ //#endregion
7448
+ //#region src/database/constants.ts
7449
+ /**
7450
+ * Default batch size for bulk database inserts.
7451
+ *
7452
+ * PostgreSQL limits a single query to at most 65,535 parameters
7453
+ * (e.g. $1, $2, ...). In bulk inserts, each row consumes one
7454
+ * parameter per column, so inserting too many rows at once can
7455
+ * exceed this limit.
7456
+ *
7457
+ * Our largest batched insert is into the `offers` table with 15 columns.
7458
+ * 15 cols × 4,000 rows = 60,000 parameters, safely under 65,535.
7459
+ */
7460
+ const DEFAULT_BATCH_SIZE$1 = 4e3;
7503
7461
 
7504
7462
  //#endregion
7505
7463
  //#region src/database/domains/Offers.ts
@@ -7554,13 +7512,13 @@ function create$15(config) {
7554
7512
  jsonb_agg(
7555
7513
  jsonb_build_object(
7556
7514
  'asset', ${obligationCollateralsV2.asset},
7557
- 'oracle', ${oracles.address},
7515
+ 'oracle', ${oracles$1.address},
7558
7516
  'lltv', ${obligationCollateralsV2.lltv}
7559
7517
  )
7560
7518
  ),
7561
7519
  '[]'::jsonb
7562
- )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7563
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7520
+ )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles$1, sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7521
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7564
7522
  const rows = (await db.select({
7565
7523
  hash: offers.hash,
7566
7524
  maker: offers.groupMaker,
@@ -7642,10 +7600,10 @@ function create$15(config) {
7642
7600
  obligationId: obligations.obligationId,
7643
7601
  chainId: obligations.chainId,
7644
7602
  loanToken: obligations.loanToken,
7645
- collaterals: sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${oracles.address}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
7603
+ collaterals: sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${oracles$1.address}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
7646
7604
  maturity: obligations.maturity
7647
- }).from(obligations).innerJoin(obligationCollateralsV2, eq(obligations.obligationId, obligationCollateralsV2.obligationId)).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7648
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).groupBy(obligations.obligationId).where(and(cursor !== null && cursor !== void 0 ? gt(obligations.obligationId, cursor) : sql`true`, ids !== void 0 && ids.length > 0 ? inArray(obligations.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? inArray(obligations.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? inArray(obligations.maturity, maturities) : gte(obligations.maturity, now$1), collateralFilter)).orderBy(asc(obligations.obligationId)).limit(limit);
7605
+ }).from(obligations).innerJoin(obligationCollateralsV2, eq(obligations.obligationId, obligationCollateralsV2.obligationId)).innerJoin(oracles$1, sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7606
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).groupBy(obligations.obligationId).where(and(cursor !== null && cursor !== void 0 ? gt(obligations.obligationId, cursor) : sql`true`, ids !== void 0 && ids.length > 0 ? inArray(obligations.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? inArray(obligations.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? inArray(obligations.maturity, maturities) : gte(obligations.maturity, now$1), collateralFilter)).orderBy(asc(obligations.obligationId)).limit(limit);
7649
7607
  const items = [];
7650
7608
  for (const row of result) items.push(from$15({
7651
7609
  chainId: row.chainId,
@@ -7716,40 +7674,28 @@ async function getOffersQuery(db, parameters) {
7716
7674
  if (cursor !== null && cursor !== void 0) {
7717
7675
  if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
7718
7676
  }
7677
+ const now = Math.floor((Date.now() - 1) / 1e3);
7719
7678
  const collateralsLateral = db.select({ collaterals: sql`COALESCE(
7720
7679
  jsonb_agg(
7721
7680
  jsonb_build_object(
7722
7681
  'asset', ${obligationCollateralsV2.asset},
7723
- 'oracle', ${oracles.address},
7682
+ 'oracle', ${oracles$1.address},
7724
7683
  'lltv', ${obligationCollateralsV2.lltv}
7725
7684
  )
7726
7685
  ),
7727
7686
  '[]'::jsonb
7728
- )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7729
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7687
+ )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles$1, sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7688
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7730
7689
  const availableLateral = db.select({ available: sql`COALESCE(SUM(
7731
7690
  CASE
7732
- -- If asset is null, position available is 0
7733
7691
  WHEN ${positions.asset} IS NULL THEN 0
7734
-
7735
- -- Position asset matches loan token: no conversion needed
7736
- WHEN ${positions.asset} = ${obligations.loanToken} THEN
7692
+ ELSE
7737
7693
  CASE
7738
7694
  WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7739
7695
  ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7740
7696
  END
7741
-
7742
- -- Position asset is collateral: apply oracle price * lltv
7743
- -- Formula: balance * price / 1e36 * lltv / 1e18
7744
- ELSE
7745
- (CASE
7746
- WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7747
- ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7748
- END)
7749
- * COALESCE(${oracles.price}, 0)::numeric / 1e36
7750
- * COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
7751
7697
  END
7752
- ), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, eq(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, and(eq(callbacks.positionChainId, positions.chainId), eq(callbacks.positionContract, positions.contract), eq(callbacks.positionUser, positions.user))).leftJoin(obligationCollateralsV2, and(eq(obligationCollateralsV2.obligationId, offers.obligationId), eq(obligationCollateralsV2.asset, positions.asset))).leftJoin(oracles, and(eq(oracles.chainId, obligationCollateralsV2.oracleChainId), eq(oracles.address, obligationCollateralsV2.oracleAddress))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
7698
+ ), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, eq(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, and(eq(callbacks.positionChainId, positions.chainId), eq(callbacks.positionContract, positions.contract), eq(callbacks.positionUser, positions.user))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
7753
7699
  const rows = (await db.select({
7754
7700
  hash: offers.hash,
7755
7701
  maker: offers.groupMaker,
@@ -7771,17 +7717,24 @@ async function getOffersQuery(db, parameters) {
7771
7717
  collaterals: collateralsLateral.collaterals,
7772
7718
  blockNumber: offers.blockNumber,
7773
7719
  available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
7774
- takeable: sql`FLOOR(GREATEST(
7775
- 0,
7776
- LEAST(
7777
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
7778
- COALESCE(${availableLateral.available}::numeric, 0)
7779
- )
7720
+ takeable: sql`FLOOR(GREATEST(0,
7721
+ CASE WHEN ${offers.buy} = false
7722
+ THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
7723
+ ELSE LEAST(
7724
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
7725
+ COALESCE(${availableLateral.available}::numeric, 0)
7726
+ )
7727
+ END
7780
7728
  ))`.as("takeable")
7781
- }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).leftJoinLateral(availableLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, maker === void 0 ? sql`GREATEST(0, LEAST(
7782
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
7783
- COALESCE(${availableLateral.available}::numeric, 0)
7784
- )) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
7729
+ }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).leftJoinLateral(availableLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, gte(offers.expiry, now), gte(offers.maturity, now), maker === void 0 ? sql`GREATEST(0,
7730
+ CASE WHEN ${offers.buy} = false
7731
+ THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
7732
+ ELSE LEAST(
7733
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
7734
+ COALESCE(${availableLateral.available}::numeric, 0)
7735
+ )
7736
+ END
7737
+ ) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
7785
7738
  return {
7786
7739
  hash: row.hash,
7787
7740
  maker: row.maker,
@@ -7891,64 +7844,6 @@ async function getUserPositions(queryParameters, db) {
7891
7844
  }
7892
7845
  }
7893
7846
 
7894
- //#endregion
7895
- //#region src/gatekeeper/CallbackTypes.ts
7896
- /**
7897
- * Resolve callback types for a list of callback addresses grouped by chain.
7898
- * @param parameters - Resolve parameters. {@link resolveCallbackTypes.Parameters}
7899
- * @returns Callback types grouped by chain. {@link resolveCallbackTypes.ReturnType}
7900
- * @throws If a chain id is unknown.
7901
- */
7902
- function resolveCallbackTypes$1(parameters) {
7903
- const { chains, request } = parameters;
7904
- const chainsById = new Map(chains.map((chain) => [chain.id, chain]));
7905
- return request.callbacks.map(({ chain_id, addresses }) => {
7906
- const chain = chainsById.get(chain_id);
7907
- if (!chain) throw new Error(`Unknown chain id ${chain_id}`);
7908
- const buckets = /* @__PURE__ */ new Map();
7909
- const uniqueAddresses = new Set(addresses.map((address) => address.toLowerCase()));
7910
- for (const address of uniqueAddresses) {
7911
- const bucketKey = getCallbackType(chain.name, address) ?? "not_supported";
7912
- const list = buckets.get(bucketKey) ?? [];
7913
- list.push(address);
7914
- buckets.set(bucketKey, list);
7915
- }
7916
- const response = { chain_id };
7917
- for (const [type, list] of buckets.entries()) response[type] = list;
7918
- if (!response.not_supported) response.not_supported = [];
7919
- return response;
7920
- });
7921
- }
7922
-
7923
- //#endregion
7924
- //#region src/api/Controllers/resolveCallbackTypes.ts
7925
- /**
7926
- * Resolve callback types for a list of callback addresses grouped by chain.
7927
- * @param body - Request body with callback addresses. {@link CallbackTypesRequest}
7928
- * @param chains - Chains to resolve callback types against. {@link Chain.Chain}
7929
- * @returns Callback types grouped by chain. {@link CallbackTypesPayload}
7930
- */
7931
- async function resolveCallbackTypes(body, chains) {
7932
- const result = safeParse("callback_types", body, (issue) => issue.message);
7933
- if (!result.success) return failure(result.error);
7934
- const request = result.data;
7935
- const chainIds = new Set(chains.map((chain) => chain.id));
7936
- const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
7937
- if (unknown) return failure(new BadRequestError(`Unknown chain id ${unknown.chain_id}`));
7938
- try {
7939
- const data = resolveCallbackTypes$1({
7940
- chains,
7941
- request
7942
- });
7943
- return success({
7944
- data,
7945
- cursor: null
7946
- });
7947
- } catch (err) {
7948
- return failure(err);
7949
- }
7950
- }
7951
-
7952
7847
  //#endregion
7953
7848
  //#region src/api/Controllers/validateOffers.ts
7954
7849
  async function validateOffers(body, gatekeeper) {
@@ -8028,7 +7923,6 @@ var Controllers_exports = /* @__PURE__ */ __exportAll({
8028
7923
  getOffersQuery: () => getOffersQuery,
8029
7924
  getSwaggerJson: () => getSwaggerJson,
8030
7925
  getUserPositions: () => getUserPositions,
8031
- resolveCallbackTypes: () => resolveCallbackTypes,
8032
7926
  validateOffers: () => validateOffers
8033
7927
  });
8034
7928
 
@@ -8101,24 +7995,9 @@ function serve$1(parameters) {
8101
7995
  const { statusCode, body } = await gatekeeper.validate(reqBody);
8102
7996
  return c.json(body, statusCode);
8103
7997
  } catch (err) {
8104
- const failure$1 = failure(err);
8105
- return c.json(failure$1.body, failure$1.statusCode);
8106
- }
8107
- });
8108
- app.post("/v1/callbacks", async (c) => {
8109
- let body;
8110
- try {
8111
- body = await c.req.json();
8112
- } catch (err) {
8113
- const failure$3 = failure(err);
8114
- return c.json(failure$3.body, failure$3.statusCode);
8115
- }
8116
- if (body === null || typeof body !== "object") {
8117
- const failure$2 = failure(new BadRequestError("Request body must be a JSON object"));
7998
+ const failure$2 = failure(err);
8118
7999
  return c.json(failure$2.body, failure$2.statusCode);
8119
8000
  }
8120
- const { statusCode, body: responseBody } = await resolveCallbackTypes(body, chainRegistry.list());
8121
- return c.json(responseBody, statusCode);
8122
8001
  });
8123
8002
  app.get("/v1/users/:userAddress/positions", async (c) => {
8124
8003
  const query = c.req.query();
@@ -8150,8 +8029,8 @@ function serve$1(parameters) {
8150
8029
  const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
8151
8030
  return c.json(body, statusCode);
8152
8031
  } catch (err) {
8153
- const failure$4 = failure(err);
8154
- return c.json(failure$4.body, failure$4.statusCode);
8032
+ const failure$1 = failure(err);
8033
+ return c.json(failure$1.body, failure$1.statusCode);
8155
8034
  }
8156
8035
  });
8157
8036
  app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
@@ -8168,7 +8047,6 @@ function serve$1(parameters) {
8168
8047
  var RouterApi_exports = /* @__PURE__ */ __exportAll({
8169
8048
  BookResponse: () => BookResponse_exports,
8170
8049
  BooksController: () => BooksController,
8171
- CallbacksController: () => CallbacksController,
8172
8050
  ChainHealth: () => ChainHealth,
8173
8051
  ChainsHealthResponse: () => ChainsHealthResponse,
8174
8052
  CollectorHealth: () => CollectorHealth,
@@ -8390,7 +8268,7 @@ var drizzle_exports = /* @__PURE__ */ __exportAll({
8390
8268
  offers: () => offers,
8391
8269
  offersCallbacks: () => offersCallbacks,
8392
8270
  offsets: () => offsets,
8393
- oracles: () => oracles,
8271
+ oracles: () => oracles$1,
8394
8272
  positionTypes: () => positionTypes,
8395
8273
  positions: () => positions,
8396
8274
  status: () => status,
@@ -8675,7 +8553,7 @@ async function _getOffers(db, params) {
8675
8553
  'lltv', oc.lltv
8676
8554
  ) ORDER BY oc.asset), '[]'::jsonb) AS collaterals
8677
8555
  FROM ${obligationCollateralsV2} oc
8678
- JOIN ${oracles} oracle
8556
+ JOIN ${oracles$1} oracle
8679
8557
  ON oracle.chain_id = oc.oracle_chain_id
8680
8558
  AND oracle.address = oc.oracle_address
8681
8559
  WHERE oc.obligation_id = ${obligationId}
@@ -8830,35 +8708,15 @@ async function _getOffers(db, params) {
8830
8708
  AND LOWER(pc.contract) = LOWER(c.position_contract)
8831
8709
  AND LOWER(pc."user") = LOWER(c.position_user)
8832
8710
  ),
8833
- -- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
8711
+ -- Compute contribution per callback in loan terms (loan token only collateral positions are not indexed)
8834
8712
  callback_loan_contribution AS (
8835
8713
  SELECT
8836
8714
  cc.*,
8837
8715
  CASE
8838
- -- No lot exists: contribution is 0
8839
8716
  WHEN cc.lot_lower IS NULL THEN 0
8840
- -- Loan token position: use lot_balance directly, apply callback limit
8841
- WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
8842
- LEAST(
8843
- cc.lot_balance,
8844
- COALESCE(cc.callback_amount::numeric, cc.lot_balance)
8845
- )
8846
- -- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
8847
- ELSE
8848
- (
8849
- LEAST(
8850
- cc.lot_balance,
8851
- COALESCE(cc.callback_amount::numeric, cc.lot_balance)
8852
- ) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
8853
- ) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
8717
+ ELSE LEAST(cc.lot_balance, COALESCE(cc.callback_amount::numeric, cc.lot_balance))
8854
8718
  END AS contribution_in_loan
8855
8719
  FROM callback_contributions cc
8856
- LEFT JOIN ${obligationCollateralsV2} collat_info
8857
- ON collat_info.obligation_id = cc.obligation_id
8858
- AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
8859
- LEFT JOIN ${oracles} collat_oracle
8860
- ON collat_oracle.chain_id = collat_info.oracle_chain_id
8861
- AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
8862
8720
  ),
8863
8721
  -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
8864
8722
  offer_contributions AS (
@@ -8895,6 +8753,22 @@ async function _getOffers(db, params) {
8895
8753
  GROUP BY hash, obligation_id, assets, price, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
8896
8754
  callback_address, callback_data, block_number, group_chain_id, group_maker,
8897
8755
  consumed, chain_id, loan_token, session
8756
+ UNION ALL
8757
+ -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
8758
+ SELECT
8759
+ p.hash, p.obligation_id, p.assets, p.price,
8760
+ p.obligation_units, p.obligation_shares,
8761
+ p.maturity, p.expiry, p.start, p.group_group,
8762
+ p.buy, p.callback_address, p.callback_data,
8763
+ p.block_number, p.group_chain_id, p.group_maker,
8764
+ p.consumed, p.chain_id, p.loan_token, p.session,
8765
+ 0 AS total_available
8766
+ FROM paged p
8767
+ WHERE p.buy = false
8768
+ AND NOT EXISTS (
8769
+ SELECT 1 FROM ${offersCallbacks} oc2
8770
+ WHERE oc2.offer_hash = p.hash
8771
+ )
8898
8772
  )
8899
8773
  -- Final SELECT with inline takeable computation
8900
8774
  SELECT
@@ -8917,18 +8791,24 @@ async function _getOffers(db, params) {
8917
8791
  oc.block_number,
8918
8792
  oc.session,
8919
8793
  COALESCE(oc.total_available, 0) AS available,
8920
- -- takeable = min(assets - consumed, total_available)
8921
- GREATEST(0, LEAST(
8922
- oc.assets::numeric - oc.consumed::numeric,
8923
- COALESCE(oc.total_available, 0)
8924
- )) AS takeable,
8794
+ -- takeable: sell offers use assets - consumed directly (collateral positions not indexed yet)
8795
+ CASE WHEN oc.buy = false
8796
+ THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
8797
+ ELSE GREATEST(0, LEAST(
8798
+ oc.assets::numeric - oc.consumed::numeric,
8799
+ COALESCE(oc.total_available, 0)
8800
+ ))
8801
+ END AS takeable,
8925
8802
  c.collaterals
8926
8803
  FROM offer_contributions oc
8927
8804
  LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
8928
- WHERE GREATEST(0, LEAST(
8929
- oc.assets::numeric - oc.consumed::numeric,
8930
- COALESCE(oc.total_available, 0)
8931
- )) > 0
8805
+ WHERE CASE WHEN oc.buy = false
8806
+ THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
8807
+ ELSE GREATEST(0, LEAST(
8808
+ oc.assets::numeric - oc.consumed::numeric,
8809
+ COALESCE(oc.total_available, 0)
8810
+ ))
8811
+ END > 0
8932
8812
  ORDER BY
8933
8813
  oc.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
8934
8814
  oc.block_number ASC,
@@ -9269,32 +9149,32 @@ function create$5(db) {
9269
9149
  return {
9270
9150
  get: async ({ chainId }) => {
9271
9151
  return (await db.select({
9272
- address: oracles.address,
9273
- price: oracles.price,
9274
- blockNumber: oracles.blockNumber,
9275
- chainId: oracles.chainId
9276
- }).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$13({
9152
+ address: oracles$1.address,
9153
+ price: oracles$1.price,
9154
+ blockNumber: oracles$1.blockNumber,
9155
+ chainId: oracles$1.chainId
9156
+ }).from(oracles$1).where(eq(oracles$1.chainId, chainId))).map((r) => from$13({
9277
9157
  chainId: r.chainId,
9278
9158
  address: r.address,
9279
9159
  price: r.price,
9280
9160
  blockNumber: r.blockNumber
9281
9161
  }));
9282
9162
  },
9283
- upsert: async (oracles$1) => {
9284
- if (oracles$1.length === 0) return;
9285
- const rows = oracles$1.map((o) => ({
9163
+ upsert: async (oracles) => {
9164
+ if (oracles.length === 0) return;
9165
+ const rows = oracles.map((o) => ({
9286
9166
  chainId: o.chainId,
9287
9167
  address: o.address.toLowerCase(),
9288
9168
  price: o.price !== null ? o.price.toString() : null,
9289
9169
  blockNumber: o.blockNumber
9290
9170
  }));
9291
9171
  await db.transaction(async (dbTx) => {
9292
- for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(oracles).values(batch).onConflictDoUpdate({
9293
- target: [oracles.chainId, oracles.address],
9172
+ for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(oracles$1).values(batch).onConflictDoUpdate({
9173
+ target: [oracles$1.chainId, oracles$1.address],
9294
9174
  set: {
9295
- price: sql`COALESCE(EXCLUDED.price, ${oracles.price})`,
9175
+ price: sql`COALESCE(EXCLUDED.price, ${oracles$1.price})`,
9296
9176
  blockNumber: sql`CASE
9297
- WHEN EXCLUDED.price IS NULL THEN ${oracles.blockNumber}
9177
+ WHEN EXCLUDED.price IS NULL THEN ${oracles$1.blockNumber}
9298
9178
  ELSE EXCLUDED.block_number
9299
9179
  END`,
9300
9180
  updatedAt: sql`NOW()`
@@ -10333,23 +10213,11 @@ function createHttpClient(config) {
10333
10213
  issues: []
10334
10214
  };
10335
10215
  };
10336
- const getCallbackTypes = async (requestPayload) => {
10337
- const response = await request("/v1/callbacks", {
10338
- method: "POST",
10339
- headers: { "content-type": "application/json" },
10340
- body: JSON.stringify(requestPayload)
10341
- });
10342
- const json = await response.json();
10343
- if (!response.ok) throw new Error(`Gatekeeper callbacks request failed: ${extractErrorMessage(json) ?? response.statusText}`);
10344
- if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper callbacks response is invalid.");
10345
- return json.data;
10346
- };
10347
10216
  return {
10348
10217
  baseUrl,
10349
10218
  validate,
10350
10219
  getConfigRules,
10351
- isAllowed,
10352
- getCallbackTypes
10220
+ isAllowed
10353
10221
  };
10354
10222
  }
10355
10223
  function mergeHeaders(base, extra) {
@@ -10478,6 +10346,7 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
10478
10346
  callback: () => callback,
10479
10347
  chains: () => chains,
10480
10348
  maturity: () => maturity,
10349
+ oracle: () => oracle,
10481
10350
  sameMaker: () => sameMaker,
10482
10351
  token: () => token,
10483
10352
  validity: () => validity
@@ -10485,109 +10354,13 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
10485
10354
  /**
10486
10355
  * set of rules to validate offers.
10487
10356
  *
10488
- * @param parameters - Validity parameters with chain and client
10357
+ * @param _parameters - Validity parameters with chain and client
10489
10358
  * @returns Array of validation rules to evaluate against offers
10490
10359
  */
10491
- function validity(parameters) {
10492
- const { client } = parameters;
10493
- const sellErc20CallbackInvalid = single("sell_erc20_callback_invalid", "Validates that sell offers have valid ERC20 callback data matching offer collaterals", (offer) => {
10494
- const callbackType = getCallbackType(client.chain.name, offer.callback.address);
10495
- if (callbackType !== Type$1.SellERC20Callback) return;
10496
- const decoded = decode$2(callbackType, offer.callback.data);
10497
- if (decoded.length === 0) return { message: "Callback data cannot be decoded or is empty." };
10498
- if (callbackType === Type$1.SellERC20Callback) {
10499
- const offerCollaterals = new Set(offer.collaterals.map((c) => c.asset.toLowerCase()));
10500
- if (decoded.length !== offer.collaterals.length) return { message: `Sell callback collateral length mismatch. Expected ${offer.collaterals.length}, got ${decoded.length}.` };
10501
- for (const { contract } of decoded) if (!offerCollaterals.has(contract.toLowerCase())) return { message: "Sell callback collateral is not part of offer collaterals." };
10502
- }
10503
- });
10504
- const buyCallbackVaultInvalid = batch("buy_offers_callback_vault_invalid", "Validates that buy offers have valid vault callbacks registered in allowed factories with matching assets", async (offers) => {
10505
- const validationIssues = /* @__PURE__ */ new Map();
10506
- const offersByVaultAddress = /* @__PURE__ */ new Map();
10507
- for (let i = 0; i < offers.length; i++) {
10508
- const offer = offers[i];
10509
- if (getCallbackType(client.chain.name, offer.callback.address) !== Type$1.BuyVaultV1Callback) continue;
10510
- try {
10511
- const callbackVaults = decodeBuyVaultV1Callback(offer.callback.data);
10512
- for (const { contract } of callbackVaults) {
10513
- const normalizedVaultAddress = contract.toLowerCase();
10514
- if (!offersByVaultAddress.has(normalizedVaultAddress)) offersByVaultAddress.set(normalizedVaultAddress, []);
10515
- offersByVaultAddress.get(normalizedVaultAddress).push({
10516
- index: i,
10517
- offer
10518
- });
10519
- }
10520
- } catch (_) {}
10521
- }
10522
- const uniqueVaultAddresses = Array.from(offersByVaultAddress.keys());
10523
- if (uniqueVaultAddresses.length === 0) return validationIssues;
10524
- const allowedFactories = getCallback(client.chain.name, Type$1.BuyVaultV1Callback)?.vaultFactories.map((f) => f.toLowerCase());
10525
- if (!allowedFactories) return validationIssues;
10526
- const multicallContracts = [];
10527
- for (const vaultAddress of uniqueVaultAddresses) {
10528
- multicallContracts.push({
10529
- address: vaultAddress,
10530
- abi: ERC4626,
10531
- functionName: "asset"
10532
- });
10533
- for (const factoryAddress of allowedFactories) multicallContracts.push({
10534
- address: factoryAddress,
10535
- abi: MetaMorphoFactory,
10536
- functionName: "isMetaMorpho",
10537
- args: [vaultAddress]
10538
- });
10539
- }
10540
- const multicallResults = await multicall(client, {
10541
- contracts: multicallContracts,
10542
- allowFailure: true
10543
- });
10544
- const vaultAssetByAddress = /* @__PURE__ */ new Map();
10545
- const registeredVaults = /* @__PURE__ */ new Set();
10546
- const numberOfFactories = allowedFactories.length;
10547
- let resultIndex = 0;
10548
- for (const vaultAddress of uniqueVaultAddresses) {
10549
- const assetCallResult = multicallResults[resultIndex++];
10550
- const assetAddress = assetCallResult.status === "success" ? assetCallResult.result : null;
10551
- vaultAssetByAddress.set(vaultAddress, assetAddress);
10552
- let isRegisteredInFactory = false;
10553
- for (let factoryIndex = 0; factoryIndex < numberOfFactories; factoryIndex++) {
10554
- const factoryCallResult = multicallResults[resultIndex++];
10555
- if (factoryCallResult.status === "success" && factoryCallResult.result === true) isRegisteredInFactory = true;
10556
- }
10557
- if (isRegisteredInFactory) registeredVaults.add(vaultAddress);
10558
- }
10559
- const uniqueOffers = /* @__PURE__ */ new Map();
10560
- for (const offersArray of offersByVaultAddress.values()) for (const { index, offer } of offersArray) uniqueOffers.set(index, offer);
10561
- for (const [index, offer] of uniqueOffers) try {
10562
- const callbackVaults = decodeBuyVaultV1Callback(offer.callback.data);
10563
- const vaultsWithIssues = [];
10564
- for (const { contract } of callbackVaults) {
10565
- const normalizedVaultAddress = contract.toLowerCase();
10566
- const assetAddress = vaultAssetByAddress.get(normalizedVaultAddress);
10567
- const isRegistered = registeredVaults.has(normalizedVaultAddress);
10568
- const failureReasons = [];
10569
- if (assetAddress === null) failureReasons.push("asset call failed");
10570
- else if (assetAddress && assetAddress.toLowerCase() !== offer.loanToken.toLowerCase()) failureReasons.push("asset mismatch");
10571
- if (!isRegistered) failureReasons.push("not registered in factory");
10572
- if (failureReasons.length > 0) vaultsWithIssues.push({
10573
- vaultAddress: contract,
10574
- failureReasons: failureReasons.join(", ")
10575
- });
10576
- }
10577
- if (vaultsWithIssues.length > 0) {
10578
- const failureDetails = vaultsWithIssues.map((v) => `${v.vaultAddress} (${v.failureReasons})`).join("; ");
10579
- validationIssues.set(index, { message: `Buy offer callback vaults are invalid: ${failureDetails}` });
10580
- }
10581
- } catch (_) {}
10582
- return validationIssues;
10583
- });
10584
- return [
10585
- single("expiry", "Validates that offer has not expired", (offer) => {
10586
- if (offer.expiry < Math.floor(Date.now() / 1e3)) return { message: "Expiry mismatch" };
10587
- }),
10588
- sellErc20CallbackInvalid,
10589
- buyCallbackVaultInvalid
10590
- ];
10360
+ function validity(_parameters) {
10361
+ return [single("expiry", "Validates that offer has not expired", (offer) => {
10362
+ if (offer.expiry < Math.floor(Date.now() / 1e3)) return { message: "Expiry mismatch" };
10363
+ })];
10591
10364
  }
10592
10365
  const chains = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
10593
10366
  const allowedChainIds = chains.map((c) => c.id);
@@ -10597,12 +10370,10 @@ const maturity = ({ maturities }) => single("maturity", `Validates that offer ma
10597
10370
  const allowedMaturities = maturities.map((m) => from$16(m));
10598
10371
  if (!allowedMaturities.includes(offer.maturity)) return { message: `Maturity must be end of current month (${allowedMaturities[0]}) or end of next month (${allowedMaturities[1]}). Got: ${offer.maturity}` };
10599
10372
  });
10600
- const callback = ({ callbacks, allowedAddresses }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell offers must use a non-empty callback; non-empty callbacks must target one of [${allowedAddresses.map((a) => a.toLowerCase()).join(", ")}]`, (offer) => {
10601
- if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
10602
- if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
10603
- if (!isEmptyCallback(offer)) {
10604
- if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
10605
- }
10373
+ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell empty callback is ${callbacks.includes(Type$1.SellWithEmptyCallback) ? "allowed" : "not allowed"}; non-empty callbacks are rejected`, (offer) => {
10374
+ if (!isEmptyCallback(offer)) return { message: "Non-empty callbacks are not supported." };
10375
+ if (isEmptyCallback(offer) && offer.buy && !callbacks.includes(Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
10376
+ if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
10606
10377
  });
10607
10378
  /**
10608
10379
  * A validation rule that checks if the offer's tokens are allowed for its chain.
@@ -10616,6 +10387,16 @@ const token = ({ assetsByChainId }) => single("token", "Validates that offer loa
10616
10387
  if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
10617
10388
  });
10618
10389
  /**
10390
+ * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
10391
+ * @param oraclesByChainId - Allowed oracles indexed by chain id.
10392
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
10393
+ */
10394
+ const oracle = ({ oraclesByChainId }) => single("oracle", "Validates that offer collateral oracles are in the allowed oracle list for the offer chain", (offer) => {
10395
+ const allowedOracles = oraclesByChainId[offer.chainId]?.map((oracle) => oracle.toLowerCase());
10396
+ if (!allowedOracles || allowedOracles.length === 0) return { message: `No allowed oracles for chain ${offer.chainId}` };
10397
+ if (offer.collaterals.some((collateral) => !allowedOracles.includes(collateral.oracle.toLowerCase()))) return { message: "Oracle is not allowed" };
10398
+ });
10399
+ /**
10619
10400
  * A batch validation rule that ensures all offers in a tree have the same maker address.
10620
10401
  * Returns an issue only for the first non-conforming offer.
10621
10402
  * This rule is signing-agnostic; signer verification is handled at the collector level.
@@ -10646,21 +10427,22 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
10646
10427
  //#region src/gatekeeper/morphoRules.ts
10647
10428
  const morphoRules = (chains$3) => {
10648
10429
  const assetsByChainId = {};
10649
- for (const chain of chains$3) assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
10430
+ const oraclesByChainId = {};
10431
+ for (const chain of chains$3) {
10432
+ assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
10433
+ oraclesByChainId[chain.id] = oracles[chain.id.toString()] ?? [];
10434
+ }
10650
10435
  return [
10651
10436
  sameMaker(),
10652
10437
  amountMutualExclusivity(),
10653
10438
  chains({ chains: chains$3 }),
10654
10439
  maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
10655
10440
  callback({
10656
- callbacks: [
10657
- Type$1.BuyWithEmptyCallback,
10658
- Type$1.BuyVaultV1Callback,
10659
- Type$1.SellERC20Callback
10660
- ],
10661
- allowedAddresses: chains$3.flatMap((c) => getCallbackAddresses(c.name))
10441
+ callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
10442
+ allowedAddresses: []
10662
10443
  }),
10663
- token({ assetsByChainId })
10444
+ token({ assetsByChainId }),
10445
+ oracle({ oraclesByChainId })
10664
10446
  ];
10665
10447
  };
10666
10448
 
@@ -10842,5 +10624,5 @@ var mempool_exports = /* @__PURE__ */ __exportAll({
10842
10624
  });
10843
10625
 
10844
10626
  //#endregion
10845
- export { Abi_exports as Abi, BookResponse_exports as BookResponse, BooksController, BrandTypeId, Callback_exports as Callback, CallbacksController, Chain_exports as Chain, ChainHealth, ChainRegistry_exports as ChainRegistry, ChainsHealthResponse, Collateral_exports as Collateral, CollectorHealth, CollectorsHealthResponse, ConfigContractsController, ConfigRulesController, Database_exports as Database, ERC4626_exports as ERC4626, Errors_exports as Errors, Format_exports as Format, Gatekeeper_exports as Gatekeeper, Client_exports as GatekeeperClient, Health_exports as Health, HealthController, Indexer_exports as Indexer, LLTV_exports as LLTV, Liquidity_exports as Liquidity, Logger_exports as Logger, Maturity_exports as Maturity, mempool_exports as Mempool, Obligation_exports as Obligation, ObligationResponse_exports as ObligationResponse, ObligationsController, Offer_exports as Offer, OfferResponse_exports as OfferResponse, OffersController, drizzle_exports as OffersSchema, OpenApi, Oracle_exports as Oracle, Position_exports as Position, PositionResponse_exports as PositionResponse, Quote_exports as Quote, RouterApi_exports as RouterApi, Client_exports$1 as RouterClient, RouterStatusResponse, Rules_exports as Rules, time_exports as Time, TradingFee_exports as TradingFee, Transfer_exports as Transfer, Tree_exports as Tree, UsersController, utils_exports as Utils, ValidateController, Gate_exports as Validation, morphoRules, parse, safeParse };
10627
+ export { Abi_exports as Abi, BookResponse_exports as BookResponse, BooksController, BrandTypeId, Callback_exports as Callback, Chain_exports as Chain, ChainHealth, ChainRegistry_exports as ChainRegistry, ChainsHealthResponse, Collateral_exports as Collateral, CollectorHealth, CollectorsHealthResponse, ConfigContractsController, ConfigRulesController, Database_exports as Database, ERC4626_exports as ERC4626, Errors_exports as Errors, Format_exports as Format, Gatekeeper_exports as Gatekeeper, Client_exports as GatekeeperClient, Health_exports as Health, HealthController, Indexer_exports as Indexer, LLTV_exports as LLTV, Liquidity_exports as Liquidity, Logger_exports as Logger, Maturity_exports as Maturity, mempool_exports as Mempool, Obligation_exports as Obligation, ObligationResponse_exports as ObligationResponse, ObligationsController, Offer_exports as Offer, OfferResponse_exports as OfferResponse, OffersController, drizzle_exports as OffersSchema, OpenApi, Oracle_exports as Oracle, Position_exports as Position, PositionResponse_exports as PositionResponse, Quote_exports as Quote, RouterApi_exports as RouterApi, Client_exports$1 as RouterClient, RouterStatusResponse, Rules_exports as Rules, time_exports as Time, TradingFee_exports as TradingFee, Transfer_exports as Transfer, Tree_exports as Tree, UsersController, utils_exports as Utils, ValidateController, Gate_exports as Validation, morphoRules, parse, safeParse };
10846
10628
  //# sourceMappingURL=index.node.mjs.map