@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.
@@ -51,11 +51,13 @@ require("@opentelemetry/propagator-aws-xray");
51
51
  require("@opentelemetry/resources");
52
52
  require("@opentelemetry/sdk-trace-node");
53
53
  require("@opentelemetry/semantic-conventions");
54
+ let drizzle_orm = require("drizzle-orm");
54
55
  let viem_chains = require("viem/chains");
55
56
  let zod = require("zod");
56
57
  zod = __toESM(zod);
57
58
  let _openzeppelin_merkle_tree = require("@openzeppelin/merkle-tree");
58
59
  let pako = require("pako");
60
+ let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
59
61
  let _hono_node_server = require("@hono/node-server");
60
62
  let hono = require("hono");
61
63
  let hono_cors = require("hono/cors");
@@ -70,8 +72,6 @@ let node_fs_promises = require("node:fs/promises");
70
72
  let node_path = require("node:path");
71
73
  let node_url = require("node:url");
72
74
  let marked = require("marked");
73
- let drizzle_orm = require("drizzle-orm");
74
- let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
75
75
  let openapi_fetch = require("openapi-fetch");
76
76
  openapi_fetch = __toESM(openapi_fetch);
77
77
  let _electric_sql_pglite = require("@electric-sql/pglite");
@@ -1146,107 +1146,14 @@ const Morpho = [
1146
1146
  //#region src/core/Callback.ts
1147
1147
  var Callback_exports = /* @__PURE__ */ __exportAll({
1148
1148
  Type: () => Type$1,
1149
- decode: () => decode$2,
1150
- decodeBuyERC20: () => decodeBuyERC20,
1151
- decodeBuyVaultV1Callback: () => decodeBuyVaultV1Callback,
1152
- decodeSellERC20Callback: () => decodeSellERC20Callback,
1153
- encode: () => encode$2,
1154
- encodeBuyERC20: () => encodeBuyERC20,
1155
- encodeBuyVaultV1Callback: () => encodeBuyVaultV1Callback,
1156
- encodeSellERC20Callback: () => encodeSellERC20Callback,
1157
1149
  isEmptyCallback: () => isEmptyCallback
1158
1150
  });
1159
1151
  let Type$1 = /* @__PURE__ */ function(Type) {
1160
1152
  Type["BuyWithEmptyCallback"] = "buy_with_empty_callback";
1161
- Type["BuyERC20"] = "buy_erc20";
1162
- Type["BuyVaultV1Callback"] = "buy_vault_v1_callback";
1163
- Type["SellERC20Callback"] = "sell_erc20_callback";
1153
+ Type["SellWithEmptyCallback"] = "sell_with_empty_callback";
1164
1154
  return Type;
1165
1155
  }({});
1166
1156
  const isEmptyCallback = (offer) => offer.callback.data === "0x";
1167
- function decode$2(type, data) {
1168
- switch (type) {
1169
- case Type$1.BuyERC20: return decodeBuyERC20(data);
1170
- case Type$1.BuyVaultV1Callback: return decodeBuyVaultV1Callback(data);
1171
- case Type$1.SellERC20Callback: return decodeSellERC20Callback(data);
1172
- default: throw new Error("Invalid callback type");
1173
- }
1174
- }
1175
- function encode$2(type, data) {
1176
- switch (type) {
1177
- case Type$1.BuyERC20:
1178
- if (!("tokens" in data)) throw new Error("Invalid callback data");
1179
- return encodeBuyERC20(data);
1180
- case Type$1.BuyVaultV1Callback:
1181
- if (!("vaults" in data)) throw new Error("Invalid callback data");
1182
- return encodeBuyVaultV1Callback(data);
1183
- case Type$1.SellERC20Callback:
1184
- if (!("collaterals" in data)) throw new Error("Invalid callback data");
1185
- return encodeSellERC20Callback(data);
1186
- default: throw new Error("Invalid callback type");
1187
- }
1188
- }
1189
- /**
1190
- * Decodes BuyERC20 callback data into positions.
1191
- * @param data - The ABI-encoded callback data containing token addresses and amounts.
1192
- * @returns Array of positions with contract address and amount.
1193
- * @throws If data is empty, malformed, or arrays have mismatched lengths.
1194
- */
1195
- function decodeBuyERC20(data) {
1196
- if (!data || data === "0x") throw new Error("Empty callback data");
1197
- let tokens;
1198
- let amounts;
1199
- try {
1200
- [tokens, amounts] = (0, viem.decodeAbiParameters)([{ type: "address[]" }, { type: "uint256[]" }], data);
1201
- } catch (_) {
1202
- throw new Error("Invalid BuyERC20 callback data");
1203
- }
1204
- if (tokens.length !== amounts.length) throw new Error("Mismatched array lengths");
1205
- return tokens.map((token, index) => ({
1206
- contract: token,
1207
- amount: amounts[index]
1208
- }));
1209
- }
1210
- /**
1211
- * Encodes BuyERC20 callback parameters into ABI-encoded data.
1212
- * @param parameters - The tokens and amounts to encode.
1213
- * @returns ABI-encoded hex string.
1214
- */
1215
- function encodeBuyERC20(parameters) {
1216
- return (0, viem.encodeAbiParameters)([{ type: "address[]" }, { type: "uint256[]" }], [parameters.tokens, parameters.amounts]);
1217
- }
1218
- function decodeBuyVaultV1Callback(data) {
1219
- if (!data || data === "0x") throw new Error("Empty callback data");
1220
- try {
1221
- const [vaults, amounts] = (0, viem.decodeAbiParameters)([{ type: "address[]" }, { type: "uint256[]" }], data);
1222
- if (vaults.length !== amounts.length) throw new Error("Mismatched array lengths");
1223
- return vaults.map((v, i) => ({
1224
- contract: v,
1225
- amount: amounts[i]
1226
- }));
1227
- } catch (_) {
1228
- throw new Error("Invalid BuyVaultV1Callback callback data");
1229
- }
1230
- }
1231
- function decodeSellERC20Callback(data) {
1232
- if (!data || data === "0x") throw new Error("Empty callback data");
1233
- try {
1234
- const [collaterals, amounts] = (0, viem.decodeAbiParameters)([{ type: "address[]" }, { type: "uint256[]" }], data);
1235
- if (collaterals.length !== amounts.length) throw new Error("Mismatched array lengths");
1236
- return collaterals.map((c, i) => ({
1237
- contract: c,
1238
- amount: amounts[i]
1239
- }));
1240
- } catch (_) {
1241
- throw new Error("Invalid SellERC20Callback callback data");
1242
- }
1243
- }
1244
- function encodeBuyVaultV1Callback(parameters) {
1245
- return (0, viem.encodeAbiParameters)([{ type: "address[]" }, { type: "uint256[]" }], [parameters.vaults, parameters.amounts]);
1246
- }
1247
- function encodeSellERC20Callback(parameters) {
1248
- return (0, viem.encodeAbiParameters)([{ type: "address[]" }, { type: "uint256[]" }], [parameters.collaterals, parameters.amounts]);
1249
- }
1250
1157
 
1251
1158
  //#endregion
1252
1159
  //#region src/core/Chain.ts
@@ -1768,11 +1675,9 @@ var Liquidity_exports = /* @__PURE__ */ __exportAll({
1768
1675
  calculateMaxDebt: () => calculateMaxDebt,
1769
1676
  generateAllowancePoolId: () => generateAllowancePoolId,
1770
1677
  generateBalancePoolId: () => generateBalancePoolId,
1771
- generateBuyVaultCallbackPoolId: () => generateBuyVaultCallbackPoolId,
1772
1678
  generateDebtPoolId: () => generateDebtPoolId,
1773
1679
  generateMarketLiquidityPoolId: () => generateMarketLiquidityPoolId,
1774
1680
  generateObligationCollateralPoolId: () => generateObligationCollateralPoolId,
1775
- generateSellERC20CallbackPoolId: () => generateSellERC20CallbackPoolId,
1776
1681
  generateUserVaultPositionPoolId: () => generateUserVaultPositionPoolId,
1777
1682
  generateVaultPositionPoolId: () => generateVaultPositionPoolId
1778
1683
  });
@@ -1801,14 +1706,6 @@ function generateAllowancePoolId(parameters) {
1801
1706
  return `${user}-${chainId.toString()}-${token}-allowance`.toLowerCase();
1802
1707
  }
1803
1708
  /**
1804
- * Generate pool ID for sell ERC20 callback pools.
1805
- * Each offer has its own callback pool to prevent liquidity conflicts.
1806
- */
1807
- function generateSellERC20CallbackPoolId(parameters) {
1808
- const { user, chainId, obligationId, token, offerHash } = parameters;
1809
- return `${user}-${chainId.toString()}-${obligationId}-${token}-${offerHash}-sell_erc20_callback`.toLowerCase();
1810
- }
1811
- /**
1812
1709
  * Generate pool ID for obligation collateral pools.
1813
1710
  * Obligation collateral pools represent collateral already deposited in the obligation.
1814
1711
  * These pools are shared across all offers with the same obligation.
@@ -1818,13 +1715,6 @@ function generateObligationCollateralPoolId(parameters) {
1818
1715
  return `${user}-${chainId.toString()}-${obligationId}-${token}-obligation-collateral`.toLowerCase();
1819
1716
  }
1820
1717
  /**
1821
- * Generate pool ID for buy vault callback pools.
1822
- */
1823
- function generateBuyVaultCallbackPoolId(parameters) {
1824
- const { user, chainId, vault, offerHash } = parameters;
1825
- return `${user}-${chainId.toString()}-${vault}-${offerHash}-${Type$1.BuyVaultV1Callback}`.toLowerCase();
1826
- }
1827
- /**
1828
1718
  * Generate pool ID for debt pools.
1829
1719
  */
1830
1720
  function generateDebtPoolId(parameters) {
@@ -2159,6 +2049,7 @@ var Offer_exports = /* @__PURE__ */ __exportAll({
2159
2049
  obligationId: () => obligationId,
2160
2050
  random: () => random$1,
2161
2051
  serialize: () => serialize,
2052
+ takeEvent: () => takeEvent,
2162
2053
  toSnakeCase: () => toSnakeCase,
2163
2054
  types: () => types
2164
2055
  });
@@ -2277,7 +2168,7 @@ function random$1(config) {
2277
2168
  const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
2278
2169
  const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
2279
2170
  const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
2280
- const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
2171
+ collateralCandidates[int(collateralCandidates.length)];
2281
2172
  const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
2282
2173
  const maturity = config?.maturity ?? from$16(maturityOption);
2283
2174
  const lltv = from$18(weightedChoice([
@@ -2304,21 +2195,10 @@ function random$1(config) {
2304
2195
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2305
2196
  const amountBase = BigInt(100 + int(999901));
2306
2197
  const assetsScaled = config?.assets ?? amountBase * unit;
2307
- const callbackBySide = (() => {
2308
- if (buy) return {
2309
- address: viem.zeroAddress,
2310
- data: "0x"
2311
- };
2312
- const sellCallbackAddress = "0x3333333333333333333333333333333333333333";
2313
- const amount = assetsScaled * 1000000000000000000000n;
2314
- return {
2315
- address: sellCallbackAddress,
2316
- data: encodeSellERC20Callback({
2317
- collaterals: [collateralAsset],
2318
- amounts: [amount]
2319
- })
2320
- };
2321
- })();
2198
+ const emptyCallback = {
2199
+ address: viem.zeroAddress,
2200
+ data: "0x"
2201
+ };
2322
2202
  return from$14({
2323
2203
  maker: config?.maker ?? address(),
2324
2204
  assets: assetsScaled,
@@ -2337,7 +2217,7 @@ function random$1(config) {
2337
2217
  ...random$3(),
2338
2218
  lltv
2339
2219
  })).sort((a, b) => a.asset.localeCompare(b.asset)),
2340
- callback: config?.callback ?? callbackBySide
2220
+ callback: config?.callback ?? emptyCallback
2341
2221
  });
2342
2222
  }
2343
2223
  const weightedChoice = (pairs) => {
@@ -2627,6 +2507,94 @@ function decode$1(data) {
2627
2507
  });
2628
2508
  }
2629
2509
  /**
2510
+ * ABI for the Take event emitted by the Morpho V2 contract.
2511
+ */
2512
+ const takeEvent = {
2513
+ type: "event",
2514
+ name: "Take",
2515
+ inputs: [
2516
+ {
2517
+ name: "caller",
2518
+ type: "address",
2519
+ indexed: false,
2520
+ internalType: "address"
2521
+ },
2522
+ {
2523
+ name: "id",
2524
+ type: "bytes32",
2525
+ indexed: true,
2526
+ internalType: "bytes32"
2527
+ },
2528
+ {
2529
+ name: "maker",
2530
+ type: "address",
2531
+ indexed: true,
2532
+ internalType: "address"
2533
+ },
2534
+ {
2535
+ name: "taker",
2536
+ type: "address",
2537
+ indexed: true,
2538
+ internalType: "address"
2539
+ },
2540
+ {
2541
+ name: "offerIsBuy",
2542
+ type: "bool",
2543
+ indexed: false,
2544
+ internalType: "bool"
2545
+ },
2546
+ {
2547
+ name: "buyerAssets",
2548
+ type: "uint256",
2549
+ indexed: false,
2550
+ internalType: "uint256"
2551
+ },
2552
+ {
2553
+ name: "sellerAssets",
2554
+ type: "uint256",
2555
+ indexed: false,
2556
+ internalType: "uint256"
2557
+ },
2558
+ {
2559
+ name: "obligationUnits",
2560
+ type: "uint256",
2561
+ indexed: false,
2562
+ internalType: "uint256"
2563
+ },
2564
+ {
2565
+ name: "obligationShares",
2566
+ type: "uint256",
2567
+ indexed: false,
2568
+ internalType: "uint256"
2569
+ },
2570
+ {
2571
+ name: "buyerIsLender",
2572
+ type: "bool",
2573
+ indexed: false,
2574
+ internalType: "bool"
2575
+ },
2576
+ {
2577
+ name: "sellerIsBorrower",
2578
+ type: "bool",
2579
+ indexed: false,
2580
+ internalType: "bool"
2581
+ },
2582
+ {
2583
+ name: "group",
2584
+ type: "bytes32",
2585
+ indexed: false,
2586
+ internalType: "bytes32"
2587
+ },
2588
+ {
2589
+ name: "consumed",
2590
+ type: "uint256",
2591
+ indexed: false,
2592
+ internalType: "uint256"
2593
+ }
2594
+ ],
2595
+ anonymous: false
2596
+ };
2597
+ /**
2630
2598
  * ABI for the Consume event emitted by the Obligation contract.
2631
2599
  */
2632
2600
  const consumedEvent = {
@@ -3378,160 +3346,701 @@ var SignatureDomainError = class extends BaseError {
3378
3346
  const BrandTypeId = Symbol.for("mempool/Brand");
3379
3347
 
3380
3348
  //#endregion
3381
- //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
3382
- async function* collectConsumedEvents(parameters) {
3383
- let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3384
- const logger = getLogger();
3385
- let startBlock = blockNumber;
3386
- let reorgDetected = false;
3387
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3388
- const stream = streamLogs({
3389
- client,
3390
- contractAddress: client.chain.custom.morpho.address,
3391
- event: consumedEvent,
3392
- blockNumberGte: blockNumber,
3393
- blockNumberLte: latestBlockNumberChain,
3394
- order: "asc",
3395
- options: {
3396
- maxBatchSize,
3397
- blockWindow
3398
- }
3399
- });
3400
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3401
- const parsedLogs = (0, viem.parseEventLogs)({
3402
- abi: [consumedEvent],
3403
- logs
3404
- });
3405
- const events = [];
3406
- for (const log of parsedLogs) {
3407
- if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
3408
- logger.debug({
3409
- collector,
3410
- chainId: client.chain.id,
3411
- msg: "Skipping log because it is missing required fields"
3412
- });
3413
- continue;
3414
- }
3415
- events.push({
3416
- id: `${log.blockNumber.toString()}-${log.logIndex.toString()}-${client.chain.id}-${log.transactionHash}`,
3417
- chainId: client.chain.id,
3418
- maker: log.args.user,
3419
- group: log.args.group,
3420
- amount: log.args.amount,
3421
- blockNumber: Number(log.blockNumber)
3422
- });
3423
- }
3424
- await db.transaction(async (dbTx) => {
3425
- try {
3426
- await dbTx.consumed.create(events);
3427
- if (events.length > 0) logger.info({
3428
- msg: `Events indexed`,
3429
- collector,
3430
- count: events.length,
3431
- chain_id: client.chain.id,
3432
- block_range: [startBlock, lastStreamBlockNumber]
3433
- });
3434
- } catch (err) {
3435
- logger.error({
3436
- err,
3437
- msg: "Failed to process offer_consumed events"
3438
- });
3439
- }
3440
- blockNumber = lastStreamBlockNumber;
3441
- try {
3442
- await dbTx.blocks.advanceCollector({
3443
- collectorName: collector,
3444
- chainId: client.chain.id,
3445
- blockNumber,
3446
- epoch
3447
- });
3448
- } catch (_) {
3449
- try {
3450
- const ancestor = await dbTx.blocks.getCollector({
3451
- collectorName: collector,
3452
- chainId: client.chain.id
3453
- });
3454
- blockNumber = ancestor.blockNumber;
3455
- const deleted = await dbTx.consumed.delete({
3456
- chainId: client.chain.id,
3457
- blockNumberGte: blockNumber + 1
3458
- });
3459
- logger.info({
3460
- collector,
3461
- chain_id: client.chain.id,
3462
- msg: `Reorg detected, events deleted`,
3463
- count: deleted,
3464
- block_number: blockNumber
3465
- });
3466
- await dbTx.blocks.advanceCollector({
3467
- collectorName: collector,
3468
- chainId: client.chain.id,
3469
- blockNumber,
3470
- epoch: ancestor.epoch
3471
- });
3472
- reorgDetected = true;
3473
- } catch (err) {
3474
- const msg = "Failed to delete consumed events when handling reorg.";
3475
- logger.error({
3476
- collector,
3477
- chainId: client.chain.id,
3478
- msg,
3479
- err
3480
- });
3481
- throw new Error(msg);
3482
- }
3483
- }
3484
- });
3485
- if (reorgDetected) return;
3486
- yield blockNumber;
3487
- startBlock = blockNumber;
3488
- }
3489
- }
3349
+ //#region src/database/drizzle/VERSION.ts
3350
+ const VERSION = "router_v1.6";
3490
3351
 
3491
3352
  //#endregion
3492
- //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
3493
- async function* collectOffersV2(parameters) {
3494
- let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3495
- const logger = getLogger();
3496
- let startBlock = blockNumber;
3497
- let reorgDetected = false;
3498
- if (client.chain.custom.morpho.address.toLowerCase() === viem.zeroAddress) {
3499
- const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
3500
- logger.error({
3501
- msg,
3502
- chain_id: client.chain.id
3503
- });
3504
- throw new Error(msg);
3505
- }
3506
- const signatureDomain = {
3507
- chainId: client.chain.id,
3508
- verifyingContract: client.chain.custom.morpho.address
3509
- };
3510
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3511
- const stream = streamLogs({
3512
- client,
3513
- contractAddress: client.chain.custom.mempool.address,
3514
- event: {
3515
- type: "event",
3516
- name: "Event",
3517
- inputs: [{
3518
- name: "data",
3519
- type: "bytes",
3520
- indexed: false,
3521
- internalType: "bytes"
3522
- }],
3523
- anonymous: false
3524
- },
3525
- blockNumberGte: blockNumber,
3526
- blockNumberLte: latestBlockNumberChain,
3527
- order: "asc",
3528
- options: {
3529
- maxBatchSize,
3530
- blockWindow
3531
- }
3532
- });
3533
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3534
- blockNumber = lastStreamBlockNumber;
3353
+ //#region src/database/drizzle/schema.ts
3354
+ var schema_exports = /* @__PURE__ */ __exportAll({
3355
+ PositionTypes: () => PositionTypes,
3356
+ StatusCode: () => StatusCode,
3357
+ TABLE_NAMES: () => TABLE_NAMES,
3358
+ VERSIONED_TABLE_NAMES: () => VERSIONED_TABLE_NAMES,
3359
+ callbacks: () => callbacks,
3360
+ chains: () => chains$1,
3361
+ collectors: () => collectors,
3362
+ consumedEvents: () => consumedEvents,
3363
+ groups: () => groups,
3364
+ lots: () => lots,
3365
+ merklePaths: () => merklePaths,
3366
+ obligationCollateralsV2: () => obligationCollateralsV2,
3367
+ obligations: () => obligations,
3368
+ offers: () => offers,
3369
+ offersCallbacks: () => offersCallbacks,
3370
+ offsets: () => offsets,
3371
+ oracles: () => oracles$1,
3372
+ positionTypes: () => positionTypes,
3373
+ positions: () => positions,
3374
+ status: () => status,
3375
+ transfers: () => transfers,
3376
+ trees: () => trees,
3377
+ validations: () => validations
3378
+ });
3379
+ const s = (0, drizzle_orm_pg_core.pgSchema)(VERSION);
3380
+ var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
3381
+ EnumTableName["OBLIGATIONS"] = "obligations";
3382
+ EnumTableName["GROUPS"] = "groups";
3383
+ EnumTableName["CONSUMED_EVENTS"] = "consumed_events";
3384
+ EnumTableName["OBLIGATION_COLLATERALS_V2"] = "obligation_collaterals_v2";
3385
+ EnumTableName["ORACLES"] = "oracles";
3386
+ EnumTableName["OFFERS"] = "offers";
3387
+ EnumTableName["OFFERS_CALLBACKS"] = "offers_callbacks";
3388
+ EnumTableName["CALLBACKS"] = "callbacks";
3389
+ EnumTableName["POSITIONS"] = "positions";
3390
+ EnumTableName["TRANSFERS"] = "transfers";
3391
+ EnumTableName["VALIDATIONS"] = "validations";
3392
+ EnumTableName["COLLECTORS"] = "collectors";
3393
+ EnumTableName["CHAINS"] = "chains";
3394
+ EnumTableName["LOTS"] = "lots";
3395
+ EnumTableName["OFFSETS"] = "offsets";
3396
+ EnumTableName["TREES"] = "trees";
3397
+ EnumTableName["MERKLE_PATHS"] = "merkle_paths";
3398
+ return EnumTableName;
3399
+ }(EnumTableName || {});
3400
+ const TABLE_NAMES = Object.values(EnumTableName);
3401
+ const VERSIONED_TABLE_NAMES = TABLE_NAMES.map((table) => `"${VERSION}"."${table}"`);
3402
+ const obligations = s.table(EnumTableName.OBLIGATIONS, {
3403
+ obligationId: (0, drizzle_orm_pg_core.varchar)("obligation_id", { length: 66 }).primaryKey(),
3404
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3405
+ loanToken: (0, drizzle_orm_pg_core.varchar)("loan_token", { length: 42 }).notNull(),
3406
+ maturity: (0, drizzle_orm_pg_core.integer)("maturity").notNull()
3407
+ });
3408
+ const groups = s.table(EnumTableName.GROUPS, {
3409
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3410
+ maker: (0, drizzle_orm_pg_core.varchar)("maker", { length: 42 }).notNull(),
3411
+ group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
3412
+ consumed: (0, drizzle_orm_pg_core.numeric)("consumed", {
3413
+ precision: 78,
3414
+ scale: 0
3415
+ }).notNull(),
3416
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3417
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3418
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
3419
+ columns: [
3420
+ table.chainId,
3421
+ table.maker,
3422
+ table.group
3423
+ ],
3424
+ name: "groups_pk"
3425
+ }), (0, drizzle_orm_pg_core.index)("groups_chain_id_maker_group_consumed_idx").on(table.chainId, table.maker, table.group, table.consumed)]);
3426
+ const consumedEvents = s.table(EnumTableName.CONSUMED_EVENTS, {
3427
+ eventId: (0, drizzle_orm_pg_core.varchar)("event_id", { length: 128 }).primaryKey(),
3428
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3429
+ maker: (0, drizzle_orm_pg_core.varchar)("maker", { length: 42 }).notNull(),
3430
+ group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
3431
+ amount: (0, drizzle_orm_pg_core.numeric)("amount", {
3432
+ precision: 78,
3433
+ scale: 0
3434
+ }).notNull(),
3435
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3436
+ createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
3437
+ }, (t) => [
3438
+ (0, drizzle_orm_pg_core.foreignKey)({
3439
+ columns: [
3440
+ t.chainId,
3441
+ t.maker,
3442
+ t.group
3443
+ ],
3444
+ foreignColumns: [
3445
+ groups.chainId,
3446
+ groups.maker,
3447
+ groups.group
3448
+ ],
3449
+ name: "consumed_events_groups_fk"
3450
+ }).onDelete("cascade"),
3451
+ (0, drizzle_orm_pg_core.index)("consumed_events_group_idx").on(t.chainId, t.maker, t.group),
3452
+ (0, drizzle_orm_pg_core.index)("consumed_events_block_number_idx").on(t.blockNumber)
3453
+ ]);
3454
+ const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2, {
3455
+ obligationId: (0, drizzle_orm_pg_core.varchar)("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
3456
+ asset: (0, drizzle_orm_pg_core.varchar)("asset", { length: 42 }).notNull(),
3457
+ oracleChainId: (0, drizzle_orm_pg_core.bigint)("oracle_chain_id", { mode: "number" }).$type().notNull(),
3458
+ oracleAddress: (0, drizzle_orm_pg_core.varchar)("oracle_address", { length: 42 }).notNull(),
3459
+ lltv: (0, drizzle_orm_pg_core.bigint)("lltv", { mode: "bigint" }).notNull(),
3460
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3461
+ }, (table) => [
3462
+ (0, drizzle_orm_pg_core.primaryKey)({
3463
+ columns: [table.obligationId, table.asset],
3464
+ name: "obligation_collaterals_v2_pk"
3465
+ }),
3466
+ (0, drizzle_orm_pg_core.foreignKey)({
3467
+ columns: [table.oracleChainId, table.oracleAddress],
3468
+ foreignColumns: [oracles$1.chainId, oracles$1.address],
3469
+ name: "obligation_collaterals_v2_oracles_fk"
3470
+ }),
3471
+ (0, drizzle_orm_pg_core.index)("obligation_collaterals_v2_obligation_id_idx").on(table.obligationId),
3472
+ (0, drizzle_orm_pg_core.index)("obligation_collaterals_v2_oracle_fk_idx").on(table.oracleChainId, table.oracleAddress)
3473
+ ]);
3474
+ const oracles$1 = s.table(EnumTableName.ORACLES, {
3475
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3476
+ address: (0, drizzle_orm_pg_core.varchar)("address", { length: 42 }).notNull(),
3477
+ price: (0, drizzle_orm_pg_core.numeric)("price", {
3478
+ precision: 78,
3479
+ scale: 0
3480
+ }),
3481
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3482
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3483
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
3484
+ columns: [table.chainId, table.address],
3485
+ name: "oracles_pk"
3486
+ })]);
3487
+ const offers = s.table(EnumTableName.OFFERS, {
3488
+ hash: (0, drizzle_orm_pg_core.varchar)("hash", { length: 66 }).primaryKey(),
3489
+ obligationId: (0, drizzle_orm_pg_core.varchar)("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
3490
+ assets: (0, drizzle_orm_pg_core.numeric)("assets", {
3491
+ precision: 78,
3492
+ scale: 0
3493
+ }).notNull(),
3494
+ obligationUnits: (0, drizzle_orm_pg_core.numeric)("obligation_units", {
3495
+ precision: 78,
3496
+ scale: 0
3497
+ }).notNull().default("0"),
3498
+ obligationShares: (0, drizzle_orm_pg_core.numeric)("obligation_shares", {
3499
+ precision: 78,
3500
+ scale: 0
3501
+ }).notNull().default("0"),
3502
+ price: (0, drizzle_orm_pg_core.numeric)("price", {
3503
+ precision: 78,
3504
+ scale: 0
3505
+ }).notNull(),
3506
+ maturity: (0, drizzle_orm_pg_core.integer)("maturity").notNull(),
3507
+ expiry: (0, drizzle_orm_pg_core.integer)("expiry").notNull(),
3508
+ start: (0, drizzle_orm_pg_core.integer)("start").notNull(),
3509
+ groupChainId: (0, drizzle_orm_pg_core.bigint)("group_chain_id", { mode: "number" }).$type().notNull(),
3510
+ groupMaker: (0, drizzle_orm_pg_core.varchar)("group_maker", { length: 42 }).notNull(),
3511
+ group: (0, drizzle_orm_pg_core.varchar)("group_group", { length: 66 }).notNull(),
3512
+ session: (0, drizzle_orm_pg_core.varchar)("session", { length: 66 }).notNull(),
3513
+ buy: (0, drizzle_orm_pg_core.boolean)("buy").notNull(),
3514
+ callbackAddress: (0, drizzle_orm_pg_core.varchar)("callback_address", { length: 42 }).notNull(),
3515
+ callbackData: (0, drizzle_orm_pg_core.text)("callback_data").notNull(),
3516
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3517
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3518
+ }, (table) => [
3519
+ (0, drizzle_orm_pg_core.foreignKey)({
3520
+ columns: [
3521
+ table.groupChainId,
3522
+ table.groupMaker,
3523
+ table.group
3524
+ ],
3525
+ foreignColumns: [
3526
+ groups.chainId,
3527
+ groups.maker,
3528
+ groups.group
3529
+ ],
3530
+ name: "offers_groups_fk"
3531
+ }).onDelete("cascade"),
3532
+ (0, drizzle_orm_pg_core.index)("offers_group_fk_idx").on(table.groupChainId, table.groupMaker, table.group),
3533
+ (0, drizzle_orm_pg_core.index)("offers_group_and_hash_idx").on(table.groupChainId, table.groupMaker, table.group, table.hash),
3534
+ (0, drizzle_orm_pg_core.index)("offers_obligation_id_side_idx").on(table.obligationId, table.buy)
3535
+ ]);
3536
+ const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
3537
+ offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
3538
+ callbackId: (0, drizzle_orm_pg_core.varchar)("callback_id", { length: 66 })
3539
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
3540
+ columns: [table.offerHash, table.callbackId],
3541
+ name: "offers_callbacks_pk"
3542
+ })]);
3543
+ const callbacks = s.table(EnumTableName.CALLBACKS, {
3544
+ id: (0, drizzle_orm_pg_core.varchar)("id", { length: 66 }).primaryKey(),
3545
+ positionChainId: (0, drizzle_orm_pg_core.bigint)("position_chain_id", { mode: "number" }).$type().notNull(),
3546
+ positionContract: (0, drizzle_orm_pg_core.varchar)("position_contract", { length: 42 }).notNull(),
3547
+ positionUser: (0, drizzle_orm_pg_core.varchar)("position_user", { length: 42 }).notNull(),
3548
+ amount: (0, drizzle_orm_pg_core.numeric)("amount", {
3549
+ precision: 78,
3550
+ scale: 0
3551
+ })
3552
+ }, (table) => [(0, drizzle_orm_pg_core.foreignKey)({
3553
+ columns: [
3554
+ table.positionChainId,
3555
+ table.positionContract,
3556
+ table.positionUser
3557
+ ],
3558
+ foreignColumns: [
3559
+ positions.chainId,
3560
+ positions.contract,
3561
+ positions.user
3562
+ ],
3563
+ name: "callbacks_positions_fk"
3564
+ }).onDelete("cascade")]);
3565
+ const lots = s.table(EnumTableName.LOTS, {
3566
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3567
+ user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
3568
+ contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
3569
+ group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
3570
+ lower: (0, drizzle_orm_pg_core.numeric)("lower", {
3571
+ precision: 78,
3572
+ scale: 0
3573
+ }).notNull(),
3574
+ upper: (0, drizzle_orm_pg_core.numeric)("upper", {
3575
+ precision: 78,
3576
+ scale: 0
3577
+ }).notNull()
3578
+ }, (table) => [
3579
+ (0, drizzle_orm_pg_core.primaryKey)({
3580
+ columns: [
3581
+ table.chainId,
3582
+ table.user,
3583
+ table.contract,
3584
+ table.group
3585
+ ],
3586
+ name: "lots_pk"
3587
+ }),
3588
+ (0, drizzle_orm_pg_core.foreignKey)({
3589
+ columns: [
3590
+ table.chainId,
3591
+ table.contract,
3592
+ table.user
3593
+ ],
3594
+ foreignColumns: [
3595
+ positions.chainId,
3596
+ positions.contract,
3597
+ positions.user
3598
+ ],
3599
+ name: "lots_positions_fk"
3600
+ }).onDelete("cascade"),
3601
+ (0, drizzle_orm_pg_core.foreignKey)({
3602
+ columns: [
3603
+ table.chainId,
3604
+ table.user,
3605
+ table.group
3606
+ ],
3607
+ foreignColumns: [
3608
+ groups.chainId,
3609
+ groups.maker,
3610
+ groups.group
3611
+ ],
3612
+ name: "lots_groups_fk"
3613
+ }).onDelete("cascade")
3614
+ ]);
3615
+ const offsets = s.table(EnumTableName.OFFSETS, {
3616
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3617
+ user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
3618
+ contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
3619
+ group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
3620
+ value: (0, drizzle_orm_pg_core.numeric)("value", {
3621
+ precision: 78,
3622
+ scale: 0
3623
+ }).notNull()
3624
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
3625
+ columns: [
3626
+ table.chainId,
3627
+ table.user,
3628
+ table.contract,
3629
+ table.group
3630
+ ],
3631
+ name: "offsets_pk"
3632
+ }), (0, drizzle_orm_pg_core.foreignKey)({
3633
+ columns: [
3634
+ table.chainId,
3635
+ table.contract,
3636
+ table.user
3637
+ ],
3638
+ foreignColumns: [
3639
+ positions.chainId,
3640
+ positions.contract,
3641
+ positions.user
3642
+ ],
3643
+ name: "offsets_positions_fk"
3644
+ }).onDelete("cascade")]);
3645
+ const PositionTypes = s.enum("position_type", Object.values(Type));
3646
+ const positionTypes = s.table("position_types", {
3647
+ id: (0, drizzle_orm_pg_core.serial)("id").primaryKey(),
3648
+ type: PositionTypes("type").notNull()
3649
+ });
3650
+ const positions = s.table(EnumTableName.POSITIONS, {
3651
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3652
+ contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
3653
+ user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
3654
+ positionTypeId: (0, drizzle_orm_pg_core.integer)("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
3655
+ balance: (0, drizzle_orm_pg_core.numeric)("balance", {
3656
+ precision: 78,
3657
+ scale: 0
3658
+ }),
3659
+ asset: (0, drizzle_orm_pg_core.varchar)("asset", { length: 42 }),
3660
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3661
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3662
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
3663
+ columns: [
3664
+ table.chainId,
3665
+ table.contract,
3666
+ table.user
3667
+ ],
3668
+ name: "positions_pk"
3669
+ })]);
3670
+ const transfers = s.table(EnumTableName.TRANSFERS, {
3671
+ eventId: (0, drizzle_orm_pg_core.varchar)("event_id", { length: 128 }).primaryKey(),
3672
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3673
+ contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
3674
+ from: (0, drizzle_orm_pg_core.varchar)("from", { length: 42 }).notNull(),
3675
+ to: (0, drizzle_orm_pg_core.varchar)("to", { length: 42 }).notNull(),
3676
+ value: (0, drizzle_orm_pg_core.numeric)("value", {
3677
+ precision: 78,
3678
+ scale: 0
3679
+ }).notNull(),
3680
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3681
+ createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
3682
+ }, (table) => [
3683
+ (0, drizzle_orm_pg_core.foreignKey)({
3684
+ columns: [
3685
+ table.chainId,
3686
+ table.contract,
3687
+ table.from
3688
+ ],
3689
+ foreignColumns: [
3690
+ positions.chainId,
3691
+ positions.contract,
3692
+ positions.user
3693
+ ],
3694
+ name: "transfers_positions_from_fk"
3695
+ }).onDelete("cascade"),
3696
+ (0, drizzle_orm_pg_core.foreignKey)({
3697
+ columns: [
3698
+ table.chainId,
3699
+ table.contract,
3700
+ table.to
3701
+ ],
3702
+ foreignColumns: [
3703
+ positions.chainId,
3704
+ positions.contract,
3705
+ positions.user
3706
+ ],
3707
+ name: "transfers_positions_to_fk"
3708
+ }).onDelete("cascade"),
3709
+ (0, drizzle_orm_pg_core.index)("transfers_chain_contract_user_idx").on(table.chainId, table.contract, table.from, table.to, table.blockNumber)
3710
+ ]);
3711
+ const StatusCode = s.enum("status_code", Object.values(Status));
3712
+ const status = s.table("status", {
3713
+ id: (0, drizzle_orm_pg_core.serial)("id").primaryKey(),
3714
+ code: StatusCode("code").unique()
3715
+ });
3716
+ const validations = s.table("validations", {
3717
+ offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
3718
+ statusId: (0, drizzle_orm_pg_core.integer)("status_id").notNull().references(() => status.id, { onDelete: "no action" }),
3719
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3720
+ });
3721
+ const collectors = s.table(EnumTableName.COLLECTORS, {
3722
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull().references(() => chains$1.chainId, { onDelete: "no action" }),
3723
+ name: (0, drizzle_orm_pg_core.text)("name").$type().notNull(),
3724
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3725
+ epoch: (0, drizzle_orm_pg_core.numeric)("epoch", {
3726
+ precision: 78,
3727
+ scale: 0
3728
+ }).default("0").notNull(),
3729
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3730
+ }, (table) => [(0, drizzle_orm_pg_core.uniqueIndex)("collectors_chain_name_idx").on(table.chainId, table.name)]);
3731
+ const chains$1 = s.table(EnumTableName.CHAINS, {
3732
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
3733
+ blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
3734
+ epoch: (0, drizzle_orm_pg_core.numeric)("epoch", {
3735
+ precision: 78,
3736
+ scale: 0
3737
+ }).default("0").notNull(),
3738
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
3739
+ }, (table) => [(0, drizzle_orm_pg_core.uniqueIndex)("chains_chain_id_idx").on(table.chainId)]);
3740
+ const trees = s.table(EnumTableName.TREES, {
3741
+ root: (0, drizzle_orm_pg_core.varchar)("root", { length: 66 }).primaryKey(),
3742
+ rootSignature: (0, drizzle_orm_pg_core.varchar)("root_signature", { length: 132 }).notNull(),
3743
+ createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
3744
+ });
3745
+ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
3746
+ offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
3747
+ treeRoot: (0, drizzle_orm_pg_core.varchar)("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
3748
+ proofNodes: (0, drizzle_orm_pg_core.text)("proof_nodes").notNull(),
3749
+ createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
3750
+ }, (table) => [(0, drizzle_orm_pg_core.index)("merkle_paths_tree_root_idx").on(table.treeRoot)]);
3751
+
3752
+ //#endregion
3753
+ //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
3754
+ const buildGroupKey = (parameters) => {
3755
+ return `${parameters.chainId}-${parameters.maker.toLowerCase()}-${parameters.group.toLowerCase()}`;
3756
+ };
3757
+ async function* collectConsumedEvents(parameters) {
3758
+ let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
3759
+ const logger = getLogger();
3760
+ let startBlock = blockNumber;
3761
+ let reorgDetected = false;
3762
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
3763
+ const stream = streamLogs({
3764
+ client,
3765
+ contractAddress: client.chain.custom.morpho.address,
3766
+ blockNumberGte: blockNumber,
3767
+ blockNumberLte: latestBlockNumberChain,
3768
+ order: "asc",
3769
+ options: {
3770
+ maxBatchSize,
3771
+ blockWindow
3772
+ }
3773
+ });
3774
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3775
+ const parsedLogs = (0, viem.parseEventLogs)({
3776
+ abi: [consumedEvent, takeEvent],
3777
+ logs,
3778
+ strict: false
3779
+ });
3780
+ const normalizedLogs = [];
3781
+ const groups$3 = /* @__PURE__ */ new Map();
3782
+ const eventIds = /* @__PURE__ */ new Set();
3783
+ const recordLog = (log) => {
3784
+ normalizedLogs.push(log);
3785
+ eventIds.add(log.id);
3786
+ const groupKey = buildGroupKey({
3787
+ chainId: log.chainId,
3788
+ maker: log.maker,
3789
+ group: log.group
3790
+ });
3791
+ if (!groups$3.has(groupKey)) groups$3.set(groupKey, {
3792
+ chainId: log.chainId,
3793
+ maker: log.maker.toLowerCase(),
3794
+ group: log.group.toLowerCase()
3795
+ });
3796
+ };
3797
+ for (const rawLog of parsedLogs) {
3798
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
3799
+ logger.debug({
3800
+ collector,
3801
+ chainId: client.chain.id,
3802
+ msg: "Skipping log because it is missing required fields"
3803
+ });
3804
+ continue;
3805
+ }
3806
+ if (rawLog.eventName === consumedEvent.name) {
3807
+ const consumeArgs = rawLog.args;
3808
+ if (consumeArgs.user === void 0 || consumeArgs.group === void 0 || consumeArgs.amount === void 0) {
3809
+ logger.debug({
3810
+ collector,
3811
+ chainId: client.chain.id,
3812
+ msg: "Skipping Consume log because it is missing required args"
3813
+ });
3814
+ continue;
3815
+ }
3816
+ recordLog({
3817
+ kind: "consume",
3818
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
3819
+ chainId: client.chain.id,
3820
+ maker: consumeArgs.user,
3821
+ group: consumeArgs.group,
3822
+ amount: consumeArgs.amount,
3823
+ blockNumber: Number(rawLog.blockNumber)
3824
+ });
3825
+ continue;
3826
+ }
3827
+ if (rawLog.eventName === takeEvent.name) {
3828
+ const takeArgs = rawLog.args;
3829
+ if (takeArgs.maker === void 0 || takeArgs.group === void 0 || takeArgs.consumed === void 0) {
3830
+ logger.debug({
3831
+ collector,
3832
+ chainId: client.chain.id,
3833
+ msg: "Skipping Take log because it is missing required args"
3834
+ });
3835
+ continue;
3836
+ }
3837
+ recordLog({
3838
+ kind: "take",
3839
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
3840
+ chainId: client.chain.id,
3841
+ maker: takeArgs.maker,
3842
+ group: takeArgs.group,
3843
+ consumed: takeArgs.consumed,
3844
+ blockNumber: Number(rawLog.blockNumber)
3845
+ });
3846
+ }
3847
+ }
3848
+ await db.transaction(async (dbTx) => {
3849
+ const existingEventIds = /* @__PURE__ */ new Set();
3850
+ if (eventIds.size > 0) {
3851
+ const ids = Array.from(eventIds);
3852
+ for (let index = 0; index < ids.length; index += 500) {
3853
+ const slice = ids.slice(index, index + 500);
3854
+ const { rows } = await dbTx.execute(drizzle_orm.sql`
3855
+ SELECT event_id
3856
+ FROM ${consumedEvents}
3857
+ WHERE event_id IN (${drizzle_orm.sql.join(slice.map((id) => drizzle_orm.sql`${id}`), drizzle_orm.sql`,`)});
3858
+ `);
3859
+ for (const row of rows) existingEventIds.add(row.event_id);
3860
+ }
3861
+ }
3862
+ const consumedByGroup = /* @__PURE__ */ new Map();
3863
+ if (groups$3.size > 0) {
3864
+ const groupList = Array.from(groups$3.values());
3865
+ for (let index = 0; index < groupList.length; index += 500) {
3866
+ const slice = groupList.slice(index, index + 500);
3867
+ const { rows } = await dbTx.execute(drizzle_orm.sql`
3868
+ WITH targets(chain_id, maker, "group") AS (
3869
+ VALUES ${drizzle_orm.sql.join(slice.map((group) => drizzle_orm.sql`(${group.chainId}::bigint, ${group.maker.toLowerCase()}::varchar(42), ${group.group.toLowerCase()}::varchar(66))`), drizzle_orm.sql`,`)}
3870
+ )
3871
+ SELECT
3872
+ targets.chain_id,
3873
+ targets.maker,
3874
+ targets."group",
3875
+ COALESCE(g.consumed, 0)::numeric AS consumed
3876
+ FROM targets
3877
+ LEFT JOIN ${groups} g
3878
+ ON g.chain_id = targets.chain_id
3879
+ AND g.maker = targets.maker
3880
+ AND g."group" = targets."group";
3881
+ `);
3882
+ for (const row of rows) {
3883
+ const groupKey = buildGroupKey({
3884
+ chainId: Number(row.chain_id),
3885
+ maker: row.maker,
3886
+ group: row.group
3887
+ });
3888
+ consumedByGroup.set(groupKey, BigInt(row.consumed ?? "0"));
3889
+ }
3890
+ }
3891
+ }
3892
+ const events = [];
3893
+ for (const log of normalizedLogs) {
3894
+ if (existingEventIds.has(log.id)) continue;
3895
+ const groupKey = buildGroupKey({
3896
+ chainId: log.chainId,
3897
+ maker: log.maker,
3898
+ group: log.group
3899
+ });
3900
+ const previousConsumed = consumedByGroup.get(groupKey) ?? 0n;
3901
+ if (log.kind === "consume") {
3902
+ events.push({
3903
+ id: log.id,
3904
+ chainId: log.chainId,
3905
+ maker: log.maker,
3906
+ group: log.group,
3907
+ amount: log.amount,
3908
+ blockNumber: log.blockNumber
3909
+ });
3910
+ consumedByGroup.set(groupKey, previousConsumed + log.amount);
3911
+ continue;
3912
+ }
3913
+ const delta = log.consumed - previousConsumed;
3914
+ if (delta <= 0n) {
3915
+ logger.debug({
3916
+ collector,
3917
+ chainId: client.chain.id,
3918
+ msg: "Skipping Take log because consumed did not increase",
3919
+ previous_consumed: previousConsumed.toString(),
3920
+ consumed: log.consumed.toString()
3921
+ });
3922
+ continue;
3923
+ }
3924
+ events.push({
3925
+ id: log.id,
3926
+ chainId: log.chainId,
3927
+ maker: log.maker,
3928
+ group: log.group,
3929
+ amount: delta,
3930
+ blockNumber: log.blockNumber
3931
+ });
3932
+ consumedByGroup.set(groupKey, log.consumed);
3933
+ }
3934
+ try {
3935
+ await dbTx.consumed.create(events);
3936
+ if (events.length > 0) logger.info({
3937
+ msg: `Events indexed`,
3938
+ collector,
3939
+ count: events.length,
3940
+ chain_id: client.chain.id,
3941
+ block_range: [startBlock, lastStreamBlockNumber]
3942
+ });
3943
+ } catch (err) {
3944
+ logger.error({
3945
+ err,
3946
+ msg: "Failed to process consumed events"
3947
+ });
3948
+ }
3949
+ blockNumber = lastStreamBlockNumber;
3950
+ try {
3951
+ await dbTx.blocks.advanceCollector({
3952
+ collectorName: collector,
3953
+ chainId: client.chain.id,
3954
+ blockNumber,
3955
+ epoch
3956
+ });
3957
+ } catch (_) {
3958
+ try {
3959
+ const ancestor = await dbTx.blocks.getCollector({
3960
+ collectorName: collector,
3961
+ chainId: client.chain.id
3962
+ });
3963
+ blockNumber = ancestor.blockNumber;
3964
+ const deleted = await dbTx.consumed.delete({
3965
+ chainId: client.chain.id,
3966
+ blockNumberGte: blockNumber + 1
3967
+ });
3968
+ logger.info({
3969
+ collector,
3970
+ chain_id: client.chain.id,
3971
+ msg: `Reorg detected, events deleted`,
3972
+ count: deleted,
3973
+ block_number: blockNumber
3974
+ });
3975
+ await dbTx.blocks.advanceCollector({
3976
+ collectorName: collector,
3977
+ chainId: client.chain.id,
3978
+ blockNumber,
3979
+ epoch: ancestor.epoch
3980
+ });
3981
+ reorgDetected = true;
3982
+ } catch (err) {
3983
+ const msg = "Failed to delete consumed events when handling reorg.";
3984
+ logger.error({
3985
+ collector,
3986
+ chainId: client.chain.id,
3987
+ msg,
3988
+ err
3989
+ });
3990
+ throw new Error(msg, { cause: err });
3991
+ }
3992
+ }
3993
+ });
3994
+ if (reorgDetected) return;
3995
+ yield blockNumber;
3996
+ startBlock = blockNumber;
3997
+ }
3998
+ }
3999
+
4000
+ //#endregion
4001
+ //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
4002
+ async function* collectOffersV2(parameters) {
4003
+ let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
4004
+ const logger = getLogger();
4005
+ let startBlock = blockNumber;
4006
+ let reorgDetected = false;
4007
+ if (client.chain.custom.morpho.address.toLowerCase() === viem.zeroAddress) {
4008
+ const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
4009
+ logger.error({
4010
+ msg,
4011
+ chain_id: client.chain.id
4012
+ });
4013
+ throw new Error(msg);
4014
+ }
4015
+ const signatureDomain = {
4016
+ chainId: client.chain.id,
4017
+ verifyingContract: client.chain.custom.morpho.address
4018
+ };
4019
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
4020
+ const stream = streamLogs({
4021
+ client,
4022
+ contractAddress: client.chain.custom.mempool.address,
4023
+ event: {
4024
+ type: "event",
4025
+ name: "Event",
4026
+ inputs: [{
4027
+ name: "data",
4028
+ type: "bytes",
4029
+ indexed: false,
4030
+ internalType: "bytes"
4031
+ }],
4032
+ anonymous: false
4033
+ },
4034
+ blockNumberGte: blockNumber,
4035
+ blockNumberLte: latestBlockNumberChain,
4036
+ order: "asc",
4037
+ options: {
4038
+ maxBatchSize,
4039
+ blockWindow
4040
+ }
4041
+ });
4042
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
4043
+ blockNumber = lastStreamBlockNumber;
3535
4044
  const decodedTrees = [];
3536
4045
  for (const log of logs) {
3537
4046
  if (!log) continue;
@@ -3617,9 +4126,8 @@ async function* collectOffersV2(parameters) {
3617
4126
  offers: offersWithBlock,
3618
4127
  hashes: insertedHashes
3619
4128
  });
3620
- const { callbacks, positions, lots } = await decodeCallbacks({
4129
+ const { callbacks, positions, lots } = decodeCallbacks({
3621
4130
  chainId: client.chain.id,
3622
- gatekeeper,
3623
4131
  offers: insertedOffers
3624
4132
  });
3625
4133
  if (positions.length > 0) await dbTx.positions.upsert(positions);
@@ -3682,83 +4190,44 @@ async function* collectOffersV2(parameters) {
3682
4190
  startBlock = blockNumber;
3683
4191
  }
3684
4192
  }
3685
- async function decodeCallbacks(parameters) {
3686
- const { chainId, gatekeeper, offers } = parameters;
4193
+ function decodeCallbacks(parameters) {
4194
+ const { offers } = parameters;
3687
4195
  if (offers.length === 0) return {
3688
4196
  callbacks: [],
3689
4197
  positions: [],
3690
4198
  lots: []
3691
4199
  };
3692
- const addresses = offers.filter((entry) => entry.offer.callback.data !== "0x").map((entry) => entry.offer.callback.address);
3693
- if (addresses.length === 0) return {
3694
- callbacks: [],
3695
- positions: [],
3696
- lots: []
3697
- };
3698
- let response;
3699
- try {
3700
- response = await gatekeeper.getCallbackTypes({ callbacks: [{
3701
- chain_id: chainId,
3702
- addresses
3703
- }] });
3704
- } catch (err) {
3705
- const error = err instanceof Error ? err : new Error(String(err));
3706
- throw new Error("Failed to resolve callback types", { cause: error });
3707
- }
3708
- const entry = response.find((item) => item.chain_id === chainId);
3709
- const typeByAddress = /* @__PURE__ */ new Map();
3710
- if (entry) for (const [key, list] of Object.entries(entry)) {
3711
- if (key === "chain_id" || key === "not_supported") continue;
3712
- if (!Array.isArray(list)) continue;
3713
- for (const address of list) typeByAddress.set(address.toLowerCase(), key);
3714
- }
3715
4200
  const callbacks = [];
3716
4201
  const positions = [];
3717
4202
  const lots = [];
3718
4203
  for (const { offer, blockNumber: offerBlockNumber } of offers) {
3719
- if (offer.callback.data === "0x") continue;
3720
- const callbackType = typeByAddress.get(offer.callback.address.toLowerCase());
3721
- if (!callbackType) continue;
3722
- let decoded;
3723
- try {
3724
- decoded = decode$2(callbackType, offer.callback.data);
3725
- } catch (err) {
3726
- const error = err instanceof Error ? err : new Error(String(err));
3727
- throw new Error("Failed to decode callback data", { cause: error });
3728
- }
3729
- if (decoded.length === 0) continue;
3730
- const offerHash = hash(offer);
3731
- const callbackInputs = decoded.map((callback) => ({
4204
+ if (!offer.buy) continue;
4205
+ if (!isEmptyCallback(offer)) continue;
4206
+ const loanToken = offer.loanToken.toLowerCase();
4207
+ positions.push(from$12({
3732
4208
  chainId: offer.chainId,
3733
- contract: callback.contract,
4209
+ contract: loanToken,
3734
4210
  user: offer.maker,
3735
- amount: callback.amount
4211
+ type: Type.ERC20,
4212
+ asset: loanToken,
4213
+ blockNumber: offerBlockNumber
3736
4214
  }));
3737
- callbacks.push({
3738
- offerHash,
3739
- callbacks: callbackInputs
4215
+ lots.push({
4216
+ positionChainId: offer.chainId,
4217
+ positionContract: loanToken,
4218
+ positionUser: offer.maker,
4219
+ group: offer.group,
4220
+ size: offer.assets
3740
4221
  });
3741
- for (const callback of decoded) {
3742
- const contract = callback.contract;
3743
- const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
3744
- const asset = callbackType === Type$1.BuyVaultV1Callback ? void 0 : contract;
3745
- positions.push(from$12({
4222
+ callbacks.push({
4223
+ offerHash: hash(offer),
4224
+ callbacks: [{
3746
4225
  chainId: offer.chainId,
3747
- contract,
4226
+ contract: loanToken,
3748
4227
  user: offer.maker,
3749
- type: positionType,
3750
- asset,
3751
- blockNumber: offerBlockNumber
3752
- }));
3753
- const isLoanPosition = offer.loanToken.toLowerCase() === asset?.toLowerCase();
3754
- lots.push({
3755
- positionChainId: offer.chainId,
3756
- positionContract: contract,
3757
- positionUser: offer.maker,
3758
- group: offer.group,
3759
- size: isLoanPosition ? offer.assets : callback.amount
3760
- });
3761
- }
4228
+ amount: offer.assets
4229
+ }]
4230
+ });
3762
4231
  }
3763
4232
  return {
3764
4233
  callbacks,
@@ -4945,8 +5414,8 @@ const offerExample = {
4945
5414
  price: "2750000000000000000",
4946
5415
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
4947
5416
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
4948
- callback: "0x1111111111111111111111111111111111111111",
4949
- callback_data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
5417
+ callback: "0x0000000000000000000000000000000000000000",
5418
+ callback_data: "0x"
4950
5419
  },
4951
5420
  offer_hash: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427",
4952
5421
  obligation_id: "0x25690ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9abc",
@@ -4998,25 +5467,10 @@ const validateOfferExample = {
4998
5467
  lltv: "860000000000000000"
4999
5468
  }],
5000
5469
  callback: {
5001
- address: "0x1111111111111111111111111111111111111111",
5002
- data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
5470
+ address: "0x0000000000000000000000000000000000000000",
5471
+ data: "0x"
5003
5472
  }
5004
5473
  };
5005
- const callbackTypesRequestExample = { callbacks: [{
5006
- chain_id: 1,
5007
- addresses: [
5008
- "0x1111111111111111111111111111111111111111",
5009
- "0x3333333333333333333333333333333333333333",
5010
- "0x9999999999999999999999999999999999999999"
5011
- ]
5012
- }] };
5013
- const callbackTypesResponseExample = [{
5014
- chain_id: 1,
5015
- sell_erc20_callback: ["0x1111111111111111111111111111111111111111"],
5016
- buy_erc20: ["0x5555555555555555555555555555555555555555"],
5017
- buy_vault_v1_callback: ["0x3333333333333333333333333333333333333333"],
5018
- not_supported: ["0x9999999999999999999999999999999999999999"]
5019
- }];
5020
5474
  const routerStatusExample = {
5021
5475
  status: "live",
5022
5476
  initialized: true,
@@ -5085,55 +5539,6 @@ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5085
5539
  type: "string",
5086
5540
  example: validateOfferExample.callback.data
5087
5541
  })], ValidateCallbackRequest.prototype, "data", void 0);
5088
- var CallbackTypesChainRequest = class {};
5089
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5090
- type: "number",
5091
- example: callbackTypesRequestExample.callbacks[0].chain_id
5092
- })], CallbackTypesChainRequest.prototype, "chain_id", void 0);
5093
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5094
- type: () => [String],
5095
- example: callbackTypesRequestExample.callbacks[0].addresses
5096
- })], CallbackTypesChainRequest.prototype, "addresses", void 0);
5097
- var CallbackTypesRequest = class {};
5098
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5099
- type: () => [CallbackTypesChainRequest],
5100
- example: callbackTypesRequestExample.callbacks
5101
- })], CallbackTypesRequest.prototype, "callbacks", void 0);
5102
- var CallbackTypesChainResponse = class {};
5103
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5104
- type: "number",
5105
- example: callbackTypesResponseExample[0].chain_id
5106
- })], CallbackTypesChainResponse.prototype, "chain_id", void 0);
5107
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5108
- type: () => [String],
5109
- required: false,
5110
- example: callbackTypesResponseExample[0].buy_vault_v1_callback
5111
- })], CallbackTypesChainResponse.prototype, "buy_vault_v1_callback", void 0);
5112
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5113
- type: () => [String],
5114
- required: false,
5115
- example: callbackTypesResponseExample[0].sell_erc20_callback
5116
- })], CallbackTypesChainResponse.prototype, "sell_erc20_callback", void 0);
5117
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5118
- type: () => [String],
5119
- required: false,
5120
- example: callbackTypesResponseExample[0].buy_erc20
5121
- })], CallbackTypesChainResponse.prototype, "buy_erc20", void 0);
5122
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5123
- type: () => [String],
5124
- example: callbackTypesResponseExample[0].not_supported
5125
- })], CallbackTypesChainResponse.prototype, "not_supported", void 0);
5126
- var CallbackTypesSuccessResponse = class extends SuccessResponse {};
5127
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5128
- type: "string",
5129
- nullable: true,
5130
- example: "maturity:1:1730415600:end_of_next_month"
5131
- })], CallbackTypesSuccessResponse.prototype, "cursor", void 0);
5132
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5133
- type: () => [CallbackTypesChainResponse],
5134
- description: "Callback types grouped by chain.",
5135
- example: callbackTypesResponseExample
5136
- })], CallbackTypesSuccessResponse.prototype, "data", void 0);
5137
5542
  var AskResponse = class {};
