@morpho-dev/router 0.2.1 → 0.3.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.
package/dist/cli.js CHANGED
@@ -9,17 +9,15 @@ import { AWSXRayPropagator } from "@opentelemetry/propagator-aws-xray";
9
9
  import { resourceFromAttributes } from "@opentelemetry/resources";
10
10
  import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
11
11
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
12
- import { createPublicClient, createTestClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, parseAbi, parseEventLogs, publicActions, stringify, toHex, walletActions, zeroAddress } from "viem";
12
+ import { spawn } from "node:child_process";
13
+ import { bytesToHex, createPublicClient, createTestClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashMessage, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, walletActions, zeroAddress } from "viem";
14
+ import { privateKeyToAccount } from "viem/accounts";
13
15
  import { anvil, base, mainnet } from "viem/chains";
14
16
  import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
15
- import path from "node:path";
16
- import { fileURLToPath } from "node:url";
17
- import { spawn } from "node:child_process";
18
- import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
17
+ import { readFile } from "node:fs/promises";
19
18
  import * as z$1 from "zod";
20
19
  import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
21
- import { ungzip } from "pako";
22
- import { readFile } from "node:fs/promises";
20
+ import { gzip, ungzip } from "pako";
23
21
  import { serve } from "@hono/node-server";
24
22
  import { Hono } from "hono";
25
23
  import { cors } from "hono/cors";
@@ -29,6 +27,8 @@ import "reflect-metadata";
29
27
  import { generateDocument } from "openapi-metadata";
30
28
  import { ApiBody, ApiOperation, ApiProperty, ApiQuery, ApiResponse, ApiTags } from "openapi-metadata/decorators";
31
29
  import { Base64 } from "js-base64";
30
+ import path from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
32
  import dotenv from "dotenv";
33
33
  import { PGlite } from "@electric-sql/pglite";
34
34
  import { drizzle } from "drizzle-orm/node-postgres";
@@ -40,15 +40,7 @@ import { and, asc, desc, eq, gt, gte, inArray, lte, ne, sql } from "drizzle-orm"
40
40
  import { bigint, boolean, foreignKey, index, integer, numeric, pgSchema, primaryKey, serial, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
41
41
 
42
42
  //#region rolldown:runtime
43
- var __create = Object.create;
44
43
  var __defProp = Object.defineProperty;
45
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
46
- var __getOwnPropNames = Object.getOwnPropertyNames;
47
- var __getProtoOf = Object.getPrototypeOf;
48
- var __hasOwnProp = Object.prototype.hasOwnProperty;
49
- var __esm = (fn, res) => function() {
50
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
51
- };
52
44
  var __export = (all) => {
53
45
  let target = {};
54
46
  for (var name$1 in all) __defProp(target, name$1, {
@@ -57,21 +49,6 @@ var __export = (all) => {
57
49
  });
58
50
  return target;
59
51
  };
60
- var __copyProps = (to, from$15, except, desc$1) => {
61
- if (from$15 && typeof from$15 === "object" || typeof from$15 === "function") for (var keys = __getOwnPropNames(from$15), i = 0, n = keys.length, key; i < n; i++) {
62
- key = keys[i];
63
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
64
- get: ((k) => from$15[k]).bind(null, key),
65
- enumerable: !(desc$1 = __getOwnPropDesc(from$15, key)) || desc$1.enumerable
66
- });
67
- }
68
- return to;
69
- };
70
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
71
- value: mod,
72
- enumerable: true
73
- }) : target, mod));
74
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
75
52
 
76
53
  //#endregion
77
54
  //#region src/tracer/Tracer.ts
