@morpho-dev/router 0.10.0 → 0.11.0

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.
@@ -1,14 +1,12 @@
1
1
  import { t as __exportAll } from "./chunk-Bo1DHCg-.mjs";
2
- import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
2
+ import { getBlockNumber, getLogs, multicall } from "viem/actions";
3
+ import { SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
4
5
  import { bytesToHex, concatHex, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, isAddress, isHex, keccak256, maxUint256, numberToHex, pad, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, zeroAddress } from "viem";
5
- import { SpanStatusCode, trace } from "@opentelemetry/api";
6
6
  import "@opentelemetry/exporter-trace-otlp-proto";
7
- import "@opentelemetry/id-generator-aws-xray";
8
7
  import "@opentelemetry/instrumentation";
9
8
  import "@opentelemetry/instrumentation-http";
10
9
  import "@opentelemetry/instrumentation-pg";
11
- import "@opentelemetry/propagator-aws-xray";
12
10
  import "@opentelemetry/resources";
13
11
  import "@opentelemetry/sdk-trace-node";
14
12
  import "@opentelemetry/semantic-conventions";
@@ -21,11 +19,11 @@ import { bigint, boolean, foreignKey, index, integer, numeric, pgSchema, primary
21
19
  import { serve } from "@hono/node-server";
22
20
  import { Hono } from "hono";
23
21
  import { cors } from "hono/cors";
22
+ import crypto, { createHash, randomUUID } from "node:crypto";
24
23
  import { z } from "zod/v4";
25
24
  import "reflect-metadata";
26
25
  import { generateDocument } from "openapi-metadata";
27
26
  import { ApiBody, ApiOperation, ApiParam, ApiProperty, ApiQuery, ApiResponse, ApiTags } from "openapi-metadata/decorators";
28
- import crypto, { createHash } from "node:crypto";
29
27
  import { existsSync } from "node:fs";
30
28
  import { readFile } from "node:fs/promises";
31
29
  import { dirname, resolve } from "node:path";
@@ -39,11 +37,75 @@ import { drizzle as drizzle$1 } from "drizzle-orm/pglite";
39
37
  import { migrate as migrate$1 } from "drizzle-orm/pglite/migrator";
40
38
  import { Pool } from "pg";
41
39
 
40
+ //#region src/observability/Events.ts
41
+ const eventNamePattern = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/;
42
+ /**
43
+ * Canonical telemetry event names used by router logs.
44
+ * Every value must stay unique and follow dot-separated snake_case segments.
45
+ */
46
+ const Events = defineEvents({
47
+ API_GET_BOOK_FAILED: "api.get_book.failed",
48
+ API_GET_HEALTH_CHAINS_FAILED: "api.get_health_chains.failed",
49
+ API_GET_HEALTH_COLLECTORS_FAILED: "api.get_health_collectors.failed",
50
+ API_GET_HEALTH_FAILED: "api.get_health.failed",
51
+ API_GET_OBLIGATION_FAILED: "api.get_obligation.failed",
52
+ API_GET_OBLIGATIONS_FAILED: "api.get_obligations.failed",
53
+ API_GET_OFFERS_FAILED: "api.get_offers.failed",
54
+ API_GET_USER_POSITIONS_FAILED: "api.get_user_positions.failed",
55
+ API_VALIDATE_OFFERS_FAILED: "api.validate_offers.failed",
56
+ HTTP_REQUEST_COMPLETED: "http.request.completed",
57
+ HTTP_REQUEST_FAILED: "http.request.failed",
58
+ INDEXER_COLLECTOR_EVENTS_INDEXED: "indexer.collector.events_indexed",
59
+ INDEXER_COLLECTOR_FAILED: "indexer.collector.failed",
60
+ INDEXER_COLLECTOR_FINALIZED_BLOCK_FAILED: "indexer.collector.finalized_block_failed",
61
+ INDEXER_COLLECTOR_GATEKEEPER_VALIDATION_FAILED: "indexer.collector.gatekeeper_validation_failed",
62
+ INDEXER_COLLECTOR_INVALID_CHAIN_CONFIG: "indexer.collector.invalid_chain_config",
63
+ INDEXER_COLLECTOR_LOG_SKIPPED: "indexer.collector.log_skipped",
64
+ INDEXER_COLLECTOR_MAX_BLOCK_REACHED: "indexer.collector.max_block_reached",
65
+ INDEXER_COLLECTOR_MISSING_BLOCKS: "indexer.collector.missing_blocks",
66
+ INDEXER_COLLECTOR_OFFERS_INDEXED: "indexer.collector.offers_indexed",
67
+ INDEXER_COLLECTOR_OFFER_TREE_DECODE_FAILED: "indexer.collector.offer_tree_decode_failed",
68
+ INDEXER_COLLECTOR_OFFER_TREE_REJECTED: "indexer.collector.offer_tree_rejected",
69
+ INDEXER_COLLECTOR_ORACLES_INDEXED: "indexer.collector.oracles_indexed",
70
+ INDEXER_COLLECTOR_ORACLE_FETCH_FAILED: "indexer.collector.oracle_fetch_failed",
71
+ INDEXER_COLLECTOR_POLL_FAILED: "indexer.collector.poll_failed",
72
+ INDEXER_COLLECTOR_POSITIONS_INDEXED: "indexer.collector.positions_indexed",
73
+ INDEXER_COLLECTOR_POSITIONS_INSERT_FAILED: "indexer.collector.positions_insert_failed",
74
+ INDEXER_COLLECTOR_POSITIONS_SNAPSHOT_FAILED: "indexer.collector.positions_snapshot_failed",
75
+ INDEXER_COLLECTOR_POSITION_CONVERSION_FAILED: "indexer.collector.position_conversion_failed",
76
+ INDEXER_COLLECTOR_REORG_COMPENSATED: "indexer.collector.reorg_compensated",
77
+ INDEXER_COLLECTOR_REORG_COMPENSATION_FAILED: "indexer.collector.reorg_compensation_failed",
78
+ INDEXER_COLLECTOR_REORG_DETECTED: "indexer.collector.reorg_detected",
79
+ INDEXER_COLLECTOR_RPC_QUERY_INVALID_RANGE: "indexer.collector.rpc_query_invalid_range",
80
+ INDEXER_COLLECTOR_STARTED: "indexer.collector.started",
81
+ INDEXER_COLLECTOR_TRANSFERS_INDEXED: "indexer.collector.transfers_indexed",
82
+ INDEXER_COLLECTOR_TRANSFERS_INSERT_FAILED: "indexer.collector.transfers_insert_failed"
83
+ });
84
+ const { API_GET_BOOK_FAILED, API_GET_HEALTH_CHAINS_FAILED, API_GET_HEALTH_COLLECTORS_FAILED, API_GET_HEALTH_FAILED, API_GET_OBLIGATION_FAILED, API_GET_OBLIGATIONS_FAILED, API_GET_OFFERS_FAILED, API_GET_USER_POSITIONS_FAILED, API_VALIDATE_OFFERS_FAILED, HTTP_REQUEST_COMPLETED, HTTP_REQUEST_FAILED, INDEXER_COLLECTOR_EVENTS_INDEXED, INDEXER_COLLECTOR_FAILED, INDEXER_COLLECTOR_FINALIZED_BLOCK_FAILED, INDEXER_COLLECTOR_GATEKEEPER_VALIDATION_FAILED, INDEXER_COLLECTOR_INVALID_CHAIN_CONFIG, INDEXER_COLLECTOR_LOG_SKIPPED, INDEXER_COLLECTOR_MAX_BLOCK_REACHED, INDEXER_COLLECTOR_MISSING_BLOCKS, INDEXER_COLLECTOR_OFFERS_INDEXED, INDEXER_COLLECTOR_OFFER_TREE_DECODE_FAILED, INDEXER_COLLECTOR_OFFER_TREE_REJECTED, INDEXER_COLLECTOR_ORACLES_INDEXED, INDEXER_COLLECTOR_ORACLE_FETCH_FAILED, INDEXER_COLLECTOR_POLL_FAILED, INDEXER_COLLECTOR_POSITIONS_INDEXED, INDEXER_COLLECTOR_POSITIONS_INSERT_FAILED, INDEXER_COLLECTOR_POSITIONS_SNAPSHOT_FAILED, INDEXER_COLLECTOR_POSITION_CONVERSION_FAILED, INDEXER_COLLECTOR_REORG_COMPENSATED, INDEXER_COLLECTOR_REORG_COMPENSATION_FAILED, INDEXER_COLLECTOR_REORG_DETECTED, INDEXER_COLLECTOR_RPC_QUERY_INVALID_RANGE, INDEXER_COLLECTOR_STARTED, INDEXER_COLLECTOR_TRANSFERS_INDEXED, INDEXER_COLLECTOR_TRANSFERS_INSERT_FAILED } = Events;
85
+ function defineEvents(events) {
86
+ const names = Object.values(events);
87
+ validateUnique(names);
88
+ validatePattern(names);
89
+ return events;
90
+ }
91
+ function validateUnique(names) {
92
+ const seen = /* @__PURE__ */ new Set();
93
+ for (const name of names) {
94
+ if (seen.has(name)) throw new Error(`Duplicate telemetry event name: ${name}`);
95
+ seen.add(name);
96
+ }
97
+ }
98
+ function validatePattern(names) {
99
+ for (const name of names) if (!eventNamePattern.test(name)) throw new Error(`Invalid telemetry event name: ${name}`);
100
+ }
101
+
102
+ //#endregion
42
103
  //#region src/logger/Logger.ts
43
104
  var Logger_exports = /* @__PURE__ */ __exportAll({
44
105
  LogLevelValues: () => LogLevelValues,
45
106
  defaultLogger: () => defaultLogger,
46
107
  getLogger: () => getLogger,
108
+ runWithLogContext: () => runWithLogContext,
47
109
  runWithLogger: () => runWithLogger,
48
110
  silentLogger: () => silentLogger
49
111
  });
@@ -65,15 +127,16 @@ function defaultLogger(minLevel, pretty) {
65
127
  }, {});
66
128
  const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
67
129
  const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
130
+ const normalizedEntry = normalizeLogEntry(entry);
68
131
  if (!prettyEnabled) {
69
132
  console[consoleMethod](stringify({
70
133
  level: methodLevel,
71
- ...entry
134
+ ...normalizedEntry
72
135
  }));
73
136
  return;
74
137
  }
75
- const { msg, ...rest } = entry;
76
- const stack = typeof rest.stack === "string" ? rest.stack : void 0;
138
+ const { msg, ...rest } = normalizedEntry;
139
+ const stack = typeof rest.stack === "string" ? rest.stack : getErrorStack(rest.err);
77
140
  if (stack) delete rest.stack;
78
141
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
79
142
  const level = methodLevel.toUpperCase();
@@ -103,11 +166,40 @@ function silentLogger() {
103
166
  };
104
167
  }
105
168
  const loggerContext = new AsyncLocalStorage();
169
+ const logEntryContext = new AsyncLocalStorage();
106
170
  function runWithLogger(logger, fn) {
107
171
  return loggerContext.run(logger, fn);
108
172
  }
173
+ /**
174
+ * Run a function with additional context fields attached to every log entry emitted via {@link getLogger}.
175
+ * Nested calls merge context (inner keys override outer keys).
176
+ * @param context - Static fields added to all log entries in this scope.
177
+ * @param fn - Async function to run with the scoped logging context.
178
+ * @returns The result of the function.
179
+ */
180
+ function runWithLogContext(context, fn) {
181
+ const inheritedContext = logEntryContext.getStore() ?? {};
182
+ return logEntryContext.run({
183
+ ...inheritedContext,
184
+ ...context
185
+ }, fn);
186
+ }
109
187
  function getLogger() {
110
- return loggerContext.getStore() ?? defaultLogger();
188
+ const scopedLogger = loggerContext.getStore() ?? defaultLogger();
189
+ const scopedContext = logEntryContext.getStore();
190
+ if (!scopedContext || Object.keys(scopedContext).length === 0) return scopedLogger;
191
+ const withContext = (logFn) => (entry) => logFn({
192
+ ...scopedContext,
193
+ ...entry
194
+ });
195
+ return {
196
+ trace: withContext(scopedLogger.trace),
197
+ debug: withContext(scopedLogger.debug),
198
+ info: withContext(scopedLogger.info),
199
+ warn: withContext(scopedLogger.warn),
200
+ error: withContext(scopedLogger.error),
201
+ fatal: withContext(scopedLogger.fatal)
202
+ };
111
203
  }
112
204
  function formatValue(value) {
113
205
  if (value === null || value === void 0 || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
@@ -125,6 +217,112 @@ function formatValue(value) {
125
217
  }
126
218
  }
127
219
  }