5138
5543
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5139
5544
  type: "string",
@@ -5298,7 +5703,8 @@ var OfferListResponse = class extends SuccessResponse {};
5298
5703
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5299
5704
  type: "string",
5300
5705
  nullable: true,
5301
- example: offerCursorExample
5706
+ example: offerCursorExample,
5707
+ description: "Pagination cursor. Offer hash (0x...) for maker queries; base64url-encoded cursor for obligation queries."
5302
5708
  })], OfferListResponse.prototype, "cursor", void 0);
5303
5709
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5304
5710
  type: () => [OfferListItemResponse],
@@ -5650,7 +6056,7 @@ __decorate([
5650
6056
  methods: ["post"],
5651
6057
  path: "/v1/validate",
5652
6058
  summary: "Validate offers",
5653
- description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
6059
+ 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."
5654
6060
  }),
5655
6061
  (0, openapi_metadata_decorators.ApiBody)({ type: ValidateOffersRequest }),
5656
6062
  (0, openapi_metadata_decorators.ApiResponse)({
@@ -5669,28 +6075,6 @@ ValidateController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Make"
5669
6075
  description: "Bad Request",
5670
6076
  type: BadRequestResponse
5671
6077
  })], ValidateController);
5672
- let CallbacksController = class CallbacksController {
5673
- async resolveCallbackTypes() {}
5674
- };
5675
- __decorate([
5676
- (0, openapi_metadata_decorators.ApiOperation)({
5677
- methods: ["post"],
5678
- path: "/v1/callbacks",
5679
- summary: "Resolve callback types",
5680
- description: "Returns callback types for callback addresses grouped by chain."
5681
- }),
5682
- (0, openapi_metadata_decorators.ApiBody)({ type: CallbackTypesRequest }),
5683
- (0, openapi_metadata_decorators.ApiResponse)({
5684
- status: 200,
5685
- description: "Success",
5686
- type: CallbackTypesSuccessResponse
5687
- })
5688
- ], CallbacksController.prototype, "resolveCallbackTypes", null);
5689
- CallbacksController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Make"), (0, openapi_metadata_decorators.ApiResponse)({
5690
- status: 400,
5691
- description: "Bad Request",
5692
- type: BadRequestResponse
5693
- })], CallbacksController);
5694
6078
  let OffersController = class OffersController {
5695
6079
  async getOffers() {}
5696
6080
  };