@@ -164,7 +141,7 @@ function startActiveSpan(tracer, name$1, fn) {
164
141
  //#endregion
165
142
  //#region package.json
166
143
  var name = "@morpho-dev/router";
167
- var version = "0.2.1";
144
+ var version = "0.3.0";
168
145
  var description = "Router package for Morpho protocol";
169
146
 
170
147
  //#endregion
@@ -391,22 +368,22 @@ const DEFAULT_BATCH_SIZE$1 = 2500;
391
368
  const MAX_BLOCK_WINDOW = 1e4;
392
369
  const DEFAULT_BLOCK_WINDOW = 8e3;
393
370
  async function* streamLogs(parameters) {
394
- const { client, contractAddress, event, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$1, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
371
+ const { client, contractAddress, event, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$1, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
395
372
  if (maxBatchSize > MAX_BATCH_SIZE) throw new InvalidBatchSizeError(maxBatchSize);
396
373
  if (blockWindow > MAX_BLOCK_WINDOW) throw new InvalidBlockWindowError(blockWindow);
397
- if (order$1 === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
374
+ if (order === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
398
375
  const latestBlock = (await getBlock(client, {
399
376
  blockTag: "latest",
400
377
  includeTransactions: false
401
378
  })).number;
402
379
  let toBlock = 0n;
403
- if (order$1 === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
404
- if (order$1 === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
380
+ if (order === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
381
+ if (order === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
405
382
  let fromBlock = 0n;
406
- if (order$1 === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
407
- if (order$1 === "desc") fromBlock = max(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
408
- if (order$1 === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
409
- if (order$1 === "desc") fromBlock = max(fromBlock, toBlock - BigInt(blockWindow));
383
+ if (order === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
384
+ if (order === "desc") fromBlock = max(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
385
+ if (order === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
386
+ if (order === "desc") fromBlock = max(fromBlock, toBlock - BigInt(blockWindow));
410
387
  if (fromBlock > toBlock) throw new InvalidBlockRangeError(fromBlock, toBlock);
411
388
  let streaming = true;
412
389
  while (streaming) {
@@ -416,29 +393,29 @@ async function* streamLogs(parameters) {
416
393
  fromBlock,
417
394
  toBlock
418
395
  });
419
- streaming = order$1 === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
396
+ streaming = order === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
420
397
  if (logs.length === 0 && !streaming) break;
421
398
  if (logs.length === 0 && streaming) yield {
422
399
  logs: [],
423
- blockNumber: order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
400
+ blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
424
401
  };
425
402
  logs.sort((a, b) => {
426
- if (a.blockNumber !== b.blockNumber) return order$1 === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
427
- if (a.transactionIndex !== b.transactionIndex) return order$1 === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
428
- return order$1 === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
403
+ if (a.blockNumber !== b.blockNumber) return order === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
404
+ if (a.transactionIndex !== b.transactionIndex) return order === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
405
+ return order === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
429
406
  });
430
407
  for (const logBatch of batch$1(logs, maxBatchSize)) yield {
431
408
  logs: logBatch,
432
- blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
409
+ blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order === "asc" ? Number(toBlock) : Number(fromBlock)
433
410
  };
434
- if (order$1 === "asc") {
411
+ if (order === "asc") {
435
412
  const upperBound = BigInt(blockNumberLte || latestBlock);
436
413
  const nextFromBlock = min(BigInt(toBlock) + 1n, upperBound);
437
414
  const nextToBlock = min(toBlock + BigInt(blockWindow) + 1n, upperBound);
438
415
  fromBlock = nextFromBlock;
439
416
  toBlock = nextToBlock;
440
417
  }
441
- if (order$1 === "desc") {
418
+ if (order === "desc") {
442
419
  const lowerBound = BigInt(blockNumberGte || 0);
443
420
  const nextToBlock = max(fromBlock - 1n, lowerBound);
444
421
  const nextFromBlock = max(fromBlock - BigInt(blockWindow) - 1n, lowerBound);
@@ -448,7 +425,7 @@ async function* streamLogs(parameters) {
448
425
  }
449
426
  yield {
450
427
  logs: [],
451
- blockNumber: order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
428
+ blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
452
429
  };
453
430
  }
454
431
  var InvalidBlockRangeError = class extends BaseError {
@@ -477,13 +454,77 @@ var MissingBlockNumberError = class extends BaseError {
477
454
  };
478
455
 
479
456
  //#endregion
480
- //#region ../../node_modules/.pnpm/tsdown@0.16.5_ms@2.1.3_typescript@5.8.3/node_modules/tsdown/esm-shims.js
481
- var getFilename, getDirname, __dirname;
482
- var init_esm_shims = __esm({ "../../node_modules/.pnpm/tsdown@0.16.5_ms@2.1.3_typescript@5.8.3/node_modules/tsdown/esm-shims.js": (() => {
483
- getFilename = () => fileURLToPath(import.meta.url);
484
- getDirname = () => path.dirname(getFilename());
485
- __dirname = /* @__PURE__ */ getDirname();
486
- }) });
457
+ //#region src/cli/commands/mempool.ts
458
+ /** Default account for test transactions (first anvil account) */
459
+ const testAccount = privateKeyToAccount("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
460
+ /**
461
+ * Start a local anvil chain.
462
+ */
463
+ async function serve$2(parameters) {
464
+ const { port, forkUrl, blockNumber } = parameters;
465
+ let started = false;
466
+ const args = [
467
+ "--chain-id",
468
+ ChainId.ANVIL.toString(),
469
+ "--fork-url",
470
+ forkUrl,
471
+ "--port",
472
+ String(port)
473
+ ];
474
+ if (blockNumber) args.push("--fork-block-number", String(blockNumber));
475
+ const stop = await new Promise((resolve, reject) => {
476
+ const subprocess = spawn("anvil", args, { env: {
477
+ ...process.env,
478
+ FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
479
+ } });
480
+ subprocess.stdout.on("data", (data) => {
481
+ if (`[port ${port}] ${data.toString()}`.includes("Listening on")) {
482
+ started = true;
483
+ resolve(() => subprocess.kill("SIGINT"));
484
+ }
485
+ });
486
+ subprocess.stderr.on("data", (data) => {
487
+ const message = `[port ${port}] ${data.toString()}`;
488
+ console.warn(message);
489
+ if (!started) reject(message);
490
+ else console.warn(message);
491
+ });
492
+ });
493
+ return {
494
+ rpcUrl: `http://localhost:${port}`,
495
+ stop
496
+ };
497
+ }
498
+ const mempoolCmd = new Command("mempool");
499
+ mempoolCmd.description("Start a local chain with the mempool contract").addOption(new Option("--port <port>").env("MEMPOOL_PORT").default(8545)).addOption(new Option("--fork-url <url>").env("MEMPOOL_FORK_URL").default("https://ethereum-rpc.publicnode.com")).addOption(new Option("--block-number <number>").default(0)).action(async (opts) => {
500
+ const { rpcUrl } = await serve$2({
501
+ port: opts.port,
502
+ forkUrl: opts.forkUrl,
503
+ blockNumber: opts.blockNumber
504
+ });
505
+ const client = createTestClient({
506
+ chain: {
507
+ ...anvil,
508
+ id: Number(ChainId.ANVIL)
509
+ },
510
+ transport: http(rpcUrl),
511
+ mode: "anvil"
512
+ }).extend(publicActions).extend(walletActions);
513
+ const mempoolHash = await client.sendTransaction({
514
+ account: testAccount,
515
+ data: "0x60808060405234601357607d908160188239f35b5f80fdfe7f758c7cf107fab3cfdc5aefab966cd9e69c3f368761f7a218a18283c1fbb5574c60808060405260208152366020820152365f60408301375f604036830101526040817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3601168101030190a100fea164736f6c634300081e000a"
516
+ });
517
+ const { contractAddress: mempoolAddress } = await client.waitForTransactionReceipt({ hash: mempoolHash });
518
+ if (!mempoolAddress) throw new Error("Failed to deploy mempool contract");
519
+ console.log(`Mempool contract deployed at ${mempoolAddress}`);
520
+ const termsHash = await client.sendTransaction({
521
+ account: testAccount,
522
+ data: "0x6080604052348015600e575f5ffd5b506101dc8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80637edab8a61461002d575b5f5ffd5b6100476004803603810190610042919061012e565b610049565b005b818373ffffffffffffffffffffffffffffffffffffffff167f1eed8a711368c40dda6427683883619012c34cf436e11e0d773b0d05169ccb8283604051610090919061018d565b60405180910390a3505050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100ca826100a1565b9050919050565b6100da816100c0565b81146100e4575f5ffd5b50565b5f813590506100f5816100d1565b92915050565b5f819050919050565b61010d816100fb565b8114610117575f5ffd5b50565b5f8135905061012881610104565b92915050565b5f5f5f606084860312156101455761014461009d565b5b5f610152868287016100e7565b93505060206101638682870161011a565b92505060406101748682870161011a565b9150509250925092565b610187816100fb565b82525050565b5f6020820190506101a05f83018461017e565b9291505056fea2646970667358221220cc06296395b7aafdb892cd7bfe25a87dc6b35fe29b2d5cb2dcb3aad43050396a64736f6c634300081e0033"
523
+ });
524
+ const { contractAddress: termsAddress } = await client.waitForTransactionReceipt({ hash: termsHash });
525
+ if (!termsAddress) throw new Error("Failed to deploy terms contract");
526
+ console.log(`Terms contract deployed at ${termsAddress}`);
527
+ });
487
528
 
488
529
  //#endregion
489
530
  //#region src/core/Abi/MetaMorpho.ts
@@ -562,6 +603,49 @@ function encodeSellERC20Callback(parameters) {
562
603
  return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.collaterals, parameters.amounts]);
563
604
  }
564
605
 
606
+ //#endregion
607
+ //#region src/utils/Random.ts
608
+ let currentRng = Math.random;
609
+ /**
610
+ * Returns a deterministic random float in [0, 1).
611
+ */
612
+ function float() {
613
+ return currentRng();
614
+ }
615
+ /**
616
+ * Returns a deterministic random integer in [min, maxExclusive).
617
+ */
618
+ function int(maxExclusive, min$1 = 0) {
619
+ return Math.floor(float() * (maxExclusive - min$1)) + min$1;
620
+ }
621
+ /**
622
+ * Returns a deterministic random boolean.
623
+ */
624
+ function bool(probability = .5) {
625
+ return float() < probability;
626
+ }
627
+ /**
628
+ * Returns deterministic random bytes.
629
+ */
630
+ function bytes(length) {
631
+ const output = new Uint8Array(length);
632
+ for (let i = 0; i < length; i += 1) output[i] = int(256);
633
+ return output;
634
+ }
635
+ /**
636
+ * Returns a deterministic random hex string for the given byte length.
637
+ */
638
+ function hex(byteLength) {
639
+ const output = bytes(byteLength);
640
+ return `0x${Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
641
+ }
642
+ /**
643
+ * Returns a deterministic random address.
644
+ */
645
+ function address() {
646
+ return hex(20);
647
+ }
648
+
565
649
  //#endregion
566
650
  //#region src/utils/zod.ts
567
651
  const transformHex = (val, ctx) => {
@@ -670,8 +754,8 @@ const from$13 = (parameters) => {
670
754
  */
671
755
  function random$1() {
672
756
  return from$13({
673
- asset: privateKeyToAccount(generatePrivateKey()).address,
674
- oracle: privateKeyToAccount(generatePrivateKey()).address,
757
+ asset: address(),
758
+ oracle: address(),
675
759
  lltv: .965
676
760
  });
677
761
  }
@@ -976,48 +1060,191 @@ var CollateralsAreNotSortedError = class extends BaseError {
976
1060
  //#endregion
977
1061
  //#region src/core/Tree.ts
978
1062
  const VERSION$1 = 1;
1063
+ const normalizeHash = (hash$1) => hash$1.toLowerCase();
979
1064
  /**
980
1065
  * Builds a Merkle tree from a list of offers.
981
1066
  *
982
1067
  * Leaves are the offer `hash` values as `bytes32` and are deterministically
983
- * ordered in ascending lexicographic order so that the resulting root is stable
984
- * regardless of the input order.
1068
+ * ordered following the StandardMerkleTree leaf ordering so that the resulting
1069
+ * root is stable regardless of the input order.
985
1070
  *
986
1071
  * @param offers - Offers to include in the tree.
987
1072
  * @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
1073
+ * @throws {TreeError} If tree building fails due to offer inconsistencies.
988
1074
  */
989
1075
  const from$10 = (offers$1) => {
990
- const leaves = order(offers$1).map((offer) => {
991
- return [offer.hash];
992
- });
1076
+ const leaves = offers$1.map((offer) => [offer.hash]);
993
1077
  const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
994
- return Object.assign(tree, { offers: offers$1 });
1078
+ const orderedOffers = orderOffers(tree, offers$1);
1079
+ return Object.assign(tree, { offers: orderedOffers });
1080
+ };
1081
+ const orderOffers = (tree, offers$1) => {
1082
+ const offerByHash = /* @__PURE__ */ new Map();
1083
+ for (const offer of offers$1) offerByHash.set(normalizeHash(offer.hash), offer);
1084
+ const entries = tree.dump().values.map((value) => {
1085
+ const hash$1 = normalizeHash(value.value[0]);
1086
+ const offer = offerByHash.get(hash$1);
1087
+ if (!offer) throw new TreeError(`missing offer for leaf ${hash$1}`);
1088
+ return {
1089
+ offer,
1090
+ treeIndex: value.treeIndex
1091
+ };
1092
+ });
1093
+ entries.sort((a, b) => b.treeIndex - a.treeIndex);
1094
+ return entries.map((item) => item.offer);
1095
+ };
1096
+ /**
1097
+ * Generates merkle proofs for all offers in a tree.
1098
+ *
1099
+ * Each proof allows independent verification that an offer is included in the tree
1100
+ * without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
1101
+ *
1102
+ * @param tree - The {@link Tree} to generate proofs for.
1103
+ * @returns Array of proofs - {@link Proof}
1104
+ */
1105
+ const proofs = (tree) => {
1106
+ return tree.offers.map((offer) => {
1107
+ return {
1108
+ offer,
1109
+ path: tree.getProof([offer.hash])
1110
+ };
1111
+ });
1112
+ };
1113
+ const assertHex = (value, expectedBytes, name$1) => {
1114
+ if (typeof value !== "string" || !isHex(value)) throw new DecodeError(`${name$1} is not a valid hex string`);
1115
+ if (hexToBytes(value).length !== expectedBytes) throw new DecodeError(`${name$1}: expected ${expectedBytes} bytes`);
1116
+ };
1117
+ const verifySignatureAndRecoverAddress = async (params) => {
1118
+ const { root, signature } = params;
1119
+ assertHex(signature, 65, "signature");
1120
+ const hash$1 = hashMessage({ raw: root });
1121
+ try {
1122
+ return await recoverAddress({
1123
+ hash: hash$1,
1124
+ signature
1125
+ });
1126
+ } catch {
1127
+ throw new DecodeError("signature recovery failed");
1128
+ }
995
1129
  };
996
- const byHashAsc = (a, b) => a.localeCompare(b);
997
- const order = (offers$1) => {
998
- return offers$1.sort((a, b) => byHashAsc(a.hash, b.hash));
1130
+ /**
1131
+ * Encodes a merkle tree without a signature into hex payload for client-side signing.
1132
+ *
1133
+ * Layout: `0x{vv}{gzip([...offers])}{root}` where:
1134
+ * - `{vv}`: 1-byte version (currently 0x01)
1135
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
1136
+ * - `{root}`: 32-byte merkle root
1137
+ *
1138
+ * Validates root integrity before encoding.
1139
+ *
1140
+ * @param tree - Merkle tree of offers
1141
+ * @returns Hex-encoded unsigned payload
1142
+ * @throws {EncodeError} If root mismatch
1143
+ */
1144
+ const encodeUnsigned = (tree) => {
1145
+ validateTreeForEncoding(tree);
1146
+ return bytesToHex(encodeUnsignedBytes(tree));
1147
+ };
1148
+ const validateTreeForEncoding = (tree) => {
1149
+ if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
1150
+ const computed = from$10(tree.offers);
1151
+ if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
1152
+ };
1153
+ const encodeUnsignedBytes = (tree) => {
1154
+ const offersPayload = tree.offers.map(serialize);
1155
+ const compressed = gzip(JSON.stringify(offersPayload));
1156
+ const rootBytes = hexToBytes(tree.root);
1157
+ const encoded = new Uint8Array(1 + compressed.length + 32);
1158
+ encoded[0] = VERSION$1;
1159
+ encoded.set(compressed, 1);
1160
+ encoded.set(rootBytes, 1 + compressed.length);
1161
+ return encoded;
999
1162
  };
1000
1163
  /**
1001
- * Decodes a Hex string produced by {@link encode} back into an `Tree`.
1164
+ * Decodes hex calldata into a validated merkle tree.
1165
+ *
1166
+ * Validates signature before decompression for fail-fast rejection of invalid payloads.
1167
+ * Returns the tree with separately validated signature and recovered signer address.
1002
1168
  *
1003
- * - Ensures the first byte version matches {@link VERSION}.
1004
- * - Decompresses with gunzip, parses JSON, validates offers, and re-checks the root.
1169
+ * Validation order:
1170
+ * 1. Version check
1171
+ * 2. Signature verification (fail-fast, before decompression)
1172
+ * 3. Decompression (only if signature valid)
1173
+ * 4. Root verification (computed from offers vs embedded root)
1005
1174
  *
1006
- * @param encoded - Hex string in the form `0x{vv}{zip...}`.
1007
- * @returns A validated `Tree` rebuilt from the offers.
1008
- * @throws Error if the version is invalid or the root does not match the offers.
1175
+ * @example
1176
+ * ```typescript
1177
+ * const { tree, signature, signer } = await Tree.decode(calldata);
1178
+ * console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
1179
+ * ```
1180
+ *
1181
+ * @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
1182
+ * @returns Validated tree, signature, and recovered signer address
1183
+ * @throws {DecodeError} If version invalid, signature invalid, or root mismatch
1184
+ */
1185
+ const decode$1 = async (encoded) => {
1186
+ const bytes$1 = hexToBytes(encoded);
1187
+ if (bytes$1.length < 98) throw new DecodeError("payload too short");
1188
+ const version$1 = bytes$1[0];
1189
+ if (version$1 !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version$1 ?? 0}`);
1190
+ const signature = bytesToHex(bytes$1.slice(-65));
1191
+ const root = bytesToHex(bytes$1.slice(-97, -65));
1192
+ assertHex(root, 32, "root");
1193
+ assertHex(signature, 65, "signature");
1194
+ const signer = await verifySignatureAndRecoverAddress({
1195
+ root,
1196
+ signature
1197
+ });
1198
+ const compressed = bytes$1.slice(1, -97);
1199
+ let decoded;
1200
+ try {
1201
+ decoded = ungzip(compressed, { to: "string" });
1202
+ } catch {
1203
+ throw new DecodeError("decompression failed");
1204
+ }
1205
+ let rawOffers;
1206
+ try {
1207
+ rawOffers = JSON.parse(decoded);
1208
+ } catch {
1209
+ throw new DecodeError("JSON parse failed");
1210
+ }
1211
+ const tree = from$10(rawOffers.map((o) => OfferSchema().parse(o)));
1212
+ if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
1213
+ return {
1214
+ tree,
1215
+ signature,
1216
+ signer
1217
+ };
1218
+ };
1219
+ /**
1220
+ * Error thrown during tree building operations.
1221
+ * Indicates structural issues with the tree (missing offers, inconsistent state).
1009
1222
  */
1010
- const decode$1 = (encoded) => {
1011
- const bytes = hexToBytes(encoded);
1012
- if (bytes.length < 2) throw new Error("Invalid payload: too short");
1013
- const version$1 = bytes[0];
1014
- if (version$1 !== (VERSION$1 & 255)) throw new Error(`Invalid version: expected ${VERSION$1}, got ${version$1}`);
1015
- const decoded = ungzip(bytes.slice(1), { to: "string" });
1016
- const data = JSON.parse(decoded);
1017
- const root = data[0];
1018
- const tree = from$10(data.slice(1).map((o) => OfferSchema().parse(o)));
1019
- if (root !== tree.root) throw new Error(`Invalid root: expected ${tree.root}, got ${root}`);
1020
- return tree;
1223
+ var TreeError = class extends BaseError {
1224
+ name = "Tree.TreeError";
1225
+ constructor(reason) {
1226
+ super(`Tree error: ${reason}`);
1227
+ }
1228
+ };
1229
+ /**
1230
+ * Error thrown during tree encoding.
1231
+ * Indicates validation failures (signature, root mismatch, mixed makers).
1232
+ */
1233
+ var EncodeError = class extends BaseError {
1234
+ name = "Tree.EncodeError";
1235
+ constructor(reason) {
1236
+ super(`Failed to encode tree: ${reason}`);
1237
+ }
1238
+ };
1239
+ /**
1240
+ * Error thrown during tree decoding.
1241
+ * Indicates payload corruption, version mismatch, or validation failures.
1242
+ */
1243
+ var DecodeError = class extends BaseError {
1244
+ name = "Tree.DecodeError";
1245
+ constructor(reason) {
1246
+ super(`Failed to decode tree: ${reason}`);
1247
+ }
1021
1248
  };
1022
1249
 
1023
1250
  //#endregion
@@ -1089,16 +1316,47 @@ function fromSnakeCase(input) {
1089
1316
  return from$9(fromSnakeCase$1(input));
1090
1317
  }
1091
1318
  /**
1319
+ * Serializes an offer for merkle tree encoding.
1320
+ * Converts BigInt fields to strings for JSON compatibility.
1321
+ *
1322
+ * @param offer - Offer to serialize
1323
+ * @returns JSON-serializable offer object
1324
+ */
1325
+ const serialize = (offer) => ({
1326
+ offering: offer.offering,
1327
+ assets: offer.assets.toString(),
1328
+ rate: offer.rate.toString(),
1329
+ maturity: Number(offer.maturity),
1330
+ expiry: Number(offer.expiry),
1331
+ start: Number(offer.start),
1332
+ nonce: offer.nonce.toString(),
1333
+ buy: offer.buy,
1334
+ chainId: offer.chainId,
1335
+ loanToken: offer.loanToken,
1336
+ collaterals: offer.collaterals.map((c) => ({
1337
+ asset: c.asset,
1338
+ oracle: c.oracle,
1339
+ lltv: c.lltv.toString()
1340
+ })),
1341
+ callback: {
1342
+ address: offer.callback.address,
1343
+ data: offer.callback.data,
1344
+ gasLimit: offer.callback.gasLimit.toString()
1345
+ },
1346
+ signature: offer.signature,
1347
+ hash: offer.hash
1348
+ });
1349
+ /**
1092
1350
  * Generates a random Offer.
1093
1351
  * The returned Offer contains randomly generated values.
1094
1352
  * @warning The generated Offer should not be used for production usage.
1095
1353
  * @returns {Offer} A randomly generated Offer object.
1096
1354
  */
1097
1355
  function random(config) {
1098
- const chain = config?.chains ? config.chains[Math.floor(Math.random() * config.chains.length)] : chains$2.ethereum;
1099
- const loanToken = config?.loanTokens ? config.loanTokens[Math.floor(Math.random() * config.loanTokens.length)] : privateKeyToAccount(generatePrivateKey()).address;
1100
- const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [privateKeyToAccount(generatePrivateKey()).address];
1101
- const collateralAsset = collateralCandidates[Math.floor(Math.random() * collateralCandidates.length)];
1356
+ const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
1357
+ const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
1358
+ const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
1359
+ const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
1102
1360
  const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
1103
1361
  const maturity$1 = config?.maturity ?? from$12(maturityOption);
1104
1362
  const lltv = from$14(weightedChoice([
@@ -1112,7 +1370,7 @@ function random(config) {
1112
1370
  [.965, 4],
1113
1371
  [.98, 2]
1114
1372
  ]));
1115
- const buy = config?.buy !== void 0 ? config.buy : Math.random() > .5;
1373
+ const buy = config?.buy !== void 0 ? config.buy : bool();
1116
1374
  const ONE = 1000000000000000000n;
1117
1375
  const qMin = buy ? 16 : 4;
1118
1376
  const len = (buy ? 32 : 16) - qMin + 1;
@@ -1123,9 +1381,9 @@ function random(config) {
1123
1381
  const rate = config?.rate ?? weightedChoice(ratePairs);
1124
1382
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
1125
1383
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
1126
- const amountBase = BigInt(100 + Math.floor(Math.random() * 999901));
1384
+ const amountBase = BigInt(100 + int(999901));
1127
1385
  const assetsScaled = config?.assets ?? amountBase * unit;
1128
- const consumed = config?.consumed !== void 0 ? config.consumed : Math.random() < .8 ? 0n : assetsScaled * BigInt(1 + Math.floor(Math.random() * 900)) / 1000n;
1386
+ const consumed = config?.consumed !== void 0 ? config.consumed : float() < .8 ? 0n : assetsScaled * BigInt(1 + int(900)) / 1000n;
1129
1387
  const callbackBySide = (() => {
1130
1388
  if (buy) return {
1131
1389
  address: zeroAddress,
@@ -1144,29 +1402,29 @@ function random(config) {
1144
1402
  };
1145
1403
  })();
1146
1404
  return from$9({
1147
- offering: config?.offering ?? privateKeyToAccount(generatePrivateKey()).address,
1405
+ offering: config?.offering ?? address(),
1148
1406
  assets: assetsScaled,
1149
1407
  rate,
1150
1408
  maturity: maturity$1,
1151
1409
  expiry: config?.expiry ?? maturity$1 - 1,
1152
1410
  start: config?.start ?? maturity$1 - 10,
1153
- nonce: BigInt(Math.floor(Math.random() * 1e6)),
1411
+ nonce: BigInt(int(1e6)),
1154
1412
  buy,
1155
1413
  chainId: chain.id,
1156
1414
  loanToken,
1157
- collaterals: config?.collaterals ?? Array.from({ length: Math.floor(Math.random() * 3) + 1 }, () => ({
1415
+ collaterals: config?.collaterals ?? Array.from({ length: int(3) + 1 }, () => ({
1158
1416
  ...random$1(),
1159
1417
  lltv
1160
1418
  })).sort((a, b) => a.asset.localeCompare(b.asset)),
1161
1419
  callback: config?.callback ?? callbackBySide,
1162
1420
  consumed,
1163
1421
  takeable: config?.takeable ?? assetsScaled - consumed,
1164
- blockNumber: config?.blockNumber ?? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
1422
+ blockNumber: config?.blockNumber ?? int(Number.MAX_SAFE_INTEGER)
1165
1423
  });
1166
1424
  }
1167
1425
  const weightedChoice = (pairs) => {
1168
1426
  const total = pairs.reduce((sum, [, weight]) => sum + weight, 0);
1169
- let roll = Math.random() * total;
1427
+ let roll = float() * total;
1170
1428
  for (const [value, weight] of pairs) {
1171
1429
  roll -= weight;
1172
1430
  if (roll < 0) return value;
@@ -1460,89 +1718,6 @@ function from$6(parameters) {
1460
1718
  //#region src/core/types.ts
1461
1719
  const BrandTypeId = Symbol.for("mempool/Brand");
1462
1720
 
1463
- //#endregion
1464
- //#region src/evm/EVM.ts
1465
- const users = [privateKeyToAccount("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")];
1466
- const mockOracleAddress = privateKeyToAccount(keccak256(toHex("ORACLE"))).address;
1467
- const mockMorphoAddress = privateKeyToAccount(keccak256(toHex("MORPHO"))).address;
1468
- const mockFactoryAddress = privateKeyToAccount(keccak256(toHex("FACTORY"))).address;
1469
- /**
1470
- * Start a local anvil chain.
1471
- * @example
1472
- * ```ts
1473
- * import { EVM } from "@morpho-dev/router";
1474
- * EVM.serve(); // local chain rpc url running on http://localhost:8545
1475
- * ```
1476
- */
1477
- async function serve$2(parameters) {
1478
- const { port, forkUrl, blockNumber } = parameters;
1479
- let started = false;
1480
- const args = [
1481
- "--chain-id",
1482
- ChainId.ANVIL.toString(),
1483
- "--fork-url",
1484
- forkUrl,
1485
- "--port",
1486
- String(port)
1487
- ];
1488
- if (blockNumber) args.push("--fork-block-number", String(blockNumber));
1489
- const stop = await new Promise((resolve, reject) => {
1490
- const subprocess = spawn("anvil", args, { env: {
1491
- ...process.env,
1492
- FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
1493
- } });
1494
- subprocess.stdout.on("data", (data) => {
1495
- if (`[port ${port}] ${data.toString()}`.includes("Listening on")) {
1496
- started = true;
1497
- resolve(() => subprocess.kill("SIGINT"));
1498
- }
1499
- });
1500
- subprocess.stderr.on("data", (data) => {
1501
- const message = `[port ${port}] ${data.toString()}`;
1502
- console.warn(message);
1503
- if (!started) reject(message);
1504
- else console.warn(message);
1505
- });
1506
- });
1507
- return {
1508
- rpcUrl: `http://localhost:${port}`,
1509
- stop
1510
- };
1511
- }
1512
-
1513
- //#endregion
1514
- //#region src/cli/commands/mempool.ts
1515
- const mempoolCmd = new Command("mempool");
1516
- mempoolCmd.description("Start a local chain with the mempool contract").addOption(new Option("--port <port>").env("MEMPOOL_PORT").default(8545)).addOption(new Option("--fork-url <url>").env("MEMPOOL_FORK_URL").default("https://ethereum-rpc.publicnode.com")).addOption(new Option("--block-number <number>").default(0)).action(async (opts) => {
1517
- const { rpcUrl } = await serve$2({
1518
- port: opts.port,
1519
- forkUrl: opts.forkUrl,
1520
- blockNumber: opts.blockNumber
1521
- });
1522
- const client = createTestClient({
1523
- chain: {
1524
- ...anvil,
1525
- id: Number(ChainId.ANVIL)
1526
- },
1527
- transport: http(rpcUrl),
1528
- mode: "anvil"
1529
- }).extend(publicActions).extend(walletActions);
1530
- const mempoolHash = await client.sendTransaction({
1531
- account: users[0],
1532
- data: "0x60808060405234601357607d908160188239f35b5f80fdfe7f758c7cf107fab3cfdc5aefab966cd9e69c3f368761f7a218a18283c1fbb5574c60808060405260208152366020820152365f60408301375f604036830101526040817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f3601168101030190a100fea164736f6c634300081e000a"
1533
- });
1534
- const { contractAddress: mempoolAddress } = await client.waitForTransactionReceipt({ hash: mempoolHash });
1535
- if (!mempoolAddress) throw new Error("Failed to deploy mempool contract");
1536
- console.log(`Mempool contract deployed at ${mempoolAddress}`);
1537
- const termsHash = await client.sendTransaction({
1538
- account: users[0],
1539
- data: "0x6080604052348015600e575f5ffd5b506101dc8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80637edab8a61461002d575b5f5ffd5b6100476004803603810190610042919061012e565b610049565b005b818373ffffffffffffffffffffffffffffffffffffffff167f1eed8a711368c40dda6427683883619012c34cf436e11e0d773b0d05169ccb8283604051610090919061018d565b60405180910390a3505050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100ca826100a1565b9050919050565b6100da816100c0565b81146100e4575f5ffd5b50565b5f813590506100f5816100d1565b92915050565b5f819050919050565b61010d816100fb565b8114610117575f5ffd5b50565b5f8135905061012881610104565b92915050565b5f5f5f606084860312156101455761014461009d565b5b5f610152868287016100e7565b93505060206101638682870161011a565b92505060406101748682870161011a565b9150509250925092565b610187816100fb565b82525050565b5f6020820190506101a05f83018461017e565b9291505056fea2646970667358221220cc06296395b7aafdb892cd7bfe25a87dc6b35fe29b2d5cb2dcb3aad43050396a64736f6c634300081e0033"
1540
- });
1541
- const { contractAddress: termsAddress } = await client.waitForTransactionReceipt({ hash: termsHash });
1542
- if (!termsAddress) throw new Error("Failed to deploy terms contract");
1543
- console.log(`Terms contract deployed at ${termsAddress}`);
1544
- });
1545
-
1546
1721
  //#endregion
1547
1722
  //#region src/logger/Logger.ts
1548
1723
  const LogLevelValues = [
@@ -1670,13 +1845,16 @@ function from$4(obligation, quote) {
1670
1845
  * Creates an `OfferResponse` from an `Offer`.
1671
1846
  * @constructor
1672
1847
  * @param offer - {@link Offer}
1848
+ * @param attestation - {@link Attestation}
1673
1849
  * @returns The created `OfferResponse`. {@link OfferResponse}
1674
1850
  */
1675
- function from$3(offer) {
1676
- const result = toSnakeCase(offer);
1851
+ function from$3(offer, attestation) {
1852
+ const { signature: _, ...rest } = toSnakeCase(offer);
1677
1853
  return {
1678
- ...result,
1679
- signature: result.signature ?? null
1854
+ ...rest,
1855
+ root: attestation?.root.toLowerCase() ?? null,
1856
+ proof: attestation?.proof.map((p) => p.toLowerCase()) ?? null,
1857
+ signature: attestation?.signature.toLowerCase() ?? null
1680
1858
  };
1681
1859
  }
1682
1860
 
@@ -1814,10 +1992,12 @@ const offerExample = {
1814
1992
  data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000",
1815
1993
  gas_limit: "500000"
1816
1994
  },
1817
- signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400",
1818
1995
  consumed: "0",
1819
1996
  takeable: "369216000000000000000000",
1820
- block_number: 0xa7495128adfb1
1997
+ block_number: 0xa7495128adfb1,
1998
+ root: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
1999
+ proof: ["0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"],
2000
+ signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400"
1821
2001
  };
1822
2002
  const collectorsHealthExample = {
1823
2003
  name: "offers",
@@ -1962,6 +2142,16 @@ __decorate([ApiProperty({
1962
2142
  type: "number",
1963
2143
  example: offerExample.block_number
1964
2144
  })], OfferListItemResponse.prototype, "block_number", void 0);
2145
+ __decorate([ApiProperty({
2146
+ type: "string",
2147
+ nullable: true,
2148
+ example: offerExample.root
2149
+ })], OfferListItemResponse.prototype, "root", void 0);
2150
+ __decorate([ApiProperty({
2151
+ type: [String],
2152
+ nullable: true,
2153
+ example: offerExample.proof
2154
+ })], OfferListItemResponse.prototype, "proof", void 0);
1965
2155
  __decorate([ApiProperty({
1966
2156
  type: "string",
1967
2157
  nullable: true,
@@ -2145,44 +2335,61 @@ __decorate([ApiProperty({
2145
2335
  var ValidateOffersRequest = class {};
2146
2336
  __decorate([ApiProperty({
2147
2337
  type: () => [ValidateOfferRequest],
2148
- description: "Array of offers in snake_case format. Mutually exclusive with 'calldata'.",
2149
- required: false
2338
+ description: "Array of offers in snake_case format. Required, non-empty.",
2339
+ required: true
2150
2340
  })], ValidateOffersRequest.prototype, "offers", void 0);
2341
+ var ValidationSuccessDataResponse = class {};
2151
2342
  __decorate([ApiProperty({
2152
2343
  type: "string",
2153
- description: "Encoded tree calldata as a hex string (0x-prefixed). Mutually exclusive with 'offers'.",
2154
- example: "0x01...",
2155
- required: false
2156
- })], ValidateOffersRequest.prototype, "calldata", void 0);
2157
- var ValidateOfferResultResponse = class {};
2344
+ description: "Unsigned payload: version (1B) + gzip(offers) + root (32B).",
2345
+ example: "0x01789c..."
2346
+ })], ValidationSuccessDataResponse.prototype, "payload", void 0);
2158
2347
  __decorate([ApiProperty({
2159
2348
  type: "string",
2160
- example: offerExample.hash
2161
- })], ValidateOfferResultResponse.prototype, "offer_hash", void 0);
2349
+ description: "Merkle tree root to sign with EIP-191.",
2350
+ example: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427"
2351
+ })], ValidationSuccessDataResponse.prototype, "root", void 0);
2352
+ var ValidationSuccessResponse = class extends SuccessResponse {};
2162
2353
  __decorate([ApiProperty({
2163
- type: "boolean",
2164
- example: false
2165
- })], ValidateOfferResultResponse.prototype, "valid", void 0);
2354
+ type: "string",
2355
+ nullable: true,
2356
+ example: null
2357
+ })], ValidationSuccessResponse.prototype, "cursor", void 0);
2358
+ __decorate([ApiProperty({
2359
+ type: () => ValidationSuccessDataResponse,
2360
+ description: "Payload and root for client-side signing."
2361
+ })], ValidationSuccessResponse.prototype, "data", void 0);
2362
+ var ValidationIssueResponse = class {};
2363
+ __decorate([ApiProperty({
2364
+ type: "number",
2365
+ description: "0-indexed position of the failed offer in the request array.",
2366
+ example: 0
2367
+ })], ValidationIssueResponse.prototype, "index", void 0);
2166
2368
  __decorate([ApiProperty({
2167
2369
  type: "string",
2168
- example: "parse_error",
2169
- nullable: true
2170
- })], ValidateOfferResultResponse.prototype, "rule", void 0);
2370
+ description: "Gatekeeper rule name that rejected the offer.",
2371
+ example: "no_buy"
2372
+ })], ValidationIssueResponse.prototype, "rule", void 0);
2171
2373
  __decorate([ApiProperty({
2172
2374
  type: "string",
2173
- example: "Invalid offer. 'offering': invalid address",
2174
- nullable: true
2175
- })], ValidateOfferResultResponse.prototype, "message", void 0);
2176
- var ValidateOffersListResponse = class extends SuccessResponse {};
2375
+ description: "Human-readable rejection reason.",
2376
+ example: "Buy offers are not supported"
2377
+ })], ValidationIssueResponse.prototype, "message", void 0);
2378
+ var ValidationFailureDataResponse = class {};
2379
+ __decorate([ApiProperty({
2380
+ type: () => [ValidationIssueResponse],
2381
+ description: "List of validation issues. Returned when any offer fails validation."
2382
+ })], ValidationFailureDataResponse.prototype, "issues", void 0);
2383
+ var ValidationFailureResponse = class extends SuccessResponse {};
2177
2384
  __decorate([ApiProperty({
2178
2385
  type: "string",
2179
2386
  nullable: true,
2180
2387
  example: null
2181
- })], ValidateOffersListResponse.prototype, "cursor", void 0);
2388
+ })], ValidationFailureResponse.prototype, "cursor", void 0);
2182
2389
  __decorate([ApiProperty({
2183
- type: () => [ValidateOfferResultResponse],
2184
- description: "Validation results for each offer."
2185
- })], ValidateOffersListResponse.prototype, "data", void 0);
2390
+ type: () => ValidationFailureDataResponse,
2391
+ description: "List of validation issues. Returned when any offer fails validation."
2392
+ })], ValidationFailureResponse.prototype, "data", void 0);
2186
2393
  var BookLevelResponse = class {};
2187
2394
  __decorate([ApiProperty({
2188
2395
  type: "string",
@@ -2247,13 +2454,18 @@ __decorate([
2247
2454
  methods: ["post"],
2248
2455
  path: "/v1/validate",
2249
2456
  summary: "Validate offers",
2250
- description: "Validates offers against router validation rules. Returns validation status for each offer. Accepts either an array of offers or encoded calldata (mutually exclusive)."
2457
+ description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
2251
2458
  }),
2252
2459
  ApiBody({ type: ValidateOffersRequest }),
2253
2460
  ApiResponse({
2254
2461
  status: 200,
2255
2462
  description: "Success",
2256
- type: ValidateOffersListResponse
2463
+ type: ValidationSuccessResponse
2464
+ }),
2465
+ ApiResponse({
2466
+ status: 200,
2467
+ description: "Validation issues",
2468
+ type: ValidationFailureResponse
2257
2469
  })
2258
2470
  ], ValidateController.prototype, "validateOffers", null);
2259
2471
  ValidateController = __decorate([ApiTags("Validate"), ApiResponse({
@@ -2422,27 +2634,33 @@ const OpenApi = async (options = {}) => {
2422
2634
  if (options.rules && options.rules.length > 0) {
2423
2635
  const rulesDescription = options.rules.map((rule) => `- **${rule.name}**: ${rule.description}`).join("\n");
2424
2636
  const validatePath = document.paths?.["/v1/validate"];
2425
- if (validatePath && "post" in validatePath && validatePath.post) validatePath.post.description = `Validates offers against router validation rules. Returns validation status for each offer.\n\n**Available validation rules:**\n${rulesDescription}`;
2637
+ if (validatePath && "post" in validatePath && validatePath.post) validatePath.post.description = `Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure.\n\n**Available validation rules:**\n${rulesDescription}`;
2426
2638
  }
2427
2639
  return document;
2428
2640
  };
2429
2641
 
2430
2642
  //#endregion
2431
2643
  //#region src/database/utils/Cursor.ts
2432
- function validate(cursor) {
2433
- if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
2434
- const c = cursor;
2435
- if (![
2644
+ const isSort = (value) => {
2645
+ return [
2436
2646
  "rate",
2437
2647
  "maturity",
2438
2648
  "expiry",
2439
2649
  "amount"
2440
- ].includes(c.sort)) throw new Error(`Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`);
2441
- if (!["asc", "desc"].includes(c.dir)) throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
2442
- if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) throw new Error(`Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`);
2443
- const validation = {
2444
- rate: {
2445
- field: "rate",
2650
+ ].includes(value);
2651
+ };
2652
+ function validate(cursor) {
2653
+ if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
2654
+ const c = cursor;
2655
+ const sort = c.sort;
2656
+ const dir = c.dir;
2657
+ const hash$1 = c.hash;
2658
+ if (typeof sort !== "string" || !isSort(sort)) throw new Error(`Invalid sort field: ${String(sort)}. Must be one of: rate, maturity, expiry, amount`);
2659
+ if (typeof dir !== "string" || !["asc", "desc"].includes(dir)) throw new Error(`Invalid direction: ${String(dir)}. Must be one of: asc, desc`);
2660
+ if (typeof hash$1 !== "string" || !/^0x[a-fA-F0-9]{64}$/.test(hash$1)) throw new Error(`Invalid hash format: ${String(hash$1)}. Must be a 64-character hex string starting with 0x`);
2661
+ const validation = {
2662
+ rate: {
2663
+ field: "rate",
2446
2664
  type: "string",
2447
2665
  pattern: /^\d+$/,
2448
2666
  error: "numeric string"
@@ -2456,24 +2674,30 @@ function validate(cursor) {
2456
2674
  maturity: {
2457
2675
  field: "maturity",
2458
2676
  type: "number",
2459
- validator: (val) => val > 0,
2677
+ min: 1,
2460
2678
  error: "positive number"
2461
2679
  },
2462
2680
  expiry: {
2463
2681
  field: "expiry",
2464
2682
  type: "number",
2465
- validator: (val) => val > 0,
2683
+ min: 1,
2466
2684
  error: "positive number"
2467
2685
  }
2468
- }[c.sort];
2469
- if (!validation) throw new Error(`Invalid sort field: ${c.sort}`);
2686
+ }[sort];
2687
+ if (!validation) throw new Error(`Invalid sort field: ${sort}`);
2470
2688
  const fieldValue = c[validation.field];
2471
- if (!fieldValue) throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
2472
- if (typeof fieldValue !== validation.type) throw new Error(`${c.sort} sort requires '${validation.field}' field of type ${validation.type}`);
2473
- if (validation.pattern && !validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
2474
- if (validation.validator && !validation.validator(fieldValue)) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
2475
- if (c.page !== void 0) {
2476
- if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) throw new Error("Invalid page: must be a positive integer");
2689
+ if (fieldValue === void 0 || fieldValue === null) throw new Error(`${sort} sort requires '${validation.field}' field to be present`);
2690
+ if (validation.type === "string") {
2691
+ if (typeof fieldValue !== "string") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
2692
+ if (!validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
2693
+ }
2694
+ if (validation.type === "number") {
2695
+ if (typeof fieldValue !== "number") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
2696
+ if (fieldValue < validation.min) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
2697
+ }
2698
+ const page = c.page;
2699
+ if (page !== void 0) {
2700
+ if (typeof page !== "number" || !Number.isInteger(page) || page < 1) throw new Error("Invalid page: must be a positive integer");
2477
2701
  }
2478
2702
  return true;
2479
2703
  }
@@ -2586,21 +2810,7 @@ const schemas = {
2586
2810
  get_obligations: GetObligationsQueryParams,
2587
2811
  get_obligation: GetObligationParams,
2588
2812
  get_book: GetBookParams,
2589
- validate_offers: z$1.object({
2590
- offers: z$1.any().refine((val) => val === void 0 || Array.isArray(val), { message: "'offers' must be an array" }),
2591
- calldata: z$1.string().regex(/^0x[a-fA-F0-9]*$/, { message: "'calldata' must be a hex string starting with '0x'" }).optional()
2592
- }).superRefine((val, ctx) => {
2593
- const hasOffers = val.offers !== void 0;
2594
- const hasCalldata = val.calldata !== void 0;
2595
- if (hasOffers && hasCalldata) ctx.addIssue({
2596
- code: "custom",
2597
- message: "Request body must contain either 'offers' or 'calldata', not both"
2598
- });
2599
- if (!hasOffers && !hasCalldata) ctx.addIssue({
2600
- code: "custom",
2601
- message: "Request body must contain either 'offers' array or 'calldata' hex string"
2602
- });
2603
- })
2813
+ validate_offers: z$1.object({ offers: z$1.array(z$1.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict()
2604
2814
  };
2605
2815
  function safeParse(action, query, error) {
2606
2816
  return schemas[action].safeParse(query, { error });
@@ -2725,7 +2935,7 @@ function now() {
2725
2935
 
2726
2936
  //#endregion
2727
2937
  //#region src/indexer/collectors/Admin.ts
2728
- function create$14(parameters) {
2938
+ function create$17(parameters) {
2729
2939
  const collector = "admin";
2730
2940
  const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
2731
2941
  const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
@@ -2974,8 +3184,8 @@ const names = [
2974
3184
  "positions",
2975
3185
  "prices"
2976
3186
  ];
2977
- function create$13({ name: name$1, collect, client, db, options }) {
2978
- const admin = create$14({
3187
+ function create$16({ name: name$1, collect, client, db, options }) {
3188
+ const admin = create$17({
2979
3189
  client,
2980
3190
  db,
2981
3191
  options
@@ -3025,7 +3235,10 @@ function create$13({ name: name$1, collect, client, db, options }) {
3025
3235
  };
3026
3236
  });
3027
3237
  if (done) iterator = null;
3028
- else yield blockNumber;
3238
+ else {
3239
+ lastBlockNumber = blockNumber;
3240
+ yield blockNumber;
3241
+ }
3029
3242
  } catch (err) {
3030
3243
  const isError = err instanceof Error;
3031
3244
  logger.error({
@@ -3183,30 +3396,79 @@ async function* collectOffersV2(parameters) {
3183
3396
  });
3184
3397
  for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
3185
3398
  blockNumber = lastStreamBlockNumber;
3186
- const offers$1 = [];
3399
+ const decodedTrees = [];
3187
3400
  for (const log of logs) {
3188
3401
  if (!log) continue;
3189
3402
  const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
3190
3403
  try {
3191
- const tree = decode$1(payload);
3192
- for (const offer of tree.offers) offers$1.push({
3404
+ const { tree, signature, signer } = await decode$1(payload);
3405
+ const signerMismatch = tree.offers.find((offer) => offer.offering.toLowerCase() !== signer.toLowerCase());
3406
+ if (signerMismatch) {
3407
+ logger.debug({
3408
+ msg: "Tree rejected: signer mismatch",
3409
+ reason: "signer_mismatch",
3410
+ signer,
3411
+ offering: signerMismatch.offering,
3412
+ chain_id: client.chain.id
3413
+ });
3414
+ continue;
3415
+ }
3416
+ const offersWithBlock = tree.offers.map((offer) => ({
3193
3417
  ...offer,
3194
3418
  blockNumber: Number(log.blockNumber)
3419
+ }));
3420
+ const treeWithBlock = Object.assign(Object.create(Object.getPrototypeOf(tree)), tree, { offers: offersWithBlock });
3421
+ decodedTrees.push({
3422
+ tree: treeWithBlock,
3423
+ signature,
3424
+ signer,
3425
+ blockNumber: Number(log.blockNumber)
3426
+ });
3427
+ } catch (err) {
3428
+ const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
3429
+ logger.debug({
3430
+ msg: "Tree decode failed",
3431
+ reason,
3432
+ chain_id: client.chain.id,
3433
+ err: err instanceof Error ? err.message : String(err)
3195
3434
  });
3196
- } catch (_) {}
3435
+ }
3197
3436
  }
3198
3437
  await db.transaction(async (dbTx) => {
3199
3438
  const { epoch, blockNumber: latestBlockNumber } = await dbTx.chains.getBlockNumber(client.chain.id);
3200
- let validOffers = [];
3201
- try {
3202
- validOffers = (await gatekeeper.isAllowed(offers$1)).valid.filter((offer) => offer.blockNumber <= latestBlockNumber);
3439
+ const treesToInsert = [];
3440
+ let totalValidOffers = 0;
3441
+ for (const { tree, signature } of decodedTrees) try {
3442
+ const allowedResults = await gatekeeper.isAllowed(tree.offers);
3443
+ const hasBlockWindowViolation = tree.offers.some((offer) => offer.blockNumber > latestBlockNumber);
3444
+ if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
3445
+ if (allowedResults.issues.length > 0) {
3446
+ const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
3447
+ logger.debug({
3448
+ msg: "Tree offers rejected by gatekeeper",
3449
+ reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
3450
+ chain_id: client.chain.id,
3451
+ issues_count: allowedResults.issues.length
3452
+ });
3453
+ } else if (hasBlockWindowViolation) logger.debug({
3454
+ msg: "Tree rejected: offers outside block window",
3455
+ reason: "block_window",
3456
+ chain_id: client.chain.id
3457
+ });
3458
+ continue;
3459
+ }
3460
+ treesToInsert.push({
3461
+ tree,
3462
+ signature
3463
+ });
3464
+ totalValidOffers += tree.offers.length;
3203
3465
  } catch (err) {
3204
3466
  logger.error({
3205
3467
  err,
3206
- msg: "Failed to validate offers"
3468
+ msg: "Failed to validate offers for tree"
3207
3469
  });
3208
3470
  }
3209
- await dbTx.offers.create(validOffers);
3471
+ if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
3210
3472
  try {
3211
3473
  await dbTx.collectors.saveBlockNumber({
3212
3474
  collectorName: collector,
@@ -3214,10 +3476,11 @@ async function* collectOffersV2(parameters) {
3214
3476
  blockNumber,
3215
3477
  epoch
3216
3478
  });
3217
- if (validOffers.length > 0) logger.info({
3479
+ if (totalValidOffers > 0) logger.info({
3218
3480
  msg: `New offers`,
3219
3481
  collector,
3220
- count: validOffers.length,
3482
+ count: totalValidOffers,
3483
+ trees_count: treesToInsert.length,
3221
3484
  chain_id: client.chain.id,
3222
3485
  block_range: [startBlock, lastStreamBlockNumber]
3223
3486
  });
@@ -3860,7 +4123,7 @@ async function* collectPrices(parameters) {
3860
4123
  //#region src/indexer/collectors/CollectorBuilder.ts
3861
4124
  function createBuilder(parameters) {
3862
4125
  const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow } = {} } = parameters;
3863
- const createCollector = (name$1, collect) => create$13({
4126
+ const createCollector = (name$1, collect) => create$16({
3864
4127
  name: name$1,
3865
4128
  collect,
3866
4129
  client,
@@ -3959,7 +4222,7 @@ function from$1(config) {
3959
4222
  retryAttempts,
3960
4223
  retryDelayMs
3961
4224
  });
3962
- return create$12({
4225
+ return create$15({
3963
4226
  db,
3964
4227
  client,
3965
4228
  collectors: [
@@ -3971,7 +4234,7 @@ function from$1(config) {
3971
4234
  interval
3972
4235
  });
3973
4236
  }
3974
- function create$12(params) {
4237
+ function create$15(params) {
3975
4238
  const { db, collectors: collectors$1, interval, client } = params;
3976
4239
  const logger = getLogger();
3977
4240
  const indexerId = `${client.chain.id.toString()}.indexer`;
@@ -4032,7 +4295,7 @@ const DEFAULT_MAX_ALLOWED_LAG = 5;
4032
4295
  /**
4033
4296
  * Create a health service that exposes collector and chain block numbers.
4034
4297
  */
4035
- function create$11(parameters) {
4298
+ function create$14(parameters) {
4036
4299
  const { db, maxAllowedLag = DEFAULT_MAX_ALLOWED_LAG, healthClients } = parameters;
4037
4300
  const loadSnapshot = async () => {
4038
4301
  const [collectorRows, chainRows, remoteBlockByChainId] = await Promise.all([
@@ -4124,7 +4387,7 @@ async function getRemoteBlockNumbers(healthClients) {
4124
4387
  async function getHealth(db) {
4125
4388
  const logger = getLogger();
4126
4389
  try {
4127
- const status$1 = await create$11({ db }).getStatus();
4390
+ const status$1 = await create$14({ db }).getStatus();
4128
4391
  return success({ data: toSnakeCase({ status: status$1 }) });
4129
4392
  } catch (err) {
4130
4393
  logger.error({
@@ -4139,7 +4402,7 @@ async function getHealth(db) {
4139
4402
  async function getHealthChains(db, healthClients) {
4140
4403
  const logger = getLogger();
4141
4404
  try {
4142
- const chains$3 = await create$11({
4405
+ const chains$3 = await create$14({
4143
4406
  db,
4144
4407
  healthClients
4145
4408
  }).getChains();
@@ -4162,7 +4425,7 @@ async function getHealthChains(db, healthClients) {
4162
4425
  async function getHealthCollectors(db) {
4163
4426
  const logger = getLogger();
4164
4427
  try {
4165
- const collectors$1 = await create$11({ db }).getCollectors();
4428
+ const collectors$1 = await create$14({ db }).getCollectors();
4166
4429
  return success({ data: collectors$1.map(({ name: name$1, chainId, blockNumber, updatedAt, lag, status: status$1 }) => toSnakeCase({
4167
4430
  name: name$1,
4168
4431
  chainId,
@@ -4263,8 +4526,10 @@ async function getOffers(queryParameters, db) {
4263
4526
  cursor: query.cursor,
4264
4527
  limit: query.limit
4265
4528
  });
4529
+ const hashes = offers$1.map((offer) => offer.hash);
4530
+ const attestationMap = await db.trees.getAttestations(hashes);
4266
4531
  return success({
4267
- data: offers$1.map(from$3),
4532
+ data: offers$1.map((offer) => from$3(offer, attestationMap.get(offer.hash.toLowerCase()))),
4268
4533
  cursor: nextCursor ?? null
4269
4534
  });
4270
4535
  } catch (err) {
@@ -4284,56 +4549,49 @@ async function validateOffers(body, gatekeeper) {
4284
4549
  const logger = getLogger();
4285
4550
  const result = safeParse("validate_offers", body, (issue) => issue.message);
4286
4551
  if (!result.success) return failure(new BadRequestError(result.error.issues[0]?.message ?? "Invalid request body"));
4287
- const { offers: rawOffers, calldata } = result.data;
4288
- const results = [];
4552
+ const { offers: rawOffers } = result.data;
4289
4553
  const parsedOffers = [];
4290
- const parsedOfferIndices = [];
4291
- const hasOffers = rawOffers !== void 0;
4292
- if (calldata !== void 0) try {
4293
- const tree = decode$1(calldata);
4294
- for (const [i, offer] of tree.offers.entries()) {
4295
- parsedOffers.push(offer);
4296
- parsedOfferIndices.push(i);
4297
- }
4298
- } catch (err) {
4299
- const message = err instanceof Error ? err.message : String(err);
4300
- return failure(new BadRequestError(`Failed to decode calldata: ${message}`));
4301
- }
4302
- if (hasOffers) for (let i = 0; i < rawOffers.length; i++) {
4554
+ const offerIndexByHash = /* @__PURE__ */ new Map();
4555
+ for (let i = 0; i < rawOffers.length; i++) {
4303
4556
  const rawOffer = rawOffers[i];
4304
4557
  try {
4305
4558
  const offer = fromSnakeCase(rawOffer);
4306
- parsedOffers.push(offer);
4307
- parsedOfferIndices.push(i);
4559
+ if (!offerIndexByHash.has(offer.hash)) {
4560
+ offerIndexByHash.set(offer.hash, i);
4561
+ parsedOffers.push(offer);
4562
+ }
4308
4563
  } catch (err) {
4309
4564
  let message = err instanceof Error ? err.message : String(err);
4310
4565
  if (err instanceof InvalidOfferError) message = err.formattedMessage;
4311
- results[i] = {
4312
- offer_hash: rawOffer?.hash ?? "unknown",
4313
- valid: false,
4314
- rule: "parse_error",
4315
- message
4316
- };
4566
+ return failure(new BadRequestError(`Offer at index ${i} failed to parse: ${message}`));
4317
4567
  }
4318
4568
  }
4319
- if (parsedOffers.length > 0) try {
4320
- const { valid, issues } = await gatekeeper.isAllowed(parsedOffers);
4321
- for (const offer of valid) {
4322
- const originalIndex = parsedOfferIndices[parsedOffers.indexOf(offer)];
4323
- if (originalIndex !== void 0) results[originalIndex] = {
4324
- offer_hash: offer.hash,
4325
- valid: true
4326
- };
4327
- }
4328
- for (const issue of issues) {
4329
- const originalIndex = parsedOfferIndices[parsedOffers.indexOf(issue.item)];
4330
- if (originalIndex !== void 0) results[originalIndex] = {
4331
- offer_hash: issue.item.hash,
4332
- valid: false,
4333
- rule: issue.ruleName,
4334
- message: issue.message
4335
- };
4569
+ try {
4570
+ const { issues } = await gatekeeper.isAllowed(parsedOffers);
4571
+ if (issues.length > 0) {
4572
+ const mappedIssues = issues.map((issue) => {
4573
+ const index$1 = offerIndexByHash.get(issue.item.hash);
4574
+ if (index$1 === void 0) return null;
4575
+ return {
4576
+ index: index$1,
4577
+ rule: issue.ruleName,
4578
+ message: issue.message
4579
+ };
4580
+ }).filter((issue) => issue !== null);
4581
+ return success({
4582
+ data: { issues: mappedIssues },
4583
+ cursor: null
4584
+ });
4336
4585
  }
4586
+ const tree = from$10(parsedOffers);
4587
+ const payload = encodeUnsigned(tree);
4588
+ return success({
4589
+ data: {
4590
+ payload,
4591
+ root: tree.root
4592
+ },
4593
+ cursor: null
4594
+ });
4337
4595
  } catch (err) {
4338
4596
  logger.error({
4339
4597
  err,
@@ -4343,24 +4601,19 @@ async function validateOffers(body, gatekeeper) {
4343
4601
  });
4344
4602
  return failure(err);
4345
4603
  }
4346
- const orderedResults = results.filter((r) => r !== void 0);
4347
- return success({
4348
- data: orderedResults,
4349
- cursor: null
4350
- });
4351
4604
  }
4352
4605
 
4353
4606
  //#endregion
4354
4607
  //#region src/api/Api.ts
4355
4608
  function from(config) {
4356
4609
  const { db, gatekeeper, port } = config;
4357
- return create$10({
4610
+ return create$13({
4358
4611
  port,
4359
4612
  db,
4360
4613
  gatekeeper
4361
4614
  });
4362
4615
  }
4363
- function create$10(params) {
4616
+ function create$13(params) {
4364
4617
  return { serve: () => serve$1(params) };
4365
4618
  }
4366
4619
  /**
@@ -4454,6 +4707,21 @@ function single(name$1, description$1, run$1) {
4454
4707
  run: run$1
4455
4708
  };
4456
4709
  }
4710
+ /**
4711
+ * Create a validation rule iterating over a batch of items at a time.
4712
+ * @param name - The name of the rule.
4713
+ * @param description - A human-readable description of the rule.
4714
+ * @param run - The function that validates the rule.
4715
+ * @returns The created rule.
4716
+ */
4717
+ function batch(name$1, description$1, run$1) {
4718
+ return {
4719
+ kind: "batch",
4720
+ name: name$1,
4721
+ description: description$1,
4722
+ run: run$1
4723
+ };
4724
+ }
4457
4725
  async function run(parameters) {
4458
4726
  const { items, rules, chunkSize } = parameters;
4459
4727
  const issues = [];
@@ -4504,7 +4772,7 @@ async function run(parameters) {
4504
4772
 
4505
4773
  //#endregion
4506
4774
  //#region src/gatekeeper/Gatekeeper.ts
4507
- function create$9(parameters) {
4775
+ function create$12(parameters) {
4508
4776
  return {
4509
4777
  rules: parameters.rules,
4510
4778
  isAllowed: async (offers$1) => {
@@ -4526,8 +4794,8 @@ function create$9(parameters) {
4526
4794
  * @param address - Callback contract address
4527
4795
  * @returns The callback type when found, otherwise undefined
4528
4796
  */
4529
- function getCallbackType(chain, address) {
4530
- return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
4797
+ function getCallbackType(chain, address$1) {
4798
+ return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address$1?.toLowerCase()))?.type;
4531
4799
  }
4532
4800
  /**
4533
4801
  * Returns the list of allowed non-empty callback addresses for a chain.
@@ -4656,10 +4924,29 @@ const token = ({ assets: assets$1 }) => single("token", "Validates that offer lo
4656
4924
  if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
4657
4925
  if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
4658
4926
  });
4927
+ /**
4928
+ * A batch validation rule that ensures all offers in a tree have the same maker (offering address).
4929
+ * Returns an issue only for the first non-conforming offer.
4930
+ * This rule is signing-agnostic; signer verification is handled at the collector level.
4931
+ */
4932
+ const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker (offering address)", (offers$1) => {
4933
+ const issues = /* @__PURE__ */ new Map();
4934
+ if (offers$1.length === 0) return issues;
4935
+ const firstMaker = offers$1[0].offering.toLowerCase();
4936
+ for (let i = 1; i < offers$1.length; i++) {
4937
+ const offer = offers$1[i];
4938
+ if (offer.offering.toLowerCase() !== firstMaker) {
4939
+ issues.set(i, { message: `Offer has different maker ${offer.offering} than first offer ${offers$1[0].offering}` });
4940
+ return issues;
4941
+ }
4942
+ }
4943
+ return issues;
4944
+ });
4659
4945
 
4660
4946
  //#endregion
4661
4947
  //#region src/gatekeeper/morphoRules.ts
4662
4948
  const morphoRules = (chains$3) => [
4949
+ sameMaker(),
4663
4950
  chains$1({ chains: chains$3 }),
4664
4951
  maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
4665
4952
  callback({
@@ -4673,6 +4960,12 @@ const morphoRules = (chains$3) => [
4673
4960
  token({ assets: chains$3.flatMap((c) => assets[c.id.toString()] ?? []) })
4674
4961
  ];
4675
4962
 
4963
+ //#endregion
4964
+ //#region ../../node_modules/.pnpm/tsdown@0.16.5_ms@2.1.3_typescript@5.8.3/node_modules/tsdown/esm-shims.js
4965
+ const getFilename = () => fileURLToPath(import.meta.url);
4966
+ const getDirname = () => path.dirname(getFilename());
4967
+ const __dirname = /* @__PURE__ */ getDirname();
4968
+
4676
4969
  //#endregion
4677
4970
  //#region src/database/drizzle/VERSION.ts
4678
4971
  const VERSION = "router_v1.6";
@@ -4689,15 +4982,19 @@ var schema_exports = /* @__PURE__ */ __export({
4689
4982
  collectors: () => collectors,
4690
4983
  consumedEvents: () => consumedEvents,
4691
4984
  groups: () => groups,
4985
+ lots: () => lots,
4986
+ merklePaths: () => merklePaths,
4692
4987
  obligationCollateralsV2: () => obligationCollateralsV2,
4693
4988
  obligations: () => obligations,
4694
4989
  offers: () => offers,
4695
4990
  offersCallbacks: () => offersCallbacks,
4991
+ offsets: () => offsets,
4696
4992
  oracles: () => oracles,
4697
4993
  positionTypes: () => positionTypes,
4698
4994
  positions: () => positions,
4699
4995
  status: () => status,
4700
4996
  transfers: () => transfers,
4997
+ trees: () => trees,
4701
4998
  validations: () => validations
4702
4999
  });
4703
5000
  const s = pgSchema(VERSION);
@@ -4715,6 +5012,10 @@ var EnumTableName = /* @__PURE__ */ function(EnumTableName$1) {
4715
5012
  EnumTableName$1["VALIDATIONS"] = "validations";
4716
5013
  EnumTableName$1["COLLECTORS"] = "collectors";
4717
5014
  EnumTableName$1["CHAINS"] = "chains";
5015
+ EnumTableName$1["LOTS"] = "lots";
5016
+ EnumTableName$1["OFFSETS"] = "offsets";
5017
+ EnumTableName$1["TREES"] = "trees";
5018
+ EnumTableName$1["MERKLE_PATHS"] = "merkle_paths";
4718
5019
  return EnumTableName$1;
4719
5020
  }(EnumTableName || {});
4720
5021
  const TABLE_NAMES = Object.values(EnumTableName);
@@ -4877,6 +5178,86 @@ const callbacks = s.table(EnumTableName.CALLBACKS, {
4877
5178
  ],
4878
5179
  name: "callbacks_positions_fk"
4879
5180
  }).onDelete("cascade")]);
5181
+ const lots = s.table(EnumTableName.LOTS, {
5182
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5183
+ user: varchar("user", { length: 42 }).notNull(),
5184
+ contract: varchar("contract", { length: 42 }).notNull(),
5185
+ group: varchar("group", { length: 66 }).notNull(),
5186
+ lower: numeric("lower", {
5187
+ precision: 78,
5188
+ scale: 0
5189
+ }).notNull(),
5190
+ upper: numeric("upper", {
5191
+ precision: 78,
5192
+ scale: 0
5193
+ }).notNull()
5194
+ }, (table) => [
5195
+ primaryKey({
5196
+ columns: [
5197
+ table.chainId,
5198
+ table.user,
5199
+ table.contract,
5200
+ table.group
5201
+ ],
5202
+ name: "lots_pk"
5203
+ }),
5204
+ foreignKey({
5205
+ columns: [
5206
+ table.chainId,
5207
+ table.contract,
5208
+ table.user
5209
+ ],
5210
+ foreignColumns: [
5211
+ positions.chainId,
5212
+ positions.contract,
5213
+ positions.user
5214
+ ],
5215
+ name: "lots_positions_fk"
5216
+ }).onDelete("cascade"),
5217
+ foreignKey({
5218
+ columns: [
5219
+ table.chainId,
5220
+ table.user,
5221
+ table.group
5222
+ ],
5223
+ foreignColumns: [
5224
+ groups.chainId,
5225
+ groups.maker,
5226
+ groups.group
5227
+ ],
5228
+ name: "lots_groups_fk"
5229
+ }).onDelete("cascade")
5230
+ ]);
5231
+ const offsets = s.table(EnumTableName.OFFSETS, {
5232
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5233
+ user: varchar("user", { length: 42 }).notNull(),
5234
+ contract: varchar("contract", { length: 42 }).notNull(),
5235
+ group: varchar("group", { length: 66 }).notNull(),
5236
+ value: numeric("value", {
5237
+ precision: 78,
5238
+ scale: 0
5239
+ }).notNull()
5240
+ }, (table) => [primaryKey({
5241
+ columns: [
5242
+ table.chainId,
5243
+ table.user,
5244
+ table.contract,
5245
+ table.group
5246
+ ],
5247
+ name: "offsets_pk"
5248
+ }), foreignKey({
5249
+ columns: [
5250
+ table.chainId,
5251
+ table.contract,
5252
+ table.user
5253
+ ],
5254
+ foreignColumns: [
5255
+ positions.chainId,
5256
+ positions.contract,
5257
+ positions.user
5258
+ ],
5259
+ name: "offsets_positions_fk"
5260
+ }).onDelete("cascade")]);
4880
5261
  const PositionTypes = s.enum("position_type", Object.values(Type));
4881
5262
  const positionTypes = s.table("position_types", {
4882
5263
  id: serial("id").primaryKey(),
@@ -4972,12 +5353,23 @@ const chains = s.table(EnumTableName.CHAINS, {
4972
5353
  }).default("0").notNull(),
4973
5354
  updatedAt: timestamp("updated_at").defaultNow().notNull()
4974
5355
  }, (table) => [uniqueIndex("chains_id_epoch_idx").on(table.chainId, table.epoch)]);
5356
+ const trees = s.table(EnumTableName.TREES, {
5357
+ root: varchar("root", { length: 66 }).primaryKey(),
5358
+ rootSignature: varchar("root_signature", { length: 132 }).notNull(),
5359
+ createdAt: timestamp("created_at").defaultNow().notNull()
5360
+ });
5361
+ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
5362
+ offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
5363
+ treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
5364
+ proofNodes: text("proof_nodes").notNull(),
5365
+ createdAt: timestamp("created_at").defaultNow().notNull()
5366
+ }, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
4975
5367
 
4976
5368
  //#endregion
4977
5369
  //#region src/database/domains/Book.ts
4978
5370
  const DEFAULT_LIMIT$3 = 100;
4979
5371
  const MAX_TOTAL_OFFERS = 500;
4980
- function create$8(config) {
5372
+ function create$11(config) {
4981
5373
  const db = config.db;
4982
5374
  const logger = getLogger();
4983
5375
  const getOffers$1 = async (parameters) => {
@@ -4996,52 +5388,20 @@ function create$8(config) {
4996
5388
  offers: [],
4997
5389
  nextCursor: null
4998
5390
  };
4999
- const effectiveLimit = Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned);
5000
- const book = [];
5001
- const prices = /* @__PURE__ */ new Map();
5002
- const callbackState = /* @__PURE__ */ new Map();
5003
- const positionState = /* @__PURE__ */ new Map();
5004
- let offerCursor = null;
5005
- let hasMoreOffers = false;
5006
- while (book.length < effectiveLimit) {
5007
- const batchSize = (effectiveLimit - book.length) * 2;
5008
- const { offers: rawOffers, nextCursor: rawNextCursor } = await _getOffersWithCallbackIds(db, {
5009
- obligationId: obligationId$1,
5010
- side,
5011
- now: now$1,
5012
- rateSortDirection,
5013
- cursor: offerCursor,
5014
- limit: batchSize
5015
- });
5016
- if (rawOffers.length === 0) break;
5017
- const newCallbackIds = rawOffers.flatMap((o) => o.callbackIds).filter((id$1) => !callbackState.has(id$1));
5018
- await _updateCallbacksByIds(callbackState, db, newCallbackIds);
5019
- await _updatePositionsByKeys(positionState, db, [...new Set(newCallbackIds.map((id$1) => callbackState.get(id$1)?.positionKey).filter((k) => k !== void 0 && !positionState.has(k)))]);
5020
- await _updatePrices(prices, db, _collectNewOracleAddresses(rawOffers, callbackState, positionState, prices));
5021
- const validOffers = _computeCrossInvalidation(rawOffers, callbackState, positionState, prices);
5022
- let isOfferInPreviousPages = inputCursor === null;
5023
- const cursorRate = inputCursor ? BigInt(inputCursor.rate) : 0n;
5024
- for (const offer of validOffers) {
5025
- if (!isOfferInPreviousPages) if (rateSortDirection === "asc" ? offer.rate > cursorRate : offer.rate < cursorRate) isOfferInPreviousPages = true;
5026
- else if (offer.hash === inputCursor.hash) {
5027
- isOfferInPreviousPages = true;
5028
- continue;
5029
- } else continue;
5030
- book.push(offer);
5031
- if (book.length >= effectiveLimit) {
5032
- hasMoreOffers = true;
5033
- break;
5034
- }
5035
- }
5036
- offerCursor = rawNextCursor;
5037
- if (!offerCursor) break;
5038
- }
5039
- const lastReturnedOffer = book[book.length - 1];
5040
- const newTotalReturned = previouslyReturned + book.length;
5391
+ const { offers: offers$1, hasMore } = await _getOffers(db, {
5392
+ obligationId: obligationId$1,
5393
+ side,
5394
+ now: now$1,
5395
+ rateSortDirection,
5396
+ cursor: inputCursor,
5397
+ limit: Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned)
5398
+ });
5399
+ const lastReturnedOffer = offers$1[offers$1.length - 1];
5400
+ const newTotalReturned = previouslyReturned + offers$1.length;
5041
5401
  const hasHitHardLimit = newTotalReturned >= MAX_TOTAL_OFFERS;
5042
5402
  return {
5043
- offers: book,
5044
- nextCursor: book.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMoreOffers ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
5403
+ offers: offers$1,
5404
+ nextCursor: offers$1.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMore ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
5045
5405
  };
5046
5406
  };
5047
5407
  return {
@@ -5088,8 +5448,8 @@ function create$8(config) {
5088
5448
  getOffers: getOffers$1
5089
5449
  };
5090
5450
  }
5091
- /** Get offers with their callback IDs for a given obligation. */
5092
- async function _getOffersWithCallbackIds(db, params) {
5451
+ /** Get offers with computed takeable based on lot balance. */
5452
+ async function _getOffers(db, params) {
5093
5453
  const { obligationId: obligationId$1, side, now: now$1, rateSortDirection, cursor, limit } = params;
5094
5454
  const raw = await db.execute(sql`
5095
5455
  WITH collats AS MATERIALIZED (
@@ -5157,32 +5517,202 @@ async function _getOffersWithCallbackIds(db, params) {
5157
5517
  ORDER BY e.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
5158
5518
  LIMIT ${limit}
5159
5519
  ),
5160
- with_callbacks AS (
5520
+ -- Compute sum of offsets per position
5521
+ position_offsets AS (
5161
5522
  SELECT
5162
- p.hash, p.obligation_id, p.assets, p.rate, p.maturity, p.expiry, p.start,
5163
- p.nonce, p.buy, p.callback_address, p.callback_data, p.block_number,
5164
- p.group_chain_id, p.group_maker, p.consumed, p.chain_id, p.loan_token,
5165
- COALESCE(ARRAY_AGG(oc.callback_id) FILTER (WHERE oc.callback_id IS NOT NULL), '{}') AS callback_ids
5523
+ chain_id,
5524
+ "user",
5525
+ contract,
5526
+ SUM(value::numeric) AS total_offset
5527
+ FROM ${offsets}
5528
+ GROUP BY chain_id, "user", contract
5529
+ ),
5530
+ -- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
5531
+ position_consumed AS (
5532
+ SELECT
5533
+ l.chain_id,
5534
+ l.contract,
5535
+ l."user",
5536
+ SUM(
5537
+ CASE
5538
+ WHEN wo.assets::numeric > 0
5539
+ THEN COALESCE(g.consumed::numeric, 0) * (l.upper::numeric - l.lower::numeric) / wo.assets::numeric
5540
+ ELSE 0
5541
+ END
5542
+ ) AS consumed
5543
+ FROM ${lots} l
5544
+ JOIN ${groups} g
5545
+ ON g.chain_id = l.chain_id
5546
+ AND LOWER(g.maker) = LOWER(l."user")
5547
+ AND g."group" = l."group"
5548
+ JOIN winners wo
5549
+ ON wo.group_chain_id = g.chain_id
5550
+ AND LOWER(wo.group_maker) = LOWER(g.maker)
5551
+ AND wo.group_group = g."group"
5552
+ GROUP BY l.chain_id, l.contract, l."user"
5553
+ ),
5554
+ -- Compute callback contributions with lot balance
5555
+ callback_contributions AS (
5556
+ SELECT
5557
+ p.hash,
5558
+ p.obligation_id,
5559
+ p.assets,
5560
+ p.rate,
5561
+ p.maturity,
5562
+ p.expiry,
5563
+ p.start,
5564
+ p.nonce,
5565
+ p.buy,
5566
+ p.callback_address,
5567
+ p.callback_data,
5568
+ p.block_number,
5569
+ p.group_chain_id,
5570
+ p.group_maker,
5571
+ p.group_group,
5572
+ p.consumed,
5573
+ p.chain_id,
5574
+ p.loan_token,
5575
+ c.id AS callback_id,
5576
+ c.position_chain_id,
5577
+ c.position_contract,
5578
+ c.position_user,
5579
+ c.amount AS callback_amount,
5580
+ pos.balance AS position_balance,
5581
+ pos.asset AS position_asset,
5582
+ l.lower AS lot_lower,
5583
+ l.upper AS lot_upper,
5584
+ -- Compute lot_balance: min(position_balance + offset + position_consumed - lot.lower, lot.size - lot_consumed)
5585
+ -- lot_consumed is converted from loan token to lot terms: consumed * lot_size / assets
5586
+ GREATEST(0, LEAST(
5587
+ COALESCE(pos.balance::numeric, 0) + COALESCE(pos_offsets.total_offset, 0) + COALESCE(pc.consumed, 0) - COALESCE(l.lower::numeric, 0),
5588
+ (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) -
5589
+ CASE
5590
+ WHEN p.assets::numeric > 0
5591
+ THEN COALESCE(p.consumed::numeric, 0) * (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) / p.assets::numeric
5592
+ ELSE 0
5593
+ END
5594
+ )) AS lot_balance
5166
5595
  FROM paged p
5167
5596
  LEFT JOIN ${offersCallbacks} oc ON oc.offer_hash = p.hash
5168
- GROUP BY p.hash, p.obligation_id, p.assets, p.rate, p.maturity, p.expiry, p.start,
5169
- p.nonce, p.buy, p.callback_address, p.callback_data, p.block_number,
5170
- p.group_chain_id, p.group_maker, p.consumed, p.chain_id, p.loan_token
5597
+ LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
5598
+ LEFT JOIN ${lots} l
5599
+ ON l.chain_id = c.position_chain_id
5600
+ AND LOWER(l.contract) = LOWER(c.position_contract)
5601
+ AND LOWER(l."user") = LOWER(c.position_user)
5602
+ AND l."group" = p.group_group
5603
+ LEFT JOIN ${positions} pos
5604
+ ON pos.chain_id = c.position_chain_id
5605
+ AND LOWER(pos.contract) = LOWER(c.position_contract)
5606
+ AND LOWER(pos."user") = LOWER(c.position_user)
5607
+ LEFT JOIN position_offsets pos_offsets
5608
+ ON pos_offsets.chain_id = c.position_chain_id
5609
+ AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
5610
+ AND LOWER(pos_offsets."user") = LOWER(c.position_user)
5611
+ LEFT JOIN position_consumed pc
5612
+ ON pc.chain_id = c.position_chain_id
5613
+ AND LOWER(pc.contract) = LOWER(c.position_contract)
5614
+ AND LOWER(pc."user") = LOWER(c.position_user)
5615
+ ),
5616
+ -- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
5617
+ callback_loan_contribution AS (
5618
+ SELECT
5619
+ cc.*,
5620
+ CASE
5621
+ -- No lot exists: contribution is 0
5622
+ WHEN cc.lot_lower IS NULL THEN 0
5623
+ -- Loan token position: use lot_balance directly, apply callback limit
5624
+ WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
5625
+ LEAST(
5626
+ cc.lot_balance,
5627
+ COALESCE(cc.callback_amount::numeric, cc.lot_balance)
5628
+ )
5629
+ -- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
5630
+ ELSE
5631
+ (
5632
+ LEAST(
5633
+ cc.lot_balance,
5634
+ COALESCE(cc.callback_amount::numeric, cc.lot_balance)
5635
+ ) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
5636
+ ) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
5637
+ END AS contribution_in_loan
5638
+ FROM callback_contributions cc
5639
+ LEFT JOIN ${obligationCollateralsV2} collat_info
5640
+ ON collat_info.obligation_id = cc.obligation_id
5641
+ AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
5642
+ LEFT JOIN ${oracles} collat_oracle
5643
+ ON collat_oracle.chain_id = collat_info.oracle_chain_id
5644
+ AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
5645
+ ),
5646
+ -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
5647
+ offer_contributions AS (
5648
+ SELECT
5649
+ hash,
5650
+ obligation_id,
5651
+ assets,
5652
+ rate,
5653
+ maturity,
5654
+ expiry,
5655
+ start,
5656
+ nonce,
5657
+ buy,
5658
+ callback_address,
5659
+ callback_data,
5660
+ block_number,
5661
+ group_chain_id,
5662
+ group_maker,
5663
+ consumed,
5664
+ chain_id,
5665
+ loan_token,
5666
+ SUM(contribution_in_loan) AS total_available
5667
+ FROM (
5668
+ -- Take max contribution per position using DISTINCT ON (idiomatic PostgreSQL)
5669
+ SELECT DISTINCT ON (clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user)
5670
+ clc.*
5671
+ FROM callback_loan_contribution clc
5672
+ WHERE clc.callback_id IS NOT NULL
5673
+ ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
5674
+ ) deduped
5675
+ GROUP BY hash, obligation_id, assets, rate, maturity, expiry, start, nonce, buy,
5676
+ callback_address, callback_data, block_number, group_chain_id, group_maker,
5677
+ consumed, chain_id, loan_token
5171
5678
  )
5679
+ -- Final SELECT with inline takeable computation
5172
5680
  SELECT
5173
- wc.hash, wc.group_maker, wc.assets, wc.consumed, wc.rate, wc.maturity, wc.expiry, wc.start,
5174
- wc.nonce, wc.buy, wc.chain_id, wc.loan_token, wc.callback_address, wc.callback_data,
5175
- wc.block_number, wc.callback_ids, c.collaterals
5176
- FROM with_callbacks wc
5177
- LEFT JOIN collats c ON c.obligation_id = wc.obligation_id
5681
+ oc.hash,
5682
+ oc.group_maker,
5683
+ oc.assets,
5684
+ oc.consumed,
5685
+ oc.rate,
5686
+ oc.maturity,
5687
+ oc.expiry,
5688
+ oc.start,
5689
+ oc.nonce,
5690
+ oc.buy,
5691
+ oc.chain_id,
5692
+ oc.loan_token,
5693
+ oc.callback_address,
5694
+ oc.callback_data,
5695
+ oc.block_number,
5696
+ -- takeable = min(assets - consumed, total_available)
5697
+ GREATEST(0, LEAST(
5698
+ oc.assets::numeric - oc.consumed::numeric,
5699
+ COALESCE(oc.total_available, 0)
5700
+ )) AS takeable,
5701
+ c.collaterals
5702
+ FROM offer_contributions oc
5703
+ LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
5704
+ WHERE GREATEST(0, LEAST(
5705
+ oc.assets::numeric - oc.consumed::numeric,
5706
+ COALESCE(oc.total_available, 0)
5707
+ )) > 0
5178
5708
  ORDER BY
5179
- wc.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`},
5180
- wc.block_number ASC,
5181
- wc.assets DESC,
5182
- wc.hash ASC;
5709
+ oc.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`},
5710
+ oc.block_number ASC,
5711
+ oc.assets DESC,
5712
+ oc.hash ASC;
5183
5713
  `);
5184
- const offers$1 = raw.rows.map((row) => ({
5185
- ...from$9({
5714
+ return {
5715
+ offers: raw.rows.map((row) => from$9({
5186
5716
  offering: row.group_maker,
5187
5717
  assets: BigInt(row.assets),
5188
5718
  rate: BigInt(row.rate),
@@ -5204,165 +5734,12 @@ async function _getOffersWithCallbackIds(db, params) {
5204
5734
  gasLimit: 0n
5205
5735
  },
5206
5736
  consumed: BigInt(row.consumed),
5207
- takeable: 0n,
5737
+ takeable: BigInt(row.takeable.split(".")[0] ?? "0"),
5208
5738
  blockNumber: row.block_number
5209
- }),
5210
- callbackIds: row.callback_ids ?? []
5211
- }));
5212
- let nextCursor = null;
5213
- if (raw.rows.length === limit) {
5214
- const last = raw.rows[raw.rows.length - 1];
5215
- nextCursor = {
5216
- rate: last.rate,
5217
- blockNumber: last.block_number,
5218
- assets: last.assets,
5219
- hash: last.hash
5220
- };
5221
- }
5222
- return {
5223
- offers: offers$1,
5224
- nextCursor
5739
+ })),
5740
+ hasMore: raw.rows.length === limit
5225
5741
  };
5226
5742
  }
5227
- /** Get callbacks by their IDs. */
5228
- async function _updateCallbacksByIds(state, db, ids) {
5229
- if (ids.length === 0) return;
5230
- const raw = await db.execute(sql`
5231
- SELECT c.id, c.position_chain_id, c.position_contract, c.position_user, c.amount
5232
- FROM ${callbacks} c
5233
- WHERE c.id IN (${sql.join(ids.map((id$1) => sql`${id$1}`), sql`, `)})
5234
- `);
5235
- for (const row of raw.rows) if (!state.has(row.id)) state.set(row.id, {
5236
- positionKey: _buildPositionKey(row.position_chain_id, row.position_contract, row.position_user),
5237
- amount: row.amount != null ? BigInt(row.amount) : null
5238
- });
5239
- }
5240
- /** Get positions by their composite keys. */
5241
- async function _updatePositionsByKeys(state, db, keys) {
5242
- if (keys.length === 0) return;
5243
- const parsedKeys = keys.map((key) => {
5244
- const parts = key.split(":");
5245
- return {
5246
- chainId: BigInt(parts[0]),
5247
- contract: parts[1],
5248
- user: parts[2]
5249
- };
5250
- });
5251
- const raw = await db.execute(sql`
5252
- SELECT p.chain_id, p.contract, p."user", p.balance, p.asset
5253
- FROM ${positions} p
5254
- WHERE (p.chain_id, LOWER(p.contract), LOWER(p."user")) IN (
5255
- ${sql.join(parsedKeys.map((k) => sql`(${k.chainId}, ${k.contract.toLowerCase()}, ${k.user.toLowerCase()})`), sql`, `)}
5256
- )
5257
- `);
5258
- for (const row of raw.rows) {
5259
- const key = _buildPositionKey(row.chain_id, row.contract, row.user);
5260
- if (!state.has(key)) state.set(key, {
5261
- balance: row.balance ? BigInt(row.balance) : 0n,
5262
- remaining: row.balance ? BigInt(row.balance) : 0n,
5263
- asset: row.asset ?? "0x0000000000000000000000000000000000000000"
5264
- });
5265
- }
5266
- }
5267
- /** Get oracle prices by chain_id and address. */
5268
- async function _updatePrices(state, db, oracles$1) {
5269
- if (oracles$1.length === 0) return;
5270
- const raw = await db.execute(sql`
5271
- SELECT o.chain_id, o.address, o.price
5272
- FROM ${oracles} o
5273
- WHERE (o.chain_id, LOWER(o.address)) IN (${sql.join(oracles$1.map((o) => sql`(${o.chainId}, ${o.address.toLowerCase()})`), sql`, `)})
5274
- `);
5275
- for (const row of raw.rows) {
5276
- const key = `${row.chain_id}:${row.address.toLowerCase()}`;
5277
- if (!state.has(key)) state.set(key, row.price ? BigInt(row.price) : 0n);
5278
- }
5279
- }
5280
- /** Build a composite position key from its components. */
5281
- function _buildPositionKey(chainId, contract, user) {
5282
- return `${chainId}:${contract.toLowerCase()}:${user.toLowerCase()}`;
5283
- }
5284
- /** Collect oracle addresses that need to be Geted for collateral positions. */
5285
- function _collectNewOracleAddresses(offers$1, callbackState, positionState, prices) {
5286
- const seen = /* @__PURE__ */ new Set();
5287
- const result = [];
5288
- for (const offer of offers$1) for (const callbackId of offer.callbackIds) {
5289
- const callback$1 = callbackState.get(callbackId);
5290
- if (!callback$1) continue;
5291
- const position = positionState.get(callback$1.positionKey);
5292
- if (!position) continue;
5293
- if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) continue;
5294
- const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
5295
- if (collateral) {
5296
- const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
5297
- if (!prices.has(key) && !seen.has(key)) {
5298
- seen.add(key);
5299
- result.push({
5300
- chainId: offer.chainId,
5301
- address: collateral.oracle.toLowerCase()
5302
- });
5303
- }
5304
- }
5305
- }
5306
- return result;
5307
- }
5308
- /**
5309
- * Compute cross-invalidation for a batch of offers.
5310
- * Deducts consumed liquidity from shared positions and returns offers with takeable amounts.
5311
- */
5312
- function _computeCrossInvalidation(offers$1, callbackState, positionState, prices) {
5313
- const result = [];
5314
- for (const offer of offers$1) {
5315
- const contributions = /* @__PURE__ */ new Map();
5316
- for (const callbackId of offer.callbackIds) {
5317
- const callback$1 = callbackState.get(callbackId);
5318
- if (!callback$1) continue;
5319
- const position = positionState.get(callback$1.positionKey);
5320
- if (!position) continue;
5321
- let conversion;
5322
- if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) conversion = null;
5323
- else {
5324
- const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
5325
- if (!collateral) conversion = {
5326
- price: 0n,
5327
- lltv: 0n
5328
- };
5329
- else {
5330
- const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
5331
- conversion = {
5332
- price: prices.get(key) ?? 0n,
5333
- lltv: collateral.lltv
5334
- };
5335
- }
5336
- }
5337
- const availableFromPosition = conversion === null ? position.remaining : Conversion.collateralToLoan(position.remaining, conversion);
5338
- const callbackLimitInLoanTerms = conversion === null || callback$1.amount === null ? callback$1.amount : Conversion.collateralToLoan(callback$1.amount, conversion);
5339
- const callbackAvailable = callbackLimitInLoanTerms === null ? availableFromPosition : min(availableFromPosition, callbackLimitInLoanTerms);
5340
- const existing = contributions.get(callback$1.positionKey);
5341
- if (existing) existing.available = min(availableFromPosition, max(existing.available, callbackAvailable));
5342
- else contributions.set(callback$1.positionKey, {
5343
- available: callbackAvailable,
5344
- conversion
5345
- });
5346
- }
5347
- let totalAvailable = 0n;
5348
- for (const [, contrib] of contributions) totalAvailable += contrib.available;
5349
- const takeable = min(offer.assets - offer.consumed, totalAvailable);
5350
- if (takeable <= 0n) continue;
5351
- for (const [key, contrib] of contributions) {
5352
- const position = positionState.get(key);
5353
- const proportionalTakeable = totalAvailable > 0n ? contrib.available * takeable / totalAvailable : 0n;
5354
- const toDeduct = contrib.conversion === null ? proportionalTakeable : Conversion.loanToCollateral(proportionalTakeable, contrib.conversion);
5355
- position.remaining = position.remaining - toDeduct;
5356
- if (position.remaining < 0n) position.remaining = 0n;
5357
- }
5358
- const { callbackIds: _, ...cleanOffer } = offer;
5359
- result.push(from$9({
5360
- ...cleanOffer,
5361
- takeable
5362
- }));
5363
- }
5364
- return result;
5365
- }
5366
5743
  let Cursor;
5367
5744
  (function(_Cursor) {
5368
5745
  function encode(offer, totalReturned, now$1, side) {
@@ -5427,7 +5804,7 @@ let LevelCursor;
5427
5804
  //#endregion
5428
5805
  //#region src/database/domains/Chains.ts
5429
5806
  /** Postgres implementation. */
5430
- const create$7 = (config) => {
5807
+ const create$10 = (config) => {
5431
5808
  const db = config.db;
5432
5809
  const logger = getLogger();
5433
5810
  return {
@@ -5483,7 +5860,7 @@ const create$7 = (config) => {
5483
5860
  //#endregion
5484
5861
  //#region src/database/domains/Collectors.ts
5485
5862
  /** Postgres implementation. */
5486
- const create$6 = (config) => {
5863
+ const create$9 = (config) => {
5487
5864
  const db = config.db;
5488
5865
  const logger = getLogger();
5489
5866
  return {
@@ -5576,7 +5953,7 @@ const DEFAULT_BATCH_SIZE = 4e3;
5576
5953
 
5577
5954
  //#endregion
5578
5955
  //#region src/database/domains/Consumed.ts
5579
- function create$5(db) {
5956
+ function create$8(db) {
5580
5957
  return {
5581
5958
  create: async (events) => {
5582
5959
  if (events.length === 0) return;
@@ -5617,10 +5994,55 @@ function create$5(db) {
5617
5994
  };
5618
5995
  }
5619
5996
 
5997
+ //#endregion
5998
+ //#region src/database/domains/Lots.ts
5999
+ function create$7(db) {
6000
+ return {
6001
+ get: async (parameters) => {
6002
+ const { chainId, user, contract, group } = parameters ?? {};
6003
+ const conditions = [];
6004
+ if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
6005
+ if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
6006
+ if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
6007
+ if (group !== void 0) conditions.push(eq(lots.group, group));
6008
+ return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
6009
+ chainId: row.chainId,
6010
+ user: row.user,
6011
+ contract: row.contract,
6012
+ group: row.group,
6013
+ lower: BigInt(row.lower),
6014
+ upper: BigInt(row.upper)
6015
+ }));
6016
+ },
6017
+ create: async (parameters) => {
6018
+ if (parameters.length === 0) return;
6019
+ const lotsByPositionGroup = /* @__PURE__ */ new Map();
6020
+ for (const offer of parameters) {
6021
+ const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
6022
+ const existing = lotsByPositionGroup.get(key);
6023
+ if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
6024
+ }
6025
+ for (const offer of lotsByPositionGroup.values()) if ((await db.select().from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.group, offer.group))).limit(1)).length === 0) {
6026
+ const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase())));
6027
+ const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
6028
+ const newUpper = newLower + offer.size;
6029
+ await db.insert(lots).values({
6030
+ chainId: offer.positionChainId,
6031
+ user: offer.positionUser.toLowerCase(),
6032
+ contract: offer.positionContract.toLowerCase(),
6033
+ group: offer.group,
6034
+ lower: newLower.toString(),
6035
+ upper: newUpper.toString()
6036
+ });
6037
+ }
6038
+ }
6039
+ };
6040
+ }
6041
+
5620
6042
  //#endregion
5621
6043
  //#region src/database/domains/Offers.ts
5622
6044
  const DEFAULT_LIMIT$2 = 100;
5623
- function create$4(config) {
6045
+ function create$6(config) {
5624
6046
  const db = config.db;
5625
6047
  return {
5626
6048
  create: async (offers$1) => {
@@ -5779,6 +6201,22 @@ function create$4(config) {
5779
6201
  amount: callback$1.amount
5780
6202
  })));
5781
6203
  for (const batch$2 of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch$2).onConflictDoNothing();
6204
+ const lotInfos = [];
6205
+ for (const [offerHash, callbacks$1] of offersCallbacksMap.entries()) {
6206
+ const offer = inserted.find((o) => o.hash === offerHash);
6207
+ if (!offer) continue;
6208
+ for (const callback$1 of callbacks$1) {
6209
+ const isLoanPosition = obligationsMap.get(offer.obligationId)?.loanToken.toLowerCase() === callback$1.asset?.toLowerCase();
6210
+ lotInfos.push({
6211
+ positionChainId: callback$1.chainId,
6212
+ positionContract: callback$1.contract,
6213
+ positionUser: callback$1.user,
6214
+ group: offer.group,
6215
+ size: isLoanPosition ? BigInt(offer.assets) : BigInt(callback$1.amount)
6216
+ });
6217
+ }
6218
+ }
6219
+ if (lotInfos.length > 0) await dbTx.lots.create(lotInfos);
5782
6220
  obligationsMap.clear();
5783
6221
  collateralsMap.clear();
5784
6222
  oraclesMap.clear();
@@ -5961,9 +6399,29 @@ function create$4(config) {
5961
6399
  };
5962
6400
  }
5963
6401
 
6402
+ //#endregion
6403
+ //#region src/database/domains/Offsets.ts
6404
+ function create$5(db) {
6405
+ return { get: async (parameters) => {
6406
+ const { chainId, user, contract, group } = parameters ?? {};
6407
+ const conditions = [];
6408
+ if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
6409
+ if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
6410
+ if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
6411
+ if (group !== void 0) conditions.push(eq(offsets.group, group));
6412
+ return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
6413
+ chainId: row.chainId,
6414
+ user: row.user,
6415
+ contract: row.contract,
6416
+ group: row.group,
6417
+ value: BigInt(row.value)
6418
+ }));
6419
+ } };
6420
+ }
6421
+
5964
6422
  //#endregion
5965
6423
  //#region src/database/domains/Oracles.ts
5966
- function create$3(db) {
6424
+ function create$4(db) {
5967
6425
  return {
5968
6426
  get: async ({ chainId }) => {
5969
6427
  return (await db.select({
@@ -6003,7 +6461,7 @@ function create$3(db) {
6003
6461
  //#endregion
6004
6462
  //#region src/database/domains/Positions.ts
6005
6463
  const DEFAULT_LIMIT$1 = 100;
6006
- const create$2 = (db) => {
6464
+ const create$3 = (db) => {
6007
6465
  return {
6008
6466
  upsert: async (positions$1) => {
6009
6467
  const positionsMap = /* @__PURE__ */ new Map();
@@ -6119,7 +6577,7 @@ const create$2 = (db) => {
6119
6577
 
6120
6578
  //#endregion
6121
6579
  //#region src/database/domains/Transfers.ts
6122
- const create$1 = (db) => ({ create: async (transfers$1) => {
6580
+ const create$2 = (db) => ({ create: async (transfers$1) => {
6123
6581
  if (transfers$1.length === 0) return 0;
6124
6582
  return await db.transaction(async (dbTx) => {
6125
6583
  let totalInserted = 0;
@@ -6216,6 +6674,91 @@ const create$1 = (db) => ({ create: async (transfers$1) => {
6216
6674
  });
6217
6675
  } });
6218
6676
 
6677
+ //#endregion
6678
+ //#region src/database/domains/Trees.ts
6679
+ /**
6680
+ * Creates a Trees domain instance for managing merkle tree metadata.
6681
+ *
6682
+ * @param config - Configuration with database instance
6683
+ * @returns TreesDomain instance
6684
+ */
6685
+ function create$1(config) {
6686
+ const db = config.db;
6687
+ return {
6688
+ create: async (trees$1) => {
6689
+ if (trees$1.length === 0) return [];
6690
+ return await db.transaction(async (dbTx) => {
6691
+ const roots = [];
6692
+ for (const { tree, signature } of trees$1) {
6693
+ const root = tree.root.toLowerCase();
6694
+ roots.push(root);
6695
+ await dbTx.insert(trees).values({
6696
+ root,
6697
+ rootSignature: signature.toLowerCase()
6698
+ }).onConflictDoUpdate({
6699
+ target: [trees.root],
6700
+ set: {
6701
+ rootSignature: signature.toLowerCase(),
6702
+ createdAt: sql`NOW()`
6703
+ }
6704
+ });
6705
+ await dbTx.offers.create(tree.offers);
6706
+ const pathRows = proofs(tree).map((proof) => ({
6707
+ offerHash: proof.offer.hash.toLowerCase(),
6708
+ treeRoot: root,
6709
+ proofNodes: concatenateProofs(proof.path)
6710
+ }));
6711
+ for (const batch$2 of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch$2).onConflictDoUpdate({
6712
+ target: [merklePaths.offerHash],
6713
+ set: {
6714
+ treeRoot: sql`excluded.tree_root`,
6715
+ proofNodes: sql`excluded.proof_nodes`,
6716
+ createdAt: sql`NOW()`
6717
+ }
6718
+ });
6719
+ }
6720
+ return roots;
6721
+ });
6722
+ },
6723
+ getAttestations: async (hashes) => {
6724
+ if (hashes.length === 0) return /* @__PURE__ */ new Map();
6725
+ const normalizedHashes = hashes.map((h) => h.toLowerCase());
6726
+ const results = await db.select({
6727
+ offerHash: merklePaths.offerHash,
6728
+ treeRoot: merklePaths.treeRoot,
6729
+ proofNodes: merklePaths.proofNodes,
6730
+ rootSignature: trees.rootSignature
6731
+ }).from(merklePaths).innerJoin(trees, eq(merklePaths.treeRoot, trees.root)).where(inArray(merklePaths.offerHash, normalizedHashes));
6732
+ const attestationMap = /* @__PURE__ */ new Map();
6733
+ for (const row of results) attestationMap.set(row.offerHash, {
6734
+ root: row.treeRoot,
6735
+ signature: row.rootSignature,
6736
+ proof: splitProofs(row.proofNodes)
6737
+ });
6738
+ return attestationMap;
6739
+ }
6740
+ };
6741
+ }
6742
+ /**
6743
+ * Concatenates an array of 32-byte hex hashes into a single hex string.
6744
+ * Empty arrays return "0x".
6745
+ */
6746
+ function concatenateProofs(proofs$1) {
6747
+ if (proofs$1.length === 0) return "0x";
6748
+ return `0x${proofs$1.map((p) => p.slice(2)).join("")}`;
6749
+ }
6750
+ /**
6751
+ * Splits a concatenated hex string back into an array of 32-byte hex hashes.
6752
+ * Returns empty array for "0x" or empty string.
6753
+ */
6754
+ function splitProofs(concatenated) {
6755
+ if (!concatenated || concatenated === "0x" || concatenated.length <= 2) return [];
6756
+ const hex$1 = concatenated.slice(2);
6757
+ const proofs$1 = [];
6758
+ for (let i = 0; i < hex$1.length; i += 64) proofs$1.push(`0x${hex$1.slice(i, i + 64)}`);
6759
+ return proofs$1;
6760
+ }
6761
+
6219
6762
  //#endregion
6220
6763
  //#region src/database/domains/Validations.ts
6221
6764
  const DEFAULT_LIMIT = 100;
@@ -6280,15 +6823,18 @@ function create(db) {
6280
6823
  //#region src/database/Database.ts
6281
6824
  function createDomains(core) {
6282
6825
  return {
6283
- book: create$8({ db: core }),
6284
- collectors: create$6({ db: core }),
6285
- offers: create$4({ db: core }),
6286
- chains: create$7({ db: core }),
6287
- consumed: create$5(core),
6288
- oracles: create$3(core),
6826
+ book: create$11({ db: core }),
6827
+ collectors: create$9({ db: core }),
6828
+ offers: create$6({ db: core }),
6829
+ chains: create$10({ db: core }),
6830
+ consumed: create$8(core),
6831
+ lots: create$7(core),
6832
+ offsets: create$5(core),
6833
+ oracles: create$4(core),
6834
+ trees: create$1({ db: core }),
6289
6835
  validations: create(core),
6290
- positions: create$2(core),
6291
- transfers: create$1(core)
6836
+ positions: create$3(core),
6837
+ transfers: create$2(core)
6292
6838
  };
6293
6839
  }
6294
6840
  const AUGMENT_CACHE = /* @__PURE__ */ new WeakMap();
@@ -6323,10 +6869,22 @@ function augmentWithDomains(base$1) {
6323
6869
  value: dms.consumed,
6324
6870
  enumerable: true
6325
6871
  },
6872
+ lots: {
6873
+ value: dms.lots,
6874
+ enumerable: true
6875
+ },
6876
+ offsets: {
6877
+ value: dms.offsets,
6878
+ enumerable: true
6879
+ },
6326
6880
  oracles: {
6327
6881
  value: dms.oracles,
6328
6882
  enumerable: true
6329
6883
  },
6884
+ trees: {
6885
+ value: dms.trees,
6886
+ enumerable: true
6887
+ },
6330
6888
  validations: {
6331
6889
  value: dms.validations,
6332
6890
  enumerable: true
@@ -6343,6 +6901,7 @@ function augmentWithDomains(base$1) {
6343
6901
  AUGMENT_CACHE.set(base$1, wrapped);
6344
6902
  return wrapped;
6345
6903
  }
6904
+ let cachedInMemoryDatabase;
6346
6905
  /**
6347
6906
  * Connect to the database.
6348
6907
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -6365,15 +6924,17 @@ function connect(connectionString) {
6365
6924
  clean: async () => await clean(driver$1)
6366
6925
  });
6367
6926
  }
6927
+ if (cachedInMemoryDatabase) return cachedInMemoryDatabase;
6368
6928
  const pool = new PGlite();
6369
6929
  const driver = drizzle$1(pool, { schema: schema_exports });
6370
6930
  const core = augmentWithDomains(driver);
6371
- return Object.assign(core, {
6931
+ cachedInMemoryDatabase = Object.assign(core, {
6372
6932
  name: "pglite",
6373
6933
  pool,
6374
6934
  applyMigrations: applyMigrations("pglite", driver),
6375
6935
  clean: async () => await clean(driver)
6376
6936
  });
6937
+ return cachedInMemoryDatabase;
6377
6938
  }
6378
6939
  const MIGRATED_DRIVERS = /* @__PURE__ */ new WeakSet();
6379
6940
  function applyMigrations(kind, driver) {
@@ -6577,6 +7138,35 @@ async function postMigrate(driver) {
6577
7138
  REFERENCING OLD TABLE AS deleted_rows
6578
7139
  FOR EACH STATEMENT
6579
7140
  EXECUTE FUNCTION cleanup_orphan_positions();
7141
+ `);
7142
+ await driver.execute(`
7143
+ CREATE OR REPLACE FUNCTION cleanup_orphan_groups()
7144
+ RETURNS TRIGGER AS $$
7145
+ BEGIN
7146
+ DELETE FROM "${VERSION}"."groups" g
7147
+ USING (
7148
+ SELECT DISTINCT group_chain_id, group_maker, group_group
7149
+ FROM deleted_rows
7150
+ ) AS affected
7151
+ WHERE g.chain_id = affected.group_chain_id
7152
+ AND g.maker = affected.group_maker
7153
+ AND g."group" = affected.group_group
7154
+ AND NOT EXISTS (
7155
+ SELECT 1 FROM "${VERSION}"."offers" o
7156
+ WHERE o.group_chain_id = g.chain_id
7157
+ AND o.group_maker = g.maker
7158
+ AND o.group_group = g."group"
7159
+ );
7160
+ RETURN NULL;
7161
+ END;
7162
+ $$ LANGUAGE plpgsql;
7163
+ `);
7164
+ await driver.execute(`
7165
+ CREATE OR REPLACE TRIGGER trg_cleanup_orphan_groups
7166
+ AFTER DELETE ON "${VERSION}"."offers"
7167
+ REFERENCING OLD TABLE AS deleted_rows
7168
+ FOR EACH STATEMENT
7169
+ EXECUTE FUNCTION cleanup_orphan_groups();
6580
7170
  `);
6581
7171
  await driver.execute(`
6582
7172
  CREATE OR REPLACE FUNCTION cleanup_orphan_obligations_and_oracles()
@@ -6629,13 +7219,64 @@ async function postMigrate(driver) {
6629
7219
  REFERENCING OLD TABLE AS deleted_rows
6630
7220
  FOR EACH STATEMENT
6631
7221
  EXECUTE FUNCTION cleanup_orphan_obligations_and_oracles();
7222
+ `);
7223
+ await driver.execute(`
7224
+ CREATE OR REPLACE FUNCTION create_offset_on_lot_delete()
7225
+ RETURNS trigger
7226
+ LANGUAGE plpgsql AS $$
7227
+ BEGIN
7228
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
7229
+ VALUES (
7230
+ OLD.chain_id,
7231
+ OLD."user",
7232
+ OLD.contract,
7233
+ OLD."group",
7234
+ OLD.upper::numeric - OLD.lower::numeric
7235
+ )
7236
+ ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
7237
+ RETURN OLD;
7238
+ END;
7239
+ $$;
7240
+ `);
7241
+ await driver.execute(`
7242
+ CREATE OR REPLACE TRIGGER trg_lots_create_offset_before_delete
7243
+ BEFORE DELETE ON "${VERSION}"."lots"
7244
+ FOR EACH ROW
7245
+ EXECUTE FUNCTION create_offset_on_lot_delete();
7246
+ `);
7247
+ await driver.execute(`
7248
+ CREATE OR REPLACE FUNCTION delete_position_if_no_lots()
7249
+ RETURNS trigger
7250
+ LANGUAGE plpgsql AS $$
7251
+ BEGIN
7252
+ -- Check if any lots remain on this position
7253
+ IF NOT EXISTS (
7254
+ SELECT 1 FROM "${VERSION}"."lots" l
7255
+ WHERE l.chain_id = OLD.chain_id
7256
+ AND l.contract = OLD.contract
7257
+ AND l."user" = OLD."user"
7258
+ ) THEN
7259
+ -- No lots remain, delete the position (cascades to offsets)
7260
+ DELETE FROM "${VERSION}"."positions" p
7261
+ WHERE p.chain_id = OLD.chain_id
7262
+ AND p.contract = OLD.contract
7263
+ AND p."user" = OLD."user";
7264
+ END IF;
7265
+ RETURN NULL;
7266
+ END;
7267
+ $$;
7268
+ `);
7269
+ await driver.execute(`
7270
+ CREATE OR REPLACE TRIGGER trg_lots_delete_position_if_empty
7271
+ AFTER DELETE ON "${VERSION}"."lots"
7272
+ FOR EACH ROW
7273
+ EXECUTE FUNCTION delete_position_if_no_lots();
6632
7274
  `);
6633
7275
  });
6634
7276
  }
6635
7277
 
6636
7278
  //#endregion
6637
7279
  //#region src/cli/commands/RouterCmd.ts
6638
- init_esm_shims();
6639
7280
  dotenv.config();
6640
7281
  var RouterCmd = class RouterCmd extends Command {
6641
7282
  constructor(name$1) {
@@ -6721,7 +7362,7 @@ mockCmd.description("Start Router mock.").addOption(new Option("--seed <n>", "Se
6721
7362
  file: opts.file
6722
7363
  });
6723
7364
  }
6724
- const gatekeeper = create$9({ rules: morphoRules([client.chain]) });
7365
+ const gatekeeper = create$12({ rules: morphoRules([client.chain]) });
6725
7366
  from({
6726
7367
  db,
6727
7368
  gatekeeper,
@@ -6766,7 +7407,7 @@ startCmd.description("Start Router services.").addOption(new Option("--block-win
6766
7407
  await runWithLogger(logger, async () => {
6767
7408
  const stops = [];
6768
7409
  const selectedServices = new Set(opts.services);
6769
- const gatekeeper = create$9({ rules: morphoRules([client.chain]) });
7410
+ const gatekeeper = create$12({ rules: morphoRules([client.chain]) });
6770
7411
  if (selectedServices.has("indexer")) {
6771
7412
  const indexer = from$1({
6772
7413
  client,