220
+ function normalizeLogEntry(entry) {
221
+ return normalizeUnknown(entry, /* @__PURE__ */ new WeakSet());
222
+ }
223
+ function normalizeUnknown(value, path) {
224
+ if (value instanceof Error) return serializeError(value, path);
225
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
226
+ if (Array.isArray(value)) {
227
+ if (path.has(value)) return "[Circular]";
228
+ path.add(value);
229
+ try {
230
+ return value.map((item) => normalizeUnknown(item, path));
231
+ } finally {
232
+ path.delete(value);
233
+ }
234
+ }
235
+ if (value && typeof value === "object") {
236
+ if (path.has(value)) return "[Circular]";
237
+ path.add(value);
238
+ try {
239
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, normalizeUnknown(nested, path)]));
240
+ } finally {
241
+ path.delete(value);
242
+ }
243
+ }
244
+ return value;
245
+ }
246
+ function serializeError(error, path) {
247
+ if (path.has(error)) return {
248
+ name: error.name,
249
+ message: error.message,
250
+ circular: true
251
+ };
252
+ path.add(error);
253
+ try {
254
+ const serialized = {
255
+ name: error.name,
256
+ message: error.message
257
+ };
258
+ if (typeof error.stack === "string") serialized.stack = error.stack;
259
+ if ("cause" in error && error.cause !== void 0) serialized.cause = normalizeUnknown(error.cause, path);
260
+ for (const [key, value] of Object.entries(error)) {
261
+ if (key === "name" || key === "message" || key === "stack" || key === "cause") continue;
262
+ serialized[key] = normalizeUnknown(value, path);
263
+ }
264
+ return serialized;
265
+ } finally {
266
+ path.delete(error);
267
+ }
268
+ }
269
+ function getErrorStack(errorValue) {
270
+ if (!errorValue || typeof errorValue !== "object") return void 0;
271
+ const nestedStack = errorValue.stack;
272
+ return typeof nestedStack === "string" ? nestedStack : void 0;
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/observability/Telemetry.ts
277
+ const invalidTraceId = "0".repeat(32);
278
+ const invalidSpanId = "0".repeat(16);
279
+ /**
280
+ * Read active trace and span identifiers when a span is currently active.
281
+ * @returns Active identifiers when available.
282
+ */
283
+ function getActiveTraceIdentifiers() {
284
+ const activeSpan = trace.getActiveSpan();
285
+ if (!activeSpan) return {};
286
+ const spanContext = activeSpan.spanContext();
287
+ return getTraceIdentifiersFromContext(spanContext.traceId, spanContext.spanId);
288
+ }
289
+ /**
290
+ * Build log-safe trace identifiers from OpenTelemetry span context values.
291
+ * @param traceId - OpenTelemetry trace identifier.
292
+ * @param spanId - OpenTelemetry span identifier.
293
+ * @returns Trace identifiers when both ids are valid and non-noop.
294
+ */
295
+ function getTraceIdentifiersFromContext(traceId, spanId) {
296
+ if (!isValidOtelIdentifier(traceId, invalidTraceId) || !isValidOtelIdentifier(spanId, invalidSpanId)) return {};
297
+ return {
298
+ trace_id: traceId,
299
+ span_id: spanId
300
+ };
301
+ }
302
+ /**
303
+ * Build a logger that enriches each entry with shared telemetry context.
304
+ * @param context - Static fields attached to every log entry.
305
+ * @returns Context-aware logger.
306
+ */
307
+ function getLoggerWithContext(context) {
308
+ const scopedLogger = getLogger();
309
+ const withContext = (logFn) => (entry) => logFn({
310
+ ...context,
311
+ ...getActiveTraceIdentifiers(),
312
+ ...entry
313
+ });
314
+ return {
315
+ trace: withContext(scopedLogger.trace),
316
+ debug: withContext(scopedLogger.debug),
317
+ info: withContext(scopedLogger.info),
318
+ warn: withContext(scopedLogger.warn),
319
+ error: withContext(scopedLogger.error),
320
+ fatal: withContext(scopedLogger.fatal)
321
+ };
322
+ }
323
+ function isValidOtelIdentifier(identifier, invalidValue) {
324
+ return identifier.length === invalidValue.length && identifier !== invalidValue;
325
+ }
128
326
 
129
327
  //#endregion
130
328
  //#region src/tracer/Tracer.ts
@@ -566,14 +764,18 @@ var utils_exports = /* @__PURE__ */ __exportAll({
566
764
 
567
765
  //#endregion
568
766
  //#region src/indexer/collectors/Admin.ts
569
- function create$21(parameters) {
767
+ function create$22(parameters) {
570
768
  const collector = "admin";
571
769
  const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
572
770
  const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
573
771
  let finalizedBlock = null;
574
772
  let unfinalizedBlocks = [];
575
773
  let initialized = false;
576
- const logger = getLogger();
774
+ const logger = getLoggerWithContext({
775
+ component: "indexer.collector.admin",
776
+ collector,
777
+ chain_id: client.chain.id
778
+ });
577
779
  let isMaxBlockNumberReached = false;
578
780
  let tick = 0;
579
781
  return { syncBlock: async () => {
@@ -593,9 +795,8 @@ function create$21(parameters) {
593
795
  const { epoch, blockNumber: latestSavedBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
594
796
  if (maxBlockNumberBI !== void 0 && head.number >= maxBlockNumberBI) {
595
797
  logger.info({
798
+ event: INDEXER_COLLECTOR_MAX_BLOCK_REACHED,
596
799
  msg: `Head is greater than max block number`,
597
- collector,
598
- chainId: client.chain.id,
599
800
  block_number: head.number,
600
801
  max_block_number: maxBlockNumber
601
802
  });
@@ -611,7 +812,6 @@ function create$21(parameters) {
611
812
  tick,
612
813
  client,
613
814
  logger,
614
- collector,
615
815
  unfinalizedBlocks,
616
816
  previousFinalizedBlock: finalizedBlock
617
817
  });
@@ -622,7 +822,6 @@ function create$21(parameters) {
622
822
  unfinalizedBlocks,
623
823
  finalizedBlock,
624
824
  logger,
625
- collector,
626
825
  maxBatchSize
627
826
  });
628
827
  unfinalizedBlocks = newUnfinalizedBlocks;
@@ -648,7 +847,7 @@ const commonAncestor = (block, unfinalizedBlocks) => {
648
847
  return null;
649
848
  };
650
849
  const fetchFinalizedBlock = async (parameters) => {
651
- let { tick, client, logger, collector, unfinalizedBlocks, previousFinalizedBlock } = parameters;
850
+ let { tick, client, logger, unfinalizedBlocks, previousFinalizedBlock } = parameters;
652
851
  let finalizedBlock = previousFinalizedBlock;
653
852
  if (tick % 20 === 0 || previousFinalizedBlock === null) {
654
853
  finalizedBlock = await client.getBlock({
@@ -658,8 +857,7 @@ const fetchFinalizedBlock = async (parameters) => {
658
857
  if (finalizedBlock === null || finalizedBlock.number === null) {
659
858
  const msg = "Failed to get finalized block";
660
859
  logger.fatal({
661
- collector,
662
- chainId: client.chain.id,
860
+ event: INDEXER_COLLECTOR_FINALIZED_BLOCK_FAILED,
663
861
  msg
664
862
  });
665
863
  throw new Error(msg);
@@ -669,8 +867,7 @@ const fetchFinalizedBlock = async (parameters) => {
669
867
  if (finalizedBlock.number === null || finalizedBlock.hash === null || finalizedBlock.parentHash === null) {
670
868
  const msg = "Failed to get finalized block";
671
869
  logger.fatal({
672
- collector,
673
- chainId: client.chain.id,
870
+ event: INDEXER_COLLECTOR_FINALIZED_BLOCK_FAILED,
674
871
  msg
675
872
  });
676
873
  throw new Error(msg);
@@ -682,8 +879,7 @@ const fetchFinalizedBlock = async (parameters) => {
682
879
  };
683
880
  };
684
881
  const reconcile = async (parameters) => {
685
- let { client, block, unfinalizedBlocks, finalizedBlock, logger, collector, maxBatchSize } = parameters;
686
- const chain = client.chain;
882
+ let { client, block, unfinalizedBlocks, finalizedBlock, logger, maxBatchSize } = parameters;
687
883
  if (block.hash === null || block.number === null || block.parentHash === null) throw new Error("Failed to get block");
688
884
  const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
689
885
  if (latestBlock === void 0) {
@@ -707,9 +903,8 @@ const reconcile = async (parameters) => {
707
903
  if (latestBlock.number >= block.number) {
708
904
  const ancestor = commonAncestor(block, unfinalizedBlocks) || finalizedBlock;
709
905
  logger.info({
906
+ event: INDEXER_COLLECTOR_REORG_DETECTED,
710
907
  msg: `Reorg detected, latestBlock.number >= block.number`,
711
- collector,
712
- chain_id: chain.id,
713
908
  ancestor: ancestor.number,
714
909
  latest_block_number: latestBlock.number,
715
910
  block_number: block.number
@@ -723,8 +918,7 @@ const reconcile = async (parameters) => {
723
918
  }
724
919
  if (latestBlock.number + 1n < block.number) {
725
920
  logger.debug({
726
- collector,
727
- chain_id: chain.id,
921
+ event: INDEXER_COLLECTOR_MISSING_BLOCKS,
728
922
  block_range: [latestBlock.number, block.number],
729
923
  msg: `Missing blocks`
730
924
  });
@@ -749,7 +943,6 @@ const reconcile = async (parameters) => {
749
943
  unfinalizedBlocks,
750
944
  finalizedBlock,
751
945
  logger,
752
- collector,
753
946
  maxBatchSize
754
947
  });
755
948
  if (returnedBlock.number !== missingBlock.number) return {
@@ -764,16 +957,14 @@ const reconcile = async (parameters) => {
764
957
  unfinalizedBlocks,
765
958
  finalizedBlock,
766
959
  logger,
767
- collector,
768
960
  maxBatchSize
769
961
  });
770
962
  }
771
963
  if (block.parentHash !== latestBlock.hash) {
772
964
  const ancestor = commonAncestor(block, unfinalizedBlocks) || finalizedBlock;
773
965
  logger.info({
966
+ event: INDEXER_COLLECTOR_REORG_DETECTED,
774
967
  msg: `Reorg detected, block parent hash !== latest block hash`,
775
- collector,
776
- chain_id: chain.id,
777
968
  ancestor: ancestor.number,
778
969
  latest_block_number: latestBlock.number,
779
970
  block_number: block.number
@@ -806,31 +997,37 @@ const names = [
806
997
  "positions",
807
998
  "prices"
808
999
  ];
809
- function create$20({ name, collect, client, db, options }) {
810
- const admin = create$21({
1000
+ function create$21({ name, collect, client, db, options }) {
1001
+ const admin = create$22({
811
1002
  client,
812
1003
  db,
813
1004
  options
814
1005
  });
1006
+ const chain = client.chain;
1007
+ const interval = options.interval ?? 1e4;
815
1008
  return {
816
1009
  name,
817
- chain: client.chain,
1010
+ chain,
818
1011
  client,
819
1012
  db,
820
- interval: options.interval ?? 1e4,
1013
+ interval,
821
1014
  collect: async function* () {
822
1015
  const collector = name;
823
1016
  const chain = client.chain;
824
- const logger = getLogger();
1017
+ const logger = getLoggerWithContext({
1018
+ component: "indexer.collector",
1019
+ collector,
1020
+ chain_id: chain.id
1021
+ });
825
1022
  const collectorId = `${client.chain.id.toString()}.collector.${collector}`;
826
1023
  const tracer = getTracer(`router.${collectorId}`);
827
1024
  logger.info({
828
- msg: `Collector started`,
829
- collector,
830
- chain_id: chain.id
1025
+ event: INDEXER_COLLECTOR_STARTED,
1026
+ msg: `Collector started`
831
1027
  });
832
1028
  let iterator = null;
833
1029
  let lastBlockNumber;
1030
+ let retryAttempt = 0;
834
1031
  while (true) try {
835
1032
  if (iterator === null) iterator = await startActiveSpan(tracer, `${collectorId}.init`, async () => {
836
1033
  const { collector: collectorBlock } = await db.blocks.init({
@@ -859,20 +1056,36 @@ function create$20({ name, collect, client, db, options }) {
859
1056
  done
860
1057
  };
861
1058
  });
862
- if (done) iterator = null;
863
- else {
1059
+ if (done) {
1060
+ iterator = null;
1061
+ retryAttempt = 0;
1062
+ } else {
864
1063
  lastBlockNumber = blockNumber;
1064
+ retryAttempt = 0;
865
1065
  yield blockNumber;
866
1066
  }
867
1067
  } catch (err) {
868
- const isError = err instanceof Error;
1068
+ const error = err instanceof Error ? err : new Error(String(err));
1069
+ const invalidRange = getInvalidBlockRange(error);
1070
+ if (invalidRange !== null) logger.warn({
1071
+ event: INDEXER_COLLECTOR_RPC_QUERY_INVALID_RANGE,
1072
+ msg: "Invalid eth_getLogs block range rejected",
1073
+ rpc_method: "eth_getLogs",
1074
+ action: "reset_iterator",
1075
+ from_block: invalidRange.fromBlock,
1076
+ to_block: invalidRange.toBlock
1077
+ });
1078
+ retryAttempt += 1;
1079
+ const retryDelayMs = Math.min(interval, 1e3 * 2 ** Math.min(retryAttempt - 1, 3));
1080
+ iterator = null;
869
1081
  logger.error({
1082
+ event: INDEXER_COLLECTOR_FAILED,
870
1083
  msg: "Collector error",
871
- collector,
872
- chain_id: chain.id,
873
- error: isError ? err.message : String(err),
874
- stack: isError ? err.stack : void 0
1084
+ err: error,
1085
+ retry_attempt: retryAttempt,
1086
+ retry_delay_ms: retryDelayMs
875
1087
  });
1088
+ await wait(retryDelayMs);
876
1089
  }
877
1090
  }
878
1091
  };
@@ -884,7 +1097,11 @@ function create$20({ name, collect, client, db, options }) {
884
1097
  function start(collector) {
885
1098
  let stopped = false;
886
1099
  const it = collector.collect();
887
- const logger = getLogger();
1100
+ const logger = getLoggerWithContext({
1101
+ component: "indexer.collector",
1102
+ collector: collector.name,
1103
+ chain_id: collector.chain.id
1104
+ });
888
1105
  const collectorId = `${collector.chain.id.toString()}.collector.${collector.name}`;
889
1106
  const tracer = getTracer(`router.${collectorId}`);
890
1107
  const blocks = collector.db.blocks;
@@ -918,9 +1135,8 @@ function start(collector) {
918
1135
  } catch (err) {
919
1136
  const error = err instanceof Error ? err : new Error(String(err));
920
1137
  logger.error({
1138
+ event: INDEXER_COLLECTOR_POLL_FAILED,
921
1139
  msg: "Collector polling error",
922
- collector: collector.name,
923
- chain_id: collector.chain.id,
924
1140
  err: error
925
1141
  });
926
1142
  }
@@ -930,6 +1146,20 @@ function start(collector) {
930
1146
  stopped = true;
931
1147
  };
932
1148
  }
1149
+ const getInvalidBlockRange = (error) => {
1150
+ if (error.name !== "Chain.InvalidBlockRangeError") return null;
1151
+ const candidate = error;
1152
+ if (typeof candidate.fromBlock === "bigint" && typeof candidate.toBlock === "bigint") return {
1153
+ fromBlock: candidate.fromBlock.toString(),
1154
+ toBlock: candidate.toBlock.toString()
1155
+ };
1156
+ const match = error.message.match(/From block (\d+) to block (\d+)\./);
1157
+ if (!match) return null;
1158
+ return {
1159
+ fromBlock: match[1],
1160
+ toBlock: match[2]
1161
+ };
1162
+ };
933
1163
 
934
1164
  //#endregion
935
1165
  //#region src/core/Abi/MetaMorpho.ts
@@ -953,55 +1183,57 @@ const MetaMorphoFactory = parseAbi(["event CreateMetaMorpho(address indexed meta
953
1183
  //#region src/core/Abi/MorphoV2.ts
954
1184
  const MorphoV2 = parseAbi([
955
1185
  "constructor()",
956
- "function collateralOf(bytes32 id, address user, address collateralToken) view returns (uint256)",
1186
+ "function collateralOf(bytes20 id, address user, uint256 collateralIndex) view returns (uint128)",
957
1187
  "function consume(bytes32 group, uint256 amount)",
958
1188
  "function consumed(address user, bytes32 group) view returns (uint256)",
959
- "function debtOf(bytes32 id, address user) view returns (uint256)",
1189
+ "function debtOf(bytes20 id, address user) view returns (uint256)",
960
1190
  "function defaultFees(address loanToken, uint256 index) view returns (uint16)",
961
1191
  "function feeSetter() view returns (address)",
962
- "function fees(bytes32 id) view returns (uint16[6])",
1192
+ "function fees(bytes20 id) view returns (uint16[6])",
963
1193
  "function flashLoan(address token, uint256 assets, address callback, bytes data)",
964
- "function isHealthy((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, bytes32 id, address borrower) view returns (bool)",
965
- "function liquidate((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, (uint256 collateralIndex, uint256 repaid, uint256 seized)[] seizures, address borrower, bytes data) returns ((uint256 collateralIndex, uint256 repaid, uint256 seized)[])",
1194
+ "function isHealthy((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, bytes20 id, address borrower) view returns (bool)",
1195
+ "function liquidate((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, bytes data) returns (uint256, uint256)",
966
1196
  "function multicall(bytes[] calls)",
967
- "function obligationCreated(bytes32 id) view returns (bool)",
968
- "function obligationState(bytes32 id) view returns (uint128 totalUnits, uint128 totalShares, uint256 withdrawable, bool created)",
1197
+ "function obligationCreated(bytes20 id) view returns (bool)",
1198
+ "function obligationState(bytes20 id) view returns (uint128 totalUnits, uint128 totalShares, uint256 withdrawable, bool created, uint16[6] fees)",
969
1199
  "function owner() view returns (address)",
970
- "function repay((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, uint256 obligationUnits, address onBehalf)",
1200
+ "function repay((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, uint256 obligationUnits, address onBehalf)",
971
1201
  "function session(address user) view returns (bytes32)",
972
1202
  "function setDefaultTradingFee(address loanToken, uint256 index, uint256 newTradingFee)",
973
1203
  "function setFeeSetter(address newFeeSetter)",
974
- "function setObligationTradingFee(bytes32 id, uint256 index, uint256 newTradingFee)",
1204
+ "function setObligationTradingFee(bytes20 id, uint256 index, uint256 newTradingFee)",
975
1205
  "function setOwner(address newOwner)",
976
1206
  "function setTradingFeeRecipient(address feeRecipient)",
977
- "function sharesOf(bytes32 id, address user) view returns (uint256)",
1207
+ "function sharesOf(bytes20 id, address user) view returns (uint256)",
978
1208
  "function shuffleSession()",
979
- "function supplyCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, address collateral, uint256 assets, address onBehalf)",
980
- "function take(uint256 buyerAssets, uint256 sellerAssets, uint256 obligationUnits, uint256 obligationShares, address taker, address takerCallback, bytes takerCallbackData, address receiverIfTakerIsSeller, ((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, bool buy, address maker, uint256 assets, uint256 obligationUnits, uint256 obligationShares, uint256 start, uint256 expiry, uint256 tick, bytes32 group, bytes32 session, address callback, bytes callbackData, address receiverIfMakerIsSeller) offer, (uint8 v, bytes32 r, bytes32 s) sig, bytes32 root, bytes32[] proof) returns (uint256, uint256, uint256, uint256)",
981
- "function totalShares(bytes32 id) view returns (uint256)",
982
- "function totalUnits(bytes32 id) view returns (uint256)",
983
- "function touchObligation((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation) returns (bytes32)",
984
- "function tradingFee(bytes32 id, uint256 timeToMaturity) view returns (uint256)",
1209
+ "function supplyCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, uint256 collateralIndex, uint256 assets, address onBehalf)",
1210
+ "function take(uint256 buyerAssets, uint256 sellerAssets, uint256 obligationUnits, uint256 obligationShares, address taker, address takerCallback, bytes takerCallbackData, address receiverIfTakerIsSeller, ((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, bool buy, address maker, uint256 assets, uint256 obligationUnits, uint256 obligationShares, uint256 start, uint256 expiry, uint256 tick, bytes32 group, bytes32 session, address callback, bytes callbackData, address receiverIfMakerIsSeller) offer, (uint8 v, bytes32 r, bytes32 s) sig, bytes32 root, bytes32[] proof) returns (uint256, uint256, uint256, uint256)",
1211
+ "function toId((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation) view returns (bytes20)",
1212
+ "function toObligation(bytes20 id) view returns ((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue))",
1213
+ "function totalShares(bytes20 id) view returns (uint256)",
1214
+ "function totalUnits(bytes20 id) view returns (uint256)",
1215
+ "function touchObligation((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation) returns (bytes20)",
1216
+ "function tradingFee(bytes20 id, uint256 timeToMaturity) view returns (uint256)",
985
1217
  "function tradingFeeRecipient() view returns (address)",
986
- "function withdraw((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, uint256 obligationUnits, uint256 shares, address onBehalf, address receiver) returns (uint256, uint256)",
987
- "function withdrawCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, address collateral, uint256 assets, address onBehalf, address receiver)",
988
- "function withdrawable(bytes32 id) view returns (uint256)",
1218
+ "function withdraw((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, uint256 obligationUnits, uint256 shares, address onBehalf, address receiver) returns (uint256, uint256)",
1219
+ "function withdrawCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation, uint256 collateralIndex, uint256 assets, address onBehalf, address receiver)",
1220
+ "function withdrawable(bytes20 id) view returns (uint256)",
989
1221
  "event Constructor(address indexed owner)",
990
1222
  "event Consume(address indexed user, bytes32 indexed group, uint256 amount)",
991
1223
  "event FlashLoan(address indexed caller, address indexed token, uint256 assets)",
992
- "event Liquidate(address indexed caller, bytes32 indexed id, (uint256 collateralIndex, uint256 repaid, uint256 seized)[] seizures, address indexed borrower, uint256 totalRepaid, uint256 badDebt)",
993
- "event ObligationCreated(bytes32 indexed id, (address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation)",
994
- "event Repay(address indexed caller, bytes32 indexed id, uint256 obligationUnits, address indexed onBehalf)",
1224
+ "event Liquidate(address indexed caller, bytes20 indexed id_, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address indexed borrower, uint256 badDebt)",
1225
+ "event ObligationCreated(bytes20 indexed id_, (address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity, uint256 minCollatValue) obligation)",
1226
+ "event Repay(address indexed caller, bytes20 indexed id_, uint256 obligationUnits, address indexed onBehalf)",
995
1227
  "event SetDefaultTradingFee(address indexed loanToken, uint256 indexed index, uint256 newTradingFee)",
996
1228
  "event SetFeeSetter(address indexed feeSetter)",
997
- "event SetObligationTradingFee(bytes32 indexed id, uint256 indexed index, uint256 newTradingFee)",
1229
+ "event SetObligationTradingFee(bytes20 indexed id_, uint256 indexed index, uint256 newTradingFee)",
998
1230
  "event SetOwner(address indexed owner)",
999
1231
  "event SetTradingFeeRecipient(address indexed feeRecipient)",
1000
1232
  "event ShuffleSession(address indexed user, bytes32 session)",
1001
- "event SupplyCollateral(address caller, bytes32 indexed id, address indexed collateral, uint256 assets, address indexed onBehalf)",
1002
- "event Take(address caller, bytes32 indexed id, address indexed maker, address indexed taker, bool offerIsBuy, uint256 buyerAssets, uint256 sellerAssets, uint256 obligationUnits, uint256 obligationShares, bool buyerIsLender, bool sellerIsBorrower, address sellerReceiver, bytes32 group, uint256 consumed)",
1003
- "event Withdraw(address caller, bytes32 indexed id, uint256 obligationUnits, uint256 shares, address indexed onBehalf, address indexed receiver)",
1004
- "event WithdrawCollateral(address caller, bytes32 indexed id, address indexed collateral, uint256 assets, address indexed onBehalf, address receiver)"
1233
+ "event SupplyCollateral(address caller, bytes20 indexed id_, address indexed collateral, uint256 assets, address indexed onBehalf)",
1234
+ "event Take(address caller, bytes20 indexed id_, address indexed maker, address indexed taker, bool offerIsBuy, uint256 buyerAssets, uint256 sellerAssets, uint256 obligationUnits, uint256 obligationShares, bool buyerIsLender, bool sellerIsBorrower, address sellerReceiver, bytes32 group, uint256 consumed)",
1235
+ "event Withdraw(address caller, bytes20 indexed id_, uint256 obligationUnits, uint256 shares, address indexed onBehalf, address indexed receiver)",
1236
+ "event WithdrawCollateral(address caller, bytes20 indexed id_, address indexed collateral, uint256 assets, address indexed onBehalf, address receiver)"
1005
1237
  ]);
1006
1238
 
1007
1239
  //#endregion
@@ -1159,6 +1391,7 @@ const Morpho = [
1159
1391
  //#endregion
1160
1392
  //#region src/core/Callback.ts
1161
1393
  var Callback_exports = /* @__PURE__ */ __exportAll({
1394
+ CallbackType: () => CallbackType,
1162
1395
  Type: () => Type$1,
1163
1396
  isEmptyCallback: () => isEmptyCallback
1164
1397
  });
@@ -1167,6 +1400,10 @@ let Type$1 = /* @__PURE__ */ function(Type) {
1167
1400
  Type["SellWithEmptyCallback"] = "sell_with_empty_callback";
1168
1401
  return Type;
1169
1402
  }({});
1403
+ let CallbackType = /* @__PURE__ */ function(CallbackType) {
1404
+ CallbackType["Empty"] = "empty";
1405
+ return CallbackType;
1406
+ }({});
1170
1407
  const isEmptyCallback = (offer) => offer.callback.data === "0x";
1171
1408
 
1172
1409
  //#endregion
@@ -1177,6 +1414,7 @@ var Chain_exports = /* @__PURE__ */ __exportAll({
1177
1414
  InvalidBlockRangeError: () => InvalidBlockRangeError,
1178
1415
  InvalidBlockWindowError: () => InvalidBlockWindowError,
1179
1416
  MissingBlockNumberError: () => MissingBlockNumberError,
1417
+ UnrecoverableLogsResponseSizeError: () => UnrecoverableLogsResponseSizeError,
1180
1418
  chainIds: () => chainIds,
1181
1419
  chainNames: () => chainNames,
1182
1420
  chains: () => chains$1,
@@ -1243,8 +1481,8 @@ const chains$1 = {
1243
1481
  name: "base",
1244
1482
  custom: {
1245
1483
  morpho: {
1246
- address: "0x3F067BC9D8898F6ec02D6480c3fF1026E512BcBF",
1247
- blockCreated: 41799989
1484
+ address: "0x4C752Cdc4b13c9A6a933CbecfE050eC0BA0B45f9",
1485
+ blockCreated: 42365274
1248
1486
  },
1249
1487
  morphoBlue: {
1250
1488
  address: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
@@ -1273,8 +1511,8 @@ const chains$1 = {
1273
1511
  name: "ethereum-virtual-testnet",
1274
1512
  custom: {
1275
1513
  morpho: {
1276
- address: "0xc9f3c65996fc46b9500608b2c9a9152c01c540f7",
1277
- blockCreated: 23226871
1514
+ address: "0x9ac49a344376964291f7289663beb78e2952de44",
1515
+ blockCreated: 23229385
1278
1516
  },
1279
1517
  morphoBlue: {
1280
1518
  address: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
@@ -1332,33 +1570,72 @@ const MAX_BATCH_SIZE = 1e4;
1332
1570
  const DEFAULT_BATCH_SIZE$2 = 2500;
1333
1571
  const MAX_BLOCK_WINDOW = 1e4;
1334
1572
  const DEFAULT_BLOCK_WINDOW = 8e3;
1573
+ const MIN_BLOCK_WINDOW = 0n;
1574
+ const oversizedLogsErrorPatterns = [
1575
+ "cannot create a string longer than",
1576
+ "response is too big",
1577
+ "response size exceeded",
1578
+ "log response size exceeded",
1579
+ "query returned more than",
1580
+ "too many results"
1581
+ ];
1582
+ const getLatestBlockNumber = async (client) => {
1583
+ return await getBlockNumber(client, { cacheTime: 0 });
1584
+ };
1335
1585
  async function* streamLogs(parameters) {
1336
1586
  const { client, contractAddress, event, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$2, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
1337
1587
  if (maxBatchSize > MAX_BATCH_SIZE) throw new InvalidBatchSizeError(maxBatchSize);
1338
1588
  if (blockWindow > MAX_BLOCK_WINDOW) throw new InvalidBlockWindowError(blockWindow);
1339
1589
  if (order === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
1340
- const latestBlock = (await getBlock(client, {
1341
- blockTag: "latest",
1342
- includeTransactions: false
1343
- })).number;
1590
+ const ascendingLowerBound = order === "asc" ? BigInt(blockNumberGte) : void 0;
1591
+ const latestBlock = await getLatestBlockNumber(client);
1592
+ let upperBound = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
1593
+ const lowerBound = BigInt(blockNumberGte || 0);
1594
+ const configuredBlockWindow = BigInt(blockWindow);
1595
+ let adaptiveBlockWindow = configuredBlockWindow;
1344
1596
  let toBlock = 0n;
1345
- if (order === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
1346
- if (order === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
1597
+ if (order === "asc") toBlock = min(ascendingLowerBound + adaptiveBlockWindow, upperBound);
1598
+ if (order === "desc") toBlock = upperBound;
1347
1599
  let fromBlock = 0n;
1348
- if (order === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
1349
- if (order === "desc") fromBlock = max$1(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
1350
- if (order === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
1351
- if (order === "desc") fromBlock = max$1(fromBlock, toBlock - BigInt(blockWindow));
1600
+ if (order === "asc") fromBlock = ascendingLowerBound;
1601
+ if (order === "desc") fromBlock = max$1(BigInt(blockNumberGte || toBlock - adaptiveBlockWindow), 0n);
1602
+ if (order === "asc") toBlock = min(toBlock, fromBlock + adaptiveBlockWindow);
1603
+ if (order === "desc") fromBlock = max$1(fromBlock, toBlock - adaptiveBlockWindow);
1352
1604
  if (fromBlock > toBlock) throw new InvalidBlockRangeError(fromBlock, toBlock);
1353
1605
  let streaming = true;
1354
1606
  while (streaming) {
1355
- const logs = await getLogs(client, {
1356
- address: contractAddress,
1357
- event,
1358
- fromBlock,
1359
- toBlock
1360
- });
1361
- streaming = order === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
1607
+ let logs;
1608
+ try {
1609
+ logs = await getLogs(client, {
1610
+ address: contractAddress,
1611
+ event,
1612
+ fromBlock,
1613
+ toBlock
1614
+ });
1615
+ } catch (err) {
1616
+ if (order === "asc" && isBlockOutOfRangeError(err)) {
1617
+ const previousUpperBound = upperBound;
1618
+ const previousFromBlock = fromBlock;
1619
+ const previousToBlock = toBlock;
1620
+ const latestBlockOnRetry = await getLatestBlockNumber(client);
1621
+ upperBound = min(upperBound, latestBlockOnRetry);
1622
+ if (upperBound < ascendingLowerBound) throw new InvalidBlockRangeError(ascendingLowerBound, upperBound);
1623
+ toBlock = min(toBlock, upperBound);
1624
+ fromBlock = max$1(min(fromBlock, upperBound), ascendingLowerBound);
1625
+ if (fromBlock > toBlock) throw new InvalidBlockRangeError(fromBlock, toBlock);
1626
+ if (!(upperBound < previousUpperBound || fromBlock < previousFromBlock || toBlock < previousToBlock)) throw err;
1627
+ continue;
1628
+ }
1629
+ if (isOversizedLogsError(err)) {
1630
+ if (fromBlock === toBlock) throw new UnrecoverableLogsResponseSizeError(fromBlock, err);
1631
+ adaptiveBlockWindow = max$1((toBlock - fromBlock) / 2n, MIN_BLOCK_WINDOW);
1632
+ if (order === "asc") toBlock = min(fromBlock + adaptiveBlockWindow, upperBound);
1633
+ else fromBlock = max$1(toBlock - adaptiveBlockWindow, lowerBound);
1634
+ continue;
1635
+ }
1636
+ throw err;
1637
+ }
1638
+ streaming = order === "asc" ? toBlock < upperBound : fromBlock > lowerBound;
1362
1639
  if (logs.length === 0 && !streaming) break;
1363
1640
  if (logs.length === 0 && streaming) yield {
1364
1641
  logs: [],
@@ -1373,17 +1650,19 @@ async function* streamLogs(parameters) {
1373
1650
  logs: logBatch,
1374
1651
  blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order === "asc" ? Number(toBlock) : Number(fromBlock)
1375
1652
  };
1653
+ if (adaptiveBlockWindow < configuredBlockWindow) {
1654
+ const expandedBlockWindow = adaptiveBlockWindow === 0n ? 1n : adaptiveBlockWindow * 2n;
1655
+ adaptiveBlockWindow = min(expandedBlockWindow, configuredBlockWindow);
1656
+ }
1376
1657
  if (order === "asc") {
1377
- const upperBound = BigInt(blockNumberLte || latestBlock);
1378
1658
  const nextFromBlock = min(BigInt(toBlock) + 1n, upperBound);
1379
- const nextToBlock = min(toBlock + BigInt(blockWindow) + 1n, upperBound);
1659
+ const nextToBlock = min(toBlock + adaptiveBlockWindow + 1n, upperBound);
1380
1660
  fromBlock = nextFromBlock;
1381
1661
  toBlock = nextToBlock;
1382
1662
  }
1383
1663
  if (order === "desc") {
1384
- const lowerBound = BigInt(blockNumberGte || 0);
1385
1664
  const nextToBlock = max$1(fromBlock - 1n, lowerBound);
1386
- const nextFromBlock = max$1(fromBlock - BigInt(blockWindow) - 1n, lowerBound);
1665
+ const nextFromBlock = max$1(fromBlock - adaptiveBlockWindow - 1n, lowerBound);
1387
1666
  toBlock = nextToBlock;
1388
1667
  fromBlock = nextFromBlock;
1389
1668
  }
@@ -1393,10 +1672,23 @@ async function* streamLogs(parameters) {
1393
1672
  blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
1394
1673
  };
1395
1674
  }
1675
+ const isBlockOutOfRangeError = (error) => {
1676
+ let cause = error;
1677
+ while (cause && typeof cause === "object") {
1678
+ const candidate = cause;
1679
+ if (typeof candidate.message === "string" && candidate.message.includes("BlockOutOfRangeError") || typeof candidate.details === "string" && candidate.details.includes("BlockOutOfRangeError")) return true;
1680
+ cause = candidate.cause;
1681
+ }
1682
+ return false;
1683
+ };
1396
1684
  var InvalidBlockRangeError = class extends BaseError {
1397
1685
  name = "Chain.InvalidBlockRangeError";
1686
+ fromBlock;
1687
+ toBlock;
1398
1688
  constructor(fromBlock, toBlock) {
1399
1689
  super(`Invalid block range while streaming data from chain. From block ${fromBlock} to block ${toBlock}.`);
1690
+ this.fromBlock = fromBlock;
1691
+ this.toBlock = toBlock;
1400
1692
  }
1401
1693
  };
1402
1694
  var InvalidBlockWindowError = class extends BaseError {
@@ -1417,16 +1709,35 @@ var MissingBlockNumberError = class extends BaseError {
1417
1709
  super("Missing block number when streaming data from chain in ascending order.");
1418
1710
  }
1419
1711
  };
1712
+ var UnrecoverableLogsResponseSizeError = class extends BaseError {
1713
+ name = "Chain.UnrecoverableLogsResponseSizeError";
1714
+ constructor(blockNumber, cause) {
1715
+ const rootCause = cause instanceof Error ? cause : cause === void 0 ? void 0 : new Error(String(cause));
1716
+ super(`Failed to stream logs because even a single-block query exceeded the RPC response size limit at block ${blockNumber}.`, { cause: rootCause });
1717
+ }
1718
+ };
1719
+ function isOversizedLogsError(err) {
1720
+ return oversizedLogsErrorPatterns.some((pattern) => collectErrorMessages(err).includes(pattern));
1721
+ }
1722
+ function collectErrorMessages(err) {
1723
+ if (!(err instanceof Error)) return "";
1724
+ const fragments = [err.message];
1725
+ const candidate = err;
1726
+ if (typeof candidate.details === "string") fragments.push(candidate.details);
1727
+ if (typeof candidate.shortMessage === "string") fragments.push(candidate.shortMessage);
1728
+ if (candidate.cause instanceof Error) fragments.push(collectErrorMessages(candidate.cause));
1729
+ return fragments.join(" ").toLowerCase();
1730
+ }
1420
1731
 
1421
1732
  //#endregion
1422
1733
  //#region src/core/ChainRegistry.ts
1423
- var ChainRegistry_exports = /* @__PURE__ */ __exportAll({ create: () => create$19 });
1734
+ var ChainRegistry_exports = /* @__PURE__ */ __exportAll({ create: () => create$20 });
1424
1735
  /**
1425
1736
  * Creates a chain registry from a list of chains.
1426
1737
  * @param chains - Array of chain objects to register.
1427
1738
  * @returns A registry for looking up chains by ID. {@link ChainRegistry}
1428
1739
  */
1429
- function create$19(chains) {
1740
+ function create$20(chains) {
1430
1741
  const byId = /* @__PURE__ */ new Map();
1431
1742
  for (const chain of chains) byId.set(chain.id, chain);
1432
1743
  return {
@@ -1717,9 +2028,9 @@ const MaturitySchema = z$1.number().int().refine((maturity) => {
1717
2028
  }
1718
2029
  }, { error: (issue) => {
1719
2030
  try {
1720
- return `The maturity is set to ${/* @__PURE__ */ new Date(issue.input * 1e3)}. It must fall on the allowed settlement cycles (Friday 15:00 UTC at the end of week/month/quarter).`;
2031
+ return `The maturity is set to ${/* @__PURE__ */ new Date(issue.input * 1e3)}. It must be at 15:00:00 UTC.`;
1721
2032
  } catch (_) {
1722
- return `The maturity is set to ${issue.input}. It must fall on the allowed settlement cycles (Friday 15:00 UTC at the end of week/month/quarter).`;
2033
+ return `The maturity is set to ${issue.input}. It must be at 15:00:00 UTC.`;
1723
2034
  }
1724
2035
  } }).transform((maturity) => maturity);
1725
2036
  let MaturityType = /* @__PURE__ */ function(MaturityType) {
@@ -1751,9 +2062,14 @@ function from$16(ts) {
1751
2062
  throw new InvalidOptionError(ts);
1752
2063
  }
1753
2064
  if (typeof ts === "number" && ts > 0xe8d4a51000) throw new InvalidFormatError();
1754
- if (!Object.values(MaturityOptions).some((option) => option() === ts)) throw new InvalidDateError(ts);
2065
+ if (!isAt15UTC(ts)) throw new InvalidDateError(ts);
1755
2066
  return ts;
1756
2067
  }
2068
+ /** Checks whether a timestamp (in seconds) falls at exactly 15:00:00 UTC. */
2069
+ function isAt15UTC(ts) {
2070
+ const date = /* @__PURE__ */ new Date(ts * 1e3);
2071
+ return date.getUTCHours() === 15 && date.getUTCMinutes() === 0 && date.getUTCSeconds() === 0 && date.getUTCMilliseconds() === 0;
2072
+ }
1757
2073
  /** Returns the end of the current week (friday at 15:00:00 UTC) */
1758
2074
  const endOfWeek = () => fridayOfWeek(0);
1759
2075
  /** Returns the end of the next week (friday at 15:00:00 UTC) */
@@ -1814,7 +2130,7 @@ var InvalidFormatError = class extends BaseError {
1814
2130
  var InvalidDateError = class extends BaseError {
1815
2131
  name = "Maturity.InvalidDateError";
1816
2132
  constructor(input) {
1817
- super(`Invalid maturity date. Input: "${input}". Accepted values are: ${Object.values(MaturityOptions).map((option) => `"${option()}"`).join(", ")}.`);
2133
+ super(`Invalid maturity date. Input: "${input}". Maturity must be at 15:00:00 UTC.`);
1818
2134
  }
1819
2135
  };
1820
2136
  var InvalidOptionError = class extends BaseError {
@@ -1841,7 +2157,8 @@ var Obligation_exports = /* @__PURE__ */ __exportAll({
1841
2157
  const ObligationSchema = z$1.object({
1842
2158
  loanToken: z$1.string().transform(transformAddress),
1843
2159
  collaterals: CollateralsSchema,
1844
- maturity: MaturitySchema
2160
+ maturity: MaturitySchema,
2161
+ minCollatValue: z$1.bigint({ coerce: true }).min(0n).optional().default(0n)
1845
2162
  });
1846
2163
  const abi = [
1847
2164
  {
@@ -1856,6 +2173,10 @@ const abi = [
1856
2173
  {
1857
2174
  type: "uint256",
1858
2175
  name: "maturity"
2176
+ },
2177
+ {
2178
+ type: "uint256",
2179
+ name: "minCollatValue"
1859
2180
  }
1860
2181
  ];
1861
2182
  const tupleAbi = [{
@@ -1893,7 +2214,8 @@ function from$15(parameters) {
1893
2214
  return {
1894
2215
  loanToken: parsedObligation.loanToken.toLowerCase(),
1895
2216
  collaterals: parsedObligation.collaterals.sort((a, b) => a.asset.localeCompare(b.asset)),
1896
- maturity: parsedObligation.maturity
2217
+ maturity: parsedObligation.maturity,
2218
+ minCollatValue: parsedObligation.minCollatValue
1897
2219
  };
1898
2220
  } catch (error) {
1899
2221
  throw new InvalidObligationError(error);
@@ -1910,7 +2232,8 @@ function fromSnakeCase$2(input) {
1910
2232
  }
1911
2233
  /**
1912
2234
  * Calculates a canonical key for an obligation payload.
1913
- * The key is computed as keccak256(abi.encode(loanToken, collaterals, maturity)).
2235
+ * The key is computed as keccak256(abi.encode(loanToken, collaterals, maturity, minCollatValue)).
2236
+ * If omitted, `minCollatValue` defaults to `0`.
1914
2237
  * @throws If the collaterals are not sorted alphabetically by address. {@link CollateralsAreNotSortedError}
1915
2238
  * @param parameters - {@link key.Parameters}
1916
2239
  * @returns The obligation key as a 32-byte hex string. {@link key.ReturnType}
@@ -1936,7 +2259,8 @@ function key(parameters) {
1936
2259
  lltv: c.lltv,
1937
2260
  oracle: c.oracle.toLowerCase()
1938
2261
  })),
1939
- BigInt(parameters.maturity)
2262
+ BigInt(parameters.maturity),
2263
+ parameters.minCollatValue ?? 0n
1940
2264
  ]));
1941
2265
  }
1942
2266
  /**
@@ -1988,39 +2312,42 @@ var Id_exports = /* @__PURE__ */ __exportAll({
1988
2312
  creationCode: () => creationCode,
1989
2313
  toId: () => toId
1990
2314
  });
1991
- const CREATION_CODE_PREFIX = "0x603f380380603f5f395ff3";
2315
+ const CREATION_CODE_PREFIX = "0x600b380380600b5f395ff3";
1992
2316
  /**
1993
2317
  * Builds the same creation code as `IdLib.creationCode` in Solidity.
1994
2318
  *
1995
- * Layout: `prefix (11 bytes) + chainId (32 bytes) + morphoV2 (20 bytes) + abi.encode(obligation)`.
2319
+ * Layout: `prefix (11 bytes) + abi.encode(obligation)`.
1996
2320
  *
1997
2321
  * @param parameters - {@link creationCode.Parameters}
1998
2322
  * @returns The CREATE2 init code bytes. {@link creationCode.ReturnType}
1999
2323
  */
2000
2324
  function creationCode(parameters) {
2001
- const encodedObligation = encodeAbiParameters(tupleAbi, [{
2325
+ return concatHex([CREATION_CODE_PREFIX, encodeAbiParameters(tupleAbi, [{
2002
2326
  loanToken: parameters.obligation.loanToken.toLowerCase(),
2003
2327
  collaterals: parameters.obligation.collaterals.map((collateral) => ({
2004
2328
  token: collateral.asset.toLowerCase(),
2005
2329
  lltv: collateral.lltv,
2006
2330
  oracle: collateral.oracle.toLowerCase()
2007
2331
  })),
2008
- maturity: BigInt(parameters.obligation.maturity)
2009
- }]);
2010
- return concatHex([
2011
- CREATION_CODE_PREFIX,
2012
- numberToHex(BigInt(parameters.chainId), { size: 32 }),
2013
- parameters.morphoV2.toLowerCase(),
2014
- encodedObligation
2015
- ]);
2332
+ maturity: BigInt(parameters.obligation.maturity),
2333
+ minCollatValue: 0n
2334
+ }])]);
2016
2335
  }
2017
2336
  /**
2018
- * Computes the same id as `IdLib.toId` in Solidity.
2337
+ * Computes the same id as `IdLib.toId` in Solidity using the CREATE2 preimage:
2338
+ * `keccak256(0xff ++ morphoV2 ++ chainId ++ keccak256(creationCode))`,
2339
+ * then truncates to the lower 20 bytes.
2340
+ *
2019
2341
  * @param parameters - {@link toId.Parameters}
2020
2342
  * @returns The obligation id. {@link toId.ReturnType}
2021
2343
  */
2022
2344
  function toId(parameters) {
2023
- return keccak256(creationCode(parameters));
2345
+ return `0x${keccak256(concatHex([
2346
+ "0xff",
2347
+ parameters.morphoV2.toLowerCase(),
2348
+ numberToHex(BigInt(parameters.chainId), { size: 32 }),
2349
+ keccak256(creationCode(parameters))
2350
+ ])).slice(-40)}`;
2024
2351
  }
2025
2352
 
2026
2353
  //#endregion
@@ -2317,7 +2644,7 @@ function hash(offer) {
2317
2644
  * The id is computed with {@link Id.toId}.
2318
2645
  * @param offer - The offer to calculate the obligation id for.
2319
2646
  * @param parameters - The chain context used by the onchain id function.
2320
- * @returns The obligation id as a 32-byte hex string.
2647
+ * @returns The obligation id as a 20-byte hex string.
2321
2648
  */
2322
2649
  function obligationId(offer, parameters) {
2323
2650
  return toId({
@@ -2480,10 +2807,10 @@ const takeEvent = {
2480
2807
  internalType: "address"
2481
2808
  },
2482
2809
  {
2483
- name: "id",
2484
- type: "bytes32",
2810
+ name: "id_",
2811
+ type: "bytes20",
2485
2812
  indexed: true,
2486
- internalType: "bytes32"
2813
+ internalType: "bytes20"
2487
2814
  },
2488
2815
  {
2489
2816
  name: "maker",
@@ -2602,10 +2929,10 @@ const repayEvent = {
2602
2929
  internalType: "address"
2603
2930
  },
2604
2931
  {
2605
- name: "id",
2606
- type: "bytes32",
2932
+ name: "id_",
2933
+ type: "bytes20",
2607
2934
  indexed: true,
2608
- internalType: "bytes32"
2935
+ internalType: "bytes20"
2609
2936
  },
2610
2937
  {
2611
2938
  name: "obligationUnits",
@@ -2636,46 +2963,35 @@ const liquidateEvent = {
2636
2963
  internalType: "address"
2637
2964
  },
2638
2965
  {
2639
- name: "id",
2640
- type: "bytes32",
2966
+ name: "id_",
2967
+ type: "bytes20",
2641
2968
  indexed: true,
2642
- internalType: "bytes32"
2969
+ internalType: "bytes20"
2643
2970
  },
2644
2971
  {
2645
- name: "seizures",
2646
- type: "tuple[]",
2972
+ name: "collateralIndex",
2973
+ type: "uint256",
2647
2974
  indexed: false,
2648
- internalType: "struct IMorphoV2.Seizure[]",
2649
- components: [
2650
- {
2651
- name: "collateralIndex",
2652
- type: "uint256",
2653
- internalType: "uint256"
2654
- },
2655
- {
2656
- name: "repaid",
2657
- type: "uint256",
2658
- internalType: "uint256"
2659
- },
2660
- {
2661
- name: "seized",
2662
- type: "uint256",
2663
- internalType: "uint256"
2664
- }
2665
- ]
2975
+ internalType: "uint256"
2666
2976
  },
2667
2977
  {
2668
- name: "borrower",
2669
- type: "address",
2670
- indexed: true,
2671
- internalType: "address"
2978
+ name: "seizedAssets",
2979
+ type: "uint256",
2980
+ indexed: false,
2981
+ internalType: "uint256"
2672
2982
  },
2673
2983
  {
2674
- name: "totalRepaid",
2984
+ name: "repaidUnits",
2675
2985
  type: "uint256",
2676
2986
  indexed: false,
2677
2987
  internalType: "uint256"
2678
2988
  },
2989
+ {
2990
+ name: "borrower",
2991
+ type: "address",
2992
+ indexed: true,
2993
+ internalType: "address"
2994
+ },
2679
2995
  {
2680
2996
  name: "badDebt",
2681
2997
  type: "uint256",
@@ -2699,10 +3015,10 @@ const supplyCollateralEvent = {
2699
3015
  internalType: "address"
2700
3016
  },
2701
3017
  {
2702
- name: "id",
2703
- type: "bytes32",
3018
+ name: "id_",
3019
+ type: "bytes20",
2704
3020
  indexed: true,
2705
- internalType: "bytes32"
3021
+ internalType: "bytes20"
2706
3022
  },
2707
3023
  {
2708
3024
  name: "collateral",
@@ -2739,10 +3055,10 @@ const withdrawCollateralEvent = {
2739
3055
  internalType: "address"
2740
3056
  },
2741
3057
  {
2742
- name: "id",
2743
- type: "bytes32",
3058
+ name: "id_",
3059
+ type: "bytes20",
2744
3060
  indexed: true,
2745
- internalType: "bytes32"
3061
+ internalType: "bytes20"
2746
3062
  },
2747
3063
  {
2748
3064
  name: "collateral",
@@ -3587,11 +3903,12 @@ const BrandTypeId = Symbol.for("mempool/Brand");
3587
3903
 
3588
3904
  //#endregion
3589
3905
  //#region src/database/drizzle/VERSION.ts
3590
- const VERSION = "router_v1.11";
3906
+ const VERSION = "router_v1.13";
3591
3907
 
3592
3908
  //#endregion
3593
3909
  //#region src/database/drizzle/schema.ts
3594
3910
  var schema_exports = /* @__PURE__ */ __exportAll({
3911
+ CallbackTypes: () => CallbackTypes,
3595
3912
  PositionTypes: () => PositionTypes,
3596
3913
  StatusCode: () => StatusCode,
3597
3914
  TABLE_NAMES: () => TABLE_NAMES,
@@ -3649,7 +3966,7 @@ const obligations = s.table(EnumTableName.OBLIGATIONS, {
3649
3966
  maturity: integer("maturity").notNull()
3650
3967
  });
3651
3968
  const obligationIdKeys = s.table(EnumTableName.OBLIGATION_ID_KEYS, {
3652
- obligationId: varchar("obligation_id", { length: 66 }).primaryKey(),
3969
+ obligationId: varchar("obligation_id", { length: 42 }).primaryKey(),
3653
3970
  obligationKey: varchar("obligation_key", { length: 66 }).notNull().references(() => obligations.obligationKey, { onDelete: "cascade" }),
3654
3971
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3655
3972
  morphoV2: varchar("morpho_v2", { length: 42 }).notNull()
@@ -3730,7 +4047,7 @@ const oracles$1 = s.table(EnumTableName.ORACLES, {
3730
4047
  })]);
3731
4048
  const offers = s.table(EnumTableName.OFFERS, {
3732
4049
  hash: varchar("hash", { length: 66 }).notNull(),
3733
- obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligationIdKeys.obligationId, { onDelete: "cascade" }),
4050
+ obligationId: varchar("obligation_id", { length: 42 }).notNull().references(() => obligationIdKeys.obligationId, { onDelete: "cascade" }),
3734
4051
  assets: numeric("assets", {
3735
4052
  precision: 78,
3736
4053
  scale: 0
@@ -3781,7 +4098,7 @@ const offers = s.table(EnumTableName.OFFERS, {
3781
4098
  ]);
3782
4099
  const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
3783
4100
  offerHash: varchar("offer_hash", { length: 66 }).notNull(),
3784
- obligationId: varchar("obligation_id", { length: 66 }).notNull(),
4101
+ obligationId: varchar("obligation_id", { length: 42 }).notNull(),
3785
4102
  callbackId: varchar("callback_id", { length: 66 })
3786
4103
  }, (table) => [foreignKey({
3787
4104
  columns: [table.offerHash, table.obligationId],
@@ -3795,20 +4112,18 @@ const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
3795
4112
  ],
3796
4113
  name: "offers_callbacks_pk"
3797
4114
  })]);
4115
+ const CallbackTypes = s.enum("callback_type", Object.values(CallbackType));
3798
4116
  const callbacks = s.table(EnumTableName.CALLBACKS, {
3799
4117
  id: varchar("id", { length: 66 }).primaryKey(),
3800
4118
  positionChainId: bigint("position_chain_id", { mode: "number" }).$type().notNull(),
3801
- positionContract: varchar("position_contract", { length: 42 }).notNull(),
4119
+ positionContract: varchar("position_contract", { length: 66 }).notNull(),
3802
4120
  positionUser: varchar("position_user", { length: 42 }).notNull(),
3803
4121
  positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
3804
- amount: numeric("amount", {
3805
- precision: 78,
3806
- scale: 0
3807
- })
4122
+ type: CallbackTypes("type").notNull()
3808
4123
  });
3809
4124
  const lotsPositions = s.table(EnumTableName.LOTS_POSITIONS, {
3810
4125
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3811
- contract: varchar("contract", { length: 42 }).notNull(),
4126
+ contract: varchar("contract", { length: 66 }).notNull(),
3812
4127
  user: varchar("user", { length: 42 }).notNull(),
3813
4128
  positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" })
3814
4129
  }, (table) => [primaryKey({
@@ -3822,9 +4137,9 @@ const lotsPositions = s.table(EnumTableName.LOTS_POSITIONS, {
3822
4137
  const lots = s.table(EnumTableName.LOTS, {
3823
4138
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3824
4139
  user: varchar("user", { length: 42 }).notNull(),
3825
- contract: varchar("contract", { length: 42 }).notNull(),
4140
+ contract: varchar("contract", { length: 66 }).notNull(),
3826
4141
  group: varchar("group", { length: 66 }).notNull(),
3827
- obligationId: varchar("obligation_id", { length: 66 }).notNull(),
4142
+ obligationId: varchar("obligation_id", { length: 42 }).notNull(),
3828
4143
  lower: numeric("lower", {
3829
4144
  precision: 78,
3830
4145
  scale: 0
@@ -3874,9 +4189,9 @@ const lots = s.table(EnumTableName.LOTS, {
3874
4189
  const offsets = s.table(EnumTableName.OFFSETS, {
3875
4190
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
3876
4191
  user: varchar("user", { length: 42 }).notNull(),
3877
- contract: varchar("contract", { length: 42 }).notNull(),
4192
+ contract: varchar("contract", { length: 66 }).notNull(),
3878
4193
  group: varchar("group", { length: 66 }).notNull(),
3879
- obligationId: varchar("obligation_id", { length: 66 }).notNull(),
4194
+ obligationId: varchar("obligation_id", { length: 42 }).notNull(),
3880
4195
  value: numeric("value", {
3881
4196
  precision: 78,
3882
4197
  scale: 0
@@ -3989,7 +4304,7 @@ const status = s.table("status", {
3989
4304
  });
3990
4305
  const validations = s.table("validations", {
3991
4306
  offerHash: varchar("offer_hash", { length: 66 }).notNull(),
3992
- obligationId: varchar("obligation_id", { length: 66 }).notNull(),
4307
+ obligationId: varchar("obligation_id", { length: 42 }).notNull(),
3993
4308
  statusId: integer("status_id").notNull().references(() => status.id, { onDelete: "no action" }),
3994
4309
  updatedAt: timestamp("updated_at").defaultNow().notNull()
3995
4310
  }, (table) => [primaryKey({
@@ -4026,7 +4341,7 @@ const trees = s.table(EnumTableName.TREES, {
4026
4341
  });
4027
4342
  const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
4028
4343
  offerHash: varchar("offer_hash", { length: 66 }).notNull(),
4029
- obligationId: varchar("obligation_id", { length: 66 }).notNull(),
4344
+ obligationId: varchar("obligation_id", { length: 42 }).notNull(),
4030
4345
  treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
4031
4346
  proofNodes: text("proof_nodes").notNull(),
4032
4347
  createdAt: timestamp("created_at").defaultNow().notNull()
@@ -4043,6 +4358,54 @@ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
4043
4358
  index("merkle_paths_tree_root_idx").on(table.treeRoot)
4044
4359
  ]);
4045
4360
 
4361
+ //#endregion
4362
+ //#region src/indexer/collectors/CollectFunctions/IndexedEventTelemetry.ts
4363
+ /**
4364
+ * Resolve the chain head block metadata used by indexed collector telemetry events.
4365
+ * @param parameters - Block lookup parameters.
4366
+ * @returns Head block number and timestamp (seconds since epoch).
4367
+ */
4368
+ async function getIndexedEventHeadContext(parameters) {
4369
+ const { client, headBlockNumber } = parameters;
4370
+ const fallbackHeadTimestampS = Math.floor(Date.now() / 1e3);
4371
+ const getBlock = client.getBlock;
4372
+ if (typeof getBlock !== "function") return {
4373
+ head_block_number: headBlockNumber,
4374
+ head_block_timestamp_s: fallbackHeadTimestampS
4375
+ };
4376
+ try {
4377
+ const headBlock = await getBlock({
4378
+ blockNumber: BigInt(headBlockNumber),
4379
+ includeTransactions: false
4380
+ });
4381
+ return {
4382
+ head_block_number: headBlockNumber,
4383
+ head_block_timestamp_s: Number(headBlock.timestamp)
4384
+ };
4385
+ } catch {
4386
+ return {
4387
+ head_block_number: headBlockNumber,
4388
+ head_block_timestamp_s: fallbackHeadTimestampS
4389
+ };
4390
+ }
4391
+ }
4392
+ /**
4393
+ * Build wide-event telemetry fields used by indexed collector logs.
4394
+ * @param parameters - Indexed event counts and head metadata.
4395
+ * @returns Computed latency and log counters.
4396
+ */
4397
+ function buildIndexedEventTelemetry(parameters) {
4398
+ const insertedAtMs = parameters.insertedAtMs ?? Date.now();
4399
+ const headTimestampMs = parameters.head_block_timestamp_s * 1e3;
4400
+ return {
4401
+ head_block_number: parameters.head_block_number,
4402
+ head_block_timestamp_s: parameters.head_block_timestamp_s,
4403
+ head_to_db_insert_latency_ms: Math.max(insertedAtMs - headTimestampMs, 0),
4404
+ logs_ingested_count: Math.max(parameters.logsIngestedCount, 0),
4405
+ logs_indexed_count: Math.max(parameters.logsIndexedCount, 0)
4406
+ };
4407
+ }
4408
+
4046
4409
  //#endregion
4047
4410
  //#region src/indexer/collectors/CollectFunctions/processors/processCollateralSeizures.ts
4048
4411
  /**
@@ -4058,39 +4421,40 @@ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
4058
4421
  */
4059
4422
  function processCollateralSeizures(parameters) {
4060
4423
  const { logs, chainId } = parameters;
4061
- const logger = getLogger();
4424
+ const logger = getLoggerWithContext({
4425
+ component: "indexer.collector.processor",
4426
+ chain_id: chainId
4427
+ });
4062
4428
  const seizureEvents = [];
4063
4429
  for (const rawLog of logs) {
4064
4430
  if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
4065
4431
  logger.debug({
4066
- chainId,
4432
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4067
4433
  msg: "Skipping collateral log because it is missing required fields"
4068
4434
  });
4069
4435
  continue;
4070
4436
  }
4071
4437
  if (rawLog.eventName !== liquidateEvent.name) continue;
4072
4438
  const args = rawLog.args;
4073
- if (args?.id === void 0 || args?.borrower === void 0 || args?.seizures === void 0) {
4439
+ const obligationId = args?.id_;
4440
+ if (obligationId === void 0 || args?.borrower === void 0 || args?.collateralIndex === void 0 || args?.seizedAssets === void 0) {
4074
4441
  logger.debug({
4075
- chainId,
4442
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4076
4443
  msg: "Skipping Liquidate log for collateral because it is missing required args"
4077
4444
  });
4078
4445
  continue;
4079
4446
  }
4447
+ if (args.seizedAssets === 0n) continue;
4080
4448
  const baseId = `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`;
4081
- for (let seizureIndex = 0; seizureIndex < args.seizures.length; seizureIndex++) {
4082
- const seizure = args.seizures[seizureIndex];
4083
- if (seizure.seized === 0n) continue;
4084
- seizureEvents.push({
4085
- id: `${baseId}-collateral-liquidate-${seizureIndex}`,
4086
- chainId,
4087
- obligationId: args.id,
4088
- collateralIndex: Number(seizure.collateralIndex),
4089
- user: args.borrower,
4090
- amount: seizure.seized,
4091
- blockNumber: Number(rawLog.blockNumber)
4092
- });
4093
- }
4449
+ seizureEvents.push({
4450
+ id: `${baseId}-collateral-liquidate`,
4451
+ chainId,
4452
+ obligationId,
4453
+ collateralIndex: Number(args.collateralIndex),
4454
+ user: args.borrower,
4455
+ amount: args.seizedAssets,
4456
+ blockNumber: Number(rawLog.blockNumber)
4457
+ });
4094
4458
  }
4095
4459
  return seizureEvents;
4096
4460
  }
@@ -4113,12 +4477,15 @@ function processCollateralSeizures(parameters) {
4113
4477
  */
4114
4478
  function processCollateralTransfers(parameters) {
4115
4479
  const { logs, chainId } = parameters;
4116
- const logger = getLogger();
4480
+ const logger = getLoggerWithContext({
4481
+ component: "indexer.collector.processor",
4482
+ chain_id: chainId
4483
+ });
4117
4484
  const transfers = [];
4118
4485
  for (const rawLog of logs) {
4119
4486
  if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
4120
4487
  logger.debug({
4121
- chainId,
4488
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4122
4489
  msg: "Skipping collateral log because it is missing required fields"
4123
4490
  });
4124
4491
  continue;
@@ -4127,9 +4494,10 @@ function processCollateralTransfers(parameters) {
4127
4494
  const baseId = `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`;
4128
4495
  if (eventName === supplyCollateralEvent.name) {
4129
4496
  const args = rawLog.args;
4130
- if (args?.id === void 0 || args?.collateral === void 0 || args?.assets === void 0 || args?.onBehalf === void 0) {
4497
+ const obligationId = args?.id_;
4498
+ if (obligationId === void 0 || args?.collateral === void 0 || args?.assets === void 0 || args?.onBehalf === void 0) {
4131
4499
  logger.debug({
4132
- chainId,
4500
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4133
4501
  msg: "Skipping SupplyCollateral log because it is missing required args"
4134
4502
  });
4135
4503
  continue;
@@ -4138,7 +4506,7 @@ function processCollateralTransfers(parameters) {
4138
4506
  transfers.push(from$9({
4139
4507
  id: `${baseId}-collateral-supply`,
4140
4508
  chainId,
4141
- contract: args.id,
4509
+ contract: obligationId,
4142
4510
  from: zeroAddress,
4143
4511
  to: args.onBehalf,
4144
4512
  value: args.assets,
@@ -4150,9 +4518,10 @@ function processCollateralTransfers(parameters) {
4150
4518
  }
4151
4519
  if (eventName === withdrawCollateralEvent.name) {
4152
4520
  const args = rawLog.args;
4153
- if (args?.id === void 0 || args?.collateral === void 0 || args?.assets === void 0 || args?.onBehalf === void 0) {
4521
+ const obligationId = args?.id_;
4522
+ if (obligationId === void 0 || args?.collateral === void 0 || args?.assets === void 0 || args?.onBehalf === void 0) {
4154
4523
  logger.debug({
4155
- chainId,
4524
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4156
4525
  msg: "Skipping WithdrawCollateral log because it is missing required args"
4157
4526
  });
4158
4527
  continue;
@@ -4161,7 +4530,7 @@ function processCollateralTransfers(parameters) {
4161
4530
  transfers.push(from$9({
4162
4531
  id: `${baseId}-collateral-withdraw`,
4163
4532
  chainId,
4164
- contract: args.id,
4533
+ contract: obligationId,
4165
4534
  from: args.onBehalf,
4166
4535
  to: zeroAddress,
4167
4536
  value: args.assets,
@@ -4187,12 +4556,15 @@ const buildGroupKey = (parameters) => {
4187
4556
  */
4188
4557
  function processConsumedLogs(parameters) {
4189
4558
  const { logs, chainId } = parameters;
4190
- const logger = getLogger();
4559
+ const logger = getLoggerWithContext({
4560
+ component: "indexer.collector.processor",
4561
+ chain_id: chainId
4562
+ });
4191
4563
  const consumedEvents = [];
4192
4564
  for (const rawLog of logs) {
4193
4565
  if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
4194
4566
  logger.debug({
4195
- chainId,
4567
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4196
4568
  msg: "Skipping log because it is missing required fields"
4197
4569
  });
4198
4570
  continue;
@@ -4202,7 +4574,7 @@ function processConsumedLogs(parameters) {
4202
4574
  const consumeArgs = rawLog.args;
4203
4575
  if (consumeArgs?.user === void 0 || consumeArgs?.group === void 0 || consumeArgs?.amount === void 0) {
4204
4576
  logger.debug({
4205
- chainId,
4577
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4206
4578
  msg: "Skipping Consume log because it is missing required args"
4207
4579
  });
4208
4580
  continue;
@@ -4222,7 +4594,7 @@ function processConsumedLogs(parameters) {
4222
4594
  const takeArgs = rawLog.args;
4223
4595
  if (takeArgs?.maker === void 0 || takeArgs?.group === void 0 || takeArgs?.consumed === void 0) {
4224
4596
  logger.debug({
4225
- chainId,
4597
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4226
4598
  msg: "Skipping Take log because it is missing required args for consumed"
4227
4599
  });
4228
4600
  continue;
@@ -4260,7 +4632,7 @@ function processConsumedLogs(parameters) {
4260
4632
  * | false | false | from: 0x0 → to: buyer | none |
4261
4633
  *
4262
4634
  * **Repay**: from: 0x0 → to: onBehalf
4263
- * **Liquidate**: from: 0x0 → to: borrower (value = totalRepaid + badDebt)
4635
+ * **Liquidate**: from: 0x0 → to: borrower (value = repaidUnits + badDebt)
4264
4636
  *
4265
4637
  * @param parameters - The parsed event logs and chain ID.
4266
4638
  * @param parameters.logs - Parsed event logs from MorphoV2 (Take, Repay, Liquidate events).
@@ -4269,12 +4641,15 @@ function processConsumedLogs(parameters) {
4269
4641
  */
4270
4642
  function processDebtTransfers(parameters) {
4271
4643
  const { logs, chainId } = parameters;
4272
- const logger = getLogger();
4644
+ const logger = getLoggerWithContext({
4645
+ component: "indexer.collector.processor",
4646
+ chain_id: chainId
4647
+ });
4273
4648
  const transfers = [];
4274
4649
  for (const rawLog of logs) {
4275
4650
  if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
4276
4651
  logger.debug({
4277
- chainId,
4652
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4278
4653
  msg: "Skipping debt log because it is missing required fields"
4279
4654
  });
4280
4655
  continue;
@@ -4283,9 +4658,10 @@ function processDebtTransfers(parameters) {
4283
4658
  const baseId = `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`;
4284
4659
  if (eventName === takeEvent.name) {
4285
4660
  const args = rawLog.args;
4286
- if (args?.id === void 0 || args?.maker === void 0 || args?.taker === void 0 || args?.offerIsBuy === void 0 || args?.obligationUnits === void 0 || args?.buyerIsLender === void 0 || args?.sellerIsBorrower === void 0) {
4661
+ const obligationId = args?.id_;
4662
+ if (obligationId === void 0 || args?.maker === void 0 || args?.taker === void 0 || args?.offerIsBuy === void 0 || args?.obligationUnits === void 0 || args?.buyerIsLender === void 0 || args?.sellerIsBorrower === void 0) {
4287
4663
  logger.debug({
4288
- chainId,
4664
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4289
4665
  msg: "Skipping Take log because it is missing required args for debt"
4290
4666
  });
4291
4667
  continue;
@@ -4297,7 +4673,7 @@ function processDebtTransfers(parameters) {
4297
4673
  if (!args.buyerIsLender) transfers.push(from$9({
4298
4674
  id: `${baseId}-debt-buyer`,
4299
4675
  chainId,
4300
- contract: args.id,
4676
+ contract: obligationId,
4301
4677
  from: zeroAddress,
4302
4678
  to: buyer,
4303
4679
  value: args.obligationUnits,
@@ -4308,7 +4684,7 @@ function processDebtTransfers(parameters) {
4308
4684
  if (args.sellerIsBorrower) transfers.push(from$9({
4309
4685
  id: `${baseId}-debt-seller`,
4310
4686
  chainId,
4311
- contract: args.id,
4687
+ contract: obligationId,
4312
4688
  from: seller,
4313
4689
  to: zeroAddress,
4314
4690
  value: args.obligationUnits,
@@ -4320,9 +4696,10 @@ function processDebtTransfers(parameters) {
4320
4696
  }
4321
4697
  if (eventName === repayEvent.name) {
4322
4698
  const args = rawLog.args;
4323
- if (args?.id === void 0 || args?.obligationUnits === void 0 || args?.onBehalf === void 0) {
4699
+ const obligationId = args?.id_;
4700
+ if (obligationId === void 0 || args?.obligationUnits === void 0 || args?.onBehalf === void 0) {
4324
4701
  logger.debug({
4325
- chainId,
4702
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4326
4703
  msg: "Skipping Repay log because it is missing required args"
4327
4704
  });
4328
4705
  continue;
@@ -4331,7 +4708,7 @@ function processDebtTransfers(parameters) {
4331
4708
  transfers.push(from$9({
4332
4709
  id: `${baseId}-debt-repay`,
4333
4710
  chainId,
4334
- contract: args.id,
4711
+ contract: obligationId,
4335
4712
  from: zeroAddress,
4336
4713
  to: args.onBehalf,
4337
4714
  value: args.obligationUnits,
@@ -4343,19 +4720,20 @@ function processDebtTransfers(parameters) {
4343
4720
  }
4344
4721
  if (eventName === liquidateEvent.name) {
4345
4722
  const args = rawLog.args;
4346
- if (args?.id === void 0 || args?.borrower === void 0 || args?.totalRepaid === void 0 || args?.badDebt === void 0) {
4723
+ const obligationId = args?.id_;
4724
+ if (obligationId === void 0 || args?.borrower === void 0 || args?.repaidUnits === void 0 || args?.badDebt === void 0) {
4347
4725
  logger.debug({
4348
- chainId,
4726
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4349
4727
  msg: "Skipping Liquidate log because it is missing required args"
4350
4728
  });
4351
4729
  continue;
4352
4730
  }
4353
- const totalReduction = args.totalRepaid + args.badDebt;
4731
+ const totalReduction = args.repaidUnits + args.badDebt;
4354
4732
  if (totalReduction === 0n) continue;
4355
4733
  transfers.push(from$9({
4356
4734
  id: `${baseId}-debt-liquidate`,
4357
4735
  chainId,
4358
- contract: args.id,
4736
+ contract: obligationId,
4359
4737
  from: zeroAddress,
4360
4738
  to: args.borrower,
4361
4739
  value: totalReduction,
@@ -4372,10 +4750,18 @@ function processDebtTransfers(parameters) {
4372
4750
  //#region src/indexer/collectors/CollectFunctions/collectMorphoV2.ts
4373
4751
  async function* collectMorphoV2(parameters) {
4374
4752
  let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
4375
- const logger = getLogger();
4753
+ const logger = getLoggerWithContext({
4754
+ component: "indexer.collector",
4755
+ collector,
4756
+ chain_id: client.chain.id
4757
+ });
4376
4758
  let startBlock = blockNumber;
4377
4759
  let reorgDetected = false;
4378
4760
  const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
4761
+ const indexedEventHeadContext = await getIndexedEventHeadContext({
4762
+ client,
4763
+ headBlockNumber: latestBlockNumberChain
4764
+ });
4379
4765
  const stream = streamLogs({
4380
4766
  client,
4381
4767
  contractAddress: client.chain.custom.morpho.address,
@@ -4423,8 +4809,6 @@ async function* collectMorphoV2(parameters) {
4423
4809
  }), resolveConsumedEvents({
4424
4810
  dbTx,
4425
4811
  consumedEvents,
4426
- chainId: client.chain.id,
4427
- collector,
4428
4812
  logger
4429
4813
  })]);
4430
4814
  if (debtTransfers.length > 0) {
@@ -4437,14 +4821,23 @@ async function* collectMorphoV2(parameters) {
4437
4821
  await dbTx.transfers.create(allCollateralTransfers);
4438
4822
  }
4439
4823
  await dbTx.consumed.create(resolvedConsumedEvents);
4440
- if (resolvedConsumedEvents.length > 0) logger.info({
4824
+ const logsIndexedCount = countIndexedLogs({
4825
+ resolvedConsumedEvents,
4826
+ debtTransfers,
4827
+ collateralTransfers: allCollateralTransfers
4828
+ });
4829
+ logger.info({
4830
+ event: INDEXER_COLLECTOR_EVENTS_INDEXED,
4441
4831
  msg: "Events indexed",
4442
- collector,
4443
4832
  consumed_count: resolvedConsumedEvents.length,
4444
4833
  debt_transfer_count: debtTransfers.length,
4445
4834
  collateral_transfer_count: allCollateralTransfers.length,
4446
- chain_id: client.chain.id,
4447
- block_range: [startBlock, lastStreamBlockNumber]
4835
+ block_range: [startBlock, lastStreamBlockNumber],
4836
+ ...buildIndexedEventTelemetry({
4837
+ ...indexedEventHeadContext,
4838
+ logsIngestedCount: parsedLogs.length,
4839
+ logsIndexedCount
4840
+ })
4448
4841
  });
4449
4842
  blockNumber = lastStreamBlockNumber;
4450
4843
  try {
@@ -4476,8 +4869,7 @@ async function* collectMorphoV2(parameters) {
4476
4869
  positionTypeId: positionTypeId(Type.COLLATERAL_OF)
4477
4870
  });
4478
4871
  logger.info({
4479
- collector,
4480
- chain_id: client.chain.id,
4872
+ event: INDEXER_COLLECTOR_REORG_COMPENSATED,
4481
4873
  msg: "Reorg detected, events deleted",
4482
4874
  consumed_deleted: deletedConsumed,
4483
4875
  debt_transfers_deleted: deletedDebtTransfers,
@@ -4493,13 +4885,13 @@ async function* collectMorphoV2(parameters) {
4493
4885
  reorgDetected = true;
4494
4886
  } catch (err) {
4495
4887
  const msg = "Failed to delete events when handling reorg.";
4888
+ const error = err instanceof Error ? err : new Error(String(err));
4496
4889
  logger.error({
4497
- collector,
4498
- chainId: client.chain.id,
4890
+ event: INDEXER_COLLECTOR_REORG_COMPENSATION_FAILED,
4499
4891
  msg,
4500
- err
4892
+ err: error
4501
4893
  });
4502
- throw new Error(msg, { cause: err });
4894
+ throw new Error(msg, { cause: error });
4503
4895
  }
4504
4896
  }
4505
4897
  });
@@ -4527,8 +4919,7 @@ async function* collectMorphoV2(parameters) {
4527
4919
  positionTypeId: positionTypeId(Type.COLLATERAL_OF)
4528
4920
  });
4529
4921
  if (deletedConsumed > 0 || deletedDebtTransfers > 0 || deletedCollateralTransfers > 0) logger.info({
4530
- collector,
4531
- chain_id: client.chain.id,
4922
+ event: INDEXER_COLLECTOR_REORG_COMPENSATED,
4532
4923
  msg: "Reorg detected, events deleted",
4533
4924
  consumed_deleted: deletedConsumed,
4534
4925
  debt_transfers_deleted: deletedDebtTransfers,
@@ -4568,7 +4959,7 @@ async function resolveSeizureTransfers(parameters) {
4568
4959
  return resolvedSeizureTransfers;
4569
4960
  }
4570
4961
  async function resolveConsumedEvents(parameters) {
4571
- const { dbTx, consumedEvents, chainId, collector, logger } = parameters;
4962
+ const { dbTx, consumedEvents, logger } = parameters;
4572
4963
  if (consumedEvents.length === 0) return [];
4573
4964
  const [existingConsumedIds, consumedByGroup] = await Promise.all([getExistingConsumedIds({
4574
4965
  dbTx,
@@ -4601,8 +4992,7 @@ async function resolveConsumedEvents(parameters) {
4601
4992
  const delta = log.consumed - previousConsumed;
4602
4993
  if (delta <= 0n) {
4603
4994
  logger.debug({
4604
- collector,
4605
- chainId,
4995
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
4606
4996
  msg: "Skipping Take log because consumed did not increase",
4607
4997
  previous_consumed: previousConsumed.toString(),
4608
4998
  consumed: log.consumed.toString()
@@ -4621,6 +5011,15 @@ async function resolveConsumedEvents(parameters) {
4621
5011
  }
4622
5012
  return resolvedEvents;
4623
5013
  }
5014
+ function countIndexedLogs(parameters) {
5015
+ const sourceLogIds = /* @__PURE__ */ new Set();
5016
+ for (const consumedEvent of parameters.resolvedConsumedEvents) sourceLogIds.add(consumedEvent.id);
5017
+ for (const transfer of [...parameters.debtTransfers, ...parameters.collateralTransfers]) sourceLogIds.add(toSourceLogId(transfer.id));
5018
+ return sourceLogIds.size;
5019
+ }
5020
+ function toSourceLogId(derivedId) {
5021
+ return derivedId.replace(/-(?:debt-(?:buyer|seller|repay|liquidate)|collateral-(?:supply|withdraw|liquidate))$/, "");
5022
+ }
4624
5023
  async function getExistingConsumedIds(parameters) {
4625
5024
  const { dbTx, consumedEvents: consumedEvents$1 } = parameters;
4626
5025
  const existingConsumedIds = /* @__PURE__ */ new Set();
@@ -4638,21 +5037,21 @@ async function getExistingConsumedIds(parameters) {
4638
5037
  }
4639
5038
  async function getConsumedByGroup(parameters) {
4640
5039
  const { dbTx, consumedEvents } = parameters;
4641
- const groups$3 = /* @__PURE__ */ new Map();
5040
+ const groups$4 = /* @__PURE__ */ new Map();
4642
5041
  for (const event of consumedEvents) {
4643
5042
  const key = buildGroupKey({
4644
5043
  chainId: event.chainId,
4645
5044
  maker: event.maker,
4646
5045
  group: event.group
4647
5046
  });
4648
- if (!groups$3.has(key)) groups$3.set(key, {
5047
+ if (!groups$4.has(key)) groups$4.set(key, {
4649
5048
  chainId: event.chainId,
4650
5049
  maker: event.maker,
4651
5050
  group: event.group
4652
5051
  });
4653
5052
  }
4654
5053
  const consumedByGroup = /* @__PURE__ */ new Map();
4655
- const groupList = Array.from(groups$3.values());
5054
+ const groupList = Array.from(groups$4.values());
4656
5055
  for (let index = 0; index < groupList.length; index += 500) {
4657
5056
  const slice = groupList.slice(index, index + 500);
4658
5057
  const { rows } = await dbTx.execute(sql`
@@ -4710,17 +5109,20 @@ function transfersToPositions(transfers) {
4710
5109
 
4711
5110
  //#endregion
4712
5111
  //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
4713
- const ERC20_TYPE_ID = Object.values(Type).indexOf(Type.ERC20) + 1;
4714
5112
  async function* collectOffersV2(parameters) {
4715
5113
  let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
4716
- const logger = getLogger();
5114
+ const logger = getLoggerWithContext({
5115
+ component: "indexer.collector",
5116
+ collector,
5117
+ chain_id: client.chain.id
5118
+ });
4717
5119
  let startBlock = blockNumber;
4718
5120
  let reorgDetected = false;
4719
5121
  if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
4720
5122
  const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
4721
5123
  logger.error({
4722
- msg,
4723
- chain_id: client.chain.id
5124
+ event: INDEXER_COLLECTOR_INVALID_CHAIN_CONFIG,
5125
+ msg
4724
5126
  });
4725
5127
  throw new Error(msg);
4726
5128
  }
@@ -4730,6 +5132,10 @@ async function* collectOffersV2(parameters) {
4730
5132
  verifyingContract: morphoV2
4731
5133
  };
4732
5134
  const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5135
+ const indexedEventHeadContext = await getIndexedEventHeadContext({
5136
+ client,
5137
+ headBlockNumber: latestBlockNumberChain
5138
+ });
4733
5139
  const stream = streamLogs({
4734
5140
  client,
4735
5141
  contractAddress: client.chain.custom.mempool.address,
@@ -4763,11 +5169,11 @@ async function* collectOffersV2(parameters) {
4763
5169
  const signerMismatch = tree.offers.find((offer) => offer.maker.toLowerCase() !== signer.toLowerCase());
4764
5170
  if (signerMismatch) {
4765
5171
  logger.debug({
5172
+ event: INDEXER_COLLECTOR_OFFER_TREE_REJECTED,
4766
5173
  msg: "Tree rejected: signer mismatch",
4767
5174
  reason: "signer_mismatch",
4768
5175
  signer,
4769
- maker: signerMismatch.maker,
4770
- chain_id: client.chain.id
5176
+ maker: signerMismatch.maker
4771
5177
  });
4772
5178
  continue;
4773
5179
  }
@@ -4780,80 +5186,81 @@ async function* collectOffersV2(parameters) {
4780
5186
  } catch (err) {
4781
5187
  const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
4782
5188
  logger.debug({
5189
+ event: INDEXER_COLLECTOR_OFFER_TREE_DECODE_FAILED,
4783
5190
  msg: "Tree decode failed",
4784
5191
  reason,
4785
- chain_id: client.chain.id,
4786
5192
  err: err instanceof Error ? err.message : String(err)
4787
5193
  });
4788
5194
  }
4789
5195
  }
4790
- await db.transaction(async (dbTx) => {
4791
- const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
4792
- const treesToInsert = [];
4793
- const pathsToInsert = [];
4794
- let totalValidOffers = 0;
4795
- const offersWithBlock = [];
4796
- for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
4797
- const allowedResults = await gatekeeper.isAllowed({
4798
- offers: tree.offers,
4799
- chainId: client.chain.id
4800
- });
4801
- const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
4802
- if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
4803
- if (allowedResults.issues.length > 0) {
4804
- const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
4805
- logger.debug({
4806
- msg: "Tree offers rejected by gatekeeper",
4807
- reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
4808
- chain_id: client.chain.id,
4809
- issues_count: allowedResults.issues.length
4810
- });
4811
- } else if (hasBlockWindowViolation) logger.debug({
4812
- msg: "Tree rejected: offers outside block window",
4813
- reason: "block_window",
4814
- chain_id: client.chain.id
5196
+ const { blockNumber: latestBlockNumber } = await db.blocks.getChain(client.chain.id);
5197
+ const treesToInsert = [];
5198
+ const pathsToInsert = [];
5199
+ let totalValidOffers = 0;
5200
+ const offersWithBlock = [];
5201
+ for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
5202
+ const allowedResults = await gatekeeper.isAllowed({
5203
+ offers: tree.offers,
5204
+ chainId: client.chain.id
5205
+ });
5206
+ const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
5207
+ if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
5208
+ if (allowedResults.issues.length > 0) {
5209
+ const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
5210
+ logger.debug({
5211
+ event: INDEXER_COLLECTOR_OFFER_TREE_REJECTED,
5212
+ msg: "Tree offers rejected by gatekeeper",
5213
+ reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
5214
+ issues_count: allowedResults.issues.length
4815
5215
  });
4816
- continue;
4817
- }
4818
- treesToInsert.push({
4819
- root: tree.root,
4820
- signature
5216
+ } else if (hasBlockWindowViolation) logger.debug({
5217
+ event: INDEXER_COLLECTOR_OFFER_TREE_REJECTED,
5218
+ msg: "Tree rejected: offers outside block window",
5219
+ reason: "block_window"
4821
5220
  });
4822
- totalValidOffers += tree.offers.length;
4823
- const obligationIdsByOfferHash = /* @__PURE__ */ new Map();
4824
- for (const offer of tree.offers) {
4825
- const offerHash = hash(offer).toLowerCase();
4826
- const obligationId$1 = obligationId(offer, {
4827
- chainId: client.chain.id,
4828
- morphoV2
4829
- }).toLowerCase();
4830
- offersWithBlock.push({
4831
- offer,
4832
- blockNumber: treeBlockNumber,
4833
- obligationId: obligationId$1
4834
- });
4835
- obligationIdsByOfferHash.set(offerHash, obligationId$1);
4836
- }
4837
- for (const proof of proofs(tree)) {
4838
- const offerHash = hash(proof.offer).toLowerCase();
4839
- const obligationId = obligationIdsByOfferHash.get(offerHash);
4840
- if (obligationId === void 0) continue;
4841
- pathsToInsert.push({
4842
- offerHash,
4843
- obligationId,
4844
- treeRoot: tree.root,
4845
- proof: proof.path
4846
- });
4847
- }
4848
- } catch (err) {
4849
- const error = err instanceof Error ? err : new Error(String(err));
4850
- logger.error({
4851
- err: error,
4852
- msg: "Gatekeeper validation failed",
4853
- chain_id: client.chain.id
5221
+ continue;
5222
+ }
5223
+ treesToInsert.push({
5224
+ root: tree.root,
5225
+ signature
5226
+ });
5227
+ totalValidOffers += tree.offers.length;
5228
+ const obligationIdsByOfferHash = /* @__PURE__ */ new Map();
5229
+ for (const offer of tree.offers) {
5230
+ const offerHash = hash(offer).toLowerCase();
5231
+ const obligationId$1 = obligationId(offer, {
5232
+ chainId: client.chain.id,
5233
+ morphoV2
5234
+ }).toLowerCase();
5235
+ offersWithBlock.push({
5236
+ offer,
5237
+ blockNumber: treeBlockNumber,
5238
+ obligationId: obligationId$1
5239
+ });
5240
+ obligationIdsByOfferHash.set(offerHash, obligationId$1);
5241
+ }
5242
+ for (const proof of proofs(tree)) {
5243
+ const offerHash = hash(proof.offer).toLowerCase();
5244
+ const obligationId = obligationIdsByOfferHash.get(offerHash);
5245
+ if (obligationId === void 0) continue;
5246
+ pathsToInsert.push({
5247
+ offerHash,
5248
+ obligationId,
5249
+ treeRoot: tree.root,
5250
+ proof: proof.path
4854
5251
  });
4855
- throw new Error("Gatekeeper validation failed", { cause: error });
4856
5252
  }
5253
+ } catch (err) {
5254
+ const error = err instanceof Error ? err : new Error(String(err));
5255
+ logger.error({
5256
+ event: INDEXER_COLLECTOR_GATEKEEPER_VALIDATION_FAILED,
5257
+ err: error,
5258
+ msg: "Gatekeeper validation failed"
5259
+ });
5260
+ throw new Error("Gatekeeper validation failed", { cause: error });
5261
+ }
5262
+ await db.transaction(async (dbTx) => {
5263
+ const { epoch } = await dbTx.blocks.getChain(client.chain.id);
4857
5264
  const dependencies = buildOfferDependencies({
4858
5265
  offers: offersWithBlock,
4859
5266
  chainId: client.chain.id,
@@ -4882,13 +5289,17 @@ async function* collectOffersV2(parameters) {
4882
5289
  blockNumber,
4883
5290
  epoch
4884
5291
  });
4885
- if (totalValidOffers > 0) logger.info({
4886
- msg: `New offers`,
4887
- collector,
5292
+ logger.info({
5293
+ event: INDEXER_COLLECTOR_OFFERS_INDEXED,
5294
+ msg: "Offers batch processed",
4888
5295
  count: totalValidOffers,
4889
5296
  trees_count: treesToInsert.length,
4890
- chain_id: client.chain.id,
4891
- block_range: [startBlock, lastStreamBlockNumber]
5297
+ block_range: [startBlock, lastStreamBlockNumber],
5298
+ ...buildIndexedEventTelemetry({
5299
+ ...indexedEventHeadContext,
5300
+ logsIngestedCount: logs.length,
5301
+ logsIndexedCount: treesToInsert.length
5302
+ })
4892
5303
  });
4893
5304
  } catch (_) {
4894
5305
  try {
@@ -4902,8 +5313,7 @@ async function* collectOffersV2(parameters) {
4902
5313
  chainId: client.chain.id
4903
5314
  });
4904
5315
  logger.info({
4905
- collector,
4906
- chain_id: client.chain.id,
5316
+ event: INDEXER_COLLECTOR_REORG_COMPENSATED,
4907
5317
  msg: `Reorg detected, offers deleted`,
4908
5318
  count: deleted,
4909
5319
  block_number: blockNumber
@@ -4917,11 +5327,11 @@ async function* collectOffersV2(parameters) {
4917
5327
  reorgDetected = true;
4918
5328
  } catch (err) {
4919
5329
  const msg = "Failed to delete offers when handling reorg.";
5330
+ const error = err instanceof Error ? err : new Error(String(err));
4920
5331
  logger.error({
4921
- collector,
4922
- chainId: client.chain.id,
5332
+ event: INDEXER_COLLECTOR_REORG_COMPENSATION_FAILED,
4923
5333
  msg,
4924
- err
5334
+ err: error
4925
5335
  });
4926
5336
  throw new Error(msg);
4927
5337
  }
@@ -4943,37 +5353,62 @@ function decodeCallbacks(parameters) {
4943
5353
  const positions = [];
4944
5354
  const lots = [];
4945
5355
  for (const { offer, blockNumber: offerBlockNumber, obligationId } of offers) {
4946
- if (!offer.buy) continue;
4947
5356
  if (!isEmptyCallback(offer)) continue;
4948
- const loanToken = offer.loanToken.toLowerCase();
4949
- positions.push(from$12({
4950
- chainId,
4951
- contract: loanToken,
4952
- user: offer.maker,
4953
- type: Type.ERC20,
4954
- asset: loanToken,
4955
- blockNumber: offerBlockNumber
4956
- }));
4957
- lots.push({
4958
- positionChainId: chainId,
4959
- positionContract: loanToken,
4960
- positionUser: offer.maker,
4961
- positionTypeId: ERC20_TYPE_ID,
4962
- group: offer.group,
4963
- obligationId,
4964
- size: offer.assets
4965
- });
4966
- callbacks.push({
4967
- offerHash: hash(offer),
4968
- obligationId,
4969
- callbacks: [{
5357
+ if (offer.buy) {
5358
+ const loanToken = offer.loanToken.toLowerCase();
5359
+ const positionTypeId$3 = positionTypeId(Type.ERC20);
5360
+ positions.push(from$12({
4970
5361
  chainId,
4971
5362
  contract: loanToken,
4972
5363
  user: offer.maker,
4973
- amount: offer.assets,
4974
- positionTypeId: ERC20_TYPE_ID
4975
- }]
4976
- });
5364
+ type: Type.ERC20,
5365
+ asset: loanToken,
5366
+ blockNumber: offerBlockNumber
5367
+ }));
5368
+ lots.push({
5369
+ positionChainId: chainId,
5370
+ positionContract: loanToken,
5371
+ positionUser: offer.maker,
5372
+ positionTypeId: positionTypeId$3,
5373
+ group: offer.group,
5374
+ obligationId,
5375
+ size: offer.assets
5376
+ });
5377
+ callbacks.push({
5378
+ offerHash: hash(offer),
5379
+ obligationId,
5380
+ callbacks: [{
5381
+ chainId,
5382
+ contract: loanToken,
5383
+ user: offer.maker,
5384
+ positionTypeId: positionTypeId$3,
5385
+ type: CallbackType.Empty
5386
+ }]
5387
+ });
5388
+ } else {
5389
+ const contract = obligationId;
5390
+ const positionTypeId$2 = positionTypeId(Type.COLLATERAL_OF);
5391
+ lots.push({
5392
+ positionChainId: chainId,
5393
+ positionContract: contract,
5394
+ positionUser: offer.maker,
5395
+ positionTypeId: positionTypeId$2,
5396
+ group: offer.group,
5397
+ obligationId,
5398
+ size: offer.assets
5399
+ });
5400
+ callbacks.push({
5401
+ offerHash: hash(offer),
5402
+ obligationId,
5403
+ callbacks: [{
5404
+ chainId,
5405
+ contract,
5406
+ user: offer.maker,
5407
+ positionTypeId: positionTypeId$2,
5408
+ type: CallbackType.Empty
5409
+ }]
5410
+ });
5411
+ }
4977
5412
  }
4978
5413
  return {
4979
5414
  callbacks,
@@ -5149,8 +5584,11 @@ async function snapshotERC20Positions(parameters) {
5149
5584
  * @returns Positions - {@link snapshotVaultPositions.ReturnType}
5150
5585
  */
5151
5586
  async function snapshotVaultPositions(parameters) {
5152
- const logger = getLogger();
5153
5587
  const { client, positions: oldPositions, blockNumber, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
5588
+ const logger = getLoggerWithContext({
5589
+ component: "indexer.collector.fetcher",
5590
+ chain_id: client.chain.id
5591
+ });
5154
5592
  const calls = [];
5155
5593
  const contracts = /* @__PURE__ */ new Map();
5156
5594
  const positions = structuredClone(oldPositions);
@@ -5176,8 +5614,8 @@ async function snapshotVaultPositions(parameters) {
5176
5614
  } catch (err) {
5177
5615
  if (err instanceof DenominatorIsZeroError) {
5178
5616
  logger.error({
5617
+ event: INDEXER_COLLECTOR_POSITION_CONVERSION_FAILED,
5179
5618
  msg: "Failed to convert shares to assets",
5180
- chain_id: client.chain.id,
5181
5619
  block_number: blockNumber,
5182
5620
  position_contract: position.contract,
5183
5621
  position_user: position.user,
@@ -5257,11 +5695,18 @@ async function snapshotVaultPositions(parameters) {
5257
5695
 
5258
5696
  //#endregion
5259
5697
  //#region src/indexer/collectors/CollectFunctions/collectPositions.ts
5698
+ const MAX_POSITIONS_BLOCK_WINDOW = 500;
5699
+ const POSITIONS_PAGE_SIZE = 1e3;
5260
5700
  async function* collectPositions(parameters) {
5261
5701
  let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500, blockWindow } = {} } = parameters;
5262
- const logger = getLogger();
5702
+ const logger = getLoggerWithContext({
5703
+ component: "indexer.collector",
5704
+ collector,
5705
+ chain_id: client.chain.id
5706
+ });
5263
5707
  let startBlock = blockNumber;
5264
5708
  let reorgDetected = false;
5709
+ const safeBlockWindow = blockWindow === void 0 ? MAX_POSITIONS_BLOCK_WINDOW : Math.min(blockWindow, MAX_POSITIONS_BLOCK_WINDOW);
5265
5710
  const TransferEvent = {
5266
5711
  type: "event",
5267
5712
  name: "Transfer",
@@ -5284,6 +5729,107 @@ async function* collectPositions(parameters) {
5284
5729
  ]
5285
5730
  };
5286
5731
  const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5732
+ const indexedEventHeadContext = await getIndexedEventHeadContext({
5733
+ client,
5734
+ headBlockNumber: latestBlockNumberChain
5735
+ });
5736
+ const hasEmptyPositions = await _hasPositions({
5737
+ db,
5738
+ chainId: client.chain.id,
5739
+ filled: false
5740
+ });
5741
+ const hasFilledPositions = await _hasPositions({
5742
+ db,
5743
+ chainId: client.chain.id,
5744
+ filled: true
5745
+ });
5746
+ let pendingNewPositions = [];
5747
+ if (hasEmptyPositions) {
5748
+ const emptyPositions = await _getPositions({
5749
+ db,
5750
+ chainId: client.chain.id,
5751
+ filled: false
5752
+ });
5753
+ try {
5754
+ pendingNewPositions = (await _snapshot({
5755
+ positions: emptyPositions,
5756
+ blockNumber: latestBlockNumberChain,
5757
+ client,
5758
+ maxBatchSize,
5759
+ retryAttempts,
5760
+ retryDelayMs
5761
+ })).map((position) => ({
5762
+ ...position,
5763
+ blockNumber: position.blockNumber + 1
5764
+ }));
5765
+ } catch (err) {
5766
+ logger.error({
5767
+ event: INDEXER_COLLECTOR_POSITIONS_SNAPSHOT_FAILED,
5768
+ msg: "Failed to snapshot new empty positions",
5769
+ block_number: latestBlockNumberChain,
5770
+ err
5771
+ });
5772
+ yield startBlock;
5773
+ return;
5774
+ }
5775
+ }
5776
+ if (!hasFilledPositions) {
5777
+ blockNumber = latestBlockNumberChain;
5778
+ const positionsToInsert = pendingNewPositions;
5779
+ try {
5780
+ await db.transaction(async (dbTx) => {
5781
+ const insertPositions = async () => {
5782
+ if (positionsToInsert.length === 0) return 0;
5783
+ try {
5784
+ return await dbTx.positions.upsert(positionsToInsert);
5785
+ } catch (err) {
5786
+ throw new InsertPositionsError(err);
5787
+ }
5788
+ };
5789
+ const saveBlockNumber = async () => {
5790
+ try {
5791
+ await dbTx.blocks.advanceCollector({
5792
+ collectorName: collector,
5793
+ chainId: client.chain.id,
5794
+ blockNumber,
5795
+ epoch
5796
+ });
5797
+ } catch (_) {
5798
+ throw new ReorgError(blockNumber);
5799
+ }
5800
+ };
5801
+ const positionsIndexedCount = await insertPositions();
5802
+ logger.info({
5803
+ event: INDEXER_COLLECTOR_POSITIONS_INDEXED,
5804
+ msg: "Positions batch processed",
5805
+ count: positionsIndexedCount,
5806
+ block_number: latestBlockNumberChain,
5807
+ ...buildIndexedEventTelemetry({
5808
+ ...indexedEventHeadContext,
5809
+ logsIngestedCount: positionsToInsert.length,
5810
+ logsIndexedCount: positionsIndexedCount
5811
+ })
5812
+ });
5813
+ await saveBlockNumber();
5814
+ });
5815
+ } catch (err) {
5816
+ if (err instanceof ReorgError) return;
5817
+ if (err instanceof InsertPositionsError) {
5818
+ logger.error({
5819
+ event: INDEXER_COLLECTOR_POSITIONS_INSERT_FAILED,
5820
+ msg: "Failed to insert positions",
5821
+ count: positionsToInsert.length,
5822
+ block_number: latestBlockNumberChain,
5823
+ err
5824
+ });
5825
+ throw err.cause;
5826
+ }
5827
+ throw err;
5828
+ }
5829
+ startBlock = blockNumber;
5830
+ yield blockNumber;
5831
+ return;
5832
+ }
5287
5833
  const stream = streamLogs({
5288
5834
  client,
5289
5835
  event: TransferEvent,
@@ -5292,7 +5838,7 @@ async function* collectPositions(parameters) {
5292
5838
  order: "asc",
5293
5839
  options: {
5294
5840
  maxBatchSize,
5295
- blockWindow
5841
+ blockWindow: safeBlockWindow
5296
5842
  }
5297
5843
  });
5298
5844
  for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
@@ -5305,8 +5851,7 @@ async function* collectPositions(parameters) {
5305
5851
  for (const log of parsedLogs) {
5306
5852
  if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
5307
5853
  logger.debug({
5308
- collector,
5309
- chainId: client.chain.id,
5854
+ event: INDEXER_COLLECTOR_LOG_SKIPPED,
5310
5855
  msg: "Skipping log because it is missing required fields"
5311
5856
  });
5312
5857
  continue;
@@ -5323,63 +5868,21 @@ async function* collectPositions(parameters) {
5323
5868
  blockNumber: Number(log.blockNumber)
5324
5869
  }));
5325
5870
  }
5326
- const { positions } = await db.positions.get({
5327
- chainId: client.chain.id,
5328
- filled: false,
5329
- type: Type.ERC20
5330
- });
5331
- const newPositions = [];
5332
- try {
5333
- newPositions.push(...(await _snapshot({
5334
- positions,
5335
- blockNumber: latestBlockNumberChain,
5336
- client,
5337
- maxBatchSize,
5338
- retryAttempts,
5339
- retryDelayMs
5340
- })).map((p) => ({
5341
- ...p,
5342
- blockNumber: p.blockNumber + 1
5343
- })));
5344
- } catch (err) {
5345
- logger.error({
5346
- msg: "Failed to snapshot new empty positions",
5347
- collector,
5348
- chain_id: client.chain.id,
5349
- block_number: latestBlockNumberChain,
5350
- err
5351
- });
5352
- yield startBlock;
5353
- return;
5354
- }
5871
+ const positionsToInsert = pendingNewPositions;
5355
5872
  try {
5356
5873
  await db.transaction(async (dbTx) => {
5357
5874
  const insertPositions = async () => {
5358
- if (newPositions.length === 0) return;
5875
+ if (positionsToInsert.length === 0) return 0;
5359
5876
  try {
5360
- const count = await dbTx.positions.upsert(newPositions);
5361
- logger.info({
5362
- msg: `New positions`,
5363
- collector,
5364
- count,
5365
- chain_id: client.chain.id,
5366
- block_number: latestBlockNumberChain
5367
- });
5877
+ return await dbTx.positions.upsert(positionsToInsert);
5368
5878
  } catch (err) {
5369
5879
  throw new InsertPositionsError(err);
5370
5880
  }
5371
5881
  };
5372
5882
  const insertTransfers = async () => {
5373
- if (transfers.length === 0) return;
5883
+ if (transfers.length === 0) return 0;
5374
5884
  try {
5375
- const created = await dbTx.transfers.create(transfers);
5376
- logger.info({
5377
- msg: `New transfers`,
5378
- collector,
5379
- count: created,
5380
- chain_id: client.chain.id,
5381
- block_range: [startBlock, blockNumber]
5382
- });
5885
+ return await dbTx.transfers.create(transfers);
5383
5886
  } catch (err) {
5384
5887
  throw new InsertTransfersError(err);
5385
5888
  }
@@ -5396,27 +5899,48 @@ async function* collectPositions(parameters) {
5396
5899
  throw new ReorgError(blockNumber);
5397
5900
  }
5398
5901
  };
5399
- await insertPositions();
5400
- await insertTransfers();
5902
+ const positionsIndexedCount = await insertPositions();
5903
+ const transfersIndexedCount = await insertTransfers();
5904
+ logger.info({
5905
+ event: INDEXER_COLLECTOR_POSITIONS_INDEXED,
5906
+ msg: "Positions batch processed",
5907
+ count: positionsIndexedCount,
5908
+ block_number: latestBlockNumberChain,
5909
+ ...buildIndexedEventTelemetry({
5910
+ ...indexedEventHeadContext,
5911
+ logsIngestedCount: positionsToInsert.length,
5912
+ logsIndexedCount: positionsIndexedCount
5913
+ })
5914
+ });
5915
+ logger.info({
5916
+ event: INDEXER_COLLECTOR_TRANSFERS_INDEXED,
5917
+ msg: "Transfers batch processed",
5918
+ count: transfersIndexedCount,
5919
+ block_range: [startBlock, blockNumber],
5920
+ ...buildIndexedEventTelemetry({
5921
+ ...indexedEventHeadContext,
5922
+ logsIngestedCount: transfers.length,
5923
+ logsIndexedCount: transfersIndexedCount
5924
+ })
5925
+ });
5401
5926
  await saveBlockNumber();
5402
5927
  });
5928
+ pendingNewPositions = [];
5403
5929
  } catch (err) {
5404
5930
  if (err instanceof ReorgError) {
5405
5931
  logger.info({
5932
+ event: INDEXER_COLLECTOR_REORG_DETECTED,
5406
5933
  msg: "Reorg detected, positions and transfers insertion aborted",
5407
- collector,
5408
- count: newPositions.length,
5409
- chain_id: client.chain.id,
5934
+ count: positionsToInsert.length,
5410
5935
  block_number: blockNumber
5411
5936
  });
5412
5937
  reorgDetected = true;
5413
5938
  }
5414
5939
  if (err instanceof InsertPositionsError) {
5415
5940
  logger.error({
5941
+ event: INDEXER_COLLECTOR_POSITIONS_INSERT_FAILED,
5416
5942
  msg: "Failed to insert positions",
5417
- collector,
5418
- count: newPositions.length,
5419
- chain_id: client.chain.id,
5943
+ count: positionsToInsert.length,
5420
5944
  block_number: latestBlockNumberChain,
5421
5945
  err
5422
5946
  });
@@ -5424,10 +5948,9 @@ async function* collectPositions(parameters) {
5424
5948
  }
5425
5949
  if (err instanceof InsertTransfersError) {
5426
5950
  logger.error({
5951
+ event: INDEXER_COLLECTOR_TRANSFERS_INSERT_FAILED,
5427
5952
  msg: "Failed to insert transfers",
5428
- collector,
5429
5953
  count: transfers.length,
5430
- chain_id: client.chain.id,
5431
5954
  block_number: blockNumber,
5432
5955
  err
5433
5956
  });
@@ -5436,7 +5959,7 @@ async function* collectPositions(parameters) {
5436
5959
  }
5437
5960
  if (!reorgDetected) {
5438
5961
  startBlock = blockNumber;
5439
- if (newPositions.length === 0 && transfers.length === 0 && lastStreamBlockNumber !== latestBlockNumberChain) continue;
5962
+ if (positionsToInsert.length === 0 && transfers.length === 0 && lastStreamBlockNumber !== latestBlockNumberChain) continue;
5440
5963
  yield blockNumber;
5441
5964
  continue;
5442
5965
  }
@@ -5453,10 +5976,9 @@ async function* collectPositions(parameters) {
5453
5976
  type: Type.ERC20
5454
5977
  });
5455
5978
  logger.info({
5979
+ event: INDEXER_COLLECTOR_REORG_COMPENSATED,
5456
5980
  msg: "Reorg detected, positions set to empty",
5457
- collector,
5458
5981
  count: emptied,
5459
- chain_id: client.chain.id,
5460
5982
  block_number_gte: blockNumber + 1
5461
5983
  });
5462
5984
  await dbTx.blocks.advanceCollector({
@@ -5467,11 +5989,11 @@ async function* collectPositions(parameters) {
5467
5989
  });
5468
5990
  } catch (err) {
5469
5991
  const msg = "Failed to revert to ancestor block when handling reorg.";
5992
+ const error = err instanceof Error ? err : new Error(String(err));
5470
5993
  logger.error({
5471
- collector,
5472
- chainId: client.chain.id,
5994
+ event: INDEXER_COLLECTOR_REORG_COMPENSATION_FAILED,
5473
5995
  msg,
5474
- err
5996
+ err: error
5475
5997
  });
5476
5998
  throw new Error(msg);
5477
5999
  }
@@ -5479,6 +6001,34 @@ async function* collectPositions(parameters) {
5479
6001
  return;
5480
6002
  }
5481
6003
  }
6004
+ async function _hasPositions(parameters) {
6005
+ const { db, chainId, filled } = parameters;
6006
+ const { positions } = await db.positions.get({
6007
+ chainId,
6008
+ filled,
6009
+ type: Type.ERC20,
6010
+ limit: 1
6011
+ });
6012
+ return positions.length > 0;
6013
+ }
6014
+ async function _getPositions(parameters) {
6015
+ const { db, chainId, filled } = parameters;
6016
+ const positions = [];
6017
+ let cursor;
6018
+ while (true) {
6019
+ const page = await db.positions.get({
6020
+ chainId,
6021
+ filled,
6022
+ type: Type.ERC20,
6023
+ limit: POSITIONS_PAGE_SIZE,
6024
+ cursor
6025
+ });
6026
+ positions.push(...page.positions);
6027
+ if (!page.nextCursor) break;
6028
+ cursor = page.nextCursor;
6029
+ }
6030
+ return positions;
6031
+ }
5482
6032
  /**
5483
6033
  * @internal
5484
6034
  *
@@ -5547,9 +6097,17 @@ var InsertTransfersError = class extends BaseError {
5547
6097
  */
5548
6098
  async function* collectPrices(parameters) {
5549
6099
  const { db, collector, client, options: { maxBatchSize = 5e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
5550
- const logger = getLogger();
6100
+ const logger = getLoggerWithContext({
6101
+ component: "indexer.collector",
6102
+ collector,
6103
+ chain_id: client.chain.id
6104
+ });
5551
6105
  let blockNumber = parameters.lastBlockNumber;
5552
6106
  const [oracles, { blockNumber: latestBlockNumberChain, epoch }] = await Promise.all([db.oracles.get({ chainId: client.chain.id }), db.blocks.getChain(client.chain.id)]);
6107
+ const indexedEventHeadContext = await getIndexedEventHeadContext({
6108
+ client,
6109
+ headBlockNumber: latestBlockNumberChain
6110
+ });
5553
6111
  const updatedOracles = [];
5554
6112
  try {
5555
6113
  const pricesMap = await fetchOraclePrices({
@@ -5573,9 +6131,8 @@ async function* collectPrices(parameters) {
5573
6131
  }
5574
6132
  } catch (err) {
5575
6133
  logger.error({
6134
+ event: INDEXER_COLLECTOR_ORACLE_FETCH_FAILED,
5576
6135
  msg: "Failed to fetch oracle prices",
5577
- collector,
5578
- chain_id: client.chain.id,
5579
6136
  block_number: latestBlockNumberChain,
5580
6137
  err
5581
6138
  });
@@ -5585,16 +6142,18 @@ async function* collectPrices(parameters) {
5585
6142
  let reorgDetected = false;
5586
6143
  try {
5587
6144
  await db.transaction(async (dbTx) => {
5588
- if (updatedOracles.length > 0) {
5589
- await dbTx.oracles.upsert(updatedOracles);
5590
- logger.info({
5591
- msg: "Oracle prices updated",
5592
- collector,
5593
- count: updatedOracles.length,
5594
- chain_id: client.chain.id,
5595
- block_number: latestBlockNumberChain
5596
- });
5597
- }
6145
+ if (updatedOracles.length > 0) await dbTx.oracles.upsert(updatedOracles);
6146
+ logger.info({
6147
+ event: INDEXER_COLLECTOR_ORACLES_INDEXED,
6148
+ msg: "Oracle prices updated",
6149
+ count: updatedOracles.length,
6150
+ block_number: latestBlockNumberChain,
6151
+ ...buildIndexedEventTelemetry({
6152
+ ...indexedEventHeadContext,
6153
+ logsIngestedCount: 0,
6154
+ logsIndexedCount: 0
6155
+ })
6156
+ });
5598
6157
  try {
5599
6158
  await dbTx.blocks.advanceCollector({
5600
6159
  collectorName: collector,
@@ -5610,10 +6169,9 @@ async function* collectPrices(parameters) {
5610
6169
  } catch (err) {
5611
6170
  if (err instanceof ReorgError) {
5612
6171
  logger.info({
6172
+ event: INDEXER_COLLECTOR_REORG_DETECTED,
5613
6173
  msg: "Reorg detected, prices update aborted",
5614
- collector,
5615
6174
  count: updatedOracles.length,
5616
- chain_id: client.chain.id,
5617
6175
  block_number: latestBlockNumberChain
5618
6176
  });
5619
6177
  reorgDetected = true;
@@ -5638,11 +6196,11 @@ async function* collectPrices(parameters) {
5638
6196
  });
5639
6197
  } catch (err) {
5640
6198
  const msg = "Failed to revert to ancestor block when handling reorg.";
6199
+ const error = err instanceof Error ? err : new Error(String(err));
5641
6200
  logger.error({
5642
- collector,
5643
- chainId: client.chain.id,
6201
+ event: INDEXER_COLLECTOR_REORG_COMPENSATION_FAILED,
5644
6202
  msg,
5645
- err
6203
+ err: error
5646
6204
  });
5647
6205
  throw new Error(msg);
5648
6206
  }
@@ -5654,7 +6212,7 @@ async function* collectPrices(parameters) {
5654
6212
  //#region src/indexer/collectors/CollectorBuilder.ts
5655
6213
  function createBuilder(parameters) {
5656
6214
  const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow, interval } = {} } = parameters;
5657
- const createCollector = (name, collect) => create$20({
6215
+ const createCollector = (name, collect) => create$21({
5658
6216
  name,
5659
6217
  collect,
5660
6218
  client,
@@ -5665,7 +6223,7 @@ function createBuilder(parameters) {
5665
6223
  }
5666
6224
  });
5667
6225
  return {
5668
- buildOffersCollector: ({ options: { maxBatchSize = 1e3 } = {} }) => {
6226
+ buildOffersCollector: ({ options: { maxBatchSize = 1e4 } = {} }) => {
5669
6227
  return createCollector("offers", (p) => collectOffersV2({
5670
6228
  ...p,
5671
6229
  gatekeeper,
@@ -5677,7 +6235,7 @@ function createBuilder(parameters) {
5677
6235
  }
5678
6236
  }));
5679
6237
  },
5680
- buildMorphoV2Collector: ({ options: { maxBatchSize = 1e3 } = {} } = {}) => {
6238
+ buildMorphoV2Collector: ({ options: { maxBatchSize = 1e4 } = {} } = {}) => {
5681
6239
  return createCollector("morpho_v2", (p) => collectMorphoV2({
5682
6240
  ...p,
5683
6241
  collector: "morpho_v2",
@@ -5687,7 +6245,7 @@ function createBuilder(parameters) {
5687
6245
  }
5688
6246
  }));
5689
6247
  },
5690
- buildPricesCollector: ({ options: { maxBatchSize = 5e3, retryAttempts, retryDelayMs } = {} } = {}) => {
6248
+ buildPricesCollector: ({ options: { maxBatchSize = 1e4, retryAttempts, retryDelayMs } = {} } = {}) => {
5691
6249
  return createCollector("prices", (p) => collectPrices({
5692
6250
  ...p,
5693
6251
  collector: "prices",
@@ -5698,7 +6256,7 @@ function createBuilder(parameters) {
5698
6256
  }
5699
6257
  }));
5700
6258
  },
5701
- buildPositionsCollector: ({ options: { maxBatchSize = 1e3, retryAttempts, retryDelayMs } = {} } = {}) => {
6259
+ buildPositionsCollector: ({ options: { maxBatchSize = 1e4, retryAttempts, retryDelayMs } = {} } = {}) => {
5702
6260
  return createCollector("positions", (p) => collectPositions({
5703
6261
  ...p,
5704
6262
  collector: "positions",
@@ -5746,11 +6304,11 @@ const from$7 = (parameters) => {
5746
6304
  //#endregion
5747
6305
  //#region src/indexer/Indexer.ts
5748
6306
  var Indexer_exports = /* @__PURE__ */ __exportAll({
5749
- create: () => create$18,
6307
+ create: () => create$19,
5750
6308
  from: () => from$6
5751
6309
  });
5752
6310
  function from$6(config) {
5753
- const { client, gatekeeper, db, interval = 1e4, maxBatchSize = 1e3, maxBlockNumber, blockWindow, retryAttempts, retryDelayMs } = config;
6311
+ const { client, gatekeeper, db, interval = 1e4, maxBatchSize = 1e4, maxBlockNumber, blockWindow, retryAttempts, retryDelayMs } = config;
5754
6312
  const { offersCollector, morphoV2Collector, positionsCollector, pricesCollector } = from$7({
5755
6313
  client,
5756
6314
  db,
@@ -5762,7 +6320,7 @@ function from$6(config) {
5762
6320
  retryAttempts,
5763
6321
  retryDelayMs
5764
6322
  });
5765
- return create$18({
6323
+ return create$19({
5766
6324
  client,
5767
6325
  collectors: [
5768
6326
  offersCollector,
@@ -5772,7 +6330,7 @@ function from$6(config) {
5772
6330
  ]
5773
6331
  });
5774
6332
  }
5775
- function create$18(params) {
6333
+ function create$19(params) {
5776
6334
  const { collectors, client } = params;
5777
6335
  const indexerId = `${client.chain.id.toString()}.indexer`;
5778
6336
  const tracer = getTracer(`router.${indexerId}`);
@@ -5801,12 +6359,12 @@ function create$18(params) {
5801
6359
 
5802
6360
  //#endregion
5803
6361
  //#region src/api/Health.ts
5804
- var Health_exports = /* @__PURE__ */ __exportAll({ create: () => create$17 });
6362
+ var Health_exports = /* @__PURE__ */ __exportAll({ create: () => create$18 });
5805
6363
  const DEFAULT_MAX_ALLOWED_LAG = 5;
5806
6364
  /**
5807
6365
  * Create a health service that exposes collector and chain block numbers.
5808
6366
  */
5809
- function create$17(parameters) {
6367
+ function create$18(parameters) {
5810
6368
  const { db, maxAllowedLag = DEFAULT_MAX_ALLOWED_LAG, healthClients, chainRegistry } = parameters;
5811
6369
  const loadSnapshot = async () => {
5812
6370
  const [collectorRows, chainRows, remoteBlockByChainId] = await Promise.all([
@@ -5821,8 +6379,8 @@ function create$17(parameters) {
5821
6379
  updatedAt: chain.updatedAt
5822
6380
  });
5823
6381
  const configuredChainIds = chainRegistry?.list().map((chain) => chain.id) ?? [];
5824
- const knownChainIds = new Set(configuredChainIds);
5825
- for (const chain of chainRows) knownChainIds.add(chain.chainId);
6382
+ const observedChainIds = /* @__PURE__ */ new Set();
6383
+ for (const chain of chainRows) observedChainIds.add(chain.chainId);
5826
6384
  const collectorKey = (chainId, name) => `${chainId}:${name}`;
5827
6385
  const collectorsByKey = /* @__PURE__ */ new Map();
5828
6386
  for (const row of collectorRows) collectorsByKey.set(collectorKey(row.chainId, row.collectorName), {
@@ -5831,15 +6389,15 @@ function create$17(parameters) {
5831
6389
  blockNumber: row.blockNumber,
5832
6390
  updatedAt: row.updatedAt
5833
6391
  });
5834
- for (const row of collectorRows) knownChainIds.add(row.chainId);
5835
- const expectedChainIds = configuredChainIds.length > 0 ? configuredChainIds : Array.from(knownChainIds);
5836
- const missingChains = expectedChainIds.filter((chainId) => !chainById.has(chainId)).sort((a, b) => a - b > 0 ? 1 : -1);
5837
- const missingCollectors = expectedChainIds.flatMap((chainId) => [...names].sort().filter((name) => !collectorsByKey.has(collectorKey(chainId, name))).map((name) => ({
6392
+ for (const row of collectorRows) observedChainIds.add(row.chainId);
6393
+ const scopedChainIds = configuredChainIds.length > 0 ? configuredChainIds : Array.from(observedChainIds);
6394
+ const missingChains = scopedChainIds.filter((chainId) => !chainById.has(chainId)).sort((a, b) => a - b > 0 ? 1 : -1);
6395
+ const missingCollectors = scopedChainIds.flatMap((chainId) => [...names].sort().filter((name) => !collectorsByKey.has(collectorKey(chainId, name))).map((name) => ({
5838
6396
  chainId,
5839
6397
  name
5840
6398
  }))).sort((a, b) => a.chainId === b.chainId ? a.name.localeCompare(b.name) : a.chainId - b.chainId);
5841
- const initialized = knownChainIds.size > 0 && missingChains.length === 0 && missingCollectors.length === 0;
5842
- const collectors = Array.from(knownChainIds).sort((a, b) => a - b > 0 ? 1 : -1).flatMap((chainId) => [...names].sort().map((name) => {
6399
+ const initialized = scopedChainIds.length > 0 && missingChains.length === 0 && missingCollectors.length === 0;
6400
+ const collectors = Array.from(scopedChainIds).sort((a, b) => a - b > 0 ? 1 : -1).flatMap((chainId) => [...names].sort().map((name) => {
5843
6401
  const row = collectorsByKey.get(collectorKey(chainId, name));
5844
6402
  const chain = chainById.get(chainId);
5845
6403
  const blockNumber = row?.blockNumber ?? null;
@@ -5858,7 +6416,7 @@ function create$17(parameters) {
5858
6416
  initialized: row !== void 0
5859
6417
  };
5860
6418
  }));
5861
- const chains = Array.from(knownChainIds).sort((a, b) => a - b > 0 ? 1 : -1).map((chainId) => {
6419
+ const chains = Array.from(scopedChainIds).sort((a, b) => a - b > 0 ? 1 : -1).map((chainId) => {
5862
6420
  const chain = chainById.get(chainId);
5863
6421
  return {
5864
6422
  chainId,
@@ -5916,6 +6474,152 @@ async function getRemoteBlockNumbers(healthClients) {
5916
6474
  return new Map(results.map((r) => [r.chainId, r.remoteBlock]));
5917
6475
  }
5918
6476
 
6477
+ //#endregion
6478
+ //#region src/observability/Hono.ts
6479
+ const requestIdContextKey = "request_id";
6480
+ const requestStartedAtContextKey = "request_started_at";
6481
+ const unmatchedRouteLabel = "/*";
6482
+ /**
6483
+ * Install shared request tracing and structured logging on a Hono app.
6484
+ * @param app - Hono app to instrument.
6485
+ * @param parameters - Observability configuration.
6486
+ */
6487
+ function init(app, parameters) {
6488
+ const requestIdHeader = (parameters.requestIdHeader ?? "x-request-id").toLowerCase();
6489
+ app.use("*", createRequestObservabilityMiddleware({
6490
+ component: parameters.component,
6491
+ tracerName: parameters.tracerName,
6492
+ requestIdHeader
6493
+ }));
6494
+ app.onError(createObservabilityErrorHandler({
6495
+ component: parameters.component,
6496
+ toFailurePayload: parameters.toFailurePayload,
6497
+ adaptFailurePayload: parameters.adaptFailurePayload,
6498
+ requestIdHeader
6499
+ }));
6500
+ }
6501
+ /**
6502
+ * Build a middleware that emits one completion event per request and enriches span attributes.
6503
+ * @param parameters - Request logging configuration.
6504
+ * @returns Hono middleware.
6505
+ */
6506
+ function createRequestObservabilityMiddleware(parameters) {
6507
+ const tracer = getTracer(parameters.tracerName);
6508
+ return async (c, next) => {
6509
+ const method = c.req.method;
6510
+ const path = c.req.path;
6511
+ const requestId = resolveRequestId(c.req.header(parameters.requestIdHeader));
6512
+ const startedAt = Date.now();
6513
+ c.set(requestIdContextKey, requestId);
6514
+ c.set(requestStartedAtContextKey, startedAt);
6515
+ return startActiveSpan(tracer, `${method} ${getRouteLabel(c.req.routePath)}`, async (span) => {
6516
+ const spanContext = span.spanContext();
6517
+ const traceIdentifiers = getTraceIdentifiersFromContext(spanContext.traceId, spanContext.spanId);
6518
+ span.setAttribute("http.method", method);
6519
+ span.setAttribute("http.target", path);
6520
+ span.setAttribute("http.request_id", requestId);
6521
+ await runWithLogContext({
6522
+ component: parameters.component,
6523
+ request_id: requestId,
6524
+ ...traceIdentifiers,
6525
+ method
6526
+ }, async () => {
6527
+ await next();
6528
+ const statusCode = c.res.status;
6529
+ const durationMs = Math.max(Date.now() - startedAt, 0);
6530
+ const routeLabel = getRouteLabel(c.req.routePath);
6531
+ span.updateName(`${method} ${routeLabel}`);
6532
+ span.setAttribute("http.route", routeLabel);
6533
+ span.setAttribute("http.status_code", statusCode);
6534
+ if (statusCode >= 500) span.setStatus({ code: SpanStatusCode.ERROR });
6535
+ c.header(parameters.requestIdHeader, requestId);
6536
+ getLogger().info({
6537
+ event: HTTP_REQUEST_COMPLETED,
6538
+ msg: "HTTP request completed",
6539
+ status_code: statusCode,
6540
+ duration_ms: durationMs,
6541
+ route: routeLabel
6542
+ });
6543
+ });
6544
+ });
6545
+ };
6546
+ }
6547
+ /**
6548
+ * Build a shared unhandled error handler that logs structured failures and serializes payload errors.
6549
+ * @param parameters - Error logging configuration.
6550
+ * @returns Hono error handler.
6551
+ */
6552
+ function createObservabilityErrorHandler(parameters) {
6553
+ return (err, c) => {
6554
+ const baseFailure = parameters.toFailurePayload(err);
6555
+ const requestId = resolveRequestId(readContextString(c.get(requestIdContextKey)) ?? c.req.header(parameters.requestIdHeader));
6556
+ const routeTemplate = getRouteLabel(c.req.routePath);
6557
+ const adaptedFailure = parameters.adaptFailurePayload?.({
6558
+ err,
6559
+ failure: baseFailure,
6560
+ method: c.req.method,
6561
+ route: routeTemplate
6562
+ });
6563
+ const responseStatusCode = adaptedFailure?.statusCode ?? baseFailure.statusCode;
6564
+ const startedAt = readContextNumber(c.get(requestStartedAtContextKey)) ?? Date.now();
6565
+ const durationMs = Math.max(Date.now() - startedAt, 0);
6566
+ const activeSpan = trace.getActiveSpan();
6567
+ if (activeSpan) {
6568
+ activeSpan.recordException(err);
6569
+ if (responseStatusCode >= 500) activeSpan.setStatus({ code: SpanStatusCode.ERROR });
6570
+ else activeSpan.setStatus({ code: SpanStatusCode.UNSET });
6571
+ activeSpan.setAttribute("http.route", routeTemplate);
6572
+ activeSpan.setAttribute("http.status_code", responseStatusCode);
6573
+ activeSpan.setAttribute("http.request_id", requestId);
6574
+ }
6575
+ const traceIdentifiers = getActiveTraceIdentifiers();
6576
+ getLogger().error({
6577
+ event: HTTP_REQUEST_FAILED,
6578
+ msg: "HTTP request failed",
6579
+ err,
6580
+ component: parameters.component,
6581
+ method: c.req.method,
6582
+ route: routeTemplate,
6583
+ request_id: requestId,
6584
+ status_code: responseStatusCode,
6585
+ duration_ms: durationMs,
6586
+ ...traceIdentifiers
6587
+ });
6588
+ c.header(parameters.requestIdHeader, requestId);
6589
+ if (adaptedFailure) return c.text(adaptedFailure.body, adaptedFailure.statusCode, {
6590
+ "Content-Type": adaptedFailure.contentType,
6591
+ ...adaptedFailure.headers
6592
+ });
6593
+ return c.json(baseFailure.body, baseFailure.statusCode);
6594
+ };
6595
+ }
6596
+ /**
6597
+ * Convert a Hono route path into a stable request route label.
6598
+ * @param routePath - Hono route path from the request.
6599
+ * @returns Route label for logs and trace attributes.
6600
+ */
6601
+ function getRouteLabel(routePath) {
6602
+ const candidate = routePath?.trim();
6603
+ if (candidate && candidate.length > 0) return candidate;
6604
+ return unmatchedRouteLabel;
6605
+ }
6606
+ /**
6607
+ * Resolve a request ID from incoming headers and generate one when missing/invalid.
6608
+ * @param value - Incoming header value.
6609
+ * @returns Stable request ID value.
6610
+ */
6611
+ function resolveRequestId(value) {
6612
+ const candidate = value?.trim();
6613
+ if (candidate && candidate.length <= 128) return candidate;
6614
+ return randomUUID();
6615
+ }
6616
+ function readContextString(value) {
6617
+ return typeof value === "string" ? value : void 0;
6618
+ }
6619
+ function readContextNumber(value) {
6620
+ return typeof value === "number" ? value : void 0;
6621
+ }
6622
+
5919
6623
  //#endregion
5920
6624
  //#region src/api/Schema/BookResponse.ts
5921
6625
  var BookResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$5 });
@@ -6202,7 +6906,7 @@ const offerExample = {
6202
6906
  receiver_if_maker_is_seller: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401"
6203
6907
  },
6204
6908
  offer_hash: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427",
6205
- obligation_id: "0x25690ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9abc",
6909
+ obligation_id: "0x25690ae1aee324a005be565f3bcdd16dbf8daf79",
6206
6910
  chain_id: 1,
6207
6911
  consumed: "0",
6208
6912
  takeable: "369216000000000000000000",
@@ -6774,7 +7478,7 @@ const positionExample = {
6774
7478
  chain_id: 1,
6775
7479
  contract: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
6776
7480
  user: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6777
- obligation_id: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
7481
+ obligation_id: "0x12590ae1aee324a005be565f3bcdd16dbf8daf79",
6778
7482
  reserved: "200000000000000000000",
6779
7483
  block_number: 21345678
6780
7484
  };
@@ -7459,11 +8163,14 @@ function isValidBase64urlJson(val) {
7459
8163
  function isValidOfferHashCursor(val) {
7460
8164
  return /^0x[a-f0-9]{64}$/i.test(val);
7461
8165
  }
8166
+ function isValidObligationIdCursor(val) {
8167
+ return /^0x[a-f0-9]{40}$/i.test(val);
8168
+ }
7462
8169
  function isValidOfferCursor(val) {
7463
8170
  const [hash, obligationId, ...rest] = val.split(":");
7464
8171
  if (rest.length !== 0) return false;
7465
8172
  if (!hash || !obligationId) return false;
7466
- return isValidOfferHashCursor(hash) && isValidOfferHashCursor(obligationId);
8173
+ return isValidOfferHashCursor(hash) && isValidObligationIdCursor(obligationId);
7467
8174
  }
7468
8175
  const csvArray = (schema) => z$1.preprocess((value) => {
7469
8176
  if (value === void 0) return void 0;
@@ -7492,10 +8199,14 @@ const ConfigRuleTypes = z$1.enum([
7492
8199
  "callback",
7493
8200
  "loan_token",
7494
8201
  "collateral_token",
7495
- "oracle"
8202
+ "oracle",
8203
+ "group_consistency",
8204
+ "group_immutability",
8205
+ "max_collaterals",
8206
+ "min_duration"
7496
8207
  ]);
7497
8208
  const GetConfigRulesQueryParams = z$1.object({
7498
- cursor: z$1.string().regex(/^(maturity|callback|loan_token|collateral_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
8209
+ cursor: z$1.string().regex(/^(maturity|callback|loan_token|collateral_token|oracle|group_consistency|group_immutability|max_collaterals|min_duration):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
7499
8210
  description: "Pagination cursor in type:chain_id:<value> format",
7500
8211
  example: "maturity:1:1730415600:end_of_next_month"
7501
8212
  }),
@@ -7535,9 +8246,9 @@ const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend
7535
8246
  description: "Side of the offer. Required when using obligation_id.",
7536
8247
  example: "buy"
7537
8248
  }),
7538
- obligation_id: z$1.string().regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).optional().meta({
8249
+ obligation_id: z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Obligation id must be a valid 20-byte hex string" }).transform((val) => val.toLowerCase()).optional().meta({
7539
8250
  description: "Offers obligation id. Required when not using maker.",
7540
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
8251
+ example: "0x1234567890123456789012345678901234567890"
7541
8252
  }),
7542
8253
  maker: z$1.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Maker must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).optional().meta({
7543
8254
  description: "Maker address to filter offers by. Alternative to obligation_id + side.",
@@ -7622,9 +8333,9 @@ const GetObligationsQueryParams = z$1.object({
7622
8333
  example: "-ask,bid,maturity"
7623
8334
  })
7624
8335
  });
7625
- const GetObligationParams = z$1.object({ obligation_id: z$1.string({ error: "Obligation id is required and must be a valid 32-byte hex string" }).regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).meta({
8336
+ const GetObligationParams = z$1.object({ obligation_id: z$1.string({ error: "Obligation id is required and must be a valid 20-byte hex string" }).regex(/^0x[a-fA-F0-9]{40}$/, { error: "Obligation id must be a valid 20-byte hex string" }).transform((val) => val.toLowerCase()).meta({
7626
8337
  description: "Obligation id",
7627
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
8338
+ example: "0x1234567890123456789012345678901234567890"
7628
8339
  }) });
7629
8340
  /** Validate a book cursor format: {side, lastTick, offersCursor} */
7630
8341
  function isValidBookCursor(cursorString) {
@@ -7659,9 +8370,9 @@ const HealthQueryParams = z$1.object({ strict: z$1.enum([
7659
8370
  }) });
7660
8371
  const GetBookParams = z$1.object({
7661
8372
  ...BookPaginationQueryParams.shape,
7662
- obligation_id: z$1.string({ error: "Obligation id is required and must be a valid 32-byte hex string" }).regex(/^0x[a-fA-F0-9]{64}$/, { error: "Obligation id must be a valid 32-byte hex string" }).transform((val) => val.toLowerCase()).meta({
8373
+ obligation_id: z$1.string({ error: "Obligation id is required and must be a valid 20-byte hex string" }).regex(/^0x[a-fA-F0-9]{40}$/, { error: "Obligation id must be a valid 20-byte hex string" }).transform((val) => val.toLowerCase()).meta({
7663
8374
  description: "Obligation id",
7664
- example: "0x1234567890123456789012345678901234567890123456789012345678901234"
8375
+ example: "0x1234567890123456789012345678901234567890"
7665
8376
  }),
7666
8377
  side: z$1.enum(["buy", "sell"]).meta({
7667
8378
  description: "Side of the book (buy or sell).",
@@ -7745,9 +8456,11 @@ async function getBook(params, db) {
7745
8456
  } catch (err) {
7746
8457
  logger.error({
7747
8458
  err,
7748
- msg: "Error get book",
7749
- errorMessage: err instanceof Error ? err.message : String(err),
7750
- errorStack: err instanceof Error ? err.stack : void 0
8459
+ event: API_GET_BOOK_FAILED,
8460
+ msg: "Failed to get book",
8461
+ endpoint: "get_book",
8462
+ obligation_id: query.obligation_id,
8463
+ side: query.side
7751
8464
  });
7752
8465
  return failure(err);
7753
8466
  }
@@ -7935,6 +8648,7 @@ const oracles = {
7935
8648
  ],
7936
8649
  [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
7937
8650
  "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
8651
+ "0xEeee770BADd886dF3864029e4B377B5F6a2B6b83",
7938
8652
  "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
7939
8653
  "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
7940
8654
  "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
@@ -7943,6 +8657,7 @@ const oracles = {
7943
8657
  ],
7944
8658
  [ChainId.ANVIL.toString()]: [
7945
8659
  "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
8660
+ "0xEeee770BADd886dF3864029e4B377B5F6a2B6b83",
7946
8661
  "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
7947
8662
  "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
7948
8663
  "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
@@ -7953,19 +8668,23 @@ const oracles = {
7953
8668
  const configs = {
7954
8669
  ethereum: {
7955
8670
  callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7956
- maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek]
8671
+ maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek],
8672
+ minDuration: 10
7957
8673
  },
7958
8674
  base: {
7959
8675
  callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7960
- maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek]
8676
+ maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek],
8677
+ minDuration: 10
7961
8678
  },
7962
8679
  "ethereum-virtual-testnet": {
7963
8680
  callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7964
- maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek]
8681
+ maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek],
8682
+ minDuration: 10
7965
8683
  },
7966
8684
  anvil: {
7967
8685
  callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
7968
- maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek]
8686
+ maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek],
8687
+ minDuration: 10
7969
8688
  }
7970
8689
  };
7971
8690
 
@@ -7979,7 +8698,8 @@ const configs = {
7979
8698
  function buildConfigRules(chains) {
7980
8699
  const rules = [];
7981
8700
  for (const chain of chains) {
7982
- const maturities = configs[chain.name].maturities ?? [];
8701
+ const config = configs[chain.name];
8702
+ const maturities = config.maturities ?? [];
7983
8703
  for (const maturityName of maturities) rules.push({
7984
8704
  type: "maturity",
7985
8705
  chain_id: chain.id,
@@ -8004,6 +8724,26 @@ function buildConfigRules(chains) {
8004
8724
  chain_id: chain.id,
8005
8725
  address: normalizeAddress(address)
8006
8726
  });
8727
+ rules.push({
8728
+ type: "group_consistency",
8729
+ chain_id: chain.id,
8730
+ description: "All offers in a group must have the same loan token, assets amount, and side (buy/sell)"
8731
+ });
8732
+ rules.push({
8733
+ type: "group_immutability",
8734
+ chain_id: chain.id,
8735
+ description: "Cannot add offers to a group after group creation"
8736
+ });
8737
+ rules.push({
8738
+ type: "max_collaterals",
8739
+ chain_id: chain.id,
8740
+ max: 128
8741
+ });
8742
+ if (config.minDuration != null) rules.push({
8743
+ type: "min_duration",
8744
+ chain_id: chain.id,
8745
+ min_seconds: config.minDuration
8746
+ });
8007
8747
  }
8008
8748
  rules.sort(compareConfigRules);
8009
8749
  return rules;
@@ -8033,6 +8773,22 @@ function buildConfigRulesChecksum(rules) {
8033
8773
  hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
8034
8774
  continue;
8035
8775
  }
8776
+ if (rule.type === "group_consistency") {
8777
+ hash.update(`group_consistency:${rule.chain_id}\n`);
8778
+ continue;
8779
+ }
8780
+ if (rule.type === "group_immutability") {
8781
+ hash.update(`group_immutability:${rule.chain_id}\n`);
8782
+ continue;
8783
+ }
8784
+ if (rule.type === "max_collaterals") {
8785
+ hash.update(`max_collaterals:${rule.chain_id}:${rule.max}\n`);
8786
+ continue;
8787
+ }
8788
+ if (rule.type === "min_duration") {
8789
+ hash.update(`min_duration:${rule.chain_id}:${rule.min_seconds}\n`);
8790
+ continue;
8791
+ }
8036
8792
  hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
8037
8793
  }
8038
8794
  return hash.digest("hex");
@@ -8051,6 +8807,8 @@ function compareConfigRules(left, right) {
8051
8807
  if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
8052
8808
  if (left.type === "collateral_token" && right.type === "collateral_token") return left.address.localeCompare(right.address);
8053
8809
  if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
8810
+ if (left.type === "max_collaterals" && right.type === "max_collaterals") return left.max - right.max;
8811
+ if (left.type === "min_duration" && right.type === "min_duration") return left.min_seconds - right.min_seconds;
8054
8812
  return 0;
8055
8813
  }
8056
8814
 
@@ -8097,6 +8855,10 @@ function formatCursor$2(rule) {
8097
8855
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
8098
8856
  if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
8099
8857
  if (rule.type === "collateral_token") return `collateral_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
8858
+ if (rule.type === "group_consistency") return `group_consistency:${rule.chain_id}:_`;
8859
+ if (rule.type === "group_immutability") return `group_immutability:${rule.chain_id}:_`;
8860
+ if (rule.type === "max_collaterals") return `max_collaterals:${rule.chain_id}:${rule.max}`;
8861
+ if (rule.type === "min_duration") return `min_duration:${rule.chain_id}:${rule.min_seconds}`;
8100
8862
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
8101
8863
  }
8102
8864
  function parseCursor$2(cursor) {
@@ -8138,6 +8900,34 @@ function parseCursor$2(cursor) {
8138
8900
  address: parseAddress(addressValue, "Cursor address")
8139
8901
  };
8140
8902
  }
8903
+ if (type === "group_consistency") return {
8904
+ type,
8905
+ chain_id,
8906
+ description: "All offers in a group must have the same loan token, assets amount, and side (buy/sell)"
8907
+ };
8908
+ if (type === "group_immutability") return {
8909
+ type,
8910
+ chain_id,
8911
+ description: "Cannot add offers to a group after group creation"
8912
+ };
8913
+ if (type === "max_collaterals") {
8914
+ const maxValue = Number.parseInt(rest[0] ?? "", 10);
8915
+ if (!Number.isFinite(maxValue) || maxValue < 0) throw new BadRequestError$1("Cursor must be in the format max_collaterals:chain_id:max");
8916
+ return {
8917
+ type,
8918
+ chain_id,
8919
+ max: maxValue
8920
+ };
8921
+ }
8922
+ if (type === "min_duration") {
8923
+ const minSecondsValue = Number.parseInt(rest[0] ?? "", 10);
8924
+ if (!Number.isFinite(minSecondsValue) || minSecondsValue < 0) throw new BadRequestError$1("Cursor must be in the format min_duration:chain_id:min_seconds");
8925
+ return {
8926
+ type,
8927
+ chain_id,
8928
+ min_seconds: minSecondsValue
8929
+ };
8930
+ }
8141
8931
  throw new BadRequestError$1("Cursor has an invalid rule type");
8142
8932
  }
8143
8933
  function findStartIndex(rules, cursor) {
@@ -8156,7 +8946,7 @@ function parseAddress(address, label) {
8156
8946
  return address.toLowerCase();
8157
8947
  }
8158
8948
  function isConfigRuleType(value) {
8159
- return value === "maturity" || value === "callback" || value === "loan_token" || value === "collateral_token" || value === "oracle";
8949
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "collateral_token" || value === "oracle" || value === "group_consistency" || value === "group_immutability" || value === "max_collaterals" || value === "min_duration";
8160
8950
  }
8161
8951
  function isMaturityType(value) {
8162
8952
  return Object.values(MaturityType).includes(value);
@@ -8261,7 +9051,7 @@ async function getHealth(query, db, chainRegistry) {
8261
9051
  try {
8262
9052
  const parsed = safeParse("get_health", query);
8263
9053
  if (!parsed.success) return failure(parsed.error);
8264
- const snapshot = await create$17({
9054
+ const snapshot = await create$18({
8265
9055
  db,
8266
9056
  chainRegistry
8267
9057
  }).getSnapshot();
@@ -8278,9 +9068,9 @@ async function getHealth(query, db, chainRegistry) {
8278
9068
  } catch (err) {
8279
9069
  logger.error({
8280
9070
  err,
8281
- msg: "Error getting health status",
8282
- errorMessage: err instanceof Error ? err.message : String(err),
8283
- errorStack: err instanceof Error ? err.stack : void 0
9071
+ event: API_GET_HEALTH_FAILED,
9072
+ msg: "Failed to get health status",
9073
+ endpoint: "get_health"
8284
9074
  });
8285
9075
  return failure(err);
8286
9076
  }
@@ -8290,7 +9080,7 @@ async function getHealthChains(query, db, healthClients, chainRegistry) {
8290
9080
  try {
8291
9081
  const parsed = safeParse("get_health_chains", query);
8292
9082
  if (!parsed.success) return failure(parsed.error);
8293
- const snapshot = await create$17({
9083
+ const snapshot = await create$18({
8294
9084
  db,
8295
9085
  healthClients,
8296
9086
  chainRegistry
@@ -8310,9 +9100,9 @@ async function getHealthChains(query, db, healthClients, chainRegistry) {
8310
9100
  } catch (err) {
8311
9101
  logger.error({
8312
9102
  err,
8313
- msg: "Error getting health status for chains",
8314
- errorMessage: err instanceof Error ? err.message : String(err),
8315
- errorStack: err instanceof Error ? err.stack : void 0
9103
+ event: API_GET_HEALTH_CHAINS_FAILED,
9104
+ msg: "Failed to get health status for chains",
9105
+ endpoint: "get_health_chains"
8316
9106
  });
8317
9107
  return failure(err);
8318
9108
  }
@@ -8322,7 +9112,7 @@ async function getHealthCollectors(query, db, chainRegistry) {
8322
9112
  try {
8323
9113
  const parsed = safeParse("get_health_collectors", query);
8324
9114
  if (!parsed.success) return failure(parsed.error);
8325
- const snapshot = await create$17({
9115
+ const snapshot = await create$18({
8326
9116
  db,
8327
9117
  chainRegistry
8328
9118
  }).getSnapshot();
@@ -8343,14 +9133,99 @@ async function getHealthCollectors(query, db, chainRegistry) {
8343
9133
  } catch (err) {
8344
9134
  logger.error({
8345
9135
  err,
8346
- msg: "Error getting health status for collectors",
8347
- errorMessage: err instanceof Error ? err.message : String(err),
8348
- errorStack: err instanceof Error ? err.stack : void 0
9136
+ event: API_GET_HEALTH_COLLECTORS_FAILED,
9137
+ msg: "Failed to get health status for collectors",
9138
+ endpoint: "get_health_collectors"
8349
9139
  });
8350
9140
  return failure(err);
8351
9141
  }
8352
9142
  }
8353
9143
 
9144
+ //#endregion
9145
+ //#region src/api/Metrics.ts
9146
+ const COLLECTOR_LAG_BLOCKS_METRIC = "router_collector_lag_blocks";
9147
+ const COLLECTOR_BLOCK_NUMBER_METRIC = "router_collector_block_number";
9148
+ const CHAIN_BLOCK_NUMBER_METRIC = "router_chain_block_number";
9149
+ /**
9150
+ * Create a metrics service that renders collector and chain metrics in Prometheus format.
9151
+ * @param parameters - Service dependencies. {@link MetricsServiceParameters}
9152
+ * @returns The metrics service. {@link MetricsService}
9153
+ */
9154
+ function create$17(parameters) {
9155
+ const { db, chainRegistry } = parameters;
9156
+ const healthService = create$18({
9157
+ db,
9158
+ chainRegistry
9159
+ });
9160
+ return { async getPrometheusMetrics() {
9161
+ const snapshot = await healthService.getSnapshot();
9162
+ return renderPrometheusMetrics({
9163
+ collectors: snapshot.collectors,
9164
+ chains: snapshot.chains
9165
+ });
9166
+ } };
9167
+ }
9168
+ /**
9169
+ * Render Prometheus exposition text for collector and chain synchronization metrics.
9170
+ * @param parameters - Snapshot state to expose. {@link RenderPrometheusMetricsParameters}
9171
+ * @returns Prometheus metrics text.
9172
+ */
9173
+ function renderPrometheusMetrics(parameters) {
9174
+ const lines = [
9175
+ `# HELP ${COLLECTOR_LAG_BLOCKS_METRIC} Collector lag in blocks behind the indexed chain head.`,
9176
+ `# TYPE ${COLLECTOR_LAG_BLOCKS_METRIC} gauge`,
9177
+ `# HELP ${COLLECTOR_BLOCK_NUMBER_METRIC} Latest indexed block number per collector.`,
9178
+ `# TYPE ${COLLECTOR_BLOCK_NUMBER_METRIC} gauge`,
9179
+ `# HELP ${CHAIN_BLOCK_NUMBER_METRIC} Latest indexed block number per chain.`,
9180
+ `# TYPE ${CHAIN_BLOCK_NUMBER_METRIC} gauge`
9181
+ ];
9182
+ lines.push(`${COLLECTOR_LAG_BLOCKS_METRIC}{collector="__merge_alarm__",chain_id="0"} 9999`);
9183
+ const collectors = [...parameters.collectors].sort((left, right) => left.chainId === right.chainId ? left.name.localeCompare(right.name) : left.chainId - right.chainId);
9184
+ for (const collector of collectors) {
9185
+ const labels = renderCollectorLabels({
9186
+ collector: collector.name,
9187
+ chainId: collector.chainId
9188
+ });
9189
+ if (collector.initialized && collector.lag !== null) lines.push(`${COLLECTOR_LAG_BLOCKS_METRIC}${labels} ${renderGaugeValue(collector.lag)}`);
9190
+ if (collector.initialized && collector.blockNumber !== null) lines.push(`${COLLECTOR_BLOCK_NUMBER_METRIC}${labels} ${renderGaugeValue(collector.blockNumber)}`);
9191
+ }
9192
+ const chains = [...parameters.chains].sort((left, right) => left.chainId - right.chainId);
9193
+ for (const chain of chains) {
9194
+ if (!chain.initialized || chain.localBlockNumber === null) continue;
9195
+ const labels = renderChainLabels({ chainId: chain.chainId });
9196
+ lines.push(`${CHAIN_BLOCK_NUMBER_METRIC}${labels} ${renderGaugeValue(chain.localBlockNumber)}`);
9197
+ }
9198
+ return `${lines.join("\n")}\n`;
9199
+ }
9200
+ function renderCollectorLabels(parameters) {
9201
+ return `{collector="${escapeLabelValue(parameters.collector)}",chain_id="${parameters.chainId}"}`;
9202
+ }
9203
+ function renderChainLabels(parameters) {
9204
+ return `{chain_id="${parameters.chainId}"}`;
9205
+ }
9206
+ function renderGaugeValue(value) {
9207
+ if (!Number.isFinite(value)) throw new Error(`Metrics value must be finite, received ${value}`);
9208
+ return `${value}`;
9209
+ }
9210
+ function escapeLabelValue(value) {
9211
+ return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, "\\\"");
9212
+ }
9213
+
9214
+ //#endregion
9215
+ //#region src/api/Controllers/getMetrics.ts
9216
+ /**
9217
+ * Get router synchronization metrics in Prometheus exposition format.
9218
+ * @param db - Database instance. {@link Database.Database}
9219
+ * @param chainRegistry - Optional chain registry used to scope expected chains.
9220
+ * @returns Prometheus exposition payload.
9221
+ */
9222
+ async function getMetrics(db, chainRegistry) {
9223
+ return await create$17({
9224
+ db,
9225
+ chainRegistry
9226
+ }).getPrometheusMetrics();
9227
+ }
9228
+
8354
9229
  //#endregion
8355
9230
  //#region src/database/readers/ObligationsListing.ts
8356
9231
  const SORT_FIELDS = [
@@ -8359,7 +9234,7 @@ const SORT_FIELDS = [
8359
9234
  "bid",
8360
9235
  "maturity"
8361
9236
  ];
8362
- const CURSOR_ID_REGEX = /^0x[a-f0-9]{64}$/i;
9237
+ const CURSOR_ID_REGEX = /^0x[a-f0-9]{40}$/i;
8363
9238
  const INT32_MIN = -2147483648;
8364
9239
  const INT32_MAX = 2147483647;
8365
9240
  const INT32_MAX_BIGINT = BigInt(INT32_MAX);
@@ -8407,7 +9282,7 @@ function create$16(parameters) {
8407
9282
  bidTick: sql`MAX(${bestBidTick.bidTick})`.as("bid_tick"),
8408
9283
  ask: sql`COALESCE(MAX(${bestAskTick.askTick}) + 1, 0)`.as("ask"),
8409
9284
  bid: sql`COALESCE(MAX(${bestBidTick.bidTick}) + 1, 0)`.as("bid")
8410
- }).from(obligationIdKeys).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoin(obligationCollateralsV2, eq(obligations.obligationKey, obligationCollateralsV2.obligationKey)).leftJoinLateral(bestAskTick, sql`true`).leftJoinLateral(bestBidTick, sql`true`).groupBy(obligationIdKeys.obligationId, obligationIdKeys.chainId, obligations.loanToken, obligations.maturity).where(and(ids !== void 0 && ids.length > 0 ? inArray(obligationIdKeys.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? inArray(obligationIdKeys.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? inArray(obligations.maturity, maturities) : gte(obligations.maturity, now$3), collateralFilter)).as("obligations_with_quotes");
9285
+ }).from(obligationIdKeys).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoin(obligationCollateralsV2, eq(obligations.obligationKey, obligationCollateralsV2.obligationKey)).leftJoinLateral(bestAskTick, sql`true`).leftJoinLateral(bestBidTick, sql`true`).groupBy(obligationIdKeys.obligationId, obligationIdKeys.chainId, obligations.loanToken, obligations.maturity).where(and(ids !== void 0 && ids.length > 0 ? inArray(obligationIdKeys.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? inArray(obligationIdKeys.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? inArray(obligations.maturity, maturities) : void 0, collateralFilter)).as("obligations_with_quotes");
8411
9286
  const sortColumns = {
8412
9287
  id: obligationsWithQuotes.obligationId,
8413
9288
  ask: obligationsWithQuotes.ask,
@@ -8609,9 +9484,10 @@ async function getObligation(params, db) {
8609
9484
  const payloadError = toPayloadError$1(err);
8610
9485
  logger.error({
8611
9486
  err: payloadError,
8612
- msg: "Error get obligation",
8613
- errorMessage: payloadError instanceof Error ? payloadError.message : String(payloadError),
8614
- errorStack: payloadError instanceof Error ? payloadError.stack : void 0
9487
+ event: API_GET_OBLIGATION_FAILED,
9488
+ msg: "Failed to get obligation",
9489
+ endpoint: "get_obligation",
9490
+ obligation_id: query.obligation_id
8615
9491
  });
8616
9492
  return failure(payloadError);
8617
9493
  }
@@ -8650,28 +9526,278 @@ async function getObligations$1(queryParameters, db) {
8650
9526
  const payloadError = toPayloadError(err);
8651
9527
  logger.error({
8652
9528
  err: payloadError,
8653
- msg: "Error get obligations",
8654
- errorMessage: payloadError instanceof Error ? payloadError.message : String(payloadError),
8655
- errorStack: payloadError instanceof Error ? payloadError.stack : void 0
9529
+ event: API_GET_OBLIGATIONS_FAILED,
9530
+ msg: "Failed to get obligations",
9531
+ endpoint: "get_obligations",
9532
+ sort: query.sort,
9533
+ has_cursor: query.cursor != null,
9534
+ limit: query.limit ?? null
8656
9535
  });
8657
9536
  return failure(payloadError);
8658
9537
  }
8659
9538
  }
8660
9539
 
8661
9540
  //#endregion
8662
- //#region src/database/constants.ts
9541
+ //#region src/database/domains/OfferFormulas.ts
9542
+ const IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_]*$/;
8663
9543
  /**
8664
- * Default batch size for bulk database inserts.
9544
+ * Assert that a value is a valid SQL identifier (lowercase alphanumeric + underscores).
9545
+ * @param value - The value to validate.
9546
+ * @param label - A descriptive label for error messages.
9547
+ */
9548
+ function assertIdentifier(value, label) {
9549
+ if (!IDENTIFIER_PATTERN.test(value)) throw new Error(`Invalid SQL identifier for ${label}: "${value}". Must match ${IDENTIFIER_PATTERN}.`);
9550
+ }
9551
+ /**
9552
+ * Single position's balance contribution in loan-token units.
8665
9553
  *
8666
- * PostgreSQL limits a single query to at most 65,535 parameters
8667
- * (e.g. $1, $2, ...). In bulk inserts, each row consumes one
8668
- * parameter per column, so inserting too many rows at once can
8669
- * exceed this limit.
9554
+ * - **ERC20 / DEBT_OF**: both price and lltv are NULL (no oracle/obligation
9555
+ * join match). The CASE falls through to the ELSE branch producing
9556
+ * `balance * 1e36 * 1e18 / 1e54 = balance` (identity).
9557
+ * - **COLLATERAL_OF with price**: real price and lltv from oracle/obligation
9558
+ * JOINs → `balance * price * lltv / 1e54`.
9559
+ * - **COLLATERAL_OF with NULL price**: oracle row exists but price has not
9560
+ * been collected yet. lltv IS NOT NULL so COALESCE(price, 0) yields 0,
9561
+ * treating the position as unavailable until the price is fetched.
8670
9562
  *
8671
- * Our largest batched insert is into the `offers` table with 15 columns.
8672
- * 15 cols × 4,000 rows = 60,000 parameters, safely under 65,535.
9563
+ * @param balance - Position balance column/expression.
9564
+ * @param price - Oracle price column/expression.
9565
+ * @param lltv - Obligation collateral LLTV column/expression.
8673
9566
  */
8674
- const DEFAULT_BATCH_SIZE$1 = 4e3;
9567
+ function offerBalanceTerm(balance, price, lltv) {
9568
+ return sql`COALESCE(${balance}::numeric, 0) * CASE WHEN ${lltv}::numeric IS NOT NULL THEN COALESCE(${price}::numeric, 0) ELSE 10::numeric ^ 36 END * COALESCE(${lltv}::numeric, 10::numeric ^ 18) / (10::numeric ^ 54)`;
9569
+ }
9570
+ /**
9571
+ * Lot balance: how much liquidity is available to a lot.
9572
+ * @param offerBalance - Aggregated offer balance for the position (from offer_balances CTE).
9573
+ * @param offset - Sum of offsets for this position+obligation.
9574
+ * @param positionConsumed - Sum of consumed capacity across groups for this position+obligation.
9575
+ * @param lotLower - Lot lower bound.
9576
+ * @param lotUpper - Lot upper bound.
9577
+ * @param consumed - Group consumed amount (in loan-token terms).
9578
+ * @param assets - Offer assets (for lot_consumed conversion).
9579
+ */
9580
+ function lotBalance(offerBalance, offset, positionConsumed, lotLower, lotUpper, consumed, assets) {
9581
+ const lotSize = sql`(COALESCE(${lotUpper}::numeric, 0) - COALESCE(${lotLower}::numeric, 0))`;
9582
+ return sql`GREATEST(0, LEAST(COALESCE(${offerBalance}, 0) + COALESCE(${offset}, 0) + COALESCE(${positionConsumed}, 0) - COALESCE(${lotLower}::numeric, 0), ${lotSize} - ${sql`CASE WHEN ${assets}::numeric > 0 THEN COALESCE(${consumed}::numeric, 0) * ${lotSize} / ${assets}::numeric ELSE 0 END`}))`;
9583
+ }
9584
+ /**
9585
+ * Callback contribution: amount contributed by one callback in loan terms.
9586
+ * Returns 0 when lot_lower IS NULL (no lot exists for this callback).
9587
+ * @param lotBalance - Computed lot balance.
9588
+ * @param lotLower - Lot lower bound (NULL means no lot exists).
9589
+ */
9590
+ function contribution(lotBalance, lotLower) {
9591
+ return sql`CASE WHEN ${lotLower} IS NULL THEN 0 ELSE ${lotBalance} END`;
9592
+ }
9593
+ /**
9594
+ * Unified takeable for buy and sell offers.
9595
+ * @param assets - Offer assets.
9596
+ * @param consumed - Group consumed amount.
9597
+ * @param available - Total available from callbacks/positions.
9598
+ */
9599
+ function takeable(assets, consumed, available) {
9600
+ return sql`GREATEST(0, LEAST(${assets}::numeric - ${consumed}::numeric, ${available}))`;
9601
+ }
9602
+ /**
9603
+ * Build a comma-prefixed SQL fragment containing 8 CTEs that compute
9604
+ * `offer_available(hash, obligation_id, available)`.
9605
+ *
9606
+ * Append the result inside a `WITH` clause after the caller's own CTEs.
9607
+ *
9608
+ * Reserved CTE names (callers must not reuse):
9609
+ * `group_winners`, `relevant_positions`, `position_offsets`,
9610
+ * `position_consumed`, `offer_balances`, `callback_contributions`,
9611
+ * `callback_loan_contribution`, `offer_available`.
9612
+ *
9613
+ * @param params - {@link OfferCTEParams}
9614
+ */
9615
+ function offerAvailabilityCTEs(params) {
9616
+ assertIdentifier(params.sourceTable, "sourceTable");
9617
+ const groupScope = params.groupScope ?? params.sourceTable;
9618
+ if (params.groupScope !== void 0) assertIdentifier(groupScope, "groupScope");
9619
+ const source = sql.raw(params.sourceTable);
9620
+ const scope = sql.raw(groupScope);
9621
+ const { now } = params;
9622
+ return sql`
9623
+ , group_winners AS (
9624
+ SELECT DISTINCT ON (o.group_chain_id, o.group_maker, o.group_group, o.obligation_id, o.buy)
9625
+ o.group_chain_id, o.group_maker, o.group_group, o.obligation_id, o.buy, o.assets
9626
+ FROM ${offers} o
9627
+ LEFT JOIN ${validations} v
9628
+ ON v.offer_hash = o.hash
9629
+ AND v.obligation_id = o.obligation_id
9630
+ LEFT JOIN ${status} s
9631
+ ON s.id = v.status_id
9632
+ WHERE (o.group_chain_id, o.group_maker, o.group_group) IN (
9633
+ SELECT DISTINCT gs.group_chain_id, gs.group_maker, gs.group_group FROM ${scope} gs
9634
+ )
9635
+ AND o.expiry > ${now}
9636
+ AND o.maturity > ${now}
9637
+ AND o.start <= ${now}
9638
+ AND (s.code IS NULL OR s.code = ${Status.VALID})
9639
+ ORDER BY
9640
+ o.group_chain_id, o.group_maker, o.group_group, o.obligation_id, o.buy,
9641
+ CASE WHEN o.buy THEN -o.tick ELSE o.tick END ASC,
9642
+ o.block_number ASC, o.assets DESC, o.hash ASC
9643
+ ),
9644
+ relevant_positions AS (
9645
+ SELECT DISTINCT c.position_chain_id, c.position_contract, c.position_user
9646
+ FROM ${source} src
9647
+ JOIN ${offersCallbacks} oc
9648
+ ON oc.offer_hash = src.hash AND oc.obligation_id = src.obligation_id
9649
+ JOIN ${callbacks} c ON c.id = oc.callback_id
9650
+ ),
9651
+ position_offsets AS (
9652
+ SELECT chain_id, "user", contract, obligation_id,
9653
+ SUM(value::numeric) AS total_offset
9654
+ FROM ${offsets}
9655
+ WHERE (chain_id, contract, "user") IN (
9656
+ SELECT position_chain_id, position_contract, position_user FROM relevant_positions
9657
+ )
9658
+ GROUP BY chain_id, "user", contract, obligation_id
9659
+ ),
9660
+ position_consumed AS (
9661
+ SELECT
9662
+ l.chain_id, l.contract, l."user", l.obligation_id, wo.buy,
9663
+ SUM(
9664
+ CASE
9665
+ WHEN wo.assets::numeric > 0
9666
+ THEN COALESCE(g.consumed::numeric, 0) * (l.upper::numeric - l.lower::numeric) / wo.assets::numeric
9667
+ ELSE 0
9668
+ END
9669
+ ) AS consumed
9670
+ FROM ${lots} l
9671
+ JOIN ${groups} g
9672
+ ON g.chain_id = l.chain_id
9673
+ AND g.maker = l."user"
9674
+ AND g."group" = l."group"
9675
+ JOIN group_winners wo
9676
+ ON wo.group_chain_id = g.chain_id
9677
+ AND wo.group_maker = g.maker
9678
+ AND wo.group_group = g."group"
9679
+ AND wo.obligation_id = l.obligation_id
9680
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id, wo.buy
9681
+ ),
9682
+ offer_balances AS (
9683
+ SELECT
9684
+ pos.chain_id, pos.contract, pos."user",
9685
+ SUM(
9686
+ ${offerBalanceTerm(sql`pos.balance`, sql`o.price`, sql`oc_col.lltv`)}
9687
+ ) AS offer_balance
9688
+ FROM ${positions} pos
9689
+ LEFT JOIN ${obligationIdKeys} oik
9690
+ ON oik.obligation_id = pos.contract
9691
+ LEFT JOIN ${obligationCollateralsV2} oc_col
9692
+ ON oc_col.obligation_key = oik.obligation_key
9693
+ AND oc_col.asset = pos.asset
9694
+ LEFT JOIN ${oracles$1} o
9695
+ ON o.chain_id = pos.chain_id
9696
+ AND o.address = oc_col.oracle_address
9697
+ WHERE (pos.chain_id, pos.contract, pos."user") IN (
9698
+ SELECT position_chain_id, position_contract, position_user FROM relevant_positions
9699
+ )
9700
+ GROUP BY pos.chain_id, pos.contract, pos."user"
9701
+ ),
9702
+ callback_contributions AS (
9703
+ SELECT
9704
+ src.hash,
9705
+ src.obligation_id,
9706
+ src.group_group,
9707
+ src.consumed,
9708
+ src.assets,
9709
+ c.id AS callback_id,
9710
+ c.position_chain_id,
9711
+ c.position_contract,
9712
+ c.position_user,
9713
+ l.lower AS lot_lower,
9714
+ l.upper AS lot_upper,
9715
+ ${lotBalance(sql`vb.offer_balance`, sql`pos_offsets.total_offset`, sql`pc.consumed`, sql`l.lower`, sql`l.upper`, sql`src.consumed`, sql`src.assets`)} AS lot_balance
9716
+ FROM ${source} src
9717
+ LEFT JOIN ${offersCallbacks} oc
9718
+ ON oc.offer_hash = src.hash
9719
+ AND oc.obligation_id = src.obligation_id
9720
+ LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
9721
+ LEFT JOIN ${lots} l
9722
+ ON l.chain_id = c.position_chain_id
9723
+ AND l.contract = c.position_contract
9724
+ AND l."user" = c.position_user
9725
+ AND l."group" = src.group_group
9726
+ AND l.obligation_id = src.obligation_id
9727
+ LEFT JOIN offer_balances vb
9728
+ ON vb.chain_id = c.position_chain_id
9729
+ AND vb.contract = c.position_contract
9730
+ AND vb."user" = c.position_user
9731
+ LEFT JOIN position_offsets pos_offsets
9732
+ ON pos_offsets.chain_id = c.position_chain_id
9733
+ AND pos_offsets.contract = c.position_contract
9734
+ AND pos_offsets."user" = c.position_user
9735
+ AND pos_offsets.obligation_id = src.obligation_id
9736
+ LEFT JOIN position_consumed pc
9737
+ ON pc.chain_id = c.position_chain_id
9738
+ AND pc.contract = c.position_contract
9739
+ AND pc."user" = c.position_user
9740
+ AND pc.obligation_id = src.obligation_id
9741
+ AND pc.buy = src.buy
9742
+ ),
9743
+ callback_loan_contribution AS (
9744
+ SELECT cc.*,
9745
+ ${contribution(sql`cc.lot_balance`, sql`cc.lot_lower`)} AS contribution_in_loan
9746
+ FROM callback_contributions cc
9747
+ ),
9748
+ offer_available AS (
9749
+ SELECT hash, obligation_id,
9750
+ SUM(max_contribution) AS available
9751
+ FROM (
9752
+ SELECT hash, obligation_id, position_chain_id, position_contract, position_user,
9753
+ MAX(contribution_in_loan) AS max_contribution
9754
+ FROM callback_loan_contribution
9755
+ WHERE callback_id IS NOT NULL
9756
+ GROUP BY hash, obligation_id, position_chain_id, position_contract, position_user
9757
+ ) per_position
9758
+ GROUP BY hash, obligation_id
9759
+ )`;
9760
+ }
9761
+ /**
9762
+ * Build a comma-prefixed SQL fragment containing 9 CTEs that compute
9763
+ * `offer_takeable(hash, obligation_id, available, takeable)`.
9764
+ *
9765
+ * Wraps {@link offerAvailabilityCTEs} and adds one CTE that LEFT JOINs
9766
+ * `offer_available` back to the source table, ensuring every source offer
9767
+ * appears (with `available = 0` when no callbacks exist).
9768
+ *
9769
+ * Reserved CTE names: all from {@link offerAvailabilityCTEs} plus `offer_takeable`.
9770
+ *
9771
+ * @param params - {@link OfferCTEParams}
9772
+ */
9773
+ function offerTakeabilityCTEs(params) {
9774
+ assertIdentifier(params.sourceTable, "sourceTable");
9775
+ const source = sql.raw(params.sourceTable);
9776
+ return sql`
9777
+ ${offerAvailabilityCTEs(params)},
9778
+ offer_takeable AS (
9779
+ SELECT src.hash, src.obligation_id,
9780
+ COALESCE(oa.available, 0) AS available,
9781
+ ${takeable(sql`src.assets`, sql`src.consumed`, sql`COALESCE(oa.available, 0)`)} AS takeable
9782
+ FROM ${source} src
9783
+ LEFT JOIN offer_available oa ON oa.hash = src.hash AND oa.obligation_id = src.obligation_id
9784
+ )`;
9785
+ }
9786
+
9787
+ //#endregion
9788
+ //#region src/database/constants.ts
9789
+ /**
9790
+ * Default batch size for bulk database inserts.
9791
+ *
9792
+ * PostgreSQL limits a single query to at most 65,535 parameters
9793
+ * (e.g. $1, $2, ...). In bulk inserts, each row consumes one
9794
+ * parameter per column, so inserting too many rows at once can
9795
+ * exceed this limit.
9796
+ *
9797
+ * Our largest batched insert is into the `offers` table with 15 columns.
9798
+ * 15 cols × 4,000 rows = 60,000 parameters, safely under 65,535.
9799
+ */
9800
+ const DEFAULT_BATCH_SIZE$1 = 4e3;
8675
9801
 
8676
9802
  //#endregion
8677
9803
  //#region src/database/domains/Offers.ts
@@ -8866,9 +9992,10 @@ function create$15(config) {
8866
9992
  };
8867
9993
  }
8868
9994
  const HEX_32$2 = /^0x[a-fA-F0-9]{64}$/;
9995
+ const HEX_20$2 = /^0x[a-fA-F0-9]{40}$/;
8869
9996
  function parseCursor$1(cursor) {
8870
9997
  const [hash, obligationId] = cursor.split(":");
8871
- if (!hash || !obligationId || !HEX_32$2.test(hash) || !HEX_32$2.test(obligationId)) throw new Error("Invalid cursor format");
9998
+ if (!hash || !obligationId || !HEX_32$2.test(hash) || !HEX_20$2.test(obligationId)) throw new Error("Invalid cursor format");
8872
9999
  return {
8873
10000
  hash: hash.toLowerCase(),
8874
10001
  obligationId: obligationId.toLowerCase()
@@ -8997,7 +10124,8 @@ function toAttestationKey(offer) {
8997
10124
  //#endregion
8998
10125
  //#region src/api/Controllers/getOffers.ts
8999
10126
  /**
9000
- * Query offers with computed consumed/available/takeable values.
10127
+ * Query offers for a maker with computed consumed/available/takeable values.
10128
+ * Uses the same CTE-based computation as Book.ts via shared OfferFormulas builders.
9001
10129
  * @param db - The database client. {@link Database.Core}
9002
10130
  * @param parameters - {@link GetOffersQueryParams}
9003
10131
  * @returns The offers with pagination cursor.
@@ -9006,149 +10134,129 @@ async function getOffersQuery(db, parameters) {
9006
10134
  const limit = parameters?.limit ?? DEFAULT_LIMIT$3;
9007
10135
  const rawCursor = parameters?.cursor;
9008
10136
  const maker = parameters?.maker;
9009
- const cursor = maker && rawCursor !== void 0 && rawCursor !== null ? parseMakerCursor(rawCursor) : void 0;
10137
+ if (!maker) throw new Error("getOffersQuery requires a maker parameter");
10138
+ const cursor = rawCursor !== void 0 && rawCursor !== null ? parseMakerCursor(rawCursor) : void 0;
9010
10139
  const now = Math.floor((Date.now() - 1) / 1e3);
9011
- const collateralsLateral = db.select({ collaterals: sql`COALESCE(
9012
- jsonb_agg(
9013
- jsonb_build_object(
9014
- 'asset', ${obligationCollateralsV2.asset},
9015
- 'oracle', ${obligationCollateralsV2.oracleAddress},
9016
- 'lltv', ${obligationCollateralsV2.lltv}
9017
- )
9018
- ),
9019
- '[]'::jsonb
9020
- )`.as("collaterals") }).from(obligationCollateralsV2).where(eq(obligationCollateralsV2.obligationKey, obligationIdKeys.obligationKey)).as("collaterals_lateral");
9021
- const lotBalanceExpr = sql`GREATEST(0, LEAST(
9022
- COALESCE(${positions.balance}, 0)::numeric
9023
- + COALESCE((
9024
- SELECT SUM(${offsets.value}::numeric)
9025
- FROM ${offsets}
9026
- WHERE ${offsets.chainId} = ${callbacks.positionChainId}
9027
- AND LOWER(${offsets.contract}) = LOWER(${callbacks.positionContract})
9028
- AND LOWER(${offsets.user}) = LOWER(${callbacks.positionUser})
9029
- ), 0)
9030
- - COALESCE(${lots.lower}::numeric, 0),
9031
- (COALESCE(${lots.upper}::numeric, 0) - COALESCE(${lots.lower}::numeric, 0))
9032
- - CASE
9033
- WHEN ${offers.assets}::numeric > 0
9034
- THEN COALESCE(${groups.consumed}::numeric, 0)
9035
- * (COALESCE(${lots.upper}::numeric, 0) - COALESCE(${lots.lower}::numeric, 0))
9036
- / ${offers.assets}::numeric
9037
- ELSE 0
9038
- END
9039
- ))`;
9040
- const contributionExpr = sql`CASE
9041
- WHEN ${positions.asset} IS NULL OR ${lots.lower} IS NULL THEN 0
9042
- ELSE LEAST(COALESCE(${callbacks.amount}::numeric, ${lotBalanceExpr}), ${lotBalanceExpr})
9043
- END`;
9044
- const availableExpr = sql`COALESCE((
9045
- SELECT SUM(deduped.contribution)
9046
- FROM (
9047
- SELECT DISTINCT ON (
9048
- ${callbacks.positionChainId},
9049
- LOWER(${callbacks.positionContract}),
9050
- LOWER(${callbacks.positionUser})
9051
- )
9052
- ${contributionExpr} AS contribution
9053
- FROM ${offersCallbacks}
9054
- INNER JOIN ${callbacks} ON ${offersCallbacks.callbackId} = ${callbacks.id}
9055
- LEFT JOIN ${positions}
9056
- ON ${positions.chainId} = ${callbacks.positionChainId}
9057
- AND LOWER(${positions.contract}) = LOWER(${callbacks.positionContract})
9058
- AND LOWER(${positions.user}) = LOWER(${callbacks.positionUser})
9059
- LEFT JOIN ${lots}
9060
- ON ${lots.chainId} = ${callbacks.positionChainId}
9061
- AND LOWER(${lots.contract}) = LOWER(${callbacks.positionContract})
9062
- AND LOWER(${lots.user}) = LOWER(${callbacks.positionUser})
9063
- AND LOWER(${lots.group}) = LOWER(${offers.group})
9064
- WHERE ${offersCallbacks.offerHash} = ${offers.hash}
9065
- AND ${offersCallbacks.obligationId} = ${offers.obligationId}
9066
- ORDER BY
9067
- ${callbacks.positionChainId},
9068
- LOWER(${callbacks.positionContract}),
9069
- LOWER(${callbacks.positionUser}),
9070
- ${contributionExpr} DESC
9071
- ) deduped
9072
- ), 0)`;
9073
- const rows = (await db.select({
9074
- hash: offers.hash,
9075
- obligationId: offers.obligationId,
9076
- maker: offers.groupMaker,
9077
- assets: offers.assets,
9078
- obligationUnits: offers.obligationUnits,
9079
- obligationShares: offers.obligationShares,
9080
- consumed: groups.consumed,
9081
- tick: offers.tick,
9082
- maturity: offers.maturity,
9083
- expiry: offers.expiry,
9084
- start: offers.start,
9085
- group: offers.group,
9086
- session: offers.session,
9087
- buy: offers.buy,
9088
- chainId: obligationIdKeys.chainId,
9089
- loanToken: obligations.loanToken,
9090
- callbackAddress: offers.callbackAddress,
9091
- callbackData: offers.callbackData,
9092
- receiverIfMakerIsSeller: offers.receiverIfMakerIsSeller,
9093
- collaterals: collateralsLateral.collaterals,
9094
- blockNumber: offers.blockNumber,
9095
- available: sql`${availableExpr}::numeric`.as("available"),
9096
- takeable: sql`FLOOR(GREATEST(0,
9097
- CASE WHEN ${offers.buy} = false
9098
- THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
9099
- ELSE LEAST(
9100
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
9101
- ${availableExpr}::numeric
9102
- )
9103
- END
9104
- ))`.as("takeable")
9105
- }).from(offers).innerJoin(obligationIdKeys, eq(offers.obligationId, obligationIdKeys.obligationId)).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== void 0 ? cursor.obligationId === void 0 ? gt(offers.hash, cursor.hash) : or(gt(offers.hash, cursor.hash), and(eq(offers.hash, cursor.hash), gt(offers.obligationId, cursor.obligationId))) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, gte(offers.expiry, now), gte(offers.maturity, now), maker === void 0 ? sql`GREATEST(0,
9106
- CASE WHEN ${offers.buy} = false
9107
- THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
9108
- ELSE LEAST(
9109
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
9110
- ${availableExpr}::numeric
9111
- )
9112
- END
9113
- ) > 0` : void 0)).orderBy(asc(offers.hash), asc(offers.obligationId)).limit(limit)).map((row) => {
9114
- const receiverIfMakerIsSeller = (row.receiverIfMakerIsSeller ?? row.maker).toLowerCase();
10140
+ const rows = (await db.execute(sql`
10141
+ WITH maker_offers AS (
10142
+ SELECT
10143
+ o.*,
10144
+ g.consumed, oik.chain_id, obl.loan_token
10145
+ FROM ${offers} o
10146
+ JOIN ${groups} g
10147
+ ON g.chain_id = o.group_chain_id
10148
+ AND g.maker = o.group_maker
10149
+ AND g."group" = o.group_group
10150
+ JOIN ${obligationIdKeys} oik
10151
+ ON oik.obligation_id = o.obligation_id
10152
+ JOIN ${obligations} obl
10153
+ ON obl.obligation_key = oik.obligation_key
10154
+ WHERE o.group_maker = ${maker.toLowerCase()}
10155
+ AND o.expiry >= ${now}
10156
+ AND o.maturity >= ${now}
10157
+ ${cursor !== void 0 ? sql`AND (o.hash, o.obligation_id) > (${cursor.hash}, ${cursor.obligationId})` : sql``}
10158
+ ORDER BY o.hash ASC, o.obligation_id ASC
10159
+ LIMIT ${limit}
10160
+ ),
10161
+ maker_groups AS (
10162
+ SELECT DISTINCT o.group_chain_id, o.group_maker, o.group_group
10163
+ FROM ${offers} o
10164
+ WHERE o.group_maker = ${maker.toLowerCase()}
10165
+ AND o.expiry >= ${now}
10166
+ AND o.maturity >= ${now}
10167
+ ),
10168
+ collats AS MATERIALIZED (
10169
+ SELECT oia.obligation_id,
10170
+ COALESCE(jsonb_agg(jsonb_build_object(
10171
+ 'asset', oc.asset,
10172
+ 'oracle', oc.oracle_address,
10173
+ 'lltv', oc.lltv
10174
+ ) ORDER BY oc.asset), '[]'::jsonb) AS collaterals
10175
+ FROM ${obligationIdKeys} oia
10176
+ JOIN ${obligationCollateralsV2} oc
10177
+ ON oc.obligation_key = oia.obligation_key
10178
+ WHERE oia.obligation_id IN (SELECT DISTINCT mo.obligation_id FROM maker_offers mo)
10179
+ GROUP BY oia.obligation_id
10180
+ )
10181
+ ${offerTakeabilityCTEs({
10182
+ sourceTable: "maker_offers",
10183
+ groupScope: "maker_groups",
10184
+ now
10185
+ })}
10186
+ SELECT
10187
+ mo.hash,
10188
+ mo.obligation_id,
10189
+ mo.group_maker,
10190
+ mo.assets,
10191
+ mo.obligation_units,
10192
+ mo.obligation_shares,
10193
+ mo.consumed,
10194
+ mo.tick,
10195
+ mo.maturity,
10196
+ mo.expiry,
10197
+ mo.start,
10198
+ mo.group_group AS "group",
10199
+ mo.buy,
10200
+ mo.chain_id,
10201
+ mo.loan_token,
10202
+ mo.callback_address,
10203
+ mo.callback_data,
10204
+ mo.receiver_if_maker_is_seller,
10205
+ mo.block_number,
10206
+ mo.session,
10207
+ COALESCE(ot.available, 0) AS available,
10208
+ COALESCE(ot.takeable, 0) AS takeable,
10209
+ c.collaterals
10210
+ FROM maker_offers mo
10211
+ LEFT JOIN offer_takeable ot
10212
+ ON ot.hash = mo.hash AND ot.obligation_id = mo.obligation_id
10213
+ LEFT JOIN collats c ON c.obligation_id = mo.obligation_id
10214
+ ORDER BY mo.hash ASC, mo.obligation_id ASC;
10215
+ `)).rows.map((row) => {
10216
+ const receiverIfMakerIsSeller = (row.receiver_if_maker_is_seller ?? row.group_maker).toLowerCase();
9115
10217
  return {
9116
10218
  hash: row.hash,
9117
- obligationId: row.obligationId,
9118
- maker: row.maker,
10219
+ obligationId: row.obligation_id,
10220
+ maker: row.group_maker,
9119
10221
  assets: BigInt(row.assets),
9120
- obligationUnits: BigInt(row.obligationUnits),
9121
- obligationShares: BigInt(row.obligationShares),
10222
+ obligationUnits: BigInt(row.obligation_units ?? 0),
10223
+ obligationShares: BigInt(row.obligation_shares ?? 0),
9122
10224
  tick: row.tick,
9123
- maturity: from$16(row.maturity),
10225
+ maturity: row.maturity,
9124
10226
  expiry: row.expiry,
9125
10227
  start: row.start,
9126
10228
  group: row.group,
9127
10229
  session: row.session,
9128
10230
  buy: row.buy,
9129
- chainId: row.chainId,
9130
- loanToken: row.loanToken,
10231
+ chainId: row.chain_id,
10232
+ loanToken: row.loan_token,
9131
10233
  collaterals: row.collaterals.map((c) => ({
9132
10234
  asset: c.asset,
9133
10235
  oracle: c.oracle,
9134
10236
  lltv: BigInt(c.lltv)
9135
10237
  })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
9136
10238
  callback: {
9137
- address: row.callbackAddress,
9138
- data: row.callbackData
10239
+ address: row.callback_address,
10240
+ data: row.callback_data
9139
10241
  },
9140
10242
  receiverIfMakerIsSeller,
9141
- consumed: BigInt(row.consumed),
10243
+ consumed: BigInt(row.consumed ?? 0),
9142
10244
  available: BigInt(String(row.available ?? "0").split(".")[0] ?? "0"),
9143
10245
  takeable: BigInt(String(row.takeable ?? "0").split(".")[0] ?? "0"),
9144
- blockNumber: row.blockNumber
10246
+ blockNumber: row.block_number
9145
10247
  };
9146
10248
  });
9147
10249
  return {
9148
10250
  rows,
9149
- nextCursor: rows.length === limit ? maker === void 0 ? rows[rows.length - 1].hash : formatOfferCursor(rows[rows.length - 1]) : null
10251
+ nextCursor: rows.length === limit ? formatOfferCursor(rows[rows.length - 1]) : null
9150
10252
  };
9151
10253
  }
10254
+ /**
10255
+ * Get offers with optional maker or obligation+side filter.
10256
+ * @param queryParameters - Raw query parameters from the API request.
10257
+ * @param db - The database client. {@link Database.Database}
10258
+ * @returns The offers response payload.
10259
+ */
9152
10260
  async function getOffers$1(queryParameters, db) {
9153
10261
  const logger = getLogger();
9154
10262
  const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
@@ -9187,9 +10295,12 @@ async function getOffers$1(queryParameters, db) {
9187
10295
  } catch (err) {
9188
10296
  logger.error({
9189
10297
  err,
9190
- msg: "Error get offers",
9191
- errorMessage: err instanceof Error ? err.message : String(err),
9192
- errorStack: err instanceof Error ? err.stack : void 0
10298
+ event: API_GET_OFFERS_FAILED,
10299
+ msg: "Failed to get offers",
10300
+ endpoint: "get_offers",
10301
+ maker: query.maker ?? null,
10302
+ obligation_id: query.obligation_id ?? null,
10303
+ side: query.side ?? null
9193
10304
  });
9194
10305
  return failure(err);
9195
10306
  }
@@ -9197,7 +10308,7 @@ async function getOffers$1(queryParameters, db) {
9197
10308
  function parseMakerCursor(cursor) {
9198
10309
  const [rawHash, rawObligationId, ...tail] = cursor.split(":");
9199
10310
  if (tail.length > 0) throw new Error("Invalid cursor format");
9200
- if (!rawHash || !rawObligationId || !HEX_32$1.test(rawHash) || !HEX_32$1.test(rawObligationId)) throw new Error("Invalid cursor format");
10311
+ if (!rawHash || !rawObligationId || !HEX_32$1.test(rawHash) || !HEX_20$1.test(rawObligationId)) throw new Error("Invalid cursor format");
9201
10312
  return {
9202
10313
  hash: rawHash.toLowerCase(),
9203
10314
  obligationId: rawObligationId.toLowerCase()
@@ -9207,6 +10318,7 @@ function formatOfferCursor(row) {
9207
10318
  return `${row.hash.toLowerCase()}:${row.obligationId.toLowerCase()}`;
9208
10319
  }
9209
10320
  const HEX_32$1 = /^0x[a-fA-F0-9]{64}$/;
10321
+ const HEX_20$1 = /^0x[a-fA-F0-9]{40}$/;
9210
10322
 
9211
10323
  //#endregion
9212
10324
  //#region src/api/Controllers/getUserPositions.ts
@@ -9234,9 +10346,12 @@ async function getUserPositions(queryParameters, db) {
9234
10346
  } catch (err) {
9235
10347
  logger.error({
9236
10348
  err,
9237
- msg: "Error get user positions",
9238
- errorMessage: err instanceof Error ? err.message : String(err),
9239
- errorStack: err instanceof Error ? err.stack : void 0
10349
+ event: API_GET_USER_POSITIONS_FAILED,
10350
+ msg: "Failed to get user positions",
10351
+ endpoint: "get_user_positions",
10352
+ user_address: query.user_address,
10353
+ has_cursor: query.cursor != null,
10354
+ limit: query.limit ?? null
9240
10355
  });
9241
10356
  return failure(err);
9242
10357
  }
@@ -9297,11 +10412,18 @@ async function validateOffers(body, gatekeeper) {
9297
10412
  cursor: null
9298
10413
  });
9299
10414
  } catch (err) {
10415
+ const span = trace.getActiveSpan();
10416
+ if (span) {
10417
+ span.recordException(err);
10418
+ span.setStatus({ code: SpanStatusCode.ERROR });
10419
+ }
9300
10420
  logger.error({
9301
10421
  err,
9302
- msg: "Error validating offers",
9303
- errorMessage: err instanceof Error ? err.message : String(err),
9304
- errorStack: err instanceof Error ? err.stack : void 0
10422
+ event: API_VALIDATE_OFFERS_FAILED,
10423
+ msg: "Failed to validate offers",
10424
+ endpoint: "validate_offers",
10425
+ chain_id: chainId,
10426
+ offers_count: parsedOffers.length
9305
10427
  });
9306
10428
  return failure(err);
9307
10429
  }
@@ -9319,6 +10441,7 @@ var Controllers_exports = /* @__PURE__ */ __exportAll({
9319
10441
  getHealthChains: () => getHealthChains,
9320
10442
  getHealthCollectors: () => getHealthCollectors,
9321
10443
  getIntegratorDocsHtml: () => getIntegratorDocsHtml,
10444
+ getMetrics: () => getMetrics,
9322
10445
  getObligation: () => getObligation,
9323
10446
  getObligations: () => getObligations$1,
9324
10447
  getOffers: () => getOffers$1,
@@ -9343,31 +10466,28 @@ function create$13(params) {
9343
10466
  return { serve: () => serve$1(params) };
9344
10467
  }
9345
10468
  /**
9346
- * Start a local router server.
9347
- * @example
9348
- * ```ts
9349
- * import { RouterApi } from "@morpho-dev/router";
9350
- * RouterApi.serve({ port: 8081 }); // local router API server running on http://localhost:8081
9351
- * ```
10469
+ * Create the router API Hono app with all routes and middleware configured.
10470
+ * @param parameters - API construction parameters.
10471
+ * @returns Configured Hono app instance.
9352
10472
  */
9353
- function serve$1(parameters) {
10473
+ function createApp(parameters) {
9354
10474
  const { db, gatekeeper, chainRegistry } = parameters;
9355
- const tracer = getTracer("router.api");
9356
10475
  const app = new Hono();
9357
- app.use("*", cors());
9358
- app.use("*", async (c, next) => {
9359
- const { req: { path, method } } = c;
9360
- const spanName = `${method} ${path}`;
9361
- return startActiveSpan(tracer, spanName, async (span) => {
9362
- const res = await next();
9363
- span.setAttribute("http.method", method);
9364
- span.setAttribute("http.target", path);
9365
- span.setAttribute("http.route", path);
9366
- span.setAttribute("http.status_code", c.res.status);
9367
- if (c.res.status >= 500) span.setStatus({ code: SpanStatusCode.ERROR });
9368
- return res;
9369
- });
10476
+ init(app, {
10477
+ component: "api",
10478
+ tracerName: "router.api",
10479
+ toFailurePayload: failure,
10480
+ adaptFailurePayload: ({ route, failure }) => route === "/metrics" ? {
10481
+ statusCode: failure.statusCode,
10482
+ body: "# Failed to render router metrics\n",
10483
+ contentType: "text/plain; version=0.0.4; charset=utf-8",
10484
+ headers: { "Cache-Control": "no-store" }
10485
+ } : void 0
9370
10486
  });
10487
+ app.use("*", cors({
10488
+ origin: "*",
10489
+ exposeHeaders: ["x-request-id"]
10490
+ }));
9371
10491
  app.get("/v1/offers", async (c) => {
9372
10492
  const { statusCode, body } = await getOffers$1(c.req.query(), db);
9373
10493
  return c.json(body, statusCode);
@@ -9392,14 +10512,9 @@ function serve$1(parameters) {
9392
10512
  return c.json(body, statusCode);
9393
10513
  });
9394
10514
  app.post("/v1/validate", async (c) => {
9395
- try {
9396
- const reqBody = await c.req.json();
9397
- const { statusCode, body } = await gatekeeper.validate(reqBody);
9398
- return c.json(body, statusCode);
9399
- } catch (err) {
9400
- const failure$2 = failure(err);
9401
- return c.json(failure$2.body, failure$2.statusCode);
9402
- }
10515
+ const reqBody = await c.req.json();
10516
+ const { statusCode, body } = await gatekeeper.validate(reqBody);
10517
+ return c.json(body, statusCode);
9403
10518
  });
9404
10519
  app.get("/v1/users/:userAddress/positions", async (c) => {
9405
10520
  const query = c.req.query();
@@ -9422,24 +10537,37 @@ function serve$1(parameters) {
9422
10537
  const { statusCode, body } = await getHealthChains(c.req.query(), db, void 0, chainRegistry);
9423
10538
  return c.json(body, statusCode);
9424
10539
  });
10540
+ app.get("/metrics", async (c) => {
10541
+ const body = await getMetrics(db, chainRegistry);
10542
+ return c.text(body, 200, {
10543
+ "Content-Type": "text/plain; version=0.0.4; charset=utf-8",
10544
+ "Cache-Control": "no-store"
10545
+ });
10546
+ });
9425
10547
  app.get("/v1/config/contracts", async (c) => {
9426
10548
  const { statusCode, body } = await getConfigContracts(c.req.query(), chainRegistry);
9427
10549
  return c.json(body, statusCode);
9428
10550
  });
9429
10551
  app.get("/v1/config/rules", async (c) => {
9430
- try {
9431
- const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
9432
- return c.json(body, statusCode);
9433
- } catch (err) {
9434
- const failure$1 = failure(err);
9435
- return c.json(failure$1.body, failure$1.statusCode);
9436
- }
10552
+ const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
10553
+ return c.json(body, statusCode);
9437
10554
  });
9438
10555
  app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
9439
10556
  app.get("/docs/api", async (c) => c.html(await getDocsHtml(), 200));
9440
10557
  app.get("/docs", async (c) => c.html(await getIntegratorDocsHtml(), 200));
10558
+ return app;
10559
+ }
10560
+ /**
10561
+ * Start a local router server.
10562
+ * @example
10563
+ * ```ts
10564
+ * import { RouterApi } from "@morpho-dev/router";
10565
+ * RouterApi.serve({ port: 8081 }); // local router API server running on http://localhost:8081
10566
+ * ```
10567
+ */
10568
+ function serve$1(parameters) {
9441
10569
  serve({
9442
- fetch: app.fetch,
10570
+ fetch: createApp(parameters).fetch,
9443
10571
  port: parameters.port
9444
10572
  });
9445
10573
  }
@@ -9467,6 +10595,7 @@ var RouterApi_exports = /* @__PURE__ */ __exportAll({
9467
10595
  UsersController: () => UsersController,
9468
10596
  ValidateController: () => ValidateController,
9469
10597
  create: () => create$13,
10598
+ createApp: () => createApp,
9470
10599
  from: () => from$1,
9471
10600
  parse: () => parse,
9472
10601
  safeParse: () => safeParse
@@ -9655,6 +10784,7 @@ var HttpGetApiFailedError = class extends BaseError {
9655
10784
  //#endregion
9656
10785
  //#region src/database/drizzle/index.ts
9657
10786
  var drizzle_exports = /* @__PURE__ */ __exportAll({
10787
+ CallbackTypes: () => CallbackTypes,
9658
10788
  PositionTypes: () => PositionTypes,
9659
10789
  StatusCode: () => StatusCode,
9660
10790
  TABLE_NAMES: () => TABLE_NAMES,
@@ -10002,13 +11132,19 @@ async function _getOffers(db, params) {
10002
11132
  ON oia.obligation_id = w.obligation_id
10003
11133
  JOIN ${obligations} obl
10004
11134
  ON obl.obligation_key = oia.obligation_key
10005
- ),
10006
- paged AS (
10007
- SELECT e.*
11135
+ )
11136
+ ${offerTakeabilityCTEs({
11137
+ sourceTable: "enriched",
11138
+ groupScope: "winners",
11139
+ now
11140
+ })}
11141
+ , paged AS (
11142
+ SELECT e.*, COALESCE(ot.available, 0) AS available, ot.takeable
10008
11143
  FROM enriched e
11144
+ JOIN offer_takeable ot ON ot.hash = e.hash AND ot.obligation_id = e.obligation_id
11145
+ WHERE ot.takeable > 0
10009
11146
  ${cursor != null ? sql`
10010
- WHERE
10011
- (e.tick_norm, e.block_norm, e.assets_norm, e.hash_norm)
11147
+ AND (e.tick_norm, e.block_norm, e.assets_norm, e.hash_norm)
10012
11148
  > (
10013
11149
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
10014
11150
  THEN ${cursor.tick}::integer ELSE -${cursor.tick}::integer END,
@@ -10018,224 +11154,38 @@ async function _getOffers(db, params) {
10018
11154
  )` : sql``}
10019
11155
  ORDER BY e.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
10020
11156
  LIMIT ${limit}
10021
- ),
10022
- -- Compute sum of offsets per position and obligation
10023
- position_offsets AS (
10024
- SELECT
10025
- chain_id,
10026
- "user",
10027
- contract,
10028
- obligation_id,
10029
- SUM(value::numeric) AS total_offset
10030
- FROM ${offsets}
10031
- GROUP BY chain_id, "user", contract, obligation_id
10032
- ),
10033
- -- Compute position_consumed: sum of consumed from all groups with lots on each position+obligation (converted to lot terms)
10034
- position_consumed AS (
10035
- SELECT
10036
- l.chain_id,
10037
- l.contract,
10038
- l."user",
10039
- l.obligation_id,
10040
- SUM(
10041
- CASE
10042
- WHEN wo.assets::numeric > 0
10043
- THEN COALESCE(g.consumed::numeric, 0) * (l.upper::numeric - l.lower::numeric) / wo.assets::numeric
10044
- ELSE 0
10045
- END
10046
- ) AS consumed
10047
- FROM ${lots} l
10048
- JOIN ${groups} g
10049
- ON g.chain_id = l.chain_id
10050
- AND LOWER(g.maker) = LOWER(l."user")
10051
- AND g."group" = l."group"
10052
- JOIN winners wo
10053
- ON wo.group_chain_id = g.chain_id
10054
- AND LOWER(wo.group_maker) = LOWER(g.maker)
10055
- AND wo.group_group = g."group"
10056
- GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
10057
- ),
10058
- -- Compute callback contributions with lot balance
10059
- callback_contributions AS (
10060
- SELECT
10061
- p.hash,
10062
- p.obligation_id,
10063
- p.assets,
10064
- p.tick,
10065
- p.obligation_units,
10066
- p.obligation_shares,
10067
- p.maturity,
10068
- p.expiry,
10069
- p.start,
10070
- p.group_group,
10071
- p.buy,
10072
- p.callback_address,
10073
- p.callback_data,
10074
- p.receiver_if_maker_is_seller,
10075
- p.block_number,
10076
- p.group_chain_id,
10077
- p.group_maker,
10078
- p.consumed,
10079
- p.chain_id,
10080
- p.loan_token,
10081
- p.session,
10082
- c.id AS callback_id,
10083
- c.position_chain_id,
10084
- c.position_contract,
10085
- c.position_user,
10086
- c.amount AS callback_amount,
10087
- pos.balance AS position_balance,
10088
- pos.asset AS position_asset,
10089
- l.lower AS lot_lower,
10090
- l.upper AS lot_upper,
10091
- -- Compute lot_balance: min(position_balance + offset + position_consumed - lot.lower, lot.size - lot_consumed)
10092
- -- lot_consumed is converted from loan token to lot terms: consumed * lot_size / assets
10093
- GREATEST(0, LEAST(
10094
- COALESCE(pos.balance::numeric, 0) + COALESCE(pos_offsets.total_offset, 0) + COALESCE(pc.consumed, 0) - COALESCE(l.lower::numeric, 0),
10095
- (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) -
10096
- CASE
10097
- WHEN p.assets::numeric > 0
10098
- THEN COALESCE(p.consumed::numeric, 0) * (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) / p.assets::numeric
10099
- ELSE 0
10100
- END
10101
- )) AS lot_balance
10102
- FROM paged p
10103
- LEFT JOIN ${offersCallbacks} oc
10104
- ON oc.offer_hash = p.hash
10105
- AND oc.obligation_id = p.obligation_id
10106
- LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
10107
- LEFT JOIN ${lots} l
10108
- ON l.chain_id = c.position_chain_id
10109
- AND LOWER(l.contract) = LOWER(c.position_contract)
10110
- AND LOWER(l."user") = LOWER(c.position_user)
10111
- AND l."group" = p.group_group
10112
- AND l.obligation_id = p.obligation_id
10113
- LEFT JOIN ${positions} pos
10114
- ON pos.chain_id = c.position_chain_id
10115
- AND LOWER(pos.contract) = LOWER(c.position_contract)
10116
- AND LOWER(pos."user") = LOWER(c.position_user)
10117
- LEFT JOIN position_offsets pos_offsets
10118
- ON pos_offsets.chain_id = c.position_chain_id
10119
- AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
10120
- AND LOWER(pos_offsets."user") = LOWER(c.position_user)
10121
- AND pos_offsets.obligation_id = p.obligation_id
10122
- LEFT JOIN position_consumed pc
10123
- ON pc.chain_id = c.position_chain_id
10124
- AND LOWER(pc.contract) = LOWER(c.position_contract)
10125
- AND LOWER(pc."user") = LOWER(c.position_user)
10126
- AND pc.obligation_id = p.obligation_id
10127
- ),
10128
- -- Compute contribution per callback in loan terms (loan token only — collateral positions are not indexed)
10129
- callback_loan_contribution AS (
10130
- SELECT
10131
- cc.*,
10132
- CASE
10133
- WHEN cc.lot_lower IS NULL THEN 0
10134
- ELSE LEAST(cc.lot_balance, COALESCE(cc.callback_amount::numeric, cc.lot_balance))
10135
- END AS contribution_in_loan
10136
- FROM callback_contributions cc
10137
- ),
10138
- -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
10139
- offer_contributions AS (
10140
- SELECT
10141
- hash,
10142
- obligation_id,
10143
- assets,
10144
- tick,
10145
- obligation_units,
10146
- obligation_shares,
10147
- maturity,
10148
- expiry,
10149
- start,
10150
- group_group,
10151
- buy,
10152
- callback_address,
10153
- callback_data,
10154
- receiver_if_maker_is_seller,
10155
- block_number,
10156
- group_chain_id,
10157
- group_maker,
10158
- consumed,
10159
- chain_id,
10160
- loan_token,
10161
- session,
10162
- SUM(contribution_in_loan) AS total_available
10163
- FROM (
10164
- -- Take max contribution per position using DISTINCT ON (idiomatic PostgreSQL)
10165
- SELECT DISTINCT ON (clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user)
10166
- clc.*
10167
- FROM callback_loan_contribution clc
10168
- WHERE clc.callback_id IS NOT NULL
10169
- ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
10170
- ) deduped
10171
- GROUP BY hash, obligation_id, assets, tick, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
10172
- callback_address, callback_data, block_number, group_chain_id, group_maker,
10173
- consumed, chain_id, loan_token, session, receiver_if_maker_is_seller
10174
- UNION ALL
10175
- -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
10176
- SELECT
10177
- p.hash, p.obligation_id, p.assets, p.tick,
10178
- p.obligation_units, p.obligation_shares,
10179
- p.maturity, p.expiry, p.start, p.group_group,
10180
- p.buy, p.callback_address, p.callback_data,
10181
- p.receiver_if_maker_is_seller,
10182
- p.block_number, p.group_chain_id, p.group_maker,
10183
- p.consumed, p.chain_id, p.loan_token, p.session,
10184
- 0 AS total_available
10185
- FROM paged p
10186
- WHERE p.buy = false
10187
- AND NOT EXISTS (
10188
- SELECT 1 FROM ${offersCallbacks} oc2
10189
- WHERE oc2.offer_hash = p.hash
10190
- AND oc2.obligation_id = p.obligation_id
10191
- )
10192
11157
  )
10193
- -- Final SELECT with inline takeable computation
10194
11158
  SELECT
10195
- oc.hash,
10196
- oc.obligation_id,
10197
- oc.group_maker,
10198
- oc.assets,
10199
- oc.obligation_units,
10200
- oc.obligation_shares,
10201
- oc.consumed,
10202
- oc.tick,
10203
- oc.maturity,
10204
- oc.expiry,
10205
- oc.start,
10206
- oc.group_group AS "group",
10207
- oc.buy,
10208
- oc.chain_id,
10209
- oc.loan_token,
10210
- oc.callback_address,
10211
- oc.callback_data,
10212
- oc.receiver_if_maker_is_seller,
10213
- oc.block_number,
10214
- oc.session,
10215
- COALESCE(oc.total_available, 0) AS available,
10216
- -- takeable: sell offers use assets - consumed directly (collateral positions not indexed yet)
10217
- CASE WHEN oc.buy = false
10218
- THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
10219
- ELSE GREATEST(0, LEAST(
10220
- oc.assets::numeric - oc.consumed::numeric,
10221
- COALESCE(oc.total_available, 0)
10222
- ))
10223
- END AS takeable,
11159
+ p.hash,
11160
+ p.obligation_id,
11161
+ p.group_maker,
11162
+ p.assets,
11163
+ p.obligation_units,
11164
+ p.obligation_shares,
11165
+ p.consumed,
11166
+ p.tick,
11167
+ p.maturity,
11168
+ p.expiry,
11169
+ p.start,
11170
+ p.group_group AS "group",
11171
+ p.buy,
11172
+ p.chain_id,
11173
+ p.loan_token,
11174
+ p.callback_address,
11175
+ p.callback_data,
11176
+ p.receiver_if_maker_is_seller,
11177
+ p.block_number,
11178
+ p.session,
11179
+ p.available,
11180
+ p.takeable,
10224
11181
  c.collaterals
10225
- FROM offer_contributions oc
10226
- LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
10227
- WHERE CASE WHEN oc.buy = false
10228
- THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
10229
- ELSE GREATEST(0, LEAST(
10230
- oc.assets::numeric - oc.consumed::numeric,
10231
- COALESCE(oc.total_available, 0)
10232
- ))
10233
- END > 0
11182
+ FROM paged p
11183
+ LEFT JOIN collats c ON c.obligation_id = p.obligation_id
10234
11184
  ORDER BY
10235
- oc.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
10236
- oc.block_number ASC,
10237
- oc.assets DESC,
10238
- oc.hash ASC;
11185
+ p.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
11186
+ p.block_number ASC,
11187
+ p.assets DESC,
11188
+ p.hash ASC;
10239
11189
  `);
10240
11190
  return {
10241
11191
  rows: raw.rows.map((row) => {
@@ -10351,7 +11301,7 @@ function create$10(db) {
10351
11301
  const callbacksRows = [];
10352
11302
  const offersCallbacksRows = [];
10353
11303
  const callbackId = (input) => {
10354
- const preimage = `0x${input.chainId}${input.contract}${input.user}${input.amount.toString()}`.toLowerCase();
11304
+ const preimage = `0x${input.chainId}${input.contract}${input.user}`.toLowerCase();
10355
11305
  const id = idCache.get(preimage) ?? keccak256(preimage);
10356
11306
  idCache.set(preimage, id);
10357
11307
  return id;
@@ -10364,8 +11314,8 @@ function create$10(db) {
10364
11314
  chainId: callback.chainId,
10365
11315
  contract: callback.contract.toLowerCase(),
10366
11316
  user: callback.user.toLowerCase(),
10367
- amount: callback.amount,
10368
- positionTypeId: callback.positionTypeId
11317
+ positionTypeId: callback.positionTypeId,
11318
+ type: callback.type
10369
11319
  };
10370
11320
  const id = callbackId(normalized);
10371
11321
  offersCallbacksRows.push({
@@ -10381,7 +11331,7 @@ function create$10(db) {
10381
11331
  positionContract: normalized.contract,
10382
11332
  positionUser: normalized.user,
10383
11333
  positionTypeId: normalized.positionTypeId,
10384
- amount: normalized.amount.toString()
11334
+ type: normalized.type
10385
11335
  });
10386
11336
  }
10387
11337
  }
@@ -10405,10 +11355,10 @@ function create$9(db) {
10405
11355
  return {
10406
11356
  create: async (events) => {
10407
11357
  if (events.length === 0) return;
10408
- const groups$2 = /* @__PURE__ */ new Map();
11358
+ const groups$3 = /* @__PURE__ */ new Map();
10409
11359
  for (const event of events) {
10410
11360
  const groupId = `${event.chainId}-${event.maker}-${event.group}`.toLowerCase();
10411
- groups$2.set(groupId, {
11361
+ groups$3.set(groupId, {
10412
11362
  chainId: event.chainId,
10413
11363
  maker: event.maker,
10414
11364
  group: event.group,
@@ -10416,7 +11366,7 @@ function create$9(db) {
10416
11366
  });
10417
11367
  }
10418
11368
  await db.transaction(async (dbTx) => {
10419
- const groupsRows = Array.from(groups$2.values()).map((group) => ({
11369
+ const groupsRows = Array.from(groups$3.values()).map((group) => ({
10420
11370
  chainId: group.chainId,
10421
11371
  maker: group.maker.toLowerCase(),
10422
11372
  group: group.group.toLowerCase(),
@@ -10444,23 +11394,42 @@ function create$9(db) {
10444
11394
 
10445
11395
  //#endregion
10446
11396
  //#region src/database/domains/Groups.ts
11397
+ /** Build composite key for a group. */
11398
+ function compositeKey(g) {
11399
+ return `${g.chainId}-${g.maker.toLowerCase()}-${g.group.toLowerCase()}`;
11400
+ }
10447
11401
  /**
10448
11402
  * Create a groups domain instance.
10449
11403
  * @param db - Database core instance.
10450
11404
  * @returns Groups domain. {@link GroupsDomain}
10451
11405
  */
10452
11406
  function create$8(db) {
10453
- return { create: async (groups$1) => {
10454
- if (groups$1.length === 0) return;
10455
- const rows = groups$1.map((group) => ({
10456
- chainId: group.chainId,
10457
- maker: group.maker.toLowerCase(),
10458
- group: group.group.toLowerCase(),
10459
- consumed: (group.consumed ?? 0n).toString(),
10460
- blockNumber: group.blockNumber
10461
- }));
10462
- for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE$1)) await db.insert(groups).values(batch).onConflictDoNothing();
10463
- } };
11407
+ return {
11408
+ create: async (groups$1) => {
11409
+ if (groups$1.length === 0) return;
11410
+ const rows = groups$1.map((group) => ({
11411
+ chainId: group.chainId,
11412
+ maker: group.maker.toLowerCase(),
11413
+ group: group.group.toLowerCase(),
11414
+ consumed: (group.consumed ?? 0n).toString(),
11415
+ blockNumber: group.blockNumber
11416
+ }));
11417
+ for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE$1)) await db.insert(groups).values(batch).onConflictDoNothing();
11418
+ },
11419
+ exists: async (groups$2) => {
11420
+ if (groups$2.length === 0) return [];
11421
+ const tuples = groups$2.map((g) => sql`(${sql.raw(g.chainId.toString())}::bigint, ${g.maker.toLowerCase()}, ${g.group.toLowerCase()})`);
11422
+ return (await db.select({
11423
+ chainId: groups.chainId,
11424
+ maker: groups.maker,
11425
+ group: groups.group
11426
+ }).from(groups).where(sql`(${groups.chainId}, ${groups.maker}, ${groups.group}) IN (${sql.join(tuples, sql`, `)})`)).map((row) => ({
11427
+ chainId: row.chainId,
11428
+ maker: row.maker,
11429
+ group: row.group
11430
+ }));
11431
+ }
11432
+ };
10464
11433
  }
10465
11434
 
10466
11435
  //#endregion
@@ -10812,13 +11781,15 @@ const create$3 = (db) => {
10812
11781
  group_chain_id,
10813
11782
  group_maker,
10814
11783
  "group_group",
11784
+ obligation_id,
10815
11785
  MAX(assets::numeric) AS assets
10816
11786
  FROM ${offers}
10817
- GROUP BY group_chain_id, group_maker, "group_group"
11787
+ GROUP BY group_chain_id, group_maker, "group_group", obligation_id
10818
11788
  ) offer_agg
10819
11789
  ON offer_agg.group_chain_id = g.chain_id
10820
11790
  AND LOWER(offer_agg.group_maker) = LOWER(g.maker)
10821
11791
  AND offer_agg."group_group" = g."group"
11792
+ AND offer_agg.obligation_id = l.obligation_id
10822
11793
  WHERE LOWER(l."user") = LOWER(${user})
10823
11794
  GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
10824
11795
  ),
@@ -11189,9 +12160,10 @@ function create$1(db) {
11189
12160
  };
11190
12161
  }
11191
12162
  const HEX_32 = /^0x[a-fA-F0-9]{64}$/;
12163
+ const HEX_20 = /^0x[a-fA-F0-9]{40}$/;
11192
12164
  function parseCursor(cursor) {
11193
12165
  const [offerHash, obligationId] = cursor.split(":");
11194
- if (!offerHash || !obligationId || !HEX_32.test(offerHash) || !HEX_32.test(obligationId)) throw new Error("Invalid cursor format");
12166
+ if (!offerHash || !obligationId || !HEX_32.test(offerHash) || !HEX_20.test(obligationId)) throw new Error("Invalid cursor format");
11195
12167
  return {
11196
12168
  offerHash: offerHash.toLowerCase(),
11197
12169
  obligationId: obligationId.toLowerCase()
@@ -11527,6 +12499,9 @@ async function postMigrate(driver) {
11527
12499
  WHERE o.group_chain_id = t.group_chain_id
11528
12500
  AND o.group_maker = t.group_maker
11529
12501
  AND o.group_group = t."group"
12502
+ AND g.chain_id = t.group_chain_id
12503
+ AND g.maker = t.group_maker
12504
+ AND g."group" = t."group"
11530
12505
  AND o.assets <= g.consumed;
11531
12506
  RETURN NULL;
11532
12507
  END;
@@ -11603,62 +12578,10 @@ async function postMigrate(driver) {
11603
12578
  EXECUTE FUNCTION cleanup_orphan_groups();
11604
12579
  `);
11605
12580
  await driver.execute(`
11606
- CREATE OR REPLACE FUNCTION cleanup_orphan_obligations_and_oracles()
11607
- RETURNS TRIGGER AS $$
11608
- DECLARE
11609
- orphan_obligation_ids TEXT[];
11610
- orphan_obligation_keys TEXT[];
11611
- BEGIN
11612
- -- 1. Find orphan obligation IDs
11613
- SELECT ARRAY_AGG(DISTINCT obligation_id) INTO orphan_obligation_ids
11614
- FROM deleted_rows d
11615
- WHERE NOT EXISTS (
11616
- SELECT 1 FROM "${VERSION}"."offers" ov
11617
- WHERE ov.obligation_id = d.obligation_id
11618
- )
11619
- AND NOT EXISTS (
11620
- SELECT 1 FROM "${VERSION}"."positions" p
11621
- JOIN "${VERSION}"."position_types" pt ON pt.id = p.position_type_id
11622
- WHERE p."contract" = d.obligation_id
11623
- AND pt.type IN ('debtOf', 'collateralOf')
11624
- );
11625
-
11626
- -- 2. If no orphan obligation IDs, exit early
11627
- IF orphan_obligation_ids IS NULL OR array_length(orphan_obligation_ids, 1) IS NULL THEN
11628
- RETURN NULL;
11629
- END IF;
11630
-
11631
- -- 3. Delete orphan obligation id keys
11632
- DELETE FROM "${VERSION}"."obligation_id_keys" oia
11633
- WHERE oia.obligation_id = ANY(orphan_obligation_ids);
11634
-
11635
- -- 4. Find canonical obligation keys with no remaining obligation id keys
11636
- SELECT ARRAY_AGG(ob.obligation_key) INTO orphan_obligation_keys
11637
- FROM "${VERSION}"."obligations" ob
11638
- WHERE NOT EXISTS (
11639
- SELECT 1 FROM "${VERSION}"."obligation_id_keys" oia
11640
- WHERE oia.obligation_key = ob.obligation_key
11641
- );
11642
-
11643
- -- 5. If no orphan canonical obligations, exit early
11644
- IF orphan_obligation_keys IS NULL OR array_length(orphan_obligation_keys, 1) IS NULL THEN
11645
- RETURN NULL;
11646
- END IF;
11647
-
11648
- -- 6. Delete orphan canonical obligations (cascades to collaterals)
11649
- DELETE FROM "${VERSION}"."obligations" ob
11650
- WHERE ob.obligation_key = ANY(orphan_obligation_keys);
11651
-
11652
- RETURN NULL;
11653
- END;
11654
- $$ LANGUAGE plpgsql;
12581
+ DROP TRIGGER IF EXISTS trg_cleanup_orphan_obligations_and_oracles ON "${VERSION}"."offers";
11655
12582
  `);
11656
12583
  await driver.execute(`
11657
- CREATE OR REPLACE TRIGGER trg_cleanup_orphan_obligations_and_oracles
11658
- AFTER DELETE ON "${VERSION}"."offers"
11659
- REFERENCING OLD TABLE AS deleted_rows
11660
- FOR EACH STATEMENT
11661
- EXECUTE FUNCTION cleanup_orphan_obligations_and_oracles();
12584
+ DROP FUNCTION IF EXISTS cleanup_orphan_obligations_and_oracles();
11662
12585
  `);
11663
12586
  await driver.execute(`
11664
12587
  CREATE OR REPLACE FUNCTION create_offset_on_lot_delete()
@@ -11736,6 +12659,21 @@ async function postMigrate(driver) {
11736
12659
  });
11737
12660
  }
11738
12661
 
12662
+ //#endregion
12663
+ //#region src/observability/TraceHeaders.ts
12664
+ /**
12665
+ * Inject active trace context headers into outgoing HTTP headers.
12666
+ * @param headers - Existing headers for the outgoing request.
12667
+ * @returns Headers enriched with active trace context.
12668
+ */
12669
+ function withActiveTraceHeaders(headers) {
12670
+ const enrichedHeaders = new Headers(headers);
12671
+ const carrier = {};
12672
+ propagation.inject(context.active(), carrier);
12673
+ for (const [key, value] of Object.entries(carrier)) enrichedHeaders.set(key, value);
12674
+ return enrichedHeaders;
12675
+ }
12676
+
11739
12677
  //#endregion
11740
12678
  //#region src/gatekeeper/Client.ts
11741
12679
  var Client_exports = /* @__PURE__ */ __exportAll({ createHttpClient: () => createHttpClient });
@@ -11756,7 +12694,7 @@ function createHttpClient(config) {
11756
12694
  try {
11757
12695
  return await fetchFn(`${baseUrl}${path}`, {
11758
12696
  ...init,
11759
- headers: mergeHeaders(baseHeaders, init.headers),
12697
+ headers: withActiveTraceHeaders(mergeHeaders(baseHeaders, init.headers)),
11760
12698
  signal: controller.signal
11761
12699
  });
11762
12700
  } finally {
@@ -11953,8 +12891,12 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
11953
12891
  amountMutualExclusivity: () => amountMutualExclusivity,
11954
12892
  callback: () => callback,
11955
12893
  collateralToken: () => collateralToken,
12894
+ groupConsistency: () => groupConsistency,
12895
+ groupImmutability: () => groupImmutability,
11956
12896
  loanToken: () => loanToken,
11957
12897
  maturity: () => maturity,
12898
+ maxCollaterals: () => maxCollaterals,
12899
+ minDuration: () => minDuration,
11958
12900
  oracle: () => oracle,
11959
12901
  sameMaker: () => sameMaker
11960
12902
  });
@@ -12021,14 +12963,98 @@ const sameMaker = () => batch("mixed_maker", "Validates that all offers in a bat
12021
12963
  * At most one of (assets, obligationUnits, obligationShares) can be non-zero.
12022
12964
  * Matches contract requirement: `atMostOneNonZero(offer.assets, offer.obligationUnits, offer.obligationShares)`.
12023
12965
  */
12966
+ /**
12967
+ * A validation rule that checks if the offer duration (expiry - start) meets a minimum threshold.
12968
+ * @param minSeconds - Minimum required duration in seconds.
12969
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
12970
+ */
12971
+ const minDuration = ({ minSeconds }) => single("min_duration", `Validates that offer duration (expiry - start) is at least ${minSeconds}s`, (offer) => {
12972
+ const duration = offer.expiry - offer.start;
12973
+ if (duration < minSeconds) return { message: `Duration ${duration}s is below minimum ${minSeconds}s` };
12974
+ });
12975
+ /**
12976
+ * A validation rule that checks if an offer exceeds the maximum number of collaterals.
12977
+ * The contract enforces this limit; this rule rejects early to avoid on-chain reverts.
12978
+ * @param max - Maximum allowed collaterals per offer.
12979
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
12980
+ */
12981
+ const maxCollaterals = ({ max }) => single("max_collaterals", `Validates that an offer has at most ${max} collaterals`, (offer) => {
12982
+ if (offer.collaterals.length > max) return { message: `Offer has ${offer.collaterals.length} collaterals, exceeding the maximum of ${max}` };
12983
+ });
12024
12984
  const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Validates that at most one of (assets, obligationUnits, obligationShares) is non-zero", (offer) => {
12025
12985
  if (!atMostOneNonZero(offer.assets, offer.obligationUnits, offer.obligationShares)) return { message: "Inconsistent offer input: at most one of (assets, obligationUnits, obligationShares) must be non-zero" };
12026
12986
  });
12987
+ /**
12988
+ * A batch validation rule that ensures all offers within the same group are consistent.
12989
+ * All offers sharing the same group must have the same loan token, assets amount, and side (buy/sell).
12990
+ */
12991
+ const groupConsistency = () => batch("group_consistency", "Validates that all offers in a group have the same loan token, assets amount, and side", (offers) => {
12992
+ const issues = /* @__PURE__ */ new Map();
12993
+ if (offers.length === 0) return issues;
12994
+ const groupMap = /* @__PURE__ */ new Map();
12995
+ for (let i = 0; i < offers.length; i++) {
12996
+ const key = offers[i].group.toLowerCase();
12997
+ const indices = groupMap.get(key);
12998
+ if (indices) indices.push(i);
12999
+ else groupMap.set(key, [i]);
13000
+ }
13001
+ for (const indices of groupMap.values()) {
13002
+ if (indices.length <= 1) continue;
13003
+ const reference = offers[indices[0]];
13004
+ const refLoanToken = reference.loanToken.toLowerCase();
13005
+ const refAssets = reference.assets;
13006
+ const refBuy = reference.buy;
13007
+ for (let j = 1; j < indices.length; j++) {
13008
+ const idx = indices[j];
13009
+ const offer = offers[idx];
13010
+ if (offer.loanToken.toLowerCase() !== refLoanToken) issues.set(idx, { message: `All offers in a group must have the same loan token. Expected ${reference.loanToken}, got ${offer.loanToken}` });
13011
+ else if (offer.assets !== refAssets) issues.set(idx, { message: `All offers in a group must have the same assets amount. Expected ${refAssets}, got ${offer.assets}` });
13012
+ else if (offer.buy !== refBuy) issues.set(idx, { message: `All offers in a group must be on the same side. Expected ${refBuy ? "buy" : "sell"}, got ${offer.buy ? "buy" : "sell"}` });
13013
+ }
13014
+ }
13015
+ return issues;
13016
+ });
13017
+ /**
13018
+ * A batch validation rule that prevents adding offers to groups that already exist in the database.
13019
+ * Groups are immutable after creation — new offers cannot be added to an existing group.
13020
+ */
13021
+ const groupImmutability = ({ db, chainId }) => batch("group_immutability", "Validates that offers do not target groups that already exist", async (offers) => {
13022
+ const issues = /* @__PURE__ */ new Map();
13023
+ if (offers.length === 0) return issues;
13024
+ const uniqueGroups = /* @__PURE__ */ new Map();
13025
+ for (const offer of offers) {
13026
+ const key = compositeKey({
13027
+ chainId,
13028
+ maker: offer.maker,
13029
+ group: offer.group
13030
+ });
13031
+ if (!uniqueGroups.has(key)) uniqueGroups.set(key, {
13032
+ chainId,
13033
+ maker: offer.maker,
13034
+ group: offer.group
13035
+ });
13036
+ }
13037
+ const existingKeys = await db.groups.exists(Array.from(uniqueGroups.values()));
13038
+ if (existingKeys.length === 0) return issues;
13039
+ const existingSet = new Set(existingKeys.map((k) => compositeKey(k)));
13040
+ for (let i = 0; i < offers.length; i++) {
13041
+ const offer = offers[i];
13042
+ const key = compositeKey({
13043
+ chainId,
13044
+ maker: offer.maker,
13045
+ group: offer.group
13046
+ });
13047
+ if (existingSet.has(key)) issues.set(i, { message: `Cannot add offers to existing group ${offer.group}` });
13048
+ }
13049
+ return issues;
13050
+ });
12027
13051
 
12028
13052
  //#endregion
12029
13053
  //#region src/gatekeeper/morphoRules.ts
12030
13054
  const morphoRules = (parameters) => {
12031
- const { chains, chainId } = parameters;
13055
+ const { chains, chainId, db } = parameters;
13056
+ const requestChain = chains.find((c) => c.id === chainId);
13057
+ const config = requestChain ? configs[requestChain.name] : void 0;
12032
13058
  const assetsByChainId = {};
12033
13059
  const collateralAssetsByChainId = {};
12034
13060
  const oraclesByChainId = {};
@@ -12037,9 +13063,11 @@ const morphoRules = (parameters) => {
12037
13063
  collateralAssetsByChainId[chain.id] = collateralAssets[chain.id.toString()] ?? [];
12038
13064
  oraclesByChainId[chain.id] = oracles[chain.id.toString()] ?? [];
12039
13065
  }
12040
- return [
13066
+ const rules = [
12041
13067
  sameMaker(),
12042
13068
  amountMutualExclusivity(),
13069
+ groupConsistency(),
13070
+ ...config?.minDuration != null ? [minDuration({ minSeconds: config.minDuration })] : [],
12043
13071
  maturity({ maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek] }),
12044
13072
  callback({
12045
13073
  callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
@@ -12049,6 +13077,7 @@ const morphoRules = (parameters) => {
12049
13077
  assetsByChainId,
12050
13078
  chainId
12051
13079
  }),
13080
+ maxCollaterals({ max: 128 }),
12052
13081
  collateralToken({
12053
13082
  collateralAssetsByChainId,
12054
13083
  chainId
@@ -12058,6 +13087,11 @@ const morphoRules = (parameters) => {
12058
13087
  chainId
12059
13088
  })
12060
13089
  ];
13090
+ if (db) rules.push(groupImmutability({
13091
+ db,
13092
+ chainId
13093
+ }));
13094
+ return rules;
12061
13095
  };
12062
13096
 
12063
13097
  //#endregion