@@ -5840,22 +6224,21 @@ const configRulesMaturityExample = {
5840
6224
  name: "end_of_next_month",
5841
6225
  timestamp: 1730415600
5842
6226
  };
5843
- const configRulesCallbackExample = {
5844
- type: "callback",
5845
- chain_id: 1,
5846
- address: "0x1111111111111111111111111111111111111111",
5847
- callback_type: "sell_erc20_callback"
5848
- };
5849
6227
  const configRulesLoanTokenExample = {
5850
6228
  type: "loan_token",
5851
6229
  chain_id: 1,
5852
6230
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
5853
6231
  };
6232
+ const configRulesOracleExample = {
6233
+ type: "oracle",
6234
+ chain_id: 1,
6235
+ address: "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83"
6236
+ };
5854
6237
  const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
5855
6238
  const configRulesPayloadExample = [
5856
6239
  configRulesMaturityExample,
5857
- configRulesCallbackExample,
5858
- configRulesLoanTokenExample
6240
+ configRulesLoanTokenExample,
6241
+ configRulesOracleExample
5859
6242
  ];
5860
6243
  const configContractNames = [
5861
6244
  "mempool",
@@ -5918,14 +6301,9 @@ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5918
6301
  })], ConfigRulesRuleResponse.prototype, "timestamp", void 0);
5919
6302
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5920
6303
  type: "string",
5921
- example: configRulesCallbackExample.address,
6304
+ example: configRulesLoanTokenExample.address,
5922
6305
  required: false
5923
6306
  })], ConfigRulesRuleResponse.prototype, "address", void 0);
5924
- __decorate([(0, openapi_metadata_decorators.ApiProperty)({
5925
- type: "string",
5926
- example: configRulesCallbackExample.callback_type,
5927
- required: false
5928
- })], ConfigRulesRuleResponse.prototype, "callback_type", void 0);
5929
6307
  var ConfigRulesSuccessResponse = class {};
5930
6308
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({ type: () => ConfigRulesMeta })], ConfigRulesSuccessResponse.prototype, "meta", void 0);
5931
6309
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
@@ -5986,7 +6364,7 @@ __decorate([
5986
6364
  methods: ["get"],
5987
6365
  path: "/v1/config/rules",
5988
6366
  summary: "Get config rules",
5989
- description: "Returns configured rules for supported chains."
6367
+ description: "Returns configured rules (maturities, loan tokens, oracles) for supported chains."
5990
6368
  }),
5991
6369
  (0, openapi_metadata_decorators.ApiQuery)({
5992
6370
  name: "cursor",
@@ -6006,7 +6384,7 @@ __decorate([
6006
6384
  name: "types",
6007
6385
  type: ["string"],
6008
6386
  required: false,
6009
- example: "maturity,loan_token",
6387
+ example: "maturity,loan_token,oracle",
6010
6388
  description: "Filter by rule types (comma-separated).",
6011
6389
  style: "form",
6012
6390
  explode: false
@@ -6023,1060 +6401,454 @@ __decorate([
6023
6401
  (0, openapi_metadata_decorators.ApiResponse)({
6024
6402
  status: 200,
6025
6403
  description: "Success",
6026
- type: ConfigRulesSuccessResponse
6027
- })
6028
- ], ConfigRulesController.prototype, "getConfigRules", null);
6029
- ConfigRulesController = __decorate([(0, openapi_metadata_decorators.ApiTags)("System")], ConfigRulesController);
6030
- let ObligationsController = class ObligationsController {
6031
- async getObligations() {}
6032
- async getObligation() {}
6033
- };
6034
- __decorate([
6035
- (0, openapi_metadata_decorators.ApiOperation)({
6036
- methods: ["get"],
6037
- path: "/v1/obligations",
6038
- summary: "List all obligations",
6039
- description: "Returns a list of obligations with their current best ask and bid. Obligations are sorted by their id in ascending order by default."
6040
- }),
6041
- (0, openapi_metadata_decorators.ApiQuery)({
6042
- name: "cursor",
6043
- type: "string",
6044
- example: obligationCursorExample,
6045
- description: "Obligation id cursor for pagination."
6046
- }),
6047
- (0, openapi_metadata_decorators.ApiQuery)({
6048
- name: "limit",
6049
- type: "number",
6050
- example: 10,
6051
- description: "Maximum number of obligations to return."
6052
- }),
6053
- (0, openapi_metadata_decorators.ApiQuery)({
6054
- name: "chains",
6055
- type: ["number"],
6056
- required: false,
6057
- example: "1,8453",
6058
- description: "Filter by chain IDs (comma-separated).",
6059
- style: "form",
6060
- explode: false
6061
- }),
6062
- (0, openapi_metadata_decorators.ApiQuery)({
6063
- name: "loan_tokens",
6064
- type: ["string"],
6065
- required: false,
6066
- example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751",
6067
- description: "Filter by loan token addresses (comma-separated).",
6068
- style: "form",
6069
- explode: false
6070
- }),
6071
- (0, openapi_metadata_decorators.ApiQuery)({
6072
- name: "collateral_tokens",
6073
- type: ["string"],
6074
- required: false,
6075
- example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
6076
- description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6077
- style: "form",
6078
- explode: false
6079
- }),
6080
- (0, openapi_metadata_decorators.ApiQuery)({
6081
- name: "maturities",
6082
- type: ["number"],
6083
- required: false,
6084
- example: "1761922800,1764524800",
6085
- description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6086
- style: "form",
6087
- explode: false
6088
- }),
6089
- (0, openapi_metadata_decorators.ApiResponse)({
6090
- status: 200,
6091
- description: "Success",
6092
- type: ObligationListResponse
6093
- })
6094
- ], ObligationsController.prototype, "getObligations", null);
6095
- __decorate([
6096
- (0, openapi_metadata_decorators.ApiOperation)({
6097
- methods: ["get"],
6098
- path: "/v1/obligations/{obligationId}",
6099
- summary: "Get an obligation",
6100
- description: "Returns an obligation by its id."
6101
- }),
6102
- (0, openapi_metadata_decorators.ApiParam)({
6103
- name: "obligationId",
6104
- type: "string",
6105
- example: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
6106
- description: "Obligation id."
6107
- }),
6108
- (0, openapi_metadata_decorators.ApiResponse)({
6109
- status: 200,
6110
- description: "Success",
6111
- type: ObligationSingleSuccessResponse
6112
- })
6113
- ], ObligationsController.prototype, "getObligation", null);
6114
- ObligationsController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Markets"), (0, openapi_metadata_decorators.ApiResponse)({
6115
- status: 400,
6116
- description: "Bad Request",
6117
- type: BadRequestResponse
6118
- })], ObligationsController);
6119
- let UsersController = class UsersController {
6120
- async getUserPositions() {}
6404
+ type: ConfigRulesSuccessResponse
6405
+ })
6406
+ ], ConfigRulesController.prototype, "getConfigRules", null);
6407
+ ConfigRulesController = __decorate([(0, openapi_metadata_decorators.ApiTags)("System")], ConfigRulesController);
6408
+ let ObligationsController = class ObligationsController {
6409
+ async getObligations() {}
6410
+ async getObligation() {}
6121
6411
  };
6122
6412
  __decorate([
6123
6413
  (0, openapi_metadata_decorators.ApiOperation)({
6124
6414
  methods: ["get"],
6125
- path: "/v1/users/{userAddress}/positions",
6126
- summary: "Get user positions",
6127
- description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
6128
- }),
6129
- (0, openapi_metadata_decorators.ApiParam)({
6130
- name: "userAddress",
6131
- type: "string",
6132
- example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6133
- description: "User address to get positions for."
6415
+ path: "/v1/obligations",
6416
+ summary: "List all obligations",
6417
+ description: "Returns a list of obligations with their current best ask and bid. Obligations are sorted by their id in ascending order by default."
6134
6418
  }),
6135
6419
  (0, openapi_metadata_decorators.ApiQuery)({
6136
6420
  name: "cursor",
6137
6421
  type: "string",
6138
- example: offerCursorExample,
6139
- description: "Pagination cursor in base64url-encoded format."
6422
+ example: obligationCursorExample,
6423
+ description: "Obligation id cursor for pagination."
6140
6424
  }),
6141
6425
  (0, openapi_metadata_decorators.ApiQuery)({
6142
6426
  name: "limit",
6143
6427
  type: "number",
6144
6428
  example: 10,
6145
- description: "Maximum number of positions to return."
6146
- }),
6147
- (0, openapi_metadata_decorators.ApiResponse)({
6148
- status: 200,
6149
- description: "Success",
6150
- type: PositionListResponse
6151
- })
6152
- ], UsersController.prototype, "getUserPositions", null);
6153
- UsersController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Make"), (0, openapi_metadata_decorators.ApiResponse)({
6154
- status: 400,
6155
- description: "Bad Request",
6156
- type: BadRequestResponse
6157
- })], UsersController);
6158
- const OpenApi = async () => {
6159
- return await (0, openapi_metadata.generateDocument)({
6160
- controllers: [
6161
- BooksController,
6162
- ConfigContractsController,
6163
- ConfigRulesController,
6164
- OffersController,
6165
- ObligationsController,
6166
- HealthController,
6167
- UsersController,
6168
- ValidateController,
6169
- CallbacksController
6170
- ],
6171
- document: {
6172
- openapi: "3.1.0",
6173
- info: {
6174
- title: "Router API",
6175
- version: "1.0.0",
6176
- description: "API for the Morpho Router"
6177
- },
6178
- servers: [{
6179
- url: "https://router.morpho.dev",
6180
- description: "Production server"
6181
- }, {
6182
- url: "http://localhost:7891",
6183
- description: "Local development server"
6184
- }],
6185
- tags: [
6186
- {
6187
- name: "Markets",
6188
- description: "Read-only endpoints to discover markets, order books and fetch current offers."
6189
- },
6190
- {
6191
- name: "Make",
6192
- description: "Utilities to ease making offers."
6193
- },
6194
- {
6195
- name: "System",
6196
- description: "Router configuration and health monitoring."
6197
- }
6198
- ]
6199
- }
6200
- });
6201
- };
6202
-
6203
- //#endregion
6204
- //#region src/api/Schema/PositionResponse.ts
6205
- var PositionResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$2 });
6206
- /**
6207
- * Creates a `PositionResponse` from a `PositionWithReserved`.
6208
- * @param position - {@link PositionWithReserved}
6209
- * @returns The created `PositionResponse`. {@link PositionResponse}
6210
- */
6211
- function from$2(position) {
6212
- return {
6213
- chain_id: position.chainId,
6214
- contract: position.contract,
6215
- user: position.user,
6216
- reserved: position.reserved.toString(),
6217
- block_number: position.blockNumber
6218
- };
6219
- }
6220
-
6221
- //#endregion
6222
- //#region src/api/Schema/requests.ts
6223
- const MAX_LIMIT = 100;
6224
- const DEFAULT_LIMIT$4 = 20;
6225
- const CONFIG_RULES_MAX_LIMIT = 1e3;
6226
- const CONFIG_RULES_DEFAULT_LIMIT = 100;
6227
- const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
6228
- const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
6229
- /** Validate cursor is a valid base64url-encoded JSON object.
6230
- * Domain layer handles semantic validation of cursor fields. */
6231
- function isValidBase64urlJson(val) {
6232
- try {
6233
- const decoded = Buffer.from(val, "base64url").toString("utf8");
6234
- JSON.parse(decoded);
6235
- return true;
6236
- } catch {
6237
- return false;
6238
- }
6239
- }
6240
- const csvArray = (schema) => zod.preprocess((value) => {
6241
- if (value === void 0) return void 0;
6242
- if (Array.isArray(value)) {
6243
- if (value.some((item) => typeof item !== "string")) return value;
6244
- return value.flatMap((item) => item.split(",")).map((item) => item.trim()).filter((item) => item.length > 0);
6245
- }
6246
- if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
6247
- return value;
6248
- }, zod.array(schema)).optional();
6249
- const PaginationQueryParams = zod.object({
6250
- cursor: zod.string().optional().refine((val) => {
6251
- if (!val) return true;
6252
- return isValidBase64urlJson(val);
6253
- }, { message: "Invalid cursor format. Must be a valid base64url-encoded cursor object" }).meta({
6254
- description: "Pagination cursor in base64url-encoded format",
6255
- example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
6256
- }),
6257
- limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6258
- description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6259
- example: 10
6260
- })
6261
- });
6262
- const ConfigRuleTypes = zod.enum([
6263
- "maturity",
6264
- "callback",
6265
- "loan_token"
6266
- ]);
6267
- const GetConfigRulesQueryParams = zod.object({
6268
- cursor: zod.string().regex(/^(maturity|callback|loan_token):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6269
- description: "Pagination cursor in type:chain_id:<value> format",
6270
- example: "maturity:1:1730415600:end_of_next_month"
6271
- }),
6272
- limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(CONFIG_RULES_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_RULES_MAX_LIMIT}` })).optional().default(CONFIG_RULES_DEFAULT_LIMIT).meta({
6273
- description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
6274
- example: 100
6275
- }),
6276
- types: csvArray(ConfigRuleTypes).meta({
6277
- description: "Filter by rule types (comma-separated).",
6278
- example: "maturity,loan_token"
6279
- }),
6280
- chains: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6281
- description: "Filter by chain IDs (comma-separated).",
6282
- example: "1,8453"
6283
- })
6284
- });
6285
- const GetConfigContractsQueryParams = zod.object({
6286
- cursor: zod.string().regex(/^[1-9]\d*:0x[a-fA-F0-9]{40}$/, { message: "Cursor must be in the format chain_id:0x..." }).optional().meta({
6287
- description: "Pagination cursor in chain_id:address format (lowercase address).",
6288
- example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
6289
- }),
6290
- limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(CONFIG_CONTRACTS_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_CONTRACTS_MAX_LIMIT}` })).optional().default(CONFIG_CONTRACTS_DEFAULT_LIMIT).meta({
6291
- description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
6292
- example: 1e3
6293
- }),
6294
- chains: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6295
- description: "Filter by chain IDs (comma-separated).",
6296
- example: "1,8453"
6297
- })
6298
- });
6299
- const GetOffersQueryParams = zod.object({
6300
- ...PaginationQueryParams.shape,
6301
- side: zod.enum(["buy", "sell"]).optional().meta({
6302
- description: "Side of the offer. Required when using obligation_id.",
6303
- example: "buy"
6304
- }),
6305
- obligation_id: zod.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({
6306
- description: "Offers obligation id. Required when not using maker.",
6307
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6308
- }),
6309
- maker: zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Maker must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).optional().meta({
6310
- description: "Maker address to filter offers by. Alternative to obligation_id + side.",
6311
- example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6312
- })
6313
- }).superRefine((val, ctx) => {
6314
- const hasObligation = val.obligation_id !== void 0;
6315
- const hasSide = val.side !== void 0;
6316
- const hasMaker = val.maker !== void 0;
6317
- if (hasMaker && (hasObligation || hasSide)) {
6318
- ctx.addIssue({
6319
- code: "custom",
6320
- message: "Cannot use both maker and obligation_id/side parameters"
6321
- });
6322
- return;
6323
- }
6324
- if (hasMaker) return;
6325
- if (!hasObligation || !hasSide) ctx.addIssue({
6326
- code: "custom",
6327
- message: "Must provide either maker or both obligation_id and side"
6328
- });
6329
- });
6330
- const GetObligationsQueryParams = zod.object({
6331
- ...PaginationQueryParams.shape,
6332
- cursor: zod.string().optional().meta({
6333
- description: "Obligation id cursor",
6334
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6429
+ description: "Maximum number of obligations to return."
6335
6430
  }),
6336
- chains: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6431
+ (0, openapi_metadata_decorators.ApiQuery)({
6432
+ name: "chains",
6433
+ type: ["number"],
6434
+ required: false,
6435
+ example: "1,8453",
6337
6436
  description: "Filter by chain IDs (comma-separated).",
6338
- example: "1,8453"
6437
+ style: "form",
6438
+ explode: false
6339
6439
  }),
6340
- loan_tokens: csvArray(zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Loan token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6440
+ (0, openapi_metadata_decorators.ApiQuery)({
6441
+ name: "loan_tokens",
6442
+ type: ["string"],
6443
+ required: false,
6444
+ example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751",
6341
6445
  description: "Filter by loan token addresses (comma-separated).",
6342
- example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751"
6343
- }),
6344
- collateral_tokens: csvArray(zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Collateral token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6345
- description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6346
- example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078"
6347
- }),
6348
- maturities: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Maturity must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6349
- description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6350
- example: "1761922800,1764524800"
6351
- })
6352
- });
6353
- const GetObligationParams = zod.object({ obligation_id: zod.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({
6354
- description: "Obligation id",
6355
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6356
- }) });
6357
- /** Validate a book cursor format: {side, lastPrice, offersCursor} */
6358
- function isValidBookCursor(cursorString) {
6359
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
6360
- try {
6361
- const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
6362
- return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6363
- } catch {
6364
- return false;
6365
- }
6366
- }
6367
- const BookPaginationQueryParams = zod.object({
6368
- cursor: zod.string().optional().refine((value) => {
6369
- if (!value) return true;
6370
- return isValidBookCursor(value);
6371
- }, { message: "Invalid cursor format. Must be a valid base64url-encoded book cursor object" }).meta({
6372
- description: "Pagination cursor in base64url-encoded format for book levels",
6373
- example: "eyJzaWRlIjoiYnV5IiwibGFzdFJhdGUiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwib2ZmZXJzQ3Vyc29yIjpudWxsfQ"
6446
+ style: "form",
6447
+ explode: false
6374
6448
  }),
6375
- limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6376
- description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6377
- example: 10
6378
- })
6379
- });
6380
- const HealthQueryParams = zod.object({ strict: zod.enum([
6381
- "true",
6382
- "false",
6383
- "1",
6384
- "0"
6385
- ]).transform((value) => value === "true" || value === "1").optional().meta({
6386
- description: "Enable strict mode to fail health checks when initialization is incomplete.",
6387
- example: "true"
6388
- }) });
6389
- const GetBookParams = zod.object({
6390
- ...BookPaginationQueryParams.shape,
6391
- obligation_id: zod.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({
6392
- description: "Obligation id",
6393
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6449
+ (0, openapi_metadata_decorators.ApiQuery)({
6450
+ name: "collateral_tokens",
6451
+ type: ["string"],
6452
+ required: false,
6453
+ example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
6454
+ description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6455
+ style: "form",
6456
+ explode: false
6394
6457
  }),
6395
- side: zod.enum(["buy", "sell"]).meta({
6396
- description: "Side of the book (buy or sell).",
6397
- example: "buy"
6398
- })
6399
- });
6400
- const ValidateOffersBody = zod.object({ offers: zod.array(zod.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
6401
- const CallbackTypesBody = zod.object({ callbacks: zod.array(zod.object({
6402
- chain_id: zod.number().int().positive().meta({
6403
- description: "Chain id.",
6404
- example: 1
6458
+ (0, openapi_metadata_decorators.ApiQuery)({
6459
+ name: "maturities",
6460
+ type: ["number"],
6461
+ required: false,
6462
+ example: "1761922800,1764524800",
6463
+ description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6464
+ style: "form",
6465
+ explode: false
6405
6466
  }),
6406
- addresses: zod.array(zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Callback address must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6407
- description: "Callback contract addresses.",
6408
- example: ["0x1111111111111111111111111111111111111111", "0x3333333333333333333333333333333333333333"]
6409
- })
6410
- }).strict()) }).strict();
6411
- const GetUserPositionsParams = zod.object({
6412
- ...PaginationQueryParams.shape,
6413
- user_address: zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
6414
- description: "User address to get positions for",
6415
- example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6467
+ (0, openapi_metadata_decorators.ApiResponse)({
6468
+ status: 200,
6469
+ description: "Success",
6470
+ type: ObligationListResponse
6416
6471
  })
6417
- });
6418
- const schemas = {
6419
- get_health: HealthQueryParams,
6420
- get_health_collectors: HealthQueryParams,
6421
- get_health_chains: HealthQueryParams,
6422
- get_config_contracts: GetConfigContractsQueryParams,
6423
- get_config_rules: GetConfigRulesQueryParams,
6424
- get_offers: GetOffersQueryParams,
6425
- get_obligations: GetObligationsQueryParams,
6426
- get_obligation: GetObligationParams,
6427
- get_book: GetBookParams,
6428
- validate_offers: ValidateOffersBody,
6429
- callback_types: CallbackTypesBody,
6430
- get_user_positions: GetUserPositionsParams
6431
- };
6432
- function parse(action, query) {
6433
- return schemas[action].parse(query);
6434
- }
6435
- function safeParse(action, query, error) {
6436
- return schemas[action].safeParse(query, { error });
6437
- }
6438
-
6439
- //#endregion
6440
- //#region src/api/Controllers/getBook.ts
6441
- async function getBook(params, db) {
6442
- const logger = getLogger();
6443
- const result = safeParse("get_book", params, (issue) => issue.message);
6444
- if (!result.success) return failure(result.error);
6445
- const query = result.data;
6446
- try {
6447
- const { levels, nextCursor } = await db.book.get({
6448
- side: query.side,
6449
- obligationId: query.obligation_id,
6450
- cursor: query.cursor,
6451
- limit: query.limit
6452
- });
6453
- return success({
6454
- data: levels.map(from$5),
6455
- cursor: nextCursor
6456
- });
6457
- } catch (err) {
6458
- logger.error({
6459
- err,
6460
- msg: "Error get book",
6461
- errorMessage: err instanceof Error ? err.message : String(err),
6462
- errorStack: err instanceof Error ? err.stack : void 0
6463
- });
6464
- return failure(err);
6465
- }
6466
- }
6467
-
6468
- //#endregion
6469
- //#region src/api/Controllers/getConfigContracts.ts
6470
- const CONFIG_CONTRACT_NAMES = [
6471
- "mempool",
6472
- "multicall",
6473
- "v2"
6474
- ];
6475
- /**
6476
- * Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
6477
- * @param query - Raw query parameters containing optional chain filters.
6478
- * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
6479
- * @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
6480
- */
6481
- async function getConfigContracts(query, chainRegistry) {
6482
- const parsed = safeParse("get_config_contracts", query ?? {});
6483
- if (!parsed.success) return failure(parsed.error);
6484
- const { chains: chainsFilter, cursor, limit } = parsed.data;
6485
- const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
6486
- const contracts = [];
6487
- const seenAddresses = /* @__PURE__ */ new Set();
6488
- for (const chain of chainRegistry.list()) {
6489
- if (chainFilter && !chainFilter.has(chain.id)) continue;
6490
- const mempool = chain.custom?.mempool?.address;
6491
- if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
6492
- const multicall = chain.contracts?.multicall3?.address;
6493
- if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
6494
- const v2 = chain.custom?.morpho?.address;
6495
- if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
6496
- const chainContracts = [
6497
- {
6498
- chain_id: chain.id,
6499
- name: "mempool",
6500
- address: mempool
6501
- },
6502
- {
6503
- chain_id: chain.id,
6504
- name: "multicall",
6505
- address: multicall
6506
- },
6507
- {
6508
- chain_id: chain.id,
6509
- name: "v2",
6510
- address: v2
6511
- }
6512
- ];
6513
- for (const contract of chainContracts) {
6514
- const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
6515
- if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
6516
- seenAddresses.add(cursorKey);
6517
- contracts.push(contract);
6518
- }
6519
- }
6520
- contracts.sort((a, b) => {
6521
- if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
6522
- const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
6523
- if (addressCompare !== 0) return addressCompare;
6524
- return a.name.localeCompare(b.name);
6525
- });
6526
- let cursorContract = null;
6527
- if (cursor) try {
6528
- cursorContract = parseCursor$1(cursor);
6529
- } catch (err) {
6530
- return failure(err);
6531
- }
6532
- const startIndex = cursorContract ? findStartIndex$1(contracts, cursorContract) : 0;
6533
- const page = contracts.slice(startIndex, startIndex + limit);
6534
- const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
6535
- return success({
6536
- data: page,
6537
- cursor: nextCursor
6538
- });
6539
- }
6540
- function parseCursor$1(cursor) {
6541
- const [chain, address] = cursor.split(":", 2);
6542
- if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
6543
- return {
6544
- chain_id: Number.parseInt(chain, 10),
6545
- address: address.toLowerCase()
6546
- };
6547
- }
6548
- function formatCursor$1(contract) {
6549
- return `${contract.chain_id}:${contract.address.toLowerCase()}`;
6550
- }
6551
- function findStartIndex$1(contracts, cursor) {
6552
- let low = 0;
6553
- let high = contracts.length;
6554
- while (low < high) {
6555
- const mid = Math.floor((low + high) / 2);
6556
- const current = contracts[mid];
6557
- if (compareContract(current, cursor) <= 0) low = mid + 1;
6558
- else high = mid;
6559
- }
6560
- return low;
6561
- }
6562
- function compareContract(contract, cursor) {
6563
- if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
6564
- return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
6565
- }
6566
-
6567
- //#endregion
6568
- //#region src/gatekeeper/GateConfig.ts
6569
- /**
6570
- * Returns the callback configuration for a given chain and callback type, if it exists.
6571
- *
6572
- * @param chain - Chain name for which to read the validation configuration
6573
- * @param type - Callback type to retrieve
6574
- * @returns The matching callback configuration or undefined if not configured
6575
- */
6576
- function getCallback(chain, type) {
6577
- return configs[chain].callbacks?.find((c) => c.type === type);
6578
- }
6579
- /**
6580
- * Attempts to infer the configured callback type from a callback address on a chain.
6581
- * Skips the empty callback type as it does not carry addresses.
6582
- *
6583
- * @param chain - Chain name for which to infer the callback type
6584
- * @param address - Callback contract address
6585
- * @returns The callback type when found, otherwise undefined
6586
- */
6587
- function getCallbackType(chain, address) {
6588
- return configs[chain].callbacks?.find((c) => c.type !== Type$1.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
6589
- }
6590
- /**
6591
- * Returns the list of allowed non-empty callback addresses for a chain.
6592
- *
6593
- * @param chain - Chain name
6594
- * @returns Array of allowed callback addresses (lowercased). Empty when none configured
6595
- */
6596
- const getCallbackAddresses = (chain) => {
6597
- return configs[chain].callbacks?.filter((c) => c.type !== Type$1.BuyWithEmptyCallback).flatMap((c) => c.addresses) ?? [];
6598
- };
6599
- const assets = {
6600
- [ChainId.ETHEREUM.toString()]: [
6601
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6602
- "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6603
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6604
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6605
- ],
6606
- [ChainId.BASE.toString()]: [
6607
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
6608
- "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
6609
- "0x4200000000000000000000000000000000000006",
6610
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6611
- ],
6612
- [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
6613
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6614
- "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6615
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6616
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6617
- "0xce79ddb3152d52ff8fe65a4c7e058b035fcb560a"
6618
- ],
6619
- [ChainId.ANVIL.toString()]: [
6620
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6621
- "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6622
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6623
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6624
- ]
6472
+ ], ObligationsController.prototype, "getObligations", null);
6473
+ __decorate([
6474
+ (0, openapi_metadata_decorators.ApiOperation)({
6475
+ methods: ["get"],
6476
+ path: "/v1/obligations/{obligationId}",
6477
+ summary: "Get an obligation",
6478
+ description: "Returns an obligation by its id."
6479
+ }),
6480
+ (0, openapi_metadata_decorators.ApiParam)({
6481
+ name: "obligationId",
6482
+ type: "string",
6483
+ example: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
6484
+ description: "Obligation id."
6485
+ }),
6486
+ (0, openapi_metadata_decorators.ApiResponse)({
6487
+ status: 200,
6488
+ description: "Success",
6489
+ type: ObligationSingleSuccessResponse
6490
+ })
6491
+ ], ObligationsController.prototype, "getObligation", null);
6492
+ ObligationsController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Markets"), (0, openapi_metadata_decorators.ApiResponse)({
6493
+ status: 400,
6494
+ description: "Bad Request",
6495
+ type: BadRequestResponse
6496
+ })], ObligationsController);
6497
+ let UsersController = class UsersController {
6498
+ async getUserPositions() {}
6625
6499
  };
6626
- const configs = {
6627
- ethereum: {
6628
- callbacks: [
6629
- {
6630
- type: Type$1.BuyVaultV1Callback,
6631
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6632
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
6633
- },
6634
- {
6635
- type: Type$1.SellERC20Callback,
6636
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6637
- },
6638
- { type: Type$1.BuyWithEmptyCallback }
6639
- ],
6640
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6641
- },
6642
- base: {
6643
- callbacks: [
6644
- {
6645
- type: Type$1.BuyVaultV1Callback,
6646
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6647
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0xFf62A7c278C62eD665133147129245053Bbf5918"]
6648
- },
6649
- {
6650
- type: Type$1.SellERC20Callback,
6651
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6652
- },
6653
- { type: Type$1.BuyWithEmptyCallback }
6654
- ],
6655
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6656
- },
6657
- "ethereum-virtual-testnet": {
6658
- callbacks: [
6659
- {
6660
- type: Type$1.BuyVaultV1Callback,
6661
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6662
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
6663
- },
6664
- {
6665
- type: Type$1.SellERC20Callback,
6666
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6667
- },
6668
- { type: Type$1.BuyWithEmptyCallback }
6500
+ __decorate([
6501
+ (0, openapi_metadata_decorators.ApiOperation)({
6502
+ methods: ["get"],
6503
+ path: "/v1/users/{userAddress}/positions",
6504
+ summary: "Get user positions",
6505
+ description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
6506
+ }),
6507
+ (0, openapi_metadata_decorators.ApiParam)({
6508
+ name: "userAddress",
6509
+ type: "string",
6510
+ example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6511
+ description: "User address to get positions for."
6512
+ }),
6513
+ (0, openapi_metadata_decorators.ApiQuery)({
6514
+ name: "cursor",
6515
+ type: "string",
6516
+ example: offerCursorExample,
6517
+ description: "Pagination cursor in base64url-encoded format."
6518
+ }),
6519
+ (0, openapi_metadata_decorators.ApiQuery)({
6520
+ name: "limit",
6521
+ type: "number",
6522
+ example: 10,
6523
+ description: "Maximum number of positions to return."
6524
+ }),
6525
+ (0, openapi_metadata_decorators.ApiResponse)({
6526
+ status: 200,
6527
+ description: "Success",
6528
+ type: PositionListResponse
6529
+ })
6530
+ ], UsersController.prototype, "getUserPositions", null);
6531
+ UsersController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Make"), (0, openapi_metadata_decorators.ApiResponse)({
6532
+ status: 400,
6533
+ description: "Bad Request",
6534
+ type: BadRequestResponse
6535
+ })], UsersController);
6536
+ const OpenApi = async () => {
6537
+ return await (0, openapi_metadata.generateDocument)({
6538
+ controllers: [
6539
+ BooksController,
6540
+ ConfigContractsController,
6541
+ ConfigRulesController,
6542
+ OffersController,
6543
+ ObligationsController,
6544
+ HealthController,
6545
+ UsersController,
6546
+ ValidateController
6669
6547
  ],
6670
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6671
- },
6672
- anvil: {
6673
- callbacks: [
6674
- {
6675
- type: Type$1.BuyVaultV1Callback,
6676
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
6677
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
6678
- },
6679
- {
6680
- type: Type$1.SellERC20Callback,
6681
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
6548
+ document: {
6549
+ openapi: "3.1.0",
6550
+ info: {
6551
+ title: "Router API",
6552
+ version: "1.0.0",
6553
+ description: "API for the Morpho Router"
6682
6554
  },
6683
- { type: Type$1.BuyWithEmptyCallback }
6684
- ],
6685
- maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
6686
- }
6687
- };
6688
-
6689
- //#endregion
6690
- //#region src/gatekeeper/ConfigRules.ts
6691
- /**
6692
- * Build the configured rules (maturities + callback addresses + loan tokens) for the provided chains.
6693
- * @param chains - Chains to include in the configured rules.
6694
- * @returns Sorted list of config rules.
6695
- */
6696
- function buildConfigRules(chains) {
6697
- const rules = [];
6698
- for (const chain of chains) {
6699
- const config = configs[chain.name];
6700
- const maturities = config.maturities ?? [];
6701
- for (const maturityName of maturities) rules.push({
6702
- type: "maturity",
6703
- chain_id: chain.id,
6704
- name: maturityName,
6705
- timestamp: from$16(maturityName)
6706
- });
6707
- const callbacks = config.callbacks ?? [];
6708
- for (const callback of callbacks) {
6709
- if (callback.type === Type$1.BuyWithEmptyCallback) continue;
6710
- if (!("addresses" in callback)) continue;
6711
- for (const address of callback.addresses) rules.push({
6712
- type: "callback",
6713
- chain_id: chain.id,
6714
- address: normalizeAddress(address),
6715
- callback_type: callback.type
6716
- });
6717
- }
6718
- const loanTokens = assets[chain.id.toString()] ?? [];
6719
- for (const address of loanTokens) rules.push({
6720
- type: "loan_token",
6721
- chain_id: chain.id,
6722
- address: normalizeAddress(address)
6723
- });
6724
- }
6725
- rules.sort(compareConfigRules);
6726
- return rules;
6727
- }
6728
- /**
6729
- * Compute a stable checksum for the provided configured rules.
6730
- * @param rules - Configured rules to checksum.
6731
- * @returns MD5 checksum.
6732
- */
6733
- function buildConfigRulesChecksum(rules) {
6734
- const hash = (0, node_crypto.createHash)("md5");
6735
- const orderedRules = [...rules].sort(compareConfigRules);
6736
- for (const rule of orderedRules) {
6737
- if (rule.type === "maturity") {
6738
- hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
6739
- continue;
6740
- }
6741
- if (rule.type === "callback") {
6742
- hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
6743
- continue;
6555
+ servers: [{
6556
+ url: "https://router.morpho.dev",
6557
+ description: "Production server"
6558
+ }, {
6559
+ url: "http://localhost:7891",
6560
+ description: "Local development server"
6561
+ }],
6562
+ tags: [
6563
+ {
6564
+ name: "Markets",
6565
+ description: "Read-only endpoints to discover markets, order books and fetch current offers."
6566
+ },
6567
+ {
6568
+ name: "Make",
6569
+ description: "Utilities to ease making offers."
6570
+ },
6571
+ {
6572
+ name: "System",
6573
+ description: "Router configuration and health monitoring."
6574
+ }
6575
+ ]
6744
6576
  }
6745
- hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
6746
- }
6747
- return hash.digest("hex");
6748
- }
6749
- function normalizeAddress(address) {
6750
- return address.toLowerCase();
6751
- }
6752
- function compareConfigRules(left, right) {
6753
- if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
6754
- if (left.type !== right.type) return left.type.localeCompare(right.type);
6755
- if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
6756
- if (left.type === "callback" && right.type === "callback") {
6757
- if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
6758
- return left.address.localeCompare(right.address);
6759
- }
6760
- if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
6761
- return 0;
6762
- }
6577
+ });
6578
+ };
6763
6579
 
6764
6580
  //#endregion
6765
- //#region src/api/Controllers/getConfigRules.ts
6581
+ //#region src/api/Schema/PositionResponse.ts
6582
+ var PositionResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$2 });
6766
6583
  /**
6767
- * Returns configured rules for the configured chains.
6768
- * @param query - Raw query parameters containing filters/cursor/limit.
6769
- * @param chains - Chains to include in the configured rules.
6770
- * @returns Config rules response payload. {@link ApiPayload.Payload}
6584
+ * Creates a `PositionResponse` from a `PositionWithReserved`.
6585
+ * @param position - {@link PositionWithReserved}
6586
+ * @returns The created `PositionResponse`. {@link PositionResponse}
6771
6587
  */
6772
- async function getConfigRules(query, chains) {
6773
- const parsed = safeParse("get_config_rules", query ?? {});
6774
- if (!parsed.success) return failure(parsed.error);
6775
- const { cursor, limit, types, chains: chainIds } = parsed.data;
6776
- const typeFilter = types?.length ? new Set(types) : null;
6777
- const chainFilter = chainIds?.length ? new Set(chainIds) : null;
6778
- const filteredRules = buildConfigRules(chains).filter((rule) => {
6779
- if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
6780
- if (typeFilter && !typeFilter.has(rule.type)) return false;
6781
- return true;
6782
- });
6783
- const checksum = buildConfigRulesChecksum(filteredRules);
6784
- let cursorRule = null;
6785
- if (cursor) try {
6786
- cursorRule = parseCursor(cursor);
6787
- } catch (err) {
6788
- return failure(err);
6789
- }
6790
- if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
6791
- if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
6792
- const startIndex = cursorRule ? findStartIndex(filteredRules, cursorRule) : 0;
6793
- const page = filteredRules.slice(startIndex, startIndex + limit);
6794
- const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
6795
- const response = success({
6796
- data: page,
6797
- cursor: nextCursor
6798
- });
6799
- response.body.meta.checksum = checksum;
6800
- return response;
6801
- }
6802
- function formatCursor(rule) {
6803
- if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
6804
- if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
6805
- return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
6806
- }
6807
- function parseCursor(cursor) {
6808
- const [type, chain, ...rest] = cursor.split(":");
6809
- if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
6810
- if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
6811
- const chain_id = Number.parseInt(chain, 10);
6812
- if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
6813
- if (type === "maturity") {
6814
- const timestampValue = Number.parseInt(rest[0] ?? "", 10);
6815
- const nameValue = rest.slice(1).join(":");
6816
- if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
6817
- if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
6818
- return {
6819
- type,
6820
- chain_id,
6821
- timestamp: parseMaturity(timestampValue),
6822
- name: nameValue
6823
- };
6824
- }
6825
- if (type === "callback") {
6826
- const callbackTypeValue = rest[0] ?? "";
6827
- const addressValue = rest.slice(1).join(":");
6828
- if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
6829
- if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
6830
- return {
6831
- type,
6832
- chain_id,
6833
- callback_type: callbackTypeValue,
6834
- address: parseAddress(addressValue, "Cursor address")
6835
- };
6836
- }
6837
- const addressValue = rest.join(":");
6838
- if (!addressValue) throw new BadRequestError("Cursor must be in the format loan_token:chain_id:address");
6588
+ function from$2(position) {
6839
6589
  return {
6840
- type,
6841
- chain_id,
6842
- address: parseAddress(addressValue, "Cursor address")
6590
+ chain_id: position.chainId,
6591
+ contract: position.contract,
6592
+ user: position.user,
6593
+ reserved: position.reserved.toString(),
6594
+ block_number: position.blockNumber
6843
6595
  };
6844
6596
  }
6845
- function findStartIndex(rules, cursor) {
6846
- let low = 0;
6847
- let high = rules.length;
6848
- while (low < high) {
6849
- const mid = Math.floor((low + high) / 2);
6850
- const current = rules[mid];
6851
- if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
6852
- else high = mid;
6853
- }
6854
- return low;
6855
- }
6856
- function parseAddress(address, label) {
6857
- if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
6858
- return address.toLowerCase();
6859
- }
6860
- function isConfigRuleType(value) {
6861
- return value === "maturity" || value === "callback" || value === "loan_token";
6862
- }
6863
- function isMaturityType(value) {
6864
- return Object.values(MaturityType).includes(value);
6865
- }
6866
- function parseMaturity(value) {
6867
- try {
6868
- return from$16(value);
6869
- } catch (err) {
6870
- throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
6871
- }
6872
- }
6873
- function isCallbackType(value) {
6874
- if (value === Type$1.BuyWithEmptyCallback) return false;
6875
- return Object.values(Type$1).includes(value);
6876
- }
6877
6597
 
6878
6598
  //#endregion
6879
- //#region src/api/Controllers/getDocs.ts
6880
- const __dirname$1 = (() => {
6599
+ //#region src/api/Schema/requests.ts
6600
+ const MAX_LIMIT = 100;
6601
+ const DEFAULT_LIMIT$4 = 20;
6602
+ const CONFIG_RULES_MAX_LIMIT = 1e3;
6603
+ const CONFIG_RULES_DEFAULT_LIMIT = 100;
6604
+ const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
6605
+ const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
6606
+ /** Validate cursor is a valid base64url-encoded JSON object.
6607
+ * Domain layer handles semantic validation of cursor fields. */
6608
+ function isValidBase64urlJson(val) {
6881
6609
  try {
6882
- return (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
6610
+ const decoded = Buffer.from(val, "base64url").toString("utf8");
6611
+ JSON.parse(decoded);
6612
+ return true;
6883
6613
  } catch {
6884
- return process.cwd();
6614
+ return false;
6885
6615
  }
6886
- })();
6887
- /**
6888
- * Build the OpenAPI document for the router.
6889
- * @returns OpenAPI document. {@link OpenAPIDocument}
6890
- */
6891
- async function getSwaggerJson() {
6892
- return OpenApi();
6893
- }
6894
- /**
6895
- * Render the API documentation HTML page.
6896
- * @returns HTML page as string.
6897
- */
6898
- async function getDocsHtml() {
6899
- const spec = await OpenApi();
6900
- return `<!DOCTYPE html>
6901
- <html>
6902
- <head>
6903
- <meta charset="UTF-8">
6904
- <title>Router API Docs (Scalar)</title>
6905
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
6906
- <style>
6907
- html, body { margin: 0; height: 100%; }
6908
- api-reference { height: 100%; width: 100%; }
6909
- </style>
6910
- </head>
6911
- <body>
6912
- <div id="api-container" style="height:100%;width:100%;"></div>
6913
- <script>
6914
- window.addEventListener('load', function () {
6915
- const spec = ${JSON.stringify(spec)};
6916
- Scalar.createApiReference('#api-container', { spec: { content: spec, hideModels: true } });
6917
- });
6918
- <\/script>
6919
- </body>
6920
- </html>`;
6921
- }
6922
- /**
6923
- * Finds the integrator.md file.
6924
- * Handles source, bundled CLI, and Lambda scenarios.
6925
- */
6926
- function findIntegratorMd() {
6927
- const candidates = [
6928
- (0, node_path.resolve)(__dirname$1, "../../../docs/integrator.md"),
6929
- (0, node_path.resolve)(__dirname$1, "../docs/integrator.md"),
6930
- (0, node_path.resolve)(process.cwd(), "docs/integrator.md")
6931
- ];
6932
- for (const candidate of candidates) if ((0, node_fs.existsSync)(candidate)) return candidate;
6933
- throw new Error(`integrator.md not found. Tried: ${candidates.join(", ")}`);
6934
6616
  }
6935
- /**
6936
- * Renders the integrator documentation as HTML.
6937
- * @returns HTML page with the rendered markdown documentation.
6938
- */
6939
- async function getIntegratorDocsHtml() {
6940
- return `<!DOCTYPE html>
6941
- <html>
6942
- <head>
6943
- <meta charset="UTF-8">
6944
- <title>Documentation</title>
6945
- <style>
6946
- body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
6947
- pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; }
6948
- code { background: #f4f4f4; padding: 0.2rem 0.4rem; }
6949
- a { color: #0066cc; }
6950
- </style>
6951
- </head>
6952
- <body>
6953
- <nav><a href="/docs/api">API Reference &rarr;</a></nav>
6954
- ${await (0, marked.marked)(await (0, node_fs_promises.readFile)(findIntegratorMd(), "utf-8"))}
6955
- </body>
6956
- </html>`;
6617
+ function isValidOfferHashCursor(val) {
6618
+ return /^0x[a-f0-9]{64}$/i.test(val);
6957
6619
  }
6958
-
6959
- //#endregion
6960
- //#region src/api/Controllers/getHealth.ts
6961
- async function getHealth(query, db, chainRegistry) {
6962
- const logger = getLogger();
6963
- try {
6964
- const parsed = safeParse("get_health", query);
6965
- if (!parsed.success) return failure(parsed.error);
6966
- const snapshot = await create$16({
6967
- db,
6968
- chainRegistry
6969
- }).getSnapshot();
6970
- 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({
6971
- missingChains: snapshot.missingChains,
6972
- missingCollectors: snapshot.missingCollectors
6973
- })));
6974
- return success({ data: toSnakeCase$1({
6975
- status: snapshot.status,
6976
- initialized: snapshot.initialized,
6977
- missingChains: snapshot.missingChains,
6978
- missingCollectors: snapshot.missingCollectors
6979
- }) });
6980
- } catch (err) {
6981
- logger.error({
6982
- err,
6983
- msg: "Error getting health status",
6984
- errorMessage: err instanceof Error ? err.message : String(err),
6985
- errorStack: err instanceof Error ? err.stack : void 0
6986
- });
6987
- return failure(err);
6620
+ const csvArray = (schema) => zod.preprocess((value) => {
6621
+ if (value === void 0) return void 0;
6622
+ if (Array.isArray(value)) {
6623
+ if (value.some((item) => typeof item !== "string")) return value;
6624
+ return value.flatMap((item) => item.split(",")).map((item) => item.trim()).filter((item) => item.length > 0);
6988
6625
  }
6989
- }
6990
- async function getHealthChains(query, db, healthClients, chainRegistry) {
6991
- const logger = getLogger();
6992
- try {
6993
- const parsed = safeParse("get_health_chains", query);
6994
- if (!parsed.success) return failure(parsed.error);
6995
- const snapshot = await create$16({
6996
- db,
6997
- healthClients,
6998
- chainRegistry
6999
- }).getSnapshot();
7000
- 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({
7001
- missingChains: snapshot.missingChains,
7002
- missingCollectors: snapshot.missingCollectors
7003
- })));
7004
- const chains = snapshot.chains;
7005
- return success({ data: chains.map(({ chainId, localBlockNumber, remoteBlockNumber, updatedAt, initialized }) => toSnakeCase$1({
7006
- chainId,
7007
- localBlockNumber,
7008
- remoteBlockNumber,
7009
- updatedAt,
7010
- initialized
7011
- })) });
7012
- } catch (err) {
7013
- logger.error({
7014
- err,
7015
- msg: "Error getting health status for chains",
7016
- errorMessage: err instanceof Error ? err.message : String(err),
7017
- errorStack: err instanceof Error ? err.stack : void 0
6626
+ if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
6627
+ return value;
6628
+ }, zod.array(schema)).optional();
6629
+ const PaginationQueryParams = zod.object({
6630
+ cursor: zod.string().optional().refine((val) => {
6631
+ if (!val) return true;
6632
+ return isValidBase64urlJson(val);
6633
+ }, { message: "Invalid cursor format. Must be a valid base64url-encoded cursor object" }).meta({
6634
+ description: "Pagination cursor in base64url-encoded format",
6635
+ example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
6636
+ }),
6637
+ limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6638
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6639
+ example: 10
6640
+ })
6641
+ });
6642
+ const ConfigRuleTypes = zod.enum([
6643
+ "maturity",
6644
+ "callback",
6645
+ "loan_token",
6646
+ "oracle"
6647
+ ]);
6648
+ const GetConfigRulesQueryParams = zod.object({
6649
+ cursor: zod.string().regex(/^(maturity|callback|loan_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6650
+ description: "Pagination cursor in type:chain_id:<value> format",
6651
+ example: "maturity:1:1730415600:end_of_next_month"
6652
+ }),
6653
+ limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(CONFIG_RULES_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_RULES_MAX_LIMIT}` })).optional().default(CONFIG_RULES_DEFAULT_LIMIT).meta({
6654
+ description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
6655
+ example: 100
6656
+ }),
6657
+ types: csvArray(ConfigRuleTypes).meta({
6658
+ description: "Filter by rule types (comma-separated).",
6659
+ example: "maturity,loan_token,oracle"
6660
+ }),
6661
+ chains: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6662
+ description: "Filter by chain IDs (comma-separated).",
6663
+ example: "1,8453"
6664
+ })
6665
+ });
6666
+ const GetConfigContractsQueryParams = zod.object({
6667
+ cursor: zod.string().regex(/^[1-9]\d*:0x[a-fA-F0-9]{40}$/, { message: "Cursor must be in the format chain_id:0x..." }).optional().meta({
6668
+ description: "Pagination cursor in chain_id:address format (lowercase address).",
6669
+ example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
6670
+ }),
6671
+ limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(CONFIG_CONTRACTS_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_CONTRACTS_MAX_LIMIT}` })).optional().default(CONFIG_CONTRACTS_DEFAULT_LIMIT).meta({
6672
+ description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
6673
+ example: 1e3
6674
+ }),
6675
+ chains: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6676
+ description: "Filter by chain IDs (comma-separated).",
6677
+ example: "1,8453"
6678
+ })
6679
+ });
6680
+ const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend({
6681
+ cursor: zod.string().optional().meta({
6682
+ description: "Pagination cursor. Use offer hash (0x...) for maker queries, base64url for obligation queries.",
6683
+ example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
6684
+ }),
6685
+ side: zod.enum(["buy", "sell"]).optional().meta({
6686
+ description: "Side of the offer. Required when using obligation_id.",
6687
+ example: "buy"
6688
+ }),
6689
+ obligation_id: zod.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({
6690
+ description: "Offers obligation id. Required when not using maker.",
6691
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6692
+ }),
6693
+ maker: zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Maker must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).optional().meta({
6694
+ description: "Maker address to filter offers by. Alternative to obligation_id + side.",
6695
+ example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6696
+ })
6697
+ }).superRefine((val, ctx) => {
6698
+ const hasObligation = val.obligation_id !== void 0;
6699
+ const hasSide = val.side !== void 0;
6700
+ const hasMaker = val.maker !== void 0;
6701
+ if (hasMaker && (hasObligation || hasSide)) {
6702
+ ctx.addIssue({
6703
+ code: "custom",
6704
+ message: "Cannot use both maker and obligation_id/side parameters"
7018
6705
  });
7019
- return failure(err);
6706
+ return;
7020
6707
  }
7021
- }
7022
- async function getHealthCollectors(query, db, chainRegistry) {
7023
- const logger = getLogger();
7024
- try {
7025
- const parsed = safeParse("get_health_collectors", query);
7026
- if (!parsed.success) return failure(parsed.error);
7027
- const snapshot = await create$16({
7028
- db,
7029
- chainRegistry
7030
- }).getSnapshot();
7031
- 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({
7032
- missingChains: snapshot.missingChains,
7033
- missingCollectors: snapshot.missingCollectors
7034
- })));
7035
- const collectors = snapshot.collectors;
7036
- return success({ data: collectors.map(({ name, chainId, blockNumber, updatedAt, lag, status, initialized }) => toSnakeCase$1({
7037
- name,
7038
- chainId,
7039
- blockNumber,
7040
- updatedAt,
7041
- lag,
7042
- status,
7043
- initialized
7044
- })) });
7045
- } catch (err) {
7046
- logger.error({
7047
- err,
7048
- msg: "Error getting health status for collectors",
7049
- errorMessage: err instanceof Error ? err.message : String(err),
7050
- errorStack: err instanceof Error ? err.stack : void 0
6708
+ if (hasMaker) {
6709
+ if (val.cursor !== void 0 && !isValidOfferHashCursor(val.cursor)) ctx.addIssue({
6710
+ code: "custom",
6711
+ path: ["cursor"],
6712
+ message: "Cursor must be a 32-byte hex offer hash when filtering by maker"
7051
6713
  });
7052
- return failure(err);
6714
+ return;
6715
+ }
6716
+ if (!hasObligation || !hasSide) ctx.addIssue({
6717
+ code: "custom",
6718
+ message: "Must provide either maker or both obligation_id and side"
6719
+ });
6720
+ if (val.cursor !== void 0 && !isValidBase64urlJson(val.cursor)) ctx.addIssue({
6721
+ code: "custom",
6722
+ path: ["cursor"],
6723
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
6724
+ });
6725
+ }).transform((val) => {
6726
+ if (val.maker && val.cursor) return {
6727
+ ...val,
6728
+ cursor: val.cursor.toLowerCase()
6729
+ };
6730
+ return val;
6731
+ });
6732
+ const GetObligationsQueryParams = zod.object({
6733
+ ...PaginationQueryParams.shape,
6734
+ cursor: zod.string().optional().meta({
6735
+ description: "Obligation id cursor",
6736
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6737
+ }),
6738
+ chains: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6739
+ description: "Filter by chain IDs (comma-separated).",
6740
+ example: "1,8453"
6741
+ }),
6742
+ loan_tokens: csvArray(zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Loan token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6743
+ description: "Filter by loan token addresses (comma-separated).",
6744
+ example: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078,0x34Cf890dB685FC536E05652FB41f02090c3fb751"
6745
+ }),
6746
+ collateral_tokens: csvArray(zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Collateral token must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
6747
+ description: "Filter by collateral tokens (comma-separated, matches any collateral).",
6748
+ example: "0x34Cf890dB685FC536E05652FB41f02090c3fb751,0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078"
6749
+ }),
6750
+ maturities: csvArray(zod.string().regex(/^[1-9]\d*$/, { message: "Maturity must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6751
+ description: "Filter by exact maturity timestamps (comma-separated, unix seconds).",
6752
+ example: "1761922800,1764524800"
6753
+ })
6754
+ });
6755
+ const GetObligationParams = zod.object({ obligation_id: zod.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({
6756
+ description: "Obligation id",
6757
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6758
+ }) });
6759
+ /** Validate a book cursor format: {side, lastPrice, offersCursor} */
6760
+ function isValidBookCursor(cursorString) {
6761
+ const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
6762
+ try {
6763
+ const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
6764
+ return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6765
+ } catch {
6766
+ return false;
7053
6767
  }
7054
6768
  }
6769
+ const BookPaginationQueryParams = zod.object({
6770
+ cursor: zod.string().optional().refine((value) => {
6771
+ if (!value) return true;
6772
+ return isValidBookCursor(value);
6773
+ }, { message: "Invalid cursor format. Must be a valid base64url-encoded book cursor object" }).meta({
6774
+ description: "Pagination cursor in base64url-encoded format for book levels",
6775
+ example: "eyJzaWRlIjoiYnV5IiwibGFzdFJhdGUiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwib2ZmZXJzQ3Vyc29yIjpudWxsfQ"
6776
+ }),
6777
+ limit: zod.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(zod.number().max(MAX_LIMIT, { message: `Limit cannot exceed ${MAX_LIMIT}` })).optional().default(DEFAULT_LIMIT$4).meta({
6778
+ description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT$4}`,
6779
+ example: 10
6780
+ })
6781
+ });
6782
+ const HealthQueryParams = zod.object({ strict: zod.enum([
6783
+ "true",
6784
+ "false",
6785
+ "1",
6786
+ "0"
6787
+ ]).transform((value) => value === "true" || value === "1").optional().meta({
6788
+ description: "Enable strict mode to fail health checks when initialization is incomplete.",
6789
+ example: "true"
6790
+ }) });
6791
+ const GetBookParams = zod.object({
6792
+ ...BookPaginationQueryParams.shape,
6793
+ obligation_id: zod.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({
6794
+ description: "Obligation id",
6795
+ example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6796
+ }),
6797
+ side: zod.enum(["buy", "sell"]).meta({
6798
+ description: "Side of the book (buy or sell).",
6799
+ example: "buy"
6800
+ })
6801
+ });
6802
+ const ValidateOffersBody = zod.object({ offers: zod.array(zod.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
6803
+ const GetUserPositionsParams = zod.object({
6804
+ ...PaginationQueryParams.shape,
6805
+ user_address: zod.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
6806
+ description: "User address to get positions for",
6807
+ example: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6808
+ })
6809
+ });
6810
+ const schemas = {
6811
+ get_health: HealthQueryParams,
6812
+ get_health_collectors: HealthQueryParams,
6813
+ get_health_chains: HealthQueryParams,
6814
+ get_config_contracts: GetConfigContractsQueryParams,
6815
+ get_config_rules: GetConfigRulesQueryParams,
6816
+ get_offers: GetOffersQueryParams,
6817
+ get_obligations: GetObligationsQueryParams,
6818
+ get_obligation: GetObligationParams,
6819
+ get_book: GetBookParams,
6820
+ validate_offers: ValidateOffersBody,
6821
+ get_user_positions: GetUserPositionsParams
6822
+ };
6823
+ function parse(action, query) {
6824
+ return schemas[action].parse(query);
6825
+ }
6826
+ function safeParse(action, query, error) {
6827
+ return schemas[action].safeParse(query, { error });
6828
+ }
7055
6829
 
7056
6830
  //#endregion
7057
- //#region src/api/Controllers/getObligation.ts
7058
- async function getObligation(params, db) {
6831
+ //#region src/api/Controllers/getBook.ts
6832
+ async function getBook(params, db) {
7059
6833
  const logger = getLogger();
7060
- const result = safeParse("get_obligation", params, (issue) => issue.message);
6834
+ const result = safeParse("get_book", params, (issue) => issue.message);
7061
6835
  if (!result.success) return failure(result.error);
7062
6836
  const query = result.data;
7063
6837
  try {
7064
- const { obligations } = await db.offers.getObligations({ ids: [query.obligation_id] });
7065
- if (obligations.length === 0) return failure(new NotFoundError("Obligation not found"));
7066
- const obligation = obligations[0];
7067
- const [quote] = await db.offers.getQuotes({ obligationIds: [id(obligation)] });
6838
+ const { levels, nextCursor } = await db.book.get({
6839
+ side: query.side,
6840
+ obligationId: query.obligation_id,
6841
+ cursor: query.cursor,
6842
+ limit: query.limit
6843
+ });
7068
6844
  return success({
7069
- data: from$4(obligation, quote ?? {
7070
- obligationId: id(obligation),
7071
- ask: { price: 0n },
7072
- bid: { price: 0n }
7073
- }),
7074
- cursor: null
6845
+ data: levels.map(from$5),
6846
+ cursor: nextCursor
7075
6847
  });
7076
6848
  } catch (err) {
7077
6849
  logger.error({
7078
6850
  err,
7079
- msg: "Error get obligation",
6851
+ msg: "Error get book",
7080
6852
  errorMessage: err instanceof Error ? err.message : String(err),
7081
6853
  errorStack: err instanceof Error ? err.stack : void 0
7082
6854
  });
@@ -7085,463 +6857,649 @@ async function getObligation(params, db) {
7085
6857
  }
7086
6858
 
7087
6859
  //#endregion
7088
- //#region src/api/Controllers/getObligations.ts
7089
- async function getObligations$1(queryParameters, db) {
7090
- const logger = getLogger();
7091
- const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
7092
- if (!result.success) return failure(result.error);
7093
- const query = result.data;
7094
- try {
7095
- const chainIds = query.chains?.length ? query.chains : void 0;
7096
- const loanTokens = query.loan_tokens?.length ? query.loan_tokens : void 0;
7097
- const collateralTokens = query.collateral_tokens?.length ? query.collateral_tokens : void 0;
7098
- const maturities = query.maturities?.length ? query.maturities : void 0;
7099
- const { obligations, nextCursor } = await db.offers.getObligations({
7100
- cursor: query.cursor,
7101
- limit: query.limit,
7102
- chainId: chainIds,
7103
- loanToken: loanTokens,
7104
- collateralToken: collateralTokens,
7105
- maturity: maturities
7106
- });
7107
- const quotes = await db.offers.getQuotes({ obligationIds: obligations.map((o) => id(o)) });
7108
- return success({
7109
- data: obligations.map((o) => from$4(o, quotes.find((q) => q.obligationId === id(o)) ?? {
7110
- obligationId: id(o),
7111
- ask: { price: 0n },
7112
- bid: { price: 0n }
7113
- })),
7114
- cursor: nextCursor ?? null
7115
- });
6860
+ //#region src/api/Controllers/getConfigContracts.ts
6861
+ const CONFIG_CONTRACT_NAMES = [
6862
+ "mempool",
6863
+ "multicall",
6864
+ "v2"
6865
+ ];
6866
+ /**
6867
+ * Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
6868
+ * @param query - Raw query parameters containing optional chain filters.
6869
+ * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
6870
+ * @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
6871
+ */
6872
+ async function getConfigContracts(query, chainRegistry) {
6873
+ const parsed = safeParse("get_config_contracts", query ?? {});
6874
+ if (!parsed.success) return failure(parsed.error);
6875
+ const { chains: chainsFilter, cursor, limit } = parsed.data;
6876
+ const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
6877
+ const contracts = [];
6878
+ const seenAddresses = /* @__PURE__ */ new Set();
6879
+ for (const chain of chainRegistry.list()) {
6880
+ if (chainFilter && !chainFilter.has(chain.id)) continue;
6881
+ const mempool = chain.custom?.mempool?.address;
6882
+ if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
6883
+ const multicall = chain.contracts?.multicall3?.address;
6884
+ if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
6885
+ const v2 = chain.custom?.morpho?.address;
6886
+ if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
6887
+ const chainContracts = [
6888
+ {
6889
+ chain_id: chain.id,
6890
+ name: "mempool",
6891
+ address: mempool
6892
+ },
6893
+ {
6894
+ chain_id: chain.id,
6895
+ name: "multicall",
6896
+ address: multicall
6897
+ },
6898
+ {
6899
+ chain_id: chain.id,
6900
+ name: "v2",
6901
+ address: v2
6902
+ }
6903
+ ];
6904
+ for (const contract of chainContracts) {
6905
+ const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
6906
+ if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
6907
+ seenAddresses.add(cursorKey);
6908
+ contracts.push(contract);
6909
+ }
6910
+ }
6911
+ contracts.sort((a, b) => {
6912
+ if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
6913
+ const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
6914
+ if (addressCompare !== 0) return addressCompare;
6915
+ return a.name.localeCompare(b.name);
6916
+ });
6917
+ let cursorContract = null;
6918
+ if (cursor) try {
6919
+ cursorContract = parseCursor$1(cursor);
7116
6920
  } catch (err) {
7117
- logger.error({
7118
- err,
7119
- msg: "Error get obligations",
7120
- errorMessage: err instanceof Error ? err.message : String(err),
7121
- errorStack: err instanceof Error ? err.stack : void 0
7122
- });
7123
6921
  return failure(err);
7124
6922
  }
6923
+ const startIndex = cursorContract ? findStartIndex$1(contracts, cursorContract) : 0;
6924
+ const page = contracts.slice(startIndex, startIndex + limit);
6925
+ const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
6926
+ return success({
6927
+ data: page,
6928
+ cursor: nextCursor
6929
+ });
7125
6930
  }
7126
-
7127
- //#endregion
7128
- //#region src/database/constants.ts
7129
- /**
7130
- * Default batch size for bulk database inserts.
7131
- *
7132
- * PostgreSQL limits a single query to at most 65,535 parameters
7133
- * (e.g. $1, $2, ...). In bulk inserts, each row consumes one
7134
- * parameter per column, so inserting too many rows at once can
7135
- * exceed this limit.
7136
- *
7137
- * Our largest batched insert is into the `offers` table with 15 columns.
7138
- * 15 cols × 4,000 rows = 60,000 parameters, safely under 65,535.
7139
- */
7140
- const DEFAULT_BATCH_SIZE$1 = 4e3;
7141
-
7142
- //#endregion
7143
- //#region src/database/drizzle/VERSION.ts
7144
- const VERSION = "router_v1.6";
7145
-
7146
- //#endregion
7147
- //#region src/database/drizzle/schema.ts
7148
- var schema_exports = /* @__PURE__ */ __exportAll({
7149
- PositionTypes: () => PositionTypes,
7150
- StatusCode: () => StatusCode,
7151
- TABLE_NAMES: () => TABLE_NAMES,
7152
- VERSIONED_TABLE_NAMES: () => VERSIONED_TABLE_NAMES,
7153
- callbacks: () => callbacks,
7154
- chains: () => chains$1,
7155
- collectors: () => collectors,
7156
- consumedEvents: () => consumedEvents,
7157
- groups: () => groups,
7158
- lots: () => lots,
7159
- merklePaths: () => merklePaths,
7160
- obligationCollateralsV2: () => obligationCollateralsV2,
7161
- obligations: () => obligations,
7162
- offers: () => offers,
7163
- offersCallbacks: () => offersCallbacks,
7164
- offsets: () => offsets,
7165
- oracles: () => oracles,
7166
- positionTypes: () => positionTypes,
7167
- positions: () => positions,
7168
- status: () => status,
7169
- transfers: () => transfers,
7170
- trees: () => trees,
7171
- validations: () => validations
7172
- });
7173
- const s = (0, drizzle_orm_pg_core.pgSchema)(VERSION);
7174
- var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
7175
- EnumTableName["OBLIGATIONS"] = "obligations";
7176
- EnumTableName["GROUPS"] = "groups";
7177
- EnumTableName["CONSUMED_EVENTS"] = "consumed_events";
7178
- EnumTableName["OBLIGATION_COLLATERALS_V2"] = "obligation_collaterals_v2";
7179
- EnumTableName["ORACLES"] = "oracles";
7180
- EnumTableName["OFFERS"] = "offers";
7181
- EnumTableName["OFFERS_CALLBACKS"] = "offers_callbacks";
7182
- EnumTableName["CALLBACKS"] = "callbacks";
7183
- EnumTableName["POSITIONS"] = "positions";
7184
- EnumTableName["TRANSFERS"] = "transfers";
7185
- EnumTableName["VALIDATIONS"] = "validations";
7186
- EnumTableName["COLLECTORS"] = "collectors";
7187
- EnumTableName["CHAINS"] = "chains";
7188
- EnumTableName["LOTS"] = "lots";
7189
- EnumTableName["OFFSETS"] = "offsets";
7190
- EnumTableName["TREES"] = "trees";
7191
- EnumTableName["MERKLE_PATHS"] = "merkle_paths";
7192
- return EnumTableName;
7193
- }(EnumTableName || {});
7194
- const TABLE_NAMES = Object.values(EnumTableName);
7195
- const VERSIONED_TABLE_NAMES = TABLE_NAMES.map((table) => `"${VERSION}"."${table}"`);
7196
- const obligations = s.table(EnumTableName.OBLIGATIONS, {
7197
- obligationId: (0, drizzle_orm_pg_core.varchar)("obligation_id", { length: 66 }).primaryKey(),
7198
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7199
- loanToken: (0, drizzle_orm_pg_core.varchar)("loan_token", { length: 42 }).notNull(),
7200
- maturity: (0, drizzle_orm_pg_core.integer)("maturity").notNull()
7201
- });
7202
- const groups = s.table(EnumTableName.GROUPS, {
7203
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7204
- maker: (0, drizzle_orm_pg_core.varchar)("maker", { length: 42 }).notNull(),
7205
- group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
7206
- consumed: (0, drizzle_orm_pg_core.numeric)("consumed", {
7207
- precision: 78,
7208
- scale: 0
7209
- }).notNull(),
7210
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7211
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7212
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
7213
- columns: [
7214
- table.chainId,
7215
- table.maker,
7216
- table.group
7217
- ],
7218
- name: "groups_pk"
7219
- }), (0, drizzle_orm_pg_core.index)("groups_chain_id_maker_group_consumed_idx").on(table.chainId, table.maker, table.group, table.consumed)]);
7220
- const consumedEvents = s.table(EnumTableName.CONSUMED_EVENTS, {
7221
- eventId: (0, drizzle_orm_pg_core.varchar)("event_id", { length: 128 }).primaryKey(),
7222
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7223
- maker: (0, drizzle_orm_pg_core.varchar)("maker", { length: 42 }).notNull(),
7224
- group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
7225
- amount: (0, drizzle_orm_pg_core.numeric)("amount", {
7226
- precision: 78,
7227
- scale: 0
7228
- }).notNull(),
7229
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7230
- createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
7231
- }, (t) => [
7232
- (0, drizzle_orm_pg_core.foreignKey)({
7233
- columns: [
7234
- t.chainId,
7235
- t.maker,
7236
- t.group
7237
- ],
7238
- foreignColumns: [
7239
- groups.chainId,
7240
- groups.maker,
7241
- groups.group
7242
- ],
7243
- name: "consumed_events_groups_fk"
7244
- }).onDelete("cascade"),
7245
- (0, drizzle_orm_pg_core.index)("consumed_events_group_idx").on(t.chainId, t.maker, t.group),
7246
- (0, drizzle_orm_pg_core.index)("consumed_events_block_number_idx").on(t.blockNumber)
7247
- ]);
7248
- const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2, {
7249
- obligationId: (0, drizzle_orm_pg_core.varchar)("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
7250
- asset: (0, drizzle_orm_pg_core.varchar)("asset", { length: 42 }).notNull(),
7251
- oracleChainId: (0, drizzle_orm_pg_core.bigint)("oracle_chain_id", { mode: "number" }).$type().notNull(),
7252
- oracleAddress: (0, drizzle_orm_pg_core.varchar)("oracle_address", { length: 42 }).notNull(),
7253
- lltv: (0, drizzle_orm_pg_core.bigint)("lltv", { mode: "bigint" }).notNull(),
7254
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7255
- }, (table) => [
7256
- (0, drizzle_orm_pg_core.primaryKey)({
7257
- columns: [table.obligationId, table.asset],
7258
- name: "obligation_collaterals_v2_pk"
7259
- }),
7260
- (0, drizzle_orm_pg_core.foreignKey)({
7261
- columns: [table.oracleChainId, table.oracleAddress],
7262
- foreignColumns: [oracles.chainId, oracles.address],
7263
- name: "obligation_collaterals_v2_oracles_fk"
7264
- }),
7265
- (0, drizzle_orm_pg_core.index)("obligation_collaterals_v2_obligation_id_idx").on(table.obligationId),
7266
- (0, drizzle_orm_pg_core.index)("obligation_collaterals_v2_oracle_fk_idx").on(table.oracleChainId, table.oracleAddress)
7267
- ]);
7268
- const oracles = s.table(EnumTableName.ORACLES, {
7269
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7270
- address: (0, drizzle_orm_pg_core.varchar)("address", { length: 42 }).notNull(),
7271
- price: (0, drizzle_orm_pg_core.numeric)("price", {
7272
- precision: 78,
7273
- scale: 0
7274
- }),
7275
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7276
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7277
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
7278
- columns: [table.chainId, table.address],
7279
- name: "oracles_pk"
7280
- })]);
7281
- const offers = s.table(EnumTableName.OFFERS, {
7282
- hash: (0, drizzle_orm_pg_core.varchar)("hash", { length: 66 }).primaryKey(),
7283
- obligationId: (0, drizzle_orm_pg_core.varchar)("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
7284
- assets: (0, drizzle_orm_pg_core.numeric)("assets", {
7285
- precision: 78,
7286
- scale: 0
7287
- }).notNull(),
7288
- obligationUnits: (0, drizzle_orm_pg_core.numeric)("obligation_units", {
7289
- precision: 78,
7290
- scale: 0
7291
- }).notNull().default("0"),
7292
- obligationShares: (0, drizzle_orm_pg_core.numeric)("obligation_shares", {
7293
- precision: 78,
7294
- scale: 0
7295
- }).notNull().default("0"),
7296
- price: (0, drizzle_orm_pg_core.numeric)("price", {
7297
- precision: 78,
7298
- scale: 0
7299
- }).notNull(),
7300
- maturity: (0, drizzle_orm_pg_core.integer)("maturity").notNull(),
7301
- expiry: (0, drizzle_orm_pg_core.integer)("expiry").notNull(),
7302
- start: (0, drizzle_orm_pg_core.integer)("start").notNull(),
7303
- groupChainId: (0, drizzle_orm_pg_core.bigint)("group_chain_id", { mode: "number" }).$type().notNull(),
7304
- groupMaker: (0, drizzle_orm_pg_core.varchar)("group_maker", { length: 42 }).notNull(),
7305
- group: (0, drizzle_orm_pg_core.varchar)("group_group", { length: 66 }).notNull(),
7306
- session: (0, drizzle_orm_pg_core.varchar)("session", { length: 66 }).notNull(),
7307
- buy: (0, drizzle_orm_pg_core.boolean)("buy").notNull(),
7308
- callbackAddress: (0, drizzle_orm_pg_core.varchar)("callback_address", { length: 42 }).notNull(),
7309
- callbackData: (0, drizzle_orm_pg_core.text)("callback_data").notNull(),
7310
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7311
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7312
- }, (table) => [
7313
- (0, drizzle_orm_pg_core.foreignKey)({
7314
- columns: [
7315
- table.groupChainId,
7316
- table.groupMaker,
7317
- table.group
7318
- ],
7319
- foreignColumns: [
7320
- groups.chainId,
7321
- groups.maker,
7322
- groups.group
7323
- ],
7324
- name: "offers_groups_fk"
7325
- }).onDelete("cascade"),
7326
- (0, drizzle_orm_pg_core.index)("offers_group_fk_idx").on(table.groupChainId, table.groupMaker, table.group),
7327
- (0, drizzle_orm_pg_core.index)("offers_group_and_hash_idx").on(table.groupChainId, table.groupMaker, table.group, table.hash),
7328
- (0, drizzle_orm_pg_core.index)("offers_obligation_id_side_idx").on(table.obligationId, table.buy)
7329
- ]);
7330
- const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
7331
- offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
7332
- callbackId: (0, drizzle_orm_pg_core.varchar)("callback_id", { length: 66 })
7333
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
7334
- columns: [table.offerHash, table.callbackId],
7335
- name: "offers_callbacks_pk"
7336
- })]);
7337
- const callbacks = s.table(EnumTableName.CALLBACKS, {
7338
- id: (0, drizzle_orm_pg_core.varchar)("id", { length: 66 }).primaryKey(),
7339
- positionChainId: (0, drizzle_orm_pg_core.bigint)("position_chain_id", { mode: "number" }).$type().notNull(),
7340
- positionContract: (0, drizzle_orm_pg_core.varchar)("position_contract", { length: 42 }).notNull(),
7341
- positionUser: (0, drizzle_orm_pg_core.varchar)("position_user", { length: 42 }).notNull(),
7342
- amount: (0, drizzle_orm_pg_core.numeric)("amount", {
7343
- precision: 78,
7344
- scale: 0
7345
- })
7346
- }, (table) => [(0, drizzle_orm_pg_core.foreignKey)({
7347
- columns: [
7348
- table.positionChainId,
7349
- table.positionContract,
7350
- table.positionUser
6931
+ function parseCursor$1(cursor) {
6932
+ const [chain, address] = cursor.split(":", 2);
6933
+ if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
6934
+ return {
6935
+ chain_id: Number.parseInt(chain, 10),
6936
+ address: address.toLowerCase()
6937
+ };
6938
+ }
6939
+ function formatCursor$1(contract) {
6940
+ return `${contract.chain_id}:${contract.address.toLowerCase()}`;
6941
+ }
6942
+ function findStartIndex$1(contracts, cursor) {
6943
+ let low = 0;
6944
+ let high = contracts.length;
6945
+ while (low < high) {
6946
+ const mid = Math.floor((low + high) / 2);
6947
+ const current = contracts[mid];
6948
+ if (compareContract(current, cursor) <= 0) low = mid + 1;
6949
+ else high = mid;
6950
+ }
6951
+ return low;
6952
+ }
6953
+ function compareContract(contract, cursor) {
6954
+ if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
6955
+ return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
6956
+ }
6957
+
6958
+ //#endregion
6959
+ //#region src/gatekeeper/GateConfig.ts
6960
+ const assets = {
6961
+ [ChainId.ETHEREUM.toString()]: [
6962
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6963
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6964
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6965
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6966
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
6967
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7351
6968
  ],
7352
- foreignColumns: [
7353
- positions.chainId,
7354
- positions.contract,
7355
- positions.user
6969
+ [ChainId.BASE.toString()]: [
6970
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
6971
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
6972
+ "0x4200000000000000000000000000000000000006",
6973
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6974
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
6975
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
6976
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
7356
6977
  ],
7357
- name: "callbacks_positions_fk"
7358
- }).onDelete("cascade")]);
7359
- const lots = s.table(EnumTableName.LOTS, {
7360
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7361
- user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
7362
- contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
7363
- group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
7364
- lower: (0, drizzle_orm_pg_core.numeric)("lower", {
7365
- precision: 78,
7366
- scale: 0
7367
- }).notNull(),
7368
- upper: (0, drizzle_orm_pg_core.numeric)("upper", {
7369
- precision: 78,
7370
- scale: 0
7371
- }).notNull()
7372
- }, (table) => [
7373
- (0, drizzle_orm_pg_core.primaryKey)({
7374
- columns: [
7375
- table.chainId,
7376
- table.user,
7377
- table.contract,
7378
- table.group
7379
- ],
7380
- name: "lots_pk"
7381
- }),
7382
- (0, drizzle_orm_pg_core.foreignKey)({
7383
- columns: [
7384
- table.chainId,
7385
- table.contract,
7386
- table.user
7387
- ],
7388
- foreignColumns: [
7389
- positions.chainId,
7390
- positions.contract,
7391
- positions.user
7392
- ],
7393
- name: "lots_positions_fk"
7394
- }).onDelete("cascade"),
7395
- (0, drizzle_orm_pg_core.foreignKey)({
7396
- columns: [
7397
- table.chainId,
7398
- table.user,
7399
- table.group
7400
- ],
7401
- foreignColumns: [
7402
- groups.chainId,
7403
- groups.maker,
7404
- groups.group
7405
- ],
7406
- name: "lots_groups_fk"
7407
- }).onDelete("cascade")
7408
- ]);
7409
- const offsets = s.table(EnumTableName.OFFSETS, {
7410
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7411
- user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
7412
- contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
7413
- group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
7414
- value: (0, drizzle_orm_pg_core.numeric)("value", {
7415
- precision: 78,
7416
- scale: 0
7417
- }).notNull()
7418
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
7419
- columns: [
7420
- table.chainId,
7421
- table.user,
7422
- table.contract,
7423
- table.group
6978
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
6979
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6980
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6981
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6982
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
6983
+ "0xce79ddb3152d52ff8fe65a4c7e058b035fcb560a"
7424
6984
  ],
7425
- name: "offsets_pk"
7426
- }), (0, drizzle_orm_pg_core.foreignKey)({
7427
- columns: [
7428
- table.chainId,
7429
- table.contract,
7430
- table.user
6985
+ [ChainId.ANVIL.toString()]: [
6986
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
6987
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F",
6988
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
6989
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6990
+ ]
6991
+ };
6992
+ const oracles = {
6993
+ [ChainId.ETHEREUM.toString()]: [
6994
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
6995
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
6996
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
6997
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
6998
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
6999
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
7431
7000
  ],
7432
- foreignColumns: [
7433
- positions.chainId,
7434
- positions.contract,
7435
- positions.user
7001
+ [ChainId.BASE.toString()]: [
7002
+ "0xD09048c8B568Dbf5f189302beA26c9edABFC4858",
7003
+ "0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4",
7004
+ "0x05D2618404668D725B66c0f32B39e4EC15B393dC",
7005
+ "0xE1bb8E5b4930eC9FeC7f7943FCF6227649F14B37",
7006
+ "0x663BECd10daE6C4A3Dcd89F1d76c1174199639B9",
7007
+ "0x10b95702a0ce895972C91e432C4f7E19811D320E",
7008
+ "0x8C87DbD7A0c647A4291592Bc2994dbF95880fE2F",
7009
+ "0x4A11590e5326138B514E08A9B52202D42077Ca65",
7010
+ "0xa54122f0E0766258377Ffe732e454A3248f454F4"
7436
7011
  ],
7437
- name: "offsets_positions_fk"
7438
- }).onDelete("cascade")]);
7439
- const PositionTypes = s.enum("position_type", Object.values(Type));
7440
- const positionTypes = s.table("position_types", {
7441
- id: (0, drizzle_orm_pg_core.serial)("id").primaryKey(),
7442
- type: PositionTypes("type").notNull()
7443
- });
7444
- const positions = s.table(EnumTableName.POSITIONS, {
7445
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7446
- contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
7447
- user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
7448
- positionTypeId: (0, drizzle_orm_pg_core.integer)("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
7449
- balance: (0, drizzle_orm_pg_core.numeric)("balance", {
7450
- precision: 78,
7451
- scale: 0
7452
- }),
7453
- asset: (0, drizzle_orm_pg_core.varchar)("asset", { length: 42 }),
7454
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7455
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7456
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
7457
- columns: [
7458
- table.chainId,
7459
- table.contract,
7460
- table.user
7012
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
7013
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
7014
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
7015
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
7016
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
7017
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
7018
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
7461
7019
  ],
7462
- name: "positions_pk"
7463
- })]);
7464
- const transfers = s.table(EnumTableName.TRANSFERS, {
7465
- eventId: (0, drizzle_orm_pg_core.varchar)("event_id", { length: 128 }).primaryKey(),
7466
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7467
- contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
7468
- from: (0, drizzle_orm_pg_core.varchar)("from", { length: 42 }).notNull(),
7469
- to: (0, drizzle_orm_pg_core.varchar)("to", { length: 42 }).notNull(),
7470
- value: (0, drizzle_orm_pg_core.numeric)("value", {
7471
- precision: 78,
7472
- scale: 0
7473
- }).notNull(),
7474
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7475
- createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
7476
- }, (table) => [
7477
- (0, drizzle_orm_pg_core.foreignKey)({
7478
- columns: [
7479
- table.chainId,
7480
- table.contract,
7481
- table.from
7482
- ],
7483
- foreignColumns: [
7484
- positions.chainId,
7485
- positions.contract,
7486
- positions.user
7487
- ],
7488
- name: "transfers_positions_from_fk"
7489
- }).onDelete("cascade"),
7490
- (0, drizzle_orm_pg_core.foreignKey)({
7491
- columns: [
7492
- table.chainId,
7493
- table.contract,
7494
- table.to
7495
- ],
7496
- foreignColumns: [
7497
- positions.chainId,
7498
- positions.contract,
7499
- positions.user
7500
- ],
7501
- name: "transfers_positions_to_fk"
7502
- }).onDelete("cascade"),
7503
- (0, drizzle_orm_pg_core.index)("transfers_chain_contract_user_idx").on(table.chainId, table.contract, table.from, table.to, table.blockNumber)
7504
- ]);
7505
- const StatusCode = s.enum("status_code", Object.values(Status));
7506
- const status = s.table("status", {
7507
- id: (0, drizzle_orm_pg_core.serial)("id").primaryKey(),
7508
- code: StatusCode("code").unique()
7509
- });
7510
- const validations = s.table("validations", {
7511
- offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
7512
- statusId: (0, drizzle_orm_pg_core.integer)("status_id").notNull().references(() => status.id, { onDelete: "no action" }),
7513
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7514
- });
7515
- const collectors = s.table(EnumTableName.COLLECTORS, {
7516
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull().references(() => chains$1.chainId, { onDelete: "no action" }),
7517
- name: (0, drizzle_orm_pg_core.text)("name").$type().notNull(),
7518
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7519
- epoch: (0, drizzle_orm_pg_core.numeric)("epoch", {
7520
- precision: 78,
7521
- scale: 0
7522
- }).default("0").notNull(),
7523
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7524
- }, (table) => [(0, drizzle_orm_pg_core.uniqueIndex)("collectors_chain_name_idx").on(table.chainId, table.name)]);
7525
- const chains$1 = s.table(EnumTableName.CHAINS, {
7526
- chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
7527
- blockNumber: (0, drizzle_orm_pg_core.bigint)("block_number", { mode: "number" }).notNull(),
7528
- epoch: (0, drizzle_orm_pg_core.numeric)("epoch", {
7529
- precision: 78,
7530
- scale: 0
7531
- }).default("0").notNull(),
7532
- updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
7533
- }, (table) => [(0, drizzle_orm_pg_core.uniqueIndex)("chains_chain_id_idx").on(table.chainId)]);
7534
- const trees = s.table(EnumTableName.TREES, {
7535
- root: (0, drizzle_orm_pg_core.varchar)("root", { length: 66 }).primaryKey(),
7536
- rootSignature: (0, drizzle_orm_pg_core.varchar)("root_signature", { length: 132 }).notNull(),
7537
- createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
7538
- });
7539
- const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
7540
- offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
7541
- treeRoot: (0, drizzle_orm_pg_core.varchar)("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
7542
- proofNodes: (0, drizzle_orm_pg_core.text)("proof_nodes").notNull(),
7543
- createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
7544
- }, (table) => [(0, drizzle_orm_pg_core.index)("merkle_paths_tree_root_idx").on(table.treeRoot)]);
7020
+ [ChainId.ANVIL.toString()]: [
7021
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
7022
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
7023
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
7024
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
7025
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
7026
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
7027
+ ]
7028
+ };
7029
+ const configs = {
7030
+ ethereum: {
7031
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7032
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
7033
+ },
7034
+ base: {
7035
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7036
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
7037
+ },
7038
+ "ethereum-virtual-testnet": {
7039
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7040
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
7041
+ },
7042
+ anvil: {
7043
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7044
+ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
7045
+ }
7046
+ };
7047
+
7048
+ //#endregion
7049
+ //#region src/gatekeeper/ConfigRules.ts
7050
+ /**
7051
+ * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
7052
+ * @param chains - Chains to include in the configured rules.
7053
+ * @returns Sorted list of config rules.
7054
+ */
7055
+ function buildConfigRules(chains) {
7056
+ const rules = [];
7057
+ for (const chain of chains) {
7058
+ const maturities = configs[chain.name].maturities ?? [];
7059
+ for (const maturityName of maturities) rules.push({
7060
+ type: "maturity",
7061
+ chain_id: chain.id,
7062
+ name: maturityName,
7063
+ timestamp: from$16(maturityName)
7064
+ });
7065
+ const loanTokens = assets[chain.id.toString()] ?? [];
7066
+ for (const address of loanTokens) rules.push({
7067
+ type: "loan_token",
7068
+ chain_id: chain.id,
7069
+ address: normalizeAddress(address)
7070
+ });
7071
+ const oracles$2 = oracles[chain.id.toString()] ?? [];
7072
+ for (const address of oracles$2) rules.push({
7073
+ type: "oracle",
7074
+ chain_id: chain.id,
7075
+ address: normalizeAddress(address)
7076
+ });
7077
+ }
7078
+ rules.sort(compareConfigRules);
7079
+ return rules;
7080
+ }
7081
+ /**
7082
+ * Compute a stable checksum for the provided configured rules.
7083
+ * @param rules - Configured rules to checksum.
7084
+ * @returns MD5 checksum.
7085
+ */
7086
+ function buildConfigRulesChecksum(rules) {
7087
+ const hash = (0, node_crypto.createHash)("md5");
7088
+ const orderedRules = [...rules].sort(compareConfigRules);
7089
+ for (const rule of orderedRules) {
7090
+ if (rule.type === "maturity") {
7091
+ hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
7092
+ continue;
7093
+ }
7094
+ if (rule.type === "callback") {
7095
+ hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
7096
+ continue;
7097
+ }
7098
+ if (rule.type === "oracle") {
7099
+ hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
7100
+ continue;
7101
+ }
7102
+ hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
7103
+ }
7104
+ return hash.digest("hex");
7105
+ }
7106
+ function normalizeAddress(address) {
7107
+ return address.toLowerCase();
7108
+ }
7109
+ function compareConfigRules(left, right) {
7110
+ if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
7111
+ if (left.type !== right.type) return left.type.localeCompare(right.type);
7112
+ if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
7113
+ if (left.type === "callback" && right.type === "callback") {
7114
+ if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
7115
+ return left.address.localeCompare(right.address);
7116
+ }
7117
+ if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
7118
+ if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
7119
+ return 0;
7120
+ }
7121
+
7122
+ //#endregion
7123
+ //#region src/api/Controllers/getConfigRules.ts
7124
+ /**
7125
+ * Returns configured rules for the configured chains.
7126
+ * @param query - Raw query parameters containing filters/cursor/limit.
7127
+ * @param chains - Chains to include in the configured rules.
7128
+ * @returns Config rules response payload. {@link ApiPayload.Payload}
7129
+ */
7130
+ async function getConfigRules(query, chains) {
7131
+ const parsed = safeParse("get_config_rules", query ?? {});
7132
+ if (!parsed.success) return failure(parsed.error);
7133
+ const { cursor, limit, types, chains: chainIds } = parsed.data;
7134
+ const typeFilter = types?.length ? new Set(types) : null;
7135
+ const chainFilter = chainIds?.length ? new Set(chainIds) : null;
7136
+ const filteredRules = buildConfigRules(chains).filter((rule) => {
7137
+ if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
7138
+ if (typeFilter && !typeFilter.has(rule.type)) return false;
7139
+ return true;
7140
+ });
7141
+ const checksum = buildConfigRulesChecksum(filteredRules);
7142
+ let cursorRule = null;
7143
+ if (cursor) try {
7144
+ cursorRule = parseCursor(cursor);
7145
+ } catch (err) {
7146
+ return failure(err);
7147
+ }
7148
+ if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
7149
+ if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
7150
+ const startIndex = cursorRule ? findStartIndex(filteredRules, cursorRule) : 0;
7151
+ const page = filteredRules.slice(startIndex, startIndex + limit);
7152
+ const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
7153
+ const response = success({
7154
+ data: page,
7155
+ cursor: nextCursor
7156
+ });
7157
+ response.body.meta.checksum = checksum;
7158
+ return response;
7159
+ }
7160
+ function formatCursor(rule) {
7161
+ if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
7162
+ if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
7163
+ if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
7164
+ return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
7165
+ }
7166
+ function parseCursor(cursor) {
7167
+ const [type, chain, ...rest] = cursor.split(":");
7168
+ if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
7169
+ if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
7170
+ const chain_id = Number.parseInt(chain, 10);
7171
+ if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
7172
+ if (type === "maturity") {
7173
+ const timestampValue = Number.parseInt(rest[0] ?? "", 10);
7174
+ const nameValue = rest.slice(1).join(":");
7175
+ if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
7176
+ if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
7177
+ return {
7178
+ type,
7179
+ chain_id,
7180
+ timestamp: parseMaturity(timestampValue),
7181
+ name: nameValue
7182
+ };
7183
+ }
7184
+ if (type === "callback") {
7185
+ const callbackTypeValue = rest[0] ?? "";
7186
+ const addressValue = rest.slice(1).join(":");
7187
+ if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
7188
+ if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
7189
+ return {
7190
+ type,
7191
+ chain_id,
7192
+ callback_type: callbackTypeValue,
7193
+ address: parseAddress(addressValue, "Cursor address")
7194
+ };
7195
+ }
7196
+ if (type === "loan_token" || type === "oracle") {
7197
+ const addressValue = rest.join(":");
7198
+ if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
7199
+ return {
7200
+ type,
7201
+ chain_id,
7202
+ address: parseAddress(addressValue, "Cursor address")
7203
+ };
7204
+ }
7205
+ throw new BadRequestError("Cursor has an invalid rule type");
7206
+ }
7207
+ function findStartIndex(rules, cursor) {
7208
+ let low = 0;
7209
+ let high = rules.length;
7210
+ while (low < high) {
7211
+ const mid = Math.floor((low + high) / 2);
7212
+ const current = rules[mid];
7213
+ if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
7214
+ else high = mid;
7215
+ }
7216
+ return low;
7217
+ }
7218
+ function parseAddress(address, label) {
7219
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
7220
+ return address.toLowerCase();
7221
+ }
7222
+ function isConfigRuleType(value) {
7223
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
7224
+ }
7225
+ function isMaturityType(value) {
7226
+ return Object.values(MaturityType).includes(value);
7227
+ }
7228
+ function parseMaturity(value) {
7229
+ try {
7230
+ return from$16(value);
7231
+ } catch (err) {
7232
+ throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
7233
+ }
7234
+ }
7235
+ function isCallbackType(value) {
7236
+ if (value === Type$1.BuyWithEmptyCallback) return false;
7237
+ return Object.values(Type$1).includes(value);
7238
+ }
7239
+
7240
+ //#endregion
7241
+ //#region src/api/Controllers/getDocs.ts
7242
+ const __dirname$1 = (() => {
7243
+ try {
7244
+ return (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
7245
+ } catch {
7246
+ return process.cwd();
7247
+ }
7248
+ })();
7249
+ /**
7250
+ * Build the OpenAPI document for the router.
7251
+ * @returns OpenAPI document. {@link OpenAPIDocument}
7252
+ */
7253
+ async function getSwaggerJson() {
7254
+ return OpenApi();
7255
+ }
7256
+ /**
7257
+ * Render the API documentation HTML page.
7258
+ * @returns HTML page as string.
7259
+ */
7260
+ async function getDocsHtml() {
7261
+ const spec = await OpenApi();
7262
+ return `<!DOCTYPE html>
7263
+ <html>
7264
+ <head>
7265
+ <meta charset="UTF-8">
7266
+ <title>Router API Docs (Scalar)</title>
7267
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
7268
+ <style>
7269
+ html, body { margin: 0; height: 100%; }
7270
+ api-reference { height: 100%; width: 100%; }
7271
+ </style>
7272
+ </head>
7273
+ <body>
7274
+ <div id="api-container" style="height:100%;width:100%;"></div>
7275
+ <script>
7276
+ window.addEventListener('load', function () {
7277
+ const spec = ${JSON.stringify(spec)};
7278
+ Scalar.createApiReference('#api-container', { spec: { content: spec, hideModels: true } });
7279
+ });
7280
+ <\/script>
7281
+ </body>
7282
+ </html>`;
7283
+ }
7284
+ /**
7285
+ * Finds the integrator.md file.
7286
+ * Handles source, bundled CLI, and Lambda scenarios.
7287
+ */
7288
+ function findIntegratorMd() {
7289
+ const candidates = [
7290
+ (0, node_path.resolve)(__dirname$1, "../../../docs/integrator.md"),
7291
+ (0, node_path.resolve)(__dirname$1, "../docs/integrator.md"),
7292
+ (0, node_path.resolve)(process.cwd(), "docs/integrator.md")
7293
+ ];
7294
+ for (const candidate of candidates) if ((0, node_fs.existsSync)(candidate)) return candidate;
7295
+ throw new Error(`integrator.md not found. Tried: ${candidates.join(", ")}`);
7296
+ }
7297
+ /**
7298
+ * Renders the integrator documentation as HTML.
7299
+ * @returns HTML page with the rendered markdown documentation.
7300
+ */
7301
+ async function getIntegratorDocsHtml() {
7302
+ return `<!DOCTYPE html>
7303
+ <html>
7304
+ <head>
7305
+ <meta charset="UTF-8">
7306
+ <title>Documentation</title>
7307
+ <style>
7308
+ body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
7309
+ pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; }
7310
+ code { background: #f4f4f4; padding: 0.2rem 0.4rem; }
7311
+ a { color: #0066cc; }
7312
+ </style>
7313
+ </head>
7314
+ <body>
7315
+ <nav><a href="/docs/api">API Reference &rarr;</a></nav>
7316
+ ${await (0, marked.marked)(await (0, node_fs_promises.readFile)(findIntegratorMd(), "utf-8"))}
7317
+ </body>
7318
+ </html>`;
7319
+ }
7320
+
7321
+ //#endregion
7322
+ //#region src/api/Controllers/getHealth.ts
7323
+ async function getHealth(query, db, chainRegistry) {
7324
+ const logger = getLogger();
7325
+ try {
7326
+ const parsed = safeParse("get_health", query);
7327
+ if (!parsed.success) return failure(parsed.error);
7328
+ const snapshot = await create$16({
7329
+ db,
7330
+ chainRegistry
7331
+ }).getSnapshot();
7332
+ 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({
7333
+ missingChains: snapshot.missingChains,
7334
+ missingCollectors: snapshot.missingCollectors
7335
+ })));
7336
+ return success({ data: toSnakeCase$1({
7337
+ status: snapshot.status,
7338
+ initialized: snapshot.initialized,
7339
+ missingChains: snapshot.missingChains,
7340
+ missingCollectors: snapshot.missingCollectors
7341
+ }) });
7342
+ } catch (err) {
7343
+ logger.error({
7344
+ err,
7345
+ msg: "Error getting health status",
7346
+ errorMessage: err instanceof Error ? err.message : String(err),
7347
+ errorStack: err instanceof Error ? err.stack : void 0
7348
+ });
7349
+ return failure(err);
7350
+ }
7351
+ }
7352
+ async function getHealthChains(query, db, healthClients, chainRegistry) {
7353
+ const logger = getLogger();
7354
+ try {
7355
+ const parsed = safeParse("get_health_chains", query);
7356
+ if (!parsed.success) return failure(parsed.error);
7357
+ const snapshot = await create$16({
7358
+ db,
7359
+ healthClients,
7360
+ chainRegistry
7361
+ }).getSnapshot();
7362
+ 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({
7363
+ missingChains: snapshot.missingChains,
7364
+ missingCollectors: snapshot.missingCollectors
7365
+ })));
7366
+ const chains = snapshot.chains;
7367
+ return success({ data: chains.map(({ chainId, localBlockNumber, remoteBlockNumber, updatedAt, initialized }) => toSnakeCase$1({
7368
+ chainId,
7369
+ localBlockNumber,
7370
+ remoteBlockNumber,
7371
+ updatedAt,
7372
+ initialized
7373
+ })) });
7374
+ } catch (err) {
7375
+ logger.error({
7376
+ err,
7377
+ msg: "Error getting health status for chains",
7378
+ errorMessage: err instanceof Error ? err.message : String(err),
7379
+ errorStack: err instanceof Error ? err.stack : void 0
7380
+ });
7381
+ return failure(err);
7382
+ }
7383
+ }
7384
+ async function getHealthCollectors(query, db, chainRegistry) {
7385
+ const logger = getLogger();
7386
+ try {
7387
+ const parsed = safeParse("get_health_collectors", query);
7388
+ if (!parsed.success) return failure(parsed.error);
7389
+ const snapshot = await create$16({
7390
+ db,
7391
+ chainRegistry
7392
+ }).getSnapshot();
7393
+ 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({
7394
+ missingChains: snapshot.missingChains,
7395
+ missingCollectors: snapshot.missingCollectors
7396
+ })));
7397
+ const collectors = snapshot.collectors;
7398
+ return success({ data: collectors.map(({ name, chainId, blockNumber, updatedAt, lag, status, initialized }) => toSnakeCase$1({
7399
+ name,
7400
+ chainId,
7401
+ blockNumber,
7402
+ updatedAt,
7403
+ lag,
7404
+ status,
7405
+ initialized
7406
+ })) });
7407
+ } catch (err) {
7408
+ logger.error({
7409
+ err,
7410
+ msg: "Error getting health status for collectors",
7411
+ errorMessage: err instanceof Error ? err.message : String(err),
7412
+ errorStack: err instanceof Error ? err.stack : void 0
7413
+ });
7414
+ return failure(err);
7415
+ }
7416
+ }
7417
+
7418
+ //#endregion
7419
+ //#region src/api/Controllers/getObligation.ts
7420
+ async function getObligation(params, db) {
7421
+ const logger = getLogger();
7422
+ const result = safeParse("get_obligation", params, (issue) => issue.message);
7423
+ if (!result.success) return failure(result.error);
7424
+ const query = result.data;
7425
+ try {
7426
+ const { obligations } = await db.offers.getObligations({ ids: [query.obligation_id] });
7427
+ if (obligations.length === 0) return failure(new NotFoundError("Obligation not found"));
7428
+ const obligation = obligations[0];
7429
+ const [quote] = await db.offers.getQuotes({ obligationIds: [id(obligation)] });
7430
+ return success({
7431
+ data: from$4(obligation, quote ?? {
7432
+ obligationId: id(obligation),
7433
+ ask: { price: 0n },
7434
+ bid: { price: 0n }
7435
+ }),
7436
+ cursor: null
7437
+ });
7438
+ } catch (err) {
7439
+ logger.error({
7440
+ err,
7441
+ msg: "Error get obligation",
7442
+ errorMessage: err instanceof Error ? err.message : String(err),
7443
+ errorStack: err instanceof Error ? err.stack : void 0
7444
+ });
7445
+ return failure(err);
7446
+ }
7447
+ }
7448
+
7449
+ //#endregion
7450
+ //#region src/api/Controllers/getObligations.ts
7451
+ async function getObligations$1(queryParameters, db) {
7452
+ const logger = getLogger();
7453
+ const result = safeParse("get_obligations", queryParameters, (issue) => issue.message);
7454
+ if (!result.success) return failure(result.error);
7455
+ const query = result.data;
7456
+ try {
7457
+ const chainIds = query.chains?.length ? query.chains : void 0;
7458
+ const loanTokens = query.loan_tokens?.length ? query.loan_tokens : void 0;
7459
+ const collateralTokens = query.collateral_tokens?.length ? query.collateral_tokens : void 0;
7460
+ const maturities = query.maturities?.length ? query.maturities : void 0;
7461
+ const { obligations, nextCursor } = await db.offers.getObligations({
7462
+ cursor: query.cursor,
7463
+ limit: query.limit,
7464
+ chainId: chainIds,
7465
+ loanToken: loanTokens,
7466
+ collateralToken: collateralTokens,
7467
+ maturity: maturities
7468
+ });
7469
+ const quotes = await db.offers.getQuotes({ obligationIds: obligations.map((o) => id(o)) });
7470
+ return success({
7471
+ data: obligations.map((o) => from$4(o, quotes.find((q) => q.obligationId === id(o)) ?? {
7472
+ obligationId: id(o),
7473
+ ask: { price: 0n },
7474
+ bid: { price: 0n }
7475
+ })),
7476
+ cursor: nextCursor ?? null
7477
+ });
7478
+ } catch (err) {
7479
+ logger.error({
7480
+ err,
7481
+ msg: "Error get obligations",
7482
+ errorMessage: err instanceof Error ? err.message : String(err),
7483
+ errorStack: err instanceof Error ? err.stack : void 0
7484
+ });
7485
+ return failure(err);
7486
+ }
7487
+ }
7488
+
7489
+ //#endregion
7490
+ //#region src/database/constants.ts
7491
+ /**
7492
+ * Default batch size for bulk database inserts.
7493
+ *
7494
+ * PostgreSQL limits a single query to at most 65,535 parameters
7495
+ * (e.g. $1, $2, ...). In bulk inserts, each row consumes one
7496
+ * parameter per column, so inserting too many rows at once can
7497
+ * exceed this limit.
7498
+ *
7499
+ * Our largest batched insert is into the `offers` table with 15 columns.
7500
+ * 15 cols × 4,000 rows = 60,000 parameters, safely under 65,535.
7501
+ */
7502
+ const DEFAULT_BATCH_SIZE$1 = 4e3;
7545
7503
 
7546
7504
  //#endregion
7547
7505
  //#region src/database/domains/Offers.ts
@@ -7596,13 +7554,13 @@ function create$15(config) {
7596
7554
  jsonb_agg(
7597
7555
  jsonb_build_object(
7598
7556
  'asset', ${obligationCollateralsV2.asset},
7599
- 'oracle', ${oracles.address},
7557
+ 'oracle', ${oracles$1.address},
7600
7558
  'lltv', ${obligationCollateralsV2.lltv}
7601
7559
  )
7602
7560
  ),
7603
7561
  '[]'::jsonb
7604
- )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, drizzle_orm.sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7605
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where((0, drizzle_orm.eq)(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7562
+ )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles$1, drizzle_orm.sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7563
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).where((0, drizzle_orm.eq)(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7606
7564
  const rows = (await db.select({
7607
7565
  hash: offers.hash,
7608
7566
  maker: offers.groupMaker,
@@ -7684,10 +7642,10 @@ function create$15(config) {
7684
7642
  obligationId: obligations.obligationId,
7685
7643
  chainId: obligations.chainId,
7686
7644
  loanToken: obligations.loanToken,
7687
- collaterals: drizzle_orm.sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${oracles.address}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
7645
+ collaterals: drizzle_orm.sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${oracles$1.address}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
7688
7646
  maturity: obligations.maturity
7689
- }).from(obligations).innerJoin(obligationCollateralsV2, (0, drizzle_orm.eq)(obligations.obligationId, obligationCollateralsV2.obligationId)).innerJoin(oracles, drizzle_orm.sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7690
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).groupBy(obligations.obligationId).where((0, drizzle_orm.and)(cursor !== null && cursor !== void 0 ? (0, drizzle_orm.gt)(obligations.obligationId, cursor) : drizzle_orm.sql`true`, ids !== void 0 && ids.length > 0 ? (0, drizzle_orm.inArray)(obligations.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? (0, drizzle_orm.inArray)(obligations.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? (0, drizzle_orm.inArray)(obligations.maturity, maturities) : (0, drizzle_orm.gte)(obligations.maturity, now$1), collateralFilter)).orderBy((0, drizzle_orm.asc)(obligations.obligationId)).limit(limit);
7647
+ }).from(obligations).innerJoin(obligationCollateralsV2, (0, drizzle_orm.eq)(obligations.obligationId, obligationCollateralsV2.obligationId)).innerJoin(oracles$1, drizzle_orm.sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7648
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).groupBy(obligations.obligationId).where((0, drizzle_orm.and)(cursor !== null && cursor !== void 0 ? (0, drizzle_orm.gt)(obligations.obligationId, cursor) : drizzle_orm.sql`true`, ids !== void 0 && ids.length > 0 ? (0, drizzle_orm.inArray)(obligations.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? (0, drizzle_orm.inArray)(obligations.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? (0, drizzle_orm.inArray)(obligations.maturity, maturities) : (0, drizzle_orm.gte)(obligations.maturity, now$1), collateralFilter)).orderBy((0, drizzle_orm.asc)(obligations.obligationId)).limit(limit);
7691
7649
  const items = [];
7692
7650
  for (const row of result) items.push(from$15({
7693
7651
  chainId: row.chainId,
@@ -7758,40 +7716,28 @@ async function getOffersQuery(db, parameters) {
7758
7716
  if (cursor !== null && cursor !== void 0) {
7759
7717
  if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
7760
7718
  }
7719
+ const now = Math.floor((Date.now() - 1) / 1e3);
7761
7720
  const collateralsLateral = db.select({ collaterals: drizzle_orm.sql`COALESCE(
7762
7721
  jsonb_agg(
7763
7722
  jsonb_build_object(
7764
7723
  'asset', ${obligationCollateralsV2.asset},
7765
- 'oracle', ${oracles.address},
7724
+ 'oracle', ${oracles$1.address},
7766
7725
  'lltv', ${obligationCollateralsV2.lltv}
7767
7726
  )
7768
7727
  ),
7769
7728
  '[]'::jsonb
7770
- )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, drizzle_orm.sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7771
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where((0, drizzle_orm.eq)(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7729
+ )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles$1, drizzle_orm.sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7730
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).where((0, drizzle_orm.eq)(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7772
7731
  const availableLateral = db.select({ available: drizzle_orm.sql`COALESCE(SUM(
7773
7732
  CASE
7774
- -- If asset is null, position available is 0
7775
7733
  WHEN ${positions.asset} IS NULL THEN 0
7776
-
7777
- -- Position asset matches loan token: no conversion needed
7778
- WHEN ${positions.asset} = ${obligations.loanToken} THEN
7734
+ ELSE
7779
7735
  CASE
7780
7736
  WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7781
7737
  ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7782
7738
  END
7783
-
7784
- -- Position asset is collateral: apply oracle price * lltv
7785
- -- Formula: balance * price / 1e36 * lltv / 1e18
7786
- ELSE
7787
- (CASE
7788
- WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7789
- ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7790
- END)
7791
- * COALESCE(${oracles.price}, 0)::numeric / 1e36
7792
- * COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
7793
7739
  END
7794
- ), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, (0, drizzle_orm.eq)(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, (0, drizzle_orm.and)((0, drizzle_orm.eq)(callbacks.positionChainId, positions.chainId), (0, drizzle_orm.eq)(callbacks.positionContract, positions.contract), (0, drizzle_orm.eq)(callbacks.positionUser, positions.user))).leftJoin(obligationCollateralsV2, (0, drizzle_orm.and)((0, drizzle_orm.eq)(obligationCollateralsV2.obligationId, offers.obligationId), (0, drizzle_orm.eq)(obligationCollateralsV2.asset, positions.asset))).leftJoin(oracles, (0, drizzle_orm.and)((0, drizzle_orm.eq)(oracles.chainId, obligationCollateralsV2.oracleChainId), (0, drizzle_orm.eq)(oracles.address, obligationCollateralsV2.oracleAddress))).where((0, drizzle_orm.eq)(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
7740
+ ), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, (0, drizzle_orm.eq)(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, (0, drizzle_orm.and)((0, drizzle_orm.eq)(callbacks.positionChainId, positions.chainId), (0, drizzle_orm.eq)(callbacks.positionContract, positions.contract), (0, drizzle_orm.eq)(callbacks.positionUser, positions.user))).where((0, drizzle_orm.eq)(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
7795
7741
  const rows = (await db.select({
7796
7742
  hash: offers.hash,
7797
7743
  maker: offers.groupMaker,
@@ -7813,17 +7759,24 @@ async function getOffersQuery(db, parameters) {
7813
7759
  collaterals: collateralsLateral.collaterals,
7814
7760
  blockNumber: offers.blockNumber,
7815
7761
  available: drizzle_orm.sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
7816
- takeable: drizzle_orm.sql`FLOOR(GREATEST(
7817
- 0,
7818
- LEAST(
7819
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
7820
- COALESCE(${availableLateral.available}::numeric, 0)
7821
- )
7762
+ takeable: drizzle_orm.sql`FLOOR(GREATEST(0,
7763
+ CASE WHEN ${offers.buy} = false
7764
+ THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
7765
+ ELSE LEAST(
7766
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
7767
+ COALESCE(${availableLateral.available}::numeric, 0)
7768
+ )
7769
+ END
7822
7770
  ))`.as("takeable")
7823
- }).from(offers).innerJoin(obligations, (0, drizzle_orm.eq)(offers.obligationId, obligations.obligationId)).innerJoin(groups, (0, drizzle_orm.and)((0, drizzle_orm.eq)(offers.groupChainId, groups.chainId), (0, drizzle_orm.eq)(offers.groupMaker, groups.maker), (0, drizzle_orm.eq)(offers.group, groups.group))).innerJoinLateral(collateralsLateral, drizzle_orm.sql`true`).leftJoinLateral(availableLateral, drizzle_orm.sql`true`).where((0, drizzle_orm.and)(cursor !== null && cursor !== void 0 ? (0, drizzle_orm.gt)(offers.hash, cursor) : void 0, maker !== void 0 ? (0, drizzle_orm.eq)(offers.groupMaker, maker.toLowerCase()) : void 0, maker === void 0 ? drizzle_orm.sql`GREATEST(0, LEAST(
7824
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
7825
- COALESCE(${availableLateral.available}::numeric, 0)
7826
- )) > 0` : void 0)).orderBy((0, drizzle_orm.asc)(offers.hash)).limit(limit)).map((row) => {
7771
+ }).from(offers).innerJoin(obligations, (0, drizzle_orm.eq)(offers.obligationId, obligations.obligationId)).innerJoin(groups, (0, drizzle_orm.and)((0, drizzle_orm.eq)(offers.groupChainId, groups.chainId), (0, drizzle_orm.eq)(offers.groupMaker, groups.maker), (0, drizzle_orm.eq)(offers.group, groups.group))).innerJoinLateral(collateralsLateral, drizzle_orm.sql`true`).leftJoinLateral(availableLateral, drizzle_orm.sql`true`).where((0, drizzle_orm.and)(cursor !== null && cursor !== void 0 ? (0, drizzle_orm.gt)(offers.hash, cursor) : void 0, maker !== void 0 ? (0, drizzle_orm.eq)(offers.groupMaker, maker.toLowerCase()) : void 0, (0, drizzle_orm.gte)(offers.expiry, now), (0, drizzle_orm.gte)(offers.maturity, now), maker === void 0 ? drizzle_orm.sql`GREATEST(0,
7772
+ CASE WHEN ${offers.buy} = false
7773
+ THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
7774
+ ELSE LEAST(
7775
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
7776
+ COALESCE(${availableLateral.available}::numeric, 0)
7777
+ )
7778
+ END
7779
+ ) > 0` : void 0)).orderBy((0, drizzle_orm.asc)(offers.hash)).limit(limit)).map((row) => {
7827
7780
  return {
7828
7781
  hash: row.hash,
7829
7782
  maker: row.maker,
@@ -7933,64 +7886,6 @@ async function getUserPositions(queryParameters, db) {
7933
7886
  }
7934
7887
  }
7935
7888
 
7936
- //#endregion
7937
- //#region src/gatekeeper/CallbackTypes.ts
7938
- /**
7939
- * Resolve callback types for a list of callback addresses grouped by chain.
7940
- * @param parameters - Resolve parameters. {@link resolveCallbackTypes.Parameters}
7941
- * @returns Callback types grouped by chain. {@link resolveCallbackTypes.ReturnType}
7942
- * @throws If a chain id is unknown.
7943
- */
7944
- function resolveCallbackTypes$1(parameters) {
7945
- const { chains, request } = parameters;
7946
- const chainsById = new Map(chains.map((chain) => [chain.id, chain]));
7947
- return request.callbacks.map(({ chain_id, addresses }) => {
7948
- const chain = chainsById.get(chain_id);
7949
- if (!chain) throw new Error(`Unknown chain id ${chain_id}`);
7950
- const buckets = /* @__PURE__ */ new Map();
7951
- const uniqueAddresses = new Set(addresses.map((address) => address.toLowerCase()));
7952
- for (const address of uniqueAddresses) {
7953
- const bucketKey = getCallbackType(chain.name, address) ?? "not_supported";
7954
- const list = buckets.get(bucketKey) ?? [];
7955
- list.push(address);
7956
- buckets.set(bucketKey, list);
7957
- }
7958
- const response = { chain_id };
7959
- for (const [type, list] of buckets.entries()) response[type] = list;
7960
- if (!response.not_supported) response.not_supported = [];
7961
- return response;
7962
- });
7963
- }
7964
-
7965
- //#endregion
7966
- //#region src/api/Controllers/resolveCallbackTypes.ts
7967
- /**
7968
- * Resolve callback types for a list of callback addresses grouped by chain.
7969
- * @param body - Request body with callback addresses. {@link CallbackTypesRequest}
7970
- * @param chains - Chains to resolve callback types against. {@link Chain.Chain}
7971
- * @returns Callback types grouped by chain. {@link CallbackTypesPayload}
7972
- */
7973
- async function resolveCallbackTypes(body, chains) {
7974
- const result = safeParse("callback_types", body, (issue) => issue.message);
7975
- if (!result.success) return failure(result.error);
7976
- const request = result.data;
7977
- const chainIds = new Set(chains.map((chain) => chain.id));
7978
- const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
7979
- if (unknown) return failure(new BadRequestError(`Unknown chain id ${unknown.chain_id}`));
7980
- try {
7981
- const data = resolveCallbackTypes$1({
7982
- chains,
7983
- request
7984
- });
7985
- return success({
7986
- data,
7987
- cursor: null
7988
- });
7989
- } catch (err) {
7990
- return failure(err);
7991
- }
7992
- }
7993
-
7994
7889
  //#endregion
7995
7890
  //#region src/api/Controllers/validateOffers.ts
7996
7891
  async function validateOffers(body, gatekeeper) {
@@ -8070,7 +7965,6 @@ var Controllers_exports = /* @__PURE__ */ __exportAll({
8070
7965
  getOffersQuery: () => getOffersQuery,
8071
7966
  getSwaggerJson: () => getSwaggerJson,
8072
7967
  getUserPositions: () => getUserPositions,
8073
- resolveCallbackTypes: () => resolveCallbackTypes,
8074
7968
  validateOffers: () => validateOffers
8075
7969
  });
8076
7970
 
@@ -8143,24 +8037,9 @@ function serve(parameters) {
8143
8037
  const { statusCode, body } = await gatekeeper.validate(reqBody);
8144
8038
  return c.json(body, statusCode);
8145
8039
  } catch (err) {
8146
- const failure$1 = failure(err);
8147
- return c.json(failure$1.body, failure$1.statusCode);
8148
- }
8149
- });
8150
- app.post("/v1/callbacks", async (c) => {
8151
- let body;
8152
- try {
8153
- body = await c.req.json();
8154
- } catch (err) {
8155
- const failure$3 = failure(err);
8156
- return c.json(failure$3.body, failure$3.statusCode);
8157
- }
8158
- if (body === null || typeof body !== "object") {
8159
- const failure$2 = failure(new BadRequestError("Request body must be a JSON object"));
8040
+ const failure$2 = failure(err);
8160
8041
  return c.json(failure$2.body, failure$2.statusCode);
8161
8042
  }
8162
- const { statusCode, body: responseBody } = await resolveCallbackTypes(body, chainRegistry.list());
8163
- return c.json(responseBody, statusCode);
8164
8043
  });
8165
8044
  app.get("/v1/users/:userAddress/positions", async (c) => {
8166
8045
  const query = c.req.query();
@@ -8192,8 +8071,8 @@ function serve(parameters) {
8192
8071
  const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
8193
8072
  return c.json(body, statusCode);
8194
8073
  } catch (err) {
8195
- const failure$4 = failure(err);
8196
- return c.json(failure$4.body, failure$4.statusCode);
8074
+ const failure$1 = failure(err);
8075
+ return c.json(failure$1.body, failure$1.statusCode);
8197
8076
  }
8198
8077
  });
8199
8078
  app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
@@ -8210,7 +8089,6 @@ function serve(parameters) {
8210
8089
  var RouterApi_exports = /* @__PURE__ */ __exportAll({
8211
8090
  BookResponse: () => BookResponse_exports,
8212
8091
  BooksController: () => BooksController,
8213
- CallbacksController: () => CallbacksController,
8214
8092
  ChainHealth: () => ChainHealth,
8215
8093
  ChainsHealthResponse: () => ChainsHealthResponse,
8216
8094
  CollectorHealth: () => CollectorHealth,
@@ -8432,7 +8310,7 @@ var drizzle_exports = /* @__PURE__ */ __exportAll({
8432
8310
  offers: () => offers,
8433
8311
  offersCallbacks: () => offersCallbacks,
8434
8312
  offsets: () => offsets,
8435
- oracles: () => oracles,
8313
+ oracles: () => oracles$1,
8436
8314
  positionTypes: () => positionTypes,
8437
8315
  positions: () => positions,
8438
8316
  status: () => status,
@@ -8717,7 +8595,7 @@ async function _getOffers(db, params) {
8717
8595
  'lltv', oc.lltv
8718
8596
  ) ORDER BY oc.asset), '[]'::jsonb) AS collaterals
8719
8597
  FROM ${obligationCollateralsV2} oc
8720
- JOIN ${oracles} oracle
8598
+ JOIN ${oracles$1} oracle
8721
8599
  ON oracle.chain_id = oc.oracle_chain_id
8722
8600
  AND oracle.address = oc.oracle_address
8723
8601
  WHERE oc.obligation_id = ${obligationId}
@@ -8872,35 +8750,15 @@ async function _getOffers(db, params) {
8872
8750
  AND LOWER(pc.contract) = LOWER(c.position_contract)
8873
8751
  AND LOWER(pc."user") = LOWER(c.position_user)
8874
8752
  ),
8875
- -- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
8753
+ -- Compute contribution per callback in loan terms (loan token only collateral positions are not indexed)
8876
8754
  callback_loan_contribution AS (
8877
8755
  SELECT
8878
8756
  cc.*,
8879
8757
  CASE
8880
- -- No lot exists: contribution is 0
8881
8758
  WHEN cc.lot_lower IS NULL THEN 0
8882
- -- Loan token position: use lot_balance directly, apply callback limit
8883
- WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
8884
- LEAST(
8885
- cc.lot_balance,
8886
- COALESCE(cc.callback_amount::numeric, cc.lot_balance)
8887
- )
8888
- -- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
8889
- ELSE
8890
- (
8891
- LEAST(
8892
- cc.lot_balance,
8893
- COALESCE(cc.callback_amount::numeric, cc.lot_balance)
8894
- ) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
8895
- ) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
8759
+ ELSE LEAST(cc.lot_balance, COALESCE(cc.callback_amount::numeric, cc.lot_balance))
8896
8760
  END AS contribution_in_loan
8897
8761
  FROM callback_contributions cc
8898
- LEFT JOIN ${obligationCollateralsV2} collat_info
8899
- ON collat_info.obligation_id = cc.obligation_id
8900
- AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
8901
- LEFT JOIN ${oracles} collat_oracle
8902
- ON collat_oracle.chain_id = collat_info.oracle_chain_id
8903
- AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
8904
8762
  ),
8905
8763
  -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
8906
8764
  offer_contributions AS (
@@ -8937,6 +8795,22 @@ async function _getOffers(db, params) {
8937
8795
  GROUP BY hash, obligation_id, assets, price, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
8938
8796
  callback_address, callback_data, block_number, group_chain_id, group_maker,
8939
8797
  consumed, chain_id, loan_token, session
8798
+ UNION ALL
8799
+ -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
8800
+ SELECT
8801
+ p.hash, p.obligation_id, p.assets, p.price,
8802
+ p.obligation_units, p.obligation_shares,
8803
+ p.maturity, p.expiry, p.start, p.group_group,
8804
+ p.buy, p.callback_address, p.callback_data,
8805
+ p.block_number, p.group_chain_id, p.group_maker,
8806
+ p.consumed, p.chain_id, p.loan_token, p.session,
8807
+ 0 AS total_available
8808
+ FROM paged p
8809
+ WHERE p.buy = false
8810
+ AND NOT EXISTS (
8811
+ SELECT 1 FROM ${offersCallbacks} oc2
8812
+ WHERE oc2.offer_hash = p.hash
8813
+ )
8940
8814
  )
8941
8815
  -- Final SELECT with inline takeable computation
8942
8816
  SELECT
@@ -8959,18 +8833,24 @@ async function _getOffers(db, params) {
8959
8833
  oc.block_number,
8960
8834
  oc.session,
8961
8835
  COALESCE(oc.total_available, 0) AS available,
8962
- -- takeable = min(assets - consumed, total_available)
8963
- GREATEST(0, LEAST(
8964
- oc.assets::numeric - oc.consumed::numeric,
8965
- COALESCE(oc.total_available, 0)
8966
- )) AS takeable,
8836
+ -- takeable: sell offers use assets - consumed directly (collateral positions not indexed yet)
8837
+ CASE WHEN oc.buy = false
8838
+ THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
8839
+ ELSE GREATEST(0, LEAST(
8840
+ oc.assets::numeric - oc.consumed::numeric,
8841
+ COALESCE(oc.total_available, 0)
8842
+ ))
8843
+ END AS takeable,
8967
8844
  c.collaterals
8968
8845
  FROM offer_contributions oc
8969
8846
  LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
8970
- WHERE GREATEST(0, LEAST(
8971
- oc.assets::numeric - oc.consumed::numeric,
8972
- COALESCE(oc.total_available, 0)
8973
- )) > 0
8847
+ WHERE CASE WHEN oc.buy = false
8848
+ THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
8849
+ ELSE GREATEST(0, LEAST(
8850
+ oc.assets::numeric - oc.consumed::numeric,
8851
+ COALESCE(oc.total_available, 0)
8852
+ ))
8853
+ END > 0
8974
8854
  ORDER BY
8975
8855
  oc.price::numeric ${priceSortDirection === "asc" ? drizzle_orm.sql`ASC` : drizzle_orm.sql`DESC`},
8976
8856
  oc.block_number ASC,
@@ -9311,32 +9191,32 @@ function create$5(db) {
9311
9191
  return {
9312
9192
  get: async ({ chainId }) => {
9313
9193
  return (await db.select({
9314
- address: oracles.address,
9315
- price: oracles.price,
9316
- blockNumber: oracles.blockNumber,
9317
- chainId: oracles.chainId
9318
- }).from(oracles).where((0, drizzle_orm.eq)(oracles.chainId, chainId))).map((r) => from$13({
9194
+ address: oracles$1.address,
9195
+ price: oracles$1.price,
9196
+ blockNumber: oracles$1.blockNumber,
9197
+ chainId: oracles$1.chainId
9198
+ }).from(oracles$1).where((0, drizzle_orm.eq)(oracles$1.chainId, chainId))).map((r) => from$13({
9319
9199
  chainId: r.chainId,
9320
9200
  address: r.address,
9321
9201
  price: r.price,
9322
9202
  blockNumber: r.blockNumber
9323
9203
  }));
9324
9204
  },
9325
- upsert: async (oracles$1) => {
9326
- if (oracles$1.length === 0) return;
9327
- const rows = oracles$1.map((o) => ({
9205
+ upsert: async (oracles) => {
9206
+ if (oracles.length === 0) return;
9207
+ const rows = oracles.map((o) => ({
9328
9208
  chainId: o.chainId,
9329
9209
  address: o.address.toLowerCase(),
9330
9210
  price: o.price !== null ? o.price.toString() : null,
9331
9211
  blockNumber: o.blockNumber
9332
9212
  }));
9333
9213
  await db.transaction(async (dbTx) => {
9334
- for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(oracles).values(batch).onConflictDoUpdate({
9335
- target: [oracles.chainId, oracles.address],
9214
+ for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(oracles$1).values(batch).onConflictDoUpdate({
9215
+ target: [oracles$1.chainId, oracles$1.address],
9336
9216
  set: {
9337
- price: drizzle_orm.sql`COALESCE(EXCLUDED.price, ${oracles.price})`,
9217
+ price: drizzle_orm.sql`COALESCE(EXCLUDED.price, ${oracles$1.price})`,
9338
9218
  blockNumber: drizzle_orm.sql`CASE
9339
- WHEN EXCLUDED.price IS NULL THEN ${oracles.blockNumber}
9219
+ WHEN EXCLUDED.price IS NULL THEN ${oracles$1.blockNumber}
9340
9220
  ELSE EXCLUDED.block_number
9341
9221
  END`,
9342
9222
  updatedAt: drizzle_orm.sql`NOW()`
@@ -10375,23 +10255,11 @@ function createHttpClient(config) {
10375
10255
  issues: []
10376
10256
  };
10377
10257
  };
10378
- const getCallbackTypes = async (requestPayload) => {
10379
- const response = await request("/v1/callbacks", {
10380
- method: "POST",
10381
- headers: { "content-type": "application/json" },
10382
- body: JSON.stringify(requestPayload)
10383
- });
10384
- const json = await response.json();
10385
- if (!response.ok) throw new Error(`Gatekeeper callbacks request failed: ${extractErrorMessage(json) ?? response.statusText}`);
10386
- if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper callbacks response is invalid.");
10387
- return json.data;
10388
- };
10389
10258
  return {
10390
10259
  baseUrl,
10391
10260
  validate,
10392
10261
  getConfigRules,
10393
- isAllowed,
10394
- getCallbackTypes
10262
+ isAllowed
10395
10263
  };
10396
10264
  }
10397
10265
  function mergeHeaders(base, extra) {
@@ -10520,6 +10388,7 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
10520
10388
  callback: () => callback,
10521
10389
  chains: () => chains,
10522
10390
  maturity: () => maturity,
10391
+ oracle: () => oracle,
10523
10392
  sameMaker: () => sameMaker,
10524
10393
  token: () => token,
10525
10394
  validity: () => validity
@@ -10527,109 +10396,13 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
10527
10396
  /**
10528
10397
  * set of rules to validate offers.
10529
10398
  *
10530
- * @param parameters - Validity parameters with chain and client
10399
+ * @param _parameters - Validity parameters with chain and client
10531
10400
  * @returns Array of validation rules to evaluate against offers
10532
10401
  */
10533
- function validity(parameters) {
10534
- const { client } = parameters;
10535
- const sellErc20CallbackInvalid = single("sell_erc20_callback_invalid", "Validates that sell offers have valid ERC20 callback data matching offer collaterals", (offer) => {
10536
- const callbackType = getCallbackType(client.chain.name, offer.callback.address);
10537
- if (callbackType !== Type$1.SellERC20Callback) return;
10538
- const decoded = decode$2(callbackType, offer.callback.data);
10539
- if (decoded.length === 0) return { message: "Callback data cannot be decoded or is empty." };
10540
- if (callbackType === Type$1.SellERC20Callback) {
10541
- const offerCollaterals = new Set(offer.collaterals.map((c) => c.asset.toLowerCase()));
10542
- if (decoded.length !== offer.collaterals.length) return { message: `Sell callback collateral length mismatch. Expected ${offer.collaterals.length}, got ${decoded.length}.` };
10543
- for (const { contract } of decoded) if (!offerCollaterals.has(contract.toLowerCase())) return { message: "Sell callback collateral is not part of offer collaterals." };
10544
- }
10545
- });
10546
- 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) => {
10547
- const validationIssues = /* @__PURE__ */ new Map();
10548
- const offersByVaultAddress = /* @__PURE__ */ new Map();
10549
- for (let i = 0; i < offers.length; i++) {
10550
- const offer = offers[i];
10551
- if (getCallbackType(client.chain.name, offer.callback.address) !== Type$1.BuyVaultV1Callback) continue;
10552
- try {
10553
- const callbackVaults = decodeBuyVaultV1Callback(offer.callback.data);
10554
- for (const { contract } of callbackVaults) {
10555
- const normalizedVaultAddress = contract.toLowerCase();
10556
- if (!offersByVaultAddress.has(normalizedVaultAddress)) offersByVaultAddress.set(normalizedVaultAddress, []);
10557
- offersByVaultAddress.get(normalizedVaultAddress).push({
10558
- index: i,
10559
- offer
10560
- });
10561
- }
10562
- } catch (_) {}
10563
- }
10564
- const uniqueVaultAddresses = Array.from(offersByVaultAddress.keys());
10565
- if (uniqueVaultAddresses.length === 0) return validationIssues;
10566
- const allowedFactories = getCallback(client.chain.name, Type$1.BuyVaultV1Callback)?.vaultFactories.map((f) => f.toLowerCase());
10567
- if (!allowedFactories) return validationIssues;
10568
- const multicallContracts = [];
10569
- for (const vaultAddress of uniqueVaultAddresses) {
10570
- multicallContracts.push({
10571
- address: vaultAddress,
10572
- abi: ERC4626,
10573
- functionName: "asset"
10574
- });
10575
- for (const factoryAddress of allowedFactories) multicallContracts.push({
10576
- address: factoryAddress,
10577
- abi: MetaMorphoFactory,
10578
- functionName: "isMetaMorpho",
10579
- args: [vaultAddress]
10580
- });
10581
- }
10582
- const multicallResults = await (0, viem_actions.multicall)(client, {
10583
- contracts: multicallContracts,
10584
- allowFailure: true
10585
- });
10586
- const vaultAssetByAddress = /* @__PURE__ */ new Map();
10587
- const registeredVaults = /* @__PURE__ */ new Set();
10588
- const numberOfFactories = allowedFactories.length;
10589
- let resultIndex = 0;
10590
- for (const vaultAddress of uniqueVaultAddresses) {
10591
- const assetCallResult = multicallResults[resultIndex++];
10592
- const assetAddress = assetCallResult.status === "success" ? assetCallResult.result : null;
10593
- vaultAssetByAddress.set(vaultAddress, assetAddress);
10594
- let isRegisteredInFactory = false;
10595
- for (let factoryIndex = 0; factoryIndex < numberOfFactories; factoryIndex++) {
10596
- const factoryCallResult = multicallResults[resultIndex++];
10597
- if (factoryCallResult.status === "success" && factoryCallResult.result === true) isRegisteredInFactory = true;
10598
- }
10599
- if (isRegisteredInFactory) registeredVaults.add(vaultAddress);
10600
- }
10601
- const uniqueOffers = /* @__PURE__ */ new Map();
10602
- for (const offersArray of offersByVaultAddress.values()) for (const { index, offer } of offersArray) uniqueOffers.set(index, offer);
10603
- for (const [index, offer] of uniqueOffers) try {
10604
- const callbackVaults = decodeBuyVaultV1Callback(offer.callback.data);
10605
- const vaultsWithIssues = [];
10606
- for (const { contract } of callbackVaults) {
10607
- const normalizedVaultAddress = contract.toLowerCase();
10608
- const assetAddress = vaultAssetByAddress.get(normalizedVaultAddress);
10609
- const isRegistered = registeredVaults.has(normalizedVaultAddress);
10610
- const failureReasons = [];
10611
- if (assetAddress === null) failureReasons.push("asset call failed");
10612
- else if (assetAddress && assetAddress.toLowerCase() !== offer.loanToken.toLowerCase()) failureReasons.push("asset mismatch");
10613
- if (!isRegistered) failureReasons.push("not registered in factory");
10614
- if (failureReasons.length > 0) vaultsWithIssues.push({
10615
- vaultAddress: contract,
10616
- failureReasons: failureReasons.join(", ")
10617
- });
10618
- }
10619
- if (vaultsWithIssues.length > 0) {
10620
- const failureDetails = vaultsWithIssues.map((v) => `${v.vaultAddress} (${v.failureReasons})`).join("; ");
10621
- validationIssues.set(index, { message: `Buy offer callback vaults are invalid: ${failureDetails}` });
10622
- }
10623
- } catch (_) {}
10624
- return validationIssues;
10625
- });
10626
- return [
10627
- single("expiry", "Validates that offer has not expired", (offer) => {
10628
- if (offer.expiry < Math.floor(Date.now() / 1e3)) return { message: "Expiry mismatch" };
10629
- }),
10630
- sellErc20CallbackInvalid,
10631
- buyCallbackVaultInvalid
10632
- ];
10402
+ function validity(_parameters) {
10403
+ return [single("expiry", "Validates that offer has not expired", (offer) => {
10404
+ if (offer.expiry < Math.floor(Date.now() / 1e3)) return { message: "Expiry mismatch" };
10405
+ })];
10633
10406
  }
10634
10407
  const chains = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
10635
10408
  const allowedChainIds = chains.map((c) => c.id);
@@ -10639,12 +10412,10 @@ const maturity = ({ maturities }) => single("maturity", `Validates that offer ma
10639
10412
  const allowedMaturities = maturities.map((m) => from$16(m));
10640
10413
  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}` };
10641
10414
  });
10642
- 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) => {
10643
- if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
10644
- if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
10645
- if (!isEmptyCallback(offer)) {
10646
- if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
10647
- }
10415
+ 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) => {
10416
+ if (!isEmptyCallback(offer)) return { message: "Non-empty callbacks are not supported." };
10417
+ if (isEmptyCallback(offer) && offer.buy && !callbacks.includes(Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
10418
+ if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
10648
10419
  });
10649
10420
  /**
10650
10421
  * A validation rule that checks if the offer's tokens are allowed for its chain.
@@ -10658,6 +10429,16 @@ const token = ({ assetsByChainId }) => single("token", "Validates that offer loa
10658
10429
  if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
10659
10430
  });
10660
10431
  /**
10432
+ * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
10433
+ * @param oraclesByChainId - Allowed oracles indexed by chain id.
10434
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
10435
+ */
10436
+ const oracle = ({ oraclesByChainId }) => single("oracle", "Validates that offer collateral oracles are in the allowed oracle list for the offer chain", (offer) => {
10437
+ const allowedOracles = oraclesByChainId[offer.chainId]?.map((oracle) => oracle.toLowerCase());
10438
+ if (!allowedOracles || allowedOracles.length === 0) return { message: `No allowed oracles for chain ${offer.chainId}` };
10439
+ if (offer.collaterals.some((collateral) => !allowedOracles.includes(collateral.oracle.toLowerCase()))) return { message: "Oracle is not allowed" };
10440
+ });
10441
+ /**
10661
10442
  * A batch validation rule that ensures all offers in a tree have the same maker address.
10662
10443
  * Returns an issue only for the first non-conforming offer.
10663
10444
  * This rule is signing-agnostic; signer verification is handled at the collector level.
@@ -10688,21 +10469,22 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
10688
10469
  //#region src/gatekeeper/morphoRules.ts
10689
10470
  const morphoRules = (chains$3) => {
10690
10471
  const assetsByChainId = {};
10691
- for (const chain of chains$3) assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
10472
+ const oraclesByChainId = {};
10473
+ for (const chain of chains$3) {
10474
+ assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
10475
+ oraclesByChainId[chain.id] = oracles[chain.id.toString()] ?? [];
10476
+ }
10692
10477
  return [
10693
10478
  sameMaker(),
10694
10479
  amountMutualExclusivity(),
10695
10480
  chains({ chains: chains$3 }),
10696
10481
  maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
10697
10482
  callback({
10698
- callbacks: [
10699
- Type$1.BuyWithEmptyCallback,
10700
- Type$1.BuyVaultV1Callback,
10701
- Type$1.SellERC20Callback
10702
- ],
10703
- allowedAddresses: chains$3.flatMap((c) => getCallbackAddresses(c.name))
10483
+ callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
10484
+ allowedAddresses: []
10704
10485
  }),
10705
- token({ assetsByChainId })
10486
+ token({ assetsByChainId }),
10487
+ oracle({ oraclesByChainId })
10706
10488
  ];
10707
10489
  };
10708
10490
 
@@ -10909,12 +10691,6 @@ Object.defineProperty(exports, 'Callback', {
10909
10691
  return Callback_exports;
10910
10692
  }
10911
10693
  });
10912
- Object.defineProperty(exports, 'CallbacksController', {
10913
- enumerable: true,
10914
- get: function () {
10915
- return CallbacksController;
10916
- }
10917
- });
10918
10694
  Object.defineProperty(exports, 'Chain', {
10919
10695
  enumerable: true,
10920
10696
  get: function () {