@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.
@@ -43,7 +43,6 @@ require("@opentelemetry/resources");
43
43
  require("@opentelemetry/sdk-trace-node");
44
44
  require("@opentelemetry/semantic-conventions");
45
45
  let viem_chains = require("viem/chains");
46
- let viem_accounts = require("viem/accounts");
47
46
  let zod = require("zod");
48
47
  zod = __toESM(zod);
49
48
  let __openzeppelin_merkle_tree = require("@openzeppelin/merkle-tree");
@@ -440,6 +439,96 @@ function poll(fn, { interval }) {
440
439
  return unwatch;
441
440
  }
442
441
 
442
+ //#endregion
443
+ //#region src/utils/Random.ts
444
+ var Random_exports = /* @__PURE__ */ __export({
445
+ address: () => address,
446
+ bool: () => bool,
447
+ bytes: () => bytes,
448
+ float: () => float,
449
+ hex: () => hex,
450
+ int: () => int,
451
+ seed: () => seed,
452
+ withSeed: () => withSeed
453
+ });
454
+ let currentRng = Math.random;
455
+ const FNV_OFFSET_BASIS = 2166136261;
456
+ const FNV_PRIME = 16777619;
457
+ const hashSeed = (seed$1) => {
458
+ let hash$1 = FNV_OFFSET_BASIS;
459
+ for (let i = 0; i < seed$1.length; i += 1) {
460
+ hash$1 ^= seed$1.charCodeAt(i);
461
+ hash$1 = Math.imul(hash$1, FNV_PRIME);
462
+ }
463
+ return hash$1 >>> 0;
464
+ };
465
+ const createSeededRng = (seed$1) => {
466
+ let state = hashSeed(seed$1);
467
+ return () => {
468
+ state += 1831565813;
469
+ let t = Math.imul(state ^ state >>> 15, state | 1);
470
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
471
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
472
+ };
473
+ };
474
+ /**
475
+ * Runs a function with a deterministic RNG derived from the given seed.
476
+ */
477
+ function withSeed(seed$1, fn) {
478
+ const previous = currentRng;
479
+ currentRng = createSeededRng(seed$1);
480
+ try {
481
+ return fn();
482
+ } finally {
483
+ currentRng = previous;
484
+ }
485
+ }
486
+ /**
487
+ * Seeds the global RNG for deterministic test runs.
488
+ */
489
+ function seed(seed$1) {
490
+ currentRng = createSeededRng(seed$1);
491
+ }
492
+ /**
493
+ * Returns a deterministic random float in [0, 1).
494
+ */
495
+ function float() {
496
+ return currentRng();
497
+ }
498
+ /**
499
+ * Returns a deterministic random integer in [min, maxExclusive).
500
+ */
501
+ function int(maxExclusive, min$1 = 0) {
502
+ return Math.floor(float() * (maxExclusive - min$1)) + min$1;
503
+ }
504
+ /**
505
+ * Returns a deterministic random boolean.
506
+ */
507
+ function bool(probability = .5) {
508
+ return float() < probability;
509
+ }
510
+ /**
511
+ * Returns deterministic random bytes.
512
+ */
513
+ function bytes(length) {
514
+ const output = new Uint8Array(length);
515
+ for (let i = 0; i < length; i += 1) output[i] = int(256);
516
+ return output;
517
+ }
518
+ /**
519
+ * Returns a deterministic random hex string for the given byte length.
520
+ */
521
+ function hex(byteLength) {
522
+ const output = bytes(byteLength);
523
+ return `0x${Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
524
+ }
525
+ /**
526
+ * Returns a deterministic random address.
527
+ */
528
+ function address() {
529
+ return hex(20);
530
+ }
531
+
443
532
  //#endregion
444
533
  //#region src/utils/time.ts
445
534
  var time_exports = /* @__PURE__ */ __export({
@@ -457,6 +546,7 @@ function max() {
457
546
  //#region src/utils/index.ts
458
547
  var utils_exports = /* @__PURE__ */ __export({
459
548
  BaseError: () => BaseError,
549
+ Random: () => Random_exports,
460
550
  ReorgError: () => ReorgError,
461
551
  Time: () => time_exports,
462
552
  batch: () => batch$1,
@@ -474,7 +564,7 @@ var utils_exports = /* @__PURE__ */ __export({
474
564
 
475
565
  //#endregion
476
566
  //#region src/indexer/collectors/Admin.ts
477
- function create$14(parameters) {
567
+ function create$17(parameters) {
478
568
  const collector = "admin";
479
569
  const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
480
570
  const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
@@ -723,8 +813,8 @@ const names = [
723
813
  "positions",
724
814
  "prices"
725
815
  ];
726
- function create$13({ name, collect, client, db, options }) {
727
- const admin = create$14({
816
+ function create$16({ name, collect, client, db, options }) {
817
+ const admin = create$17({
728
818
  client,
729
819
  db,
730
820
  options
@@ -774,7 +864,10 @@ function create$13({ name, collect, client, db, options }) {
774
864
  };
775
865
  });
776
866
  if (done) iterator = null;
777
- else yield blockNumber;
867
+ else {
868
+ lastBlockNumber = blockNumber;
869
+ yield blockNumber;
870
+ }
778
871
  } catch (err) {
779
872
  const isError = err instanceof Error;
780
873
  logger.error({
@@ -986,8 +1079,12 @@ function decode$3(type, data) {
986
1079
  }
987
1080
  function encode$3(type, data) {
988
1081
  switch (type) {
989
- case CallbackType.BuyVaultV1Callback: return encodeBuyVaultV1Callback(data);
990
- case CallbackType.SellERC20Callback: return encodeSellERC20Callback(data);
1082
+ case CallbackType.BuyVaultV1Callback:
1083
+ if (!("vaults" in data)) throw new Error("Invalid callback data");
1084
+ return encodeBuyVaultV1Callback(data);
1085
+ case CallbackType.SellERC20Callback:
1086
+ if (!("collaterals" in data)) throw new Error("Invalid callback data");
1087
+ return encodeSellERC20Callback(data);
991
1088
  default: throw new Error("Invalid callback type");
992
1089
  }
993
1090
  }
@@ -1184,22 +1281,22 @@ const DEFAULT_BATCH_SIZE$2 = 2500;
1184
1281
  const MAX_BLOCK_WINDOW = 1e4;
1185
1282
  const DEFAULT_BLOCK_WINDOW = 8e3;
1186
1283
  async function* streamLogs(parameters) {
1187
- const { client, contractAddress, event, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$2, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
1284
+ const { client, contractAddress, event, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$2, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
1188
1285
  if (maxBatchSize > MAX_BATCH_SIZE) throw new InvalidBatchSizeError(maxBatchSize);
1189
1286
  if (blockWindow > MAX_BLOCK_WINDOW) throw new InvalidBlockWindowError(blockWindow);
1190
- if (order$1 === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
1287
+ if (order === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
1191
1288
  const latestBlock = (await (0, viem_actions.getBlock)(client, {
1192
1289
  blockTag: "latest",
1193
1290
  includeTransactions: false
1194
1291
  })).number;
1195
1292
  let toBlock = 0n;
1196
- if (order$1 === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
1197
- if (order$1 === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
1293
+ if (order === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
1294
+ if (order === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
1198
1295
  let fromBlock = 0n;
1199
- if (order$1 === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
1200
- if (order$1 === "desc") fromBlock = max$1(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
1201
- if (order$1 === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
1202
- if (order$1 === "desc") fromBlock = max$1(fromBlock, toBlock - BigInt(blockWindow));
1296
+ if (order === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
1297
+ if (order === "desc") fromBlock = max$1(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
1298
+ if (order === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
1299
+ if (order === "desc") fromBlock = max$1(fromBlock, toBlock - BigInt(blockWindow));
1203
1300
  if (fromBlock > toBlock) throw new InvalidBlockRangeError(fromBlock, toBlock);
1204
1301
  let streaming = true;
1205
1302
  while (streaming) {
@@ -1209,29 +1306,29 @@ async function* streamLogs(parameters) {
1209
1306
  fromBlock,
1210
1307
  toBlock
1211
1308
  });
1212
- streaming = order$1 === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
1309
+ streaming = order === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
1213
1310
  if (logs.length === 0 && !streaming) break;
1214
1311
  if (logs.length === 0 && streaming) yield {
1215
1312
  logs: [],
1216
- blockNumber: order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
1313
+ blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
1217
1314
  };
1218
1315
  logs.sort((a, b) => {
1219
- if (a.blockNumber !== b.blockNumber) return order$1 === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
1220
- if (a.transactionIndex !== b.transactionIndex) return order$1 === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
1221
- return order$1 === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
1316
+ if (a.blockNumber !== b.blockNumber) return order === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
1317
+ if (a.transactionIndex !== b.transactionIndex) return order === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
1318
+ return order === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
1222
1319
  });
1223
1320
  for (const logBatch of batch$1(logs, maxBatchSize)) yield {
1224
1321
  logs: logBatch,
1225
- blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
1322
+ blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order === "asc" ? Number(toBlock) : Number(fromBlock)
1226
1323
  };
1227
- if (order$1 === "asc") {
1324
+ if (order === "asc") {
1228
1325
  const upperBound = BigInt(blockNumberLte || latestBlock);
1229
1326
  const nextFromBlock = min(BigInt(toBlock) + 1n, upperBound);
1230
1327
  const nextToBlock = min(toBlock + BigInt(blockWindow) + 1n, upperBound);
1231
1328
  fromBlock = nextFromBlock;
1232
1329
  toBlock = nextToBlock;
1233
1330
  }
1234
- if (order$1 === "desc") {
1331
+ if (order === "desc") {
1235
1332
  const lowerBound = BigInt(blockNumberGte || 0);
1236
1333
  const nextToBlock = max$1(fromBlock - 1n, lowerBound);
1237
1334
  const nextFromBlock = max$1(fromBlock - BigInt(blockWindow) - 1n, lowerBound);
@@ -1241,7 +1338,7 @@ async function* streamLogs(parameters) {
1241
1338
  }
1242
1339
  yield {
1243
1340
  logs: [],
1244
- blockNumber: order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
1341
+ blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
1245
1342
  };
1246
1343
  }
1247
1344
  var InvalidBlockRangeError = class extends BaseError {
@@ -1390,8 +1487,8 @@ const from$15 = (parameters) => {
1390
1487
  */
1391
1488
  function random$3() {
1392
1489
  return from$15({
1393
- asset: (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address,
1394
- oracle: (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address,
1490
+ asset: address(),
1491
+ oracle: address(),
1395
1492
  lltv: .965
1396
1493
  });
1397
1494
  }
@@ -1806,12 +1903,8 @@ function id(obligation) {
1806
1903
  function random$2() {
1807
1904
  return from$13({
1808
1905
  chainId: 1,
1809
- loanToken: (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address,
1810
- collaterals: [from$15({
1811
- asset: (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address,
1812
- oracle: (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address,
1813
- lltv: .965
1814
- })],
1906
+ loanToken: address(),
1907
+ collaterals: [random$3()],
1815
1908
  maturity: from$14("end_of_next_quarter")
1816
1909
  });
1817
1910
  }
@@ -1831,101 +1924,249 @@ var CollateralsAreNotSortedError = class extends BaseError {
1831
1924
  //#endregion
1832
1925
  //#region src/core/Tree.ts
1833
1926
  var Tree_exports = /* @__PURE__ */ __export({
1927
+ DecodeError: () => DecodeError,
1928
+ EncodeError: () => EncodeError,
1929
+ TreeError: () => TreeError,
1834
1930
  VERSION: () => VERSION$1,
1835
1931
  decode: () => decode$2,
1836
1932
  encode: () => encode$2,
1837
- from: () => from$12
1933
+ encodeUnsigned: () => encodeUnsigned,
1934
+ from: () => from$12,
1935
+ proofs: () => proofs
1838
1936
  });
1839
1937
  const VERSION$1 = 1;
1938
+ const normalizeHash = (hash$1) => hash$1.toLowerCase();
1840
1939
  /**
1841
1940
  * Builds a Merkle tree from a list of offers.
1842
1941
  *
1843
1942
  * Leaves are the offer `hash` values as `bytes32` and are deterministically
1844
- * ordered in ascending lexicographic order so that the resulting root is stable
1845
- * regardless of the input order.
1943
+ * ordered following the StandardMerkleTree leaf ordering so that the resulting
1944
+ * root is stable regardless of the input order.
1846
1945
  *
1847
1946
  * @param offers - Offers to include in the tree.
1848
1947
  * @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
1948
+ * @throws {TreeError} If tree building fails due to offer inconsistencies.
1849
1949
  */
1850
1950
  const from$12 = (offers$1) => {
1851
- const leaves = order(offers$1).map((offer) => {
1852
- return [offer.hash];
1853
- });
1951
+ const leaves = offers$1.map((offer) => [offer.hash]);
1854
1952
  const tree = __openzeppelin_merkle_tree.StandardMerkleTree.of(leaves, ["bytes32"]);
1855
- return Object.assign(tree, { offers: offers$1 });
1953
+ const orderedOffers = orderOffers(tree, offers$1);
1954
+ return Object.assign(tree, { offers: orderedOffers });
1856
1955
  };
1857
- const byHashAsc = (a, b) => a.localeCompare(b);
1858
- const order = (offers$1) => {
1859
- return offers$1.sort((a, b) => byHashAsc(a.hash, b.hash));
1956
+ const orderOffers = (tree, offers$1) => {
1957
+ const offerByHash = /* @__PURE__ */ new Map();
1958
+ for (const offer of offers$1) offerByHash.set(normalizeHash(offer.hash), offer);
1959
+ const entries = tree.dump().values.map((value) => {
1960
+ const hash$1 = normalizeHash(value.value[0]);
1961
+ const offer = offerByHash.get(hash$1);
1962
+ if (!offer) throw new TreeError(`missing offer for leaf ${hash$1}`);
1963
+ return {
1964
+ offer,
1965
+ treeIndex: value.treeIndex
1966
+ };
1967
+ });
1968
+ entries.sort((a, b) => b.treeIndex - a.treeIndex);
1969
+ return entries.map((item) => item.offer);
1860
1970
  };
1861
1971
  /**
1862
- * Encodes an `Tree` into a Hex string with a version byte prefix and gzipped payload.
1972
+ * Generates merkle proofs for all offers in a tree.
1863
1973
  *
1864
- * - Layout: `0x{vv}{zip...}` where `{vv}` is one-byte version as two hex chars.
1865
- * - Payload is gzip(JSON.stringify([root, ...offers])) with bigint stringified.
1974
+ * Each proof allows independent verification that an offer is included in the tree
1975
+ * without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
1866
1976
  *
1867
- * @param tree - The offer Merkle tree to encode.
1868
- * @returns Hex string starting with `0x{vv}` followed by gzipped payload bytes.
1869
- * @throws Error if the given `root` does not match the offers.
1977
+ * @param tree - The {@link Tree} to generate proofs for.
1978
+ * @returns Array of proofs - {@link Proof}
1870
1979
  */
1871
- const encode$2 = (tree) => {
1872
- assertRoot(tree.root, tree.offers);
1873
- const offersPayload = tree.offers.map((offer) => ({
1874
- offering: offer.offering,
1875
- assets: offer.assets.toString(),
1876
- rate: offer.rate.toString(),
1877
- maturity: Number(offer.maturity),
1878
- expiry: Number(offer.expiry),
1879
- start: Number(offer.start),
1880
- nonce: offer.nonce.toString(),
1881
- buy: offer.buy,
1882
- chainId: offer.chainId,
1883
- loanToken: offer.loanToken,
1884
- collaterals: offer.collaterals.map((c) => ({
1885
- asset: c.asset,
1886
- oracle: c.oracle,
1887
- lltv: c.lltv.toString()
1888
- })),
1889
- callback: {
1890
- address: offer.callback.address,
1891
- data: offer.callback.data,
1892
- gasLimit: offer.callback.gasLimit.toString()
1893
- },
1894
- signature: offer.signature,
1895
- hash: offer.hash
1896
- }));
1897
- const compressed = (0, pako.gzip)(JSON.stringify([tree.root, ...offersPayload]));
1898
- const encoded = new Uint8Array(1 + compressed.length);
1899
- if (VERSION$1 > 255) throw new Error(`Version overflow: ${VERSION$1}`);
1900
- encoded[0] = VERSION$1;
1901
- encoded.set(compressed, 1);
1980
+ const proofs = (tree) => {
1981
+ return tree.offers.map((offer) => {
1982
+ return {
1983
+ offer,
1984
+ path: tree.getProof([offer.hash])
1985
+ };
1986
+ });
1987
+ };
1988
+ const assertHex = (value, expectedBytes, name) => {
1989
+ if (typeof value !== "string" || !(0, viem.isHex)(value)) throw new DecodeError(`${name} is not a valid hex string`);
1990
+ if ((0, viem.hexToBytes)(value).length !== expectedBytes) throw new DecodeError(`${name}: expected ${expectedBytes} bytes`);
1991
+ };
1992
+ const verifySignatureAndRecoverAddress = async (params) => {
1993
+ const { root, signature } = params;
1994
+ assertHex(signature, 65, "signature");
1995
+ const hash$1 = (0, viem.hashMessage)({ raw: root });
1996
+ try {
1997
+ return await (0, viem.recoverAddress)({
1998
+ hash: hash$1,
1999
+ signature
2000
+ });
2001
+ } catch {
2002
+ throw new DecodeError("signature recovery failed");
2003
+ }
2004
+ };
2005
+ /**
2006
+ * Encodes a merkle tree with signature into hex calldata for onchain broadcast.
2007
+ *
2008
+ * Layout: `0x{vv}{gzip([...offers])}{root}{signature}` where:
2009
+ * - `{vv}`: 1-byte version (currently 0x01)
2010
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
2011
+ * - `{root}`: 32-byte merkle root
2012
+ * - `{signature}`: 65-byte EIP-191 signature over raw root bytes
2013
+ *
2014
+ * Validates signature authenticity and root integrity before encoding.
2015
+ *
2016
+ * @example
2017
+ * ```typescript
2018
+ * const tree = Tree.from(offers);
2019
+ * const signature = await wallet.signMessage({ message: { raw: tree.root } });
2020
+ * const calldata = await Tree.encode(tree, signature);
2021
+ * await broadcast(calldata);
2022
+ * ```
2023
+ *
2024
+ * @example
2025
+ * Manual construction (for advanced users):
2026
+ * ```typescript
2027
+ * const tree = Tree.from(offers);
2028
+ * const compressed = gzip(JSON.stringify(tree.offers.map(Offer.serialize)));
2029
+ * const partial = `0x01${bytesToHex(compressed)}${tree.root.slice(2)}`;
2030
+ * const signature = await wallet.signMessage({ message: { raw: tree.root } });
2031
+ * const calldata = `${partial}${signature.slice(2)}`;
2032
+ * ```
2033
+ *
2034
+ * @param tree - Merkle tree of offers
2035
+ * @param signature - EIP-191 signature over raw root bytes
2036
+ * @returns Hex-encoded calldata ready for onchain broadcast
2037
+ * @throws {EncodeError} If signature verification fails or root mismatch
2038
+ */
2039
+ const encode$2 = async (tree, signature) => {
2040
+ validateTreeForEncoding(tree);
2041
+ await verifySignatureAndRecoverAddress({
2042
+ root: tree.root,
2043
+ signature
2044
+ });
2045
+ const unsigned = encodeUnsignedBytes(tree);
2046
+ const sigBytes = (0, viem.hexToBytes)(signature);
2047
+ const encoded = new Uint8Array(unsigned.length + sigBytes.length);
2048
+ encoded.set(unsigned, 0);
2049
+ encoded.set(sigBytes, unsigned.length);
1902
2050
  return (0, viem.bytesToHex)(encoded);
1903
2051
  };
1904
- const assertRoot = (root, offers$1) => {
1905
- const tree = from$12(offers$1);
1906
- if (root !== tree.root) throw new Error(`Invalid root: expected ${tree.root}, got ${root}`);
2052
+ /**
2053
+ * Encodes a merkle tree without a signature into hex payload for client-side signing.
2054
+ *
2055
+ * Layout: `0x{vv}{gzip([...offers])}{root}` where:
2056
+ * - `{vv}`: 1-byte version (currently 0x01)
2057
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
2058
+ * - `{root}`: 32-byte merkle root
2059
+ *
2060
+ * Validates root integrity before encoding.
2061
+ *
2062
+ * @param tree - Merkle tree of offers
2063
+ * @returns Hex-encoded unsigned payload
2064
+ * @throws {EncodeError} If root mismatch
2065
+ */
2066
+ const encodeUnsigned = (tree) => {
2067
+ validateTreeForEncoding(tree);
2068
+ return (0, viem.bytesToHex)(encodeUnsignedBytes(tree));
2069
+ };
2070
+ const validateTreeForEncoding = (tree) => {
2071
+ if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
2072
+ const computed = from$12(tree.offers);
2073
+ if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
2074
+ };
2075
+ const encodeUnsignedBytes = (tree) => {
2076
+ const offersPayload = tree.offers.map(serialize);
2077
+ const compressed = (0, pako.gzip)(JSON.stringify(offersPayload));
2078
+ const rootBytes = (0, viem.hexToBytes)(tree.root);
2079
+ const encoded = new Uint8Array(1 + compressed.length + 32);
2080
+ encoded[0] = VERSION$1;
2081
+ encoded.set(compressed, 1);
2082
+ encoded.set(rootBytes, 1 + compressed.length);
2083
+ return encoded;
1907
2084
  };
1908
2085
  /**
1909
- * Decodes a Hex string produced by {@link encode} back into an `Tree`.
2086
+ * Decodes hex calldata into a validated merkle tree.
1910
2087
  *
1911
- * - Ensures the first byte version matches {@link VERSION}.
1912
- * - Decompresses with gunzip, parses JSON, validates offers, and re-checks the root.
2088
+ * Validates signature before decompression for fail-fast rejection of invalid payloads.
2089
+ * Returns the tree with separately validated signature and recovered signer address.
1913
2090
  *
1914
- * @param encoded - Hex string in the form `0x{vv}{zip...}`.
1915
- * @returns A validated `Tree` rebuilt from the offers.
1916
- * @throws Error if the version is invalid or the root does not match the offers.
2091
+ * Validation order:
2092
+ * 1. Version check
2093
+ * 2. Signature verification (fail-fast, before decompression)
2094
+ * 3. Decompression (only if signature valid)
2095
+ * 4. Root verification (computed from offers vs embedded root)
2096
+ *
2097
+ * @example
2098
+ * ```typescript
2099
+ * const { tree, signature, signer } = await Tree.decode(calldata);
2100
+ * console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
2101
+ * ```
2102
+ *
2103
+ * @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
2104
+ * @returns Validated tree, signature, and recovered signer address
2105
+ * @throws {DecodeError} If version invalid, signature invalid, or root mismatch
2106
+ */
2107
+ const decode$2 = async (encoded) => {
2108
+ const bytes$1 = (0, viem.hexToBytes)(encoded);
2109
+ if (bytes$1.length < 98) throw new DecodeError("payload too short");
2110
+ const version = bytes$1[0];
2111
+ if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
2112
+ const signature = (0, viem.bytesToHex)(bytes$1.slice(-65));
2113
+ const root = (0, viem.bytesToHex)(bytes$1.slice(-97, -65));
2114
+ assertHex(root, 32, "root");
2115
+ assertHex(signature, 65, "signature");
2116
+ const signer = await verifySignatureAndRecoverAddress({
2117
+ root,
2118
+ signature
2119
+ });
2120
+ const compressed = bytes$1.slice(1, -97);
2121
+ let decoded;
2122
+ try {
2123
+ decoded = (0, pako.ungzip)(compressed, { to: "string" });
2124
+ } catch {
2125
+ throw new DecodeError("decompression failed");
2126
+ }
2127
+ let rawOffers;
2128
+ try {
2129
+ rawOffers = JSON.parse(decoded);
2130
+ } catch {
2131
+ throw new DecodeError("JSON parse failed");
2132
+ }
2133
+ const tree = from$12(rawOffers.map((o) => OfferSchema().parse(o)));
2134
+ if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
2135
+ return {
2136
+ tree,
2137
+ signature,
2138
+ signer
2139
+ };
2140
+ };
2141
+ /**
2142
+ * Error thrown during tree building operations.
2143
+ * Indicates structural issues with the tree (missing offers, inconsistent state).
2144
+ */
2145
+ var TreeError = class extends BaseError {
2146
+ name = "Tree.TreeError";
2147
+ constructor(reason) {
2148
+ super(`Tree error: ${reason}`);
2149
+ }
2150
+ };
2151
+ /**
2152
+ * Error thrown during tree encoding.
2153
+ * Indicates validation failures (signature, root mismatch, mixed makers).
2154
+ */
2155
+ var EncodeError = class extends BaseError {
2156
+ name = "Tree.EncodeError";
2157
+ constructor(reason) {
2158
+ super(`Failed to encode tree: ${reason}`);
2159
+ }
2160
+ };
2161
+ /**
2162
+ * Error thrown during tree decoding.
2163
+ * Indicates payload corruption, version mismatch, or validation failures.
1917
2164
  */
1918
- const decode$2 = (encoded) => {
1919
- const bytes = (0, viem.hexToBytes)(encoded);
1920
- if (bytes.length < 2) throw new Error("Invalid payload: too short");
1921
- const version = bytes[0];
1922
- if (version !== (VERSION$1 & 255)) throw new Error(`Invalid version: expected ${VERSION$1}, got ${version}`);
1923
- const decoded = (0, pako.ungzip)(bytes.slice(1), { to: "string" });
1924
- const data = JSON.parse(decoded);
1925
- const root = data[0];
1926
- const tree = from$12(data.slice(1).map((o) => OfferSchema().parse(o)));
1927
- if (root !== tree.root) throw new Error(`Invalid root: expected ${tree.root}, got ${root}`);
1928
- return tree;
2165
+ var DecodeError = class extends BaseError {
2166
+ name = "Tree.DecodeError";
2167
+ constructor(reason) {
2168
+ super(`Failed to decode tree: ${reason}`);
2169
+ }
1929
2170
  };
1930
2171
 
1931
2172
  //#endregion
@@ -1945,6 +2186,7 @@ var Offer_exports = /* @__PURE__ */ __export({
1945
2186
  hash: () => hash,
1946
2187
  obligationId: () => obligationId,
1947
2188
  random: () => random$1,
2189
+ serialize: () => serialize,
1948
2190
  sign: () => sign,
1949
2191
  signatureMsg: () => signatureMsg,
1950
2192
  toSnakeCase: () => toSnakeCase,
@@ -2025,16 +2267,47 @@ function toSnakeCase(offer) {
2025
2267
  return toSnakeCase$1(offer);
2026
2268
  }
2027
2269
  /**
2270
+ * Serializes an offer for merkle tree encoding.
2271
+ * Converts BigInt fields to strings for JSON compatibility.
2272
+ *
2273
+ * @param offer - Offer to serialize
2274
+ * @returns JSON-serializable offer object
2275
+ */
2276
+ const serialize = (offer) => ({
2277
+ offering: offer.offering,
2278
+ assets: offer.assets.toString(),
2279
+ rate: offer.rate.toString(),
2280
+ maturity: Number(offer.maturity),
2281
+ expiry: Number(offer.expiry),
2282
+ start: Number(offer.start),
2283
+ nonce: offer.nonce.toString(),
2284
+ buy: offer.buy,
2285
+ chainId: offer.chainId,
2286
+ loanToken: offer.loanToken,
2287
+ collaterals: offer.collaterals.map((c) => ({
2288
+ asset: c.asset,
2289
+ oracle: c.oracle,
2290
+ lltv: c.lltv.toString()
2291
+ })),
2292
+ callback: {
2293
+ address: offer.callback.address,
2294
+ data: offer.callback.data,
2295
+ gasLimit: offer.callback.gasLimit.toString()
2296
+ },
2297
+ signature: offer.signature,
2298
+ hash: offer.hash
2299
+ });
2300
+ /**
2028
2301
  * Generates a random Offer.
2029
2302
  * The returned Offer contains randomly generated values.
2030
2303
  * @warning The generated Offer should not be used for production usage.
2031
2304
  * @returns {Offer} A randomly generated Offer object.
2032
2305
  */
2033
2306
  function random$1(config) {
2034
- const chain = config?.chains ? config.chains[Math.floor(Math.random() * config.chains.length)] : chains$2.ethereum;
2035
- const loanToken = config?.loanTokens ? config.loanTokens[Math.floor(Math.random() * config.loanTokens.length)] : (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address;
2036
- const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [(0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address];
2037
- const collateralAsset = collateralCandidates[Math.floor(Math.random() * collateralCandidates.length)];
2307
+ const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
2308
+ const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
2309
+ const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
2310
+ const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
2038
2311
  const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
2039
2312
  const maturity$1 = config?.maturity ?? from$14(maturityOption);
2040
2313
  const lltv = from$16(weightedChoice([
@@ -2048,7 +2321,7 @@ function random$1(config) {
2048
2321
  [.965, 4],
2049
2322
  [.98, 2]
2050
2323
  ]));
2051
- const buy = config?.buy !== void 0 ? config.buy : Math.random() > .5;
2324
+ const buy = config?.buy !== void 0 ? config.buy : bool();
2052
2325
  const ONE = 1000000000000000000n;
2053
2326
  const qMin = buy ? 16 : 4;
2054
2327
  const len = (buy ? 32 : 16) - qMin + 1;
@@ -2059,9 +2332,9 @@ function random$1(config) {
2059
2332
  const rate = config?.rate ?? weightedChoice(ratePairs);
2060
2333
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
2061
2334
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2062
- const amountBase = BigInt(100 + Math.floor(Math.random() * 999901));
2335
+ const amountBase = BigInt(100 + int(999901));
2063
2336
  const assetsScaled = config?.assets ?? amountBase * unit;
2064
- const consumed = config?.consumed !== void 0 ? config.consumed : Math.random() < .8 ? 0n : assetsScaled * BigInt(1 + Math.floor(Math.random() * 900)) / 1000n;
2337
+ const consumed = config?.consumed !== void 0 ? config.consumed : float() < .8 ? 0n : assetsScaled * BigInt(1 + int(900)) / 1000n;
2065
2338
  const callbackBySide = (() => {
2066
2339
  if (buy) return {
2067
2340
  address: viem.zeroAddress,
@@ -2080,29 +2353,29 @@ function random$1(config) {
2080
2353
  };
2081
2354
  })();
2082
2355
  return from$11({
2083
- offering: config?.offering ?? (0, viem_accounts.privateKeyToAccount)((0, viem_accounts.generatePrivateKey)()).address,
2356
+ offering: config?.offering ?? address(),
2084
2357
  assets: assetsScaled,
2085
2358
  rate,
2086
2359
  maturity: maturity$1,
2087
2360
  expiry: config?.expiry ?? maturity$1 - 1,
2088
2361
  start: config?.start ?? maturity$1 - 10,
2089
- nonce: BigInt(Math.floor(Math.random() * 1e6)),
2362
+ nonce: BigInt(int(1e6)),
2090
2363
  buy,
2091
2364
  chainId: chain.id,
2092
2365
  loanToken,
2093
- collaterals: config?.collaterals ?? Array.from({ length: Math.floor(Math.random() * 3) + 1 }, () => ({
2366
+ collaterals: config?.collaterals ?? Array.from({ length: int(3) + 1 }, () => ({
2094
2367
  ...random$3(),
2095
2368
  lltv
2096
2369
  })).sort((a, b) => a.asset.localeCompare(b.asset)),
2097
2370
  callback: config?.callback ?? callbackBySide,
2098
2371
  consumed,
2099
2372
  takeable: config?.takeable ?? assetsScaled - consumed,
2100
- blockNumber: config?.blockNumber ?? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
2373
+ blockNumber: config?.blockNumber ?? int(Number.MAX_SAFE_INTEGER)
2101
2374
  });
2102
2375
  }
2103
2376
  const weightedChoice = (pairs) => {
2104
2377
  const total = pairs.reduce((sum, [, weight]) => sum + weight, 0);
2105
- let roll = Math.random() * total;
2378
+ let roll = float() * total;
2106
2379
  for (const [value, weight] of pairs) {
2107
2380
  roll -= weight;
2108
2381
  if (roll < 0) return value;
@@ -2570,8 +2843,8 @@ function fromSnakeCase(snake) {
2570
2843
  function random() {
2571
2844
  return from$8({
2572
2845
  obligationId: id(random$2()),
2573
- ask: { rate: BigInt(Math.floor(Math.random() * 1e6)) },
2574
- bid: { rate: BigInt(Math.floor(Math.random() * 1e6)) }
2846
+ ask: { rate: BigInt(int(1e6)) },
2847
+ bid: { rate: BigInt(int(1e6)) }
2575
2848
  });
2576
2849
  }
2577
2850
  var InvalidQuoteError = class extends BaseError {
@@ -2755,30 +3028,79 @@ async function* collectOffersV2(parameters) {
2755
3028
  });
2756
3029
  for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
2757
3030
  blockNumber = lastStreamBlockNumber;
2758
- const offers$1 = [];
3031
+ const decodedTrees = [];
2759
3032
  for (const log of logs) {
2760
3033
  if (!log) continue;
2761
3034
  const [payload] = (0, viem.decodeAbiParameters)([{ type: "bytes" }], log.data);
2762
3035
  try {
2763
- const tree = decode$2(payload);
2764
- for (const offer of tree.offers) offers$1.push({
3036
+ const { tree, signature, signer } = await decode$2(payload);
3037
+ const signerMismatch = tree.offers.find((offer) => offer.offering.toLowerCase() !== signer.toLowerCase());
3038
+ if (signerMismatch) {
3039
+ logger.debug({
3040
+ msg: "Tree rejected: signer mismatch",
3041
+ reason: "signer_mismatch",
3042
+ signer,
3043
+ offering: signerMismatch.offering,
3044
+ chain_id: client.chain.id
3045
+ });
3046
+ continue;
3047
+ }
3048
+ const offersWithBlock = tree.offers.map((offer) => ({
2765
3049
  ...offer,
2766
3050
  blockNumber: Number(log.blockNumber)
3051
+ }));
3052
+ const treeWithBlock = Object.assign(Object.create(Object.getPrototypeOf(tree)), tree, { offers: offersWithBlock });
3053
+ decodedTrees.push({
3054
+ tree: treeWithBlock,
3055
+ signature,
3056
+ signer,
3057
+ blockNumber: Number(log.blockNumber)
2767
3058
  });
2768
- } catch (_) {}
3059
+ } catch (err) {
3060
+ const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
3061
+ logger.debug({
3062
+ msg: "Tree decode failed",
3063
+ reason,
3064
+ chain_id: client.chain.id,
3065
+ err: err instanceof Error ? err.message : String(err)
3066
+ });
3067
+ }
2769
3068
  }
2770
3069
  await db.transaction(async (dbTx) => {
2771
3070
  const { epoch, blockNumber: latestBlockNumber } = await dbTx.chains.getBlockNumber(client.chain.id);
2772
- let validOffers = [];
2773
- try {
2774
- validOffers = (await gatekeeper.isAllowed(offers$1)).valid.filter((offer) => offer.blockNumber <= latestBlockNumber);
3071
+ const treesToInsert = [];
3072
+ let totalValidOffers = 0;
3073
+ for (const { tree, signature } of decodedTrees) try {
3074
+ const allowedResults = await gatekeeper.isAllowed(tree.offers);
3075
+ const hasBlockWindowViolation = tree.offers.some((offer) => offer.blockNumber > latestBlockNumber);
3076
+ if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
3077
+ if (allowedResults.issues.length > 0) {
3078
+ const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
3079
+ logger.debug({
3080
+ msg: "Tree offers rejected by gatekeeper",
3081
+ reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
3082
+ chain_id: client.chain.id,
3083
+ issues_count: allowedResults.issues.length
3084
+ });
3085
+ } else if (hasBlockWindowViolation) logger.debug({
3086
+ msg: "Tree rejected: offers outside block window",
3087
+ reason: "block_window",
3088
+ chain_id: client.chain.id
3089
+ });
3090
+ continue;
3091
+ }
3092
+ treesToInsert.push({
3093
+ tree,
3094
+ signature
3095
+ });
3096
+ totalValidOffers += tree.offers.length;
2775
3097
  } catch (err) {
2776
3098
  logger.error({
2777
3099
  err,
2778
- msg: "Failed to validate offers"
3100
+ msg: "Failed to validate offers for tree"
2779
3101
  });
2780
3102
  }
2781
- await dbTx.offers.create(validOffers);
3103
+ if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
2782
3104
  try {
2783
3105
  await dbTx.collectors.saveBlockNumber({
2784
3106
  collectorName: collector,
@@ -2786,10 +3108,11 @@ async function* collectOffersV2(parameters) {
2786
3108
  blockNumber,
2787
3109
  epoch
2788
3110
  });
2789
- if (validOffers.length > 0) logger.info({
3111
+ if (totalValidOffers > 0) logger.info({
2790
3112
  msg: `New offers`,
2791
3113
  collector,
2792
- count: validOffers.length,
3114
+ count: totalValidOffers,
3115
+ trees_count: treesToInsert.length,
2793
3116
  chain_id: client.chain.id,
2794
3117
  block_range: [startBlock, lastStreamBlockNumber]
2795
3118
  });
@@ -3432,7 +3755,7 @@ async function* collectPrices(parameters) {
3432
3755
  //#region src/indexer/collectors/CollectorBuilder.ts
3433
3756
  function createBuilder(parameters) {
3434
3757
  const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow } = {} } = parameters;
3435
- const createCollector = (name, collect) => create$13({
3758
+ const createCollector = (name, collect) => create$16({
3436
3759
  name,
3437
3760
  collect,
3438
3761
  client,
@@ -3520,7 +3843,7 @@ const from$6 = (parameters) => {
3520
3843
  //#endregion
3521
3844
  //#region src/indexer/Indexer.ts
3522
3845
  var Indexer_exports = /* @__PURE__ */ __export({
3523
- create: () => create$12,
3846
+ create: () => create$15,
3524
3847
  from: () => from$5
3525
3848
  });
3526
3849
  function from$5(config) {
@@ -3535,7 +3858,7 @@ function from$5(config) {
3535
3858
  retryAttempts,
3536
3859
  retryDelayMs
3537
3860
  });
3538
- return create$12({
3861
+ return create$15({
3539
3862
  db,
3540
3863
  client,
3541
3864
  collectors: [
@@ -3547,7 +3870,7 @@ function from$5(config) {
3547
3870
  interval
3548
3871
  });
3549
3872
  }
3550
- function create$12(params) {
3873
+ function create$15(params) {
3551
3874
  const { db, collectors: collectors$1, interval, client } = params;
3552
3875
  const logger = getLogger();
3553
3876
  const indexerId = `${client.chain.id.toString()}.indexer`;
@@ -3604,12 +3927,12 @@ function create$12(params) {
3604
3927
 
3605
3928
  //#endregion
3606
3929
  //#region src/api/Health.ts
3607
- var Health_exports = /* @__PURE__ */ __export({ create: () => create$11 });
3930
+ var Health_exports = /* @__PURE__ */ __export({ create: () => create$14 });
3608
3931
  const DEFAULT_MAX_ALLOWED_LAG = 5;
3609
3932
  /**
3610
3933
  * Create a health service that exposes collector and chain block numbers.
3611
3934
  */
3612
- function create$11(parameters) {
3935
+ function create$14(parameters) {
3613
3936
  const { db, maxAllowedLag = DEFAULT_MAX_ALLOWED_LAG, healthClients } = parameters;
3614
3937
  const loadSnapshot = async () => {
3615
3938
  const [collectorRows, chainRows, remoteBlockByChainId] = await Promise.all([
@@ -3756,13 +4079,16 @@ var OfferResponse_exports = /* @__PURE__ */ __export({ from: () => from$2 });
3756
4079
  * Creates an `OfferResponse` from an `Offer`.
3757
4080
  * @constructor
3758
4081
  * @param offer - {@link Offer}
4082
+ * @param attestation - {@link Attestation}
3759
4083
  * @returns The created `OfferResponse`. {@link OfferResponse}
3760
4084
  */
3761
- function from$2(offer) {
3762
- const result = toSnakeCase$1(offer);
4085
+ function from$2(offer, attestation) {
4086
+ const { signature: _, ...rest } = toSnakeCase$1(offer);
3763
4087
  return {
3764
- ...result,
3765
- signature: result.signature ?? null
4088
+ ...rest,
4089
+ root: attestation?.root.toLowerCase() ?? null,
4090
+ proof: attestation?.proof.map((p) => p.toLowerCase()) ?? null,
4091
+ signature: attestation?.signature.toLowerCase() ?? null
3766
4092
  };
3767
4093
  }
3768
4094
 
@@ -3900,10 +4226,12 @@ const offerExample = {
3900
4226
  data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000",
3901
4227
  gas_limit: "500000"
3902
4228
  },
3903
- signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400",
3904
4229
  consumed: "0",
3905
4230
  takeable: "369216000000000000000000",
3906
- block_number: 0xa7495128adfb1
4231
+ block_number: 0xa7495128adfb1,
4232
+ root: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
4233
+ proof: ["0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"],
4234
+ signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400"
3907
4235
  };
3908
4236
  const collectorsHealthExample = {
3909
4237
  name: "offers",
@@ -4048,6 +4376,16 @@ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4048
4376
  type: "number",
4049
4377
  example: offerExample.block_number
4050
4378
  })], OfferListItemResponse.prototype, "block_number", void 0);
4379
+ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4380
+ type: "string",
4381
+ nullable: true,
4382
+ example: offerExample.root
4383
+ })], OfferListItemResponse.prototype, "root", void 0);
4384
+ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4385
+ type: [String],
4386
+ nullable: true,
4387
+ example: offerExample.proof
4388
+ })], OfferListItemResponse.prototype, "proof", void 0);
4051
4389
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4052
4390
  type: "string",
4053
4391
  nullable: true,
@@ -4231,44 +4569,61 @@ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4231
4569
  var ValidateOffersRequest = class {};
4232
4570
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4233
4571
  type: () => [ValidateOfferRequest],
4234
- description: "Array of offers in snake_case format. Mutually exclusive with 'calldata'.",
4235
- required: false
4572
+ description: "Array of offers in snake_case format. Required, non-empty.",
4573
+ required: true
4236
4574
  })], ValidateOffersRequest.prototype, "offers", void 0);
4575
+ var ValidationSuccessDataResponse = class {};
4237
4576
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4238
4577
  type: "string",
4239
- description: "Encoded tree calldata as a hex string (0x-prefixed). Mutually exclusive with 'offers'.",
4240
- example: "0x01...",
4241
- required: false
4242
- })], ValidateOffersRequest.prototype, "calldata", void 0);
4243
- var ValidateOfferResultResponse = class {};
4578
+ description: "Unsigned payload: version (1B) + gzip(offers) + root (32B).",
4579
+ example: "0x01789c..."
4580
+ })], ValidationSuccessDataResponse.prototype, "payload", void 0);
4244
4581
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4245
4582
  type: "string",
4246
- example: offerExample.hash
4247
- })], ValidateOfferResultResponse.prototype, "offer_hash", void 0);
4583
+ description: "Merkle tree root to sign with EIP-191.",
4584
+ example: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427"
4585
+ })], ValidationSuccessDataResponse.prototype, "root", void 0);
4586
+ var ValidationSuccessResponse = class extends SuccessResponse {};
4248
4587
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4249
- type: "boolean",
4250
- example: false
4251
- })], ValidateOfferResultResponse.prototype, "valid", void 0);
4588
+ type: "string",
4589
+ nullable: true,
4590
+ example: null
4591
+ })], ValidationSuccessResponse.prototype, "cursor", void 0);
4592
+ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4593
+ type: () => ValidationSuccessDataResponse,
4594
+ description: "Payload and root for client-side signing."
4595
+ })], ValidationSuccessResponse.prototype, "data", void 0);
4596
+ var ValidationIssueResponse = class {};
4597
+ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4598
+ type: "number",
4599
+ description: "0-indexed position of the failed offer in the request array.",
4600
+ example: 0
4601
+ })], ValidationIssueResponse.prototype, "index", void 0);
4252
4602
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4253
4603
  type: "string",
4254
- example: "parse_error",
4255
- nullable: true
4256
- })], ValidateOfferResultResponse.prototype, "rule", void 0);
4604
+ description: "Gatekeeper rule name that rejected the offer.",
4605
+ example: "no_buy"
4606
+ })], ValidationIssueResponse.prototype, "rule", void 0);
4257
4607
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4258
4608
  type: "string",
4259
- example: "Invalid offer. 'offering': invalid address",
4260
- nullable: true
4261
- })], ValidateOfferResultResponse.prototype, "message", void 0);
4262
- var ValidateOffersListResponse = class extends SuccessResponse {};
4609
+ description: "Human-readable rejection reason.",
4610
+ example: "Buy offers are not supported"
4611
+ })], ValidationIssueResponse.prototype, "message", void 0);
4612
+ var ValidationFailureDataResponse = class {};
4613
+ __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4614
+ type: () => [ValidationIssueResponse],
4615
+ description: "List of validation issues. Returned when any offer fails validation."
4616
+ })], ValidationFailureDataResponse.prototype, "issues", void 0);
4617
+ var ValidationFailureResponse = class extends SuccessResponse {};
4263
4618
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4264
4619
  type: "string",
4265
4620
  nullable: true,
4266
4621
  example: null
4267
- })], ValidateOffersListResponse.prototype, "cursor", void 0);
4622
+ })], ValidationFailureResponse.prototype, "cursor", void 0);
4268
4623
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4269
- type: () => [ValidateOfferResultResponse],
4270
- description: "Validation results for each offer."
4271
- })], ValidateOffersListResponse.prototype, "data", void 0);
4624
+ type: () => ValidationFailureDataResponse,
4625
+ description: "List of validation issues. Returned when any offer fails validation."
4626
+ })], ValidationFailureResponse.prototype, "data", void 0);
4272
4627
  var BookLevelResponse = class {};
4273
4628
  __decorate([(0, openapi_metadata_decorators.ApiProperty)({
4274
4629
  type: "string",
@@ -4333,13 +4688,18 @@ __decorate([
4333
4688
  methods: ["post"],
4334
4689
  path: "/v1/validate",
4335
4690
  summary: "Validate offers",
4336
- description: "Validates offers against router validation rules. Returns validation status for each offer. Accepts either an array of offers or encoded calldata (mutually exclusive)."
4691
+ description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
4337
4692
  }),
4338
4693
  (0, openapi_metadata_decorators.ApiBody)({ type: ValidateOffersRequest }),
4339
4694
  (0, openapi_metadata_decorators.ApiResponse)({
4340
4695
  status: 200,
4341
4696
  description: "Success",
4342
- type: ValidateOffersListResponse
4697
+ type: ValidationSuccessResponse
4698
+ }),
4699
+ (0, openapi_metadata_decorators.ApiResponse)({
4700
+ status: 200,
4701
+ description: "Validation issues",
4702
+ type: ValidationFailureResponse
4343
4703
  })
4344
4704
  ], ValidateController.prototype, "validateOffers", null);
4345
4705
  ValidateController = __decorate([(0, openapi_metadata_decorators.ApiTags)("Validate"), (0, openapi_metadata_decorators.ApiResponse)({
@@ -4508,7 +4868,7 @@ const OpenApi = async (options = {}) => {
4508
4868
  if (options.rules && options.rules.length > 0) {
4509
4869
  const rulesDescription = options.rules.map((rule) => `- **${rule.name}**: ${rule.description}`).join("\n");
4510
4870
  const validatePath = document.paths?.["/v1/validate"];
4511
- 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}`;
4871
+ 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}`;
4512
4872
  }
4513
4873
  return document;
4514
4874
  };
@@ -4520,17 +4880,23 @@ var Cursor_exports = /* @__PURE__ */ __export({
4520
4880
  encode: () => encode,
4521
4881
  validate: () => validate
4522
4882
  });
4523
- function validate(cursor) {
4524
- if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
4525
- const c = cursor;
4526
- if (![
4883
+ const isSort = (value) => {
4884
+ return [
4527
4885
  "rate",
4528
4886
  "maturity",
4529
4887
  "expiry",
4530
4888
  "amount"
4531
- ].includes(c.sort)) throw new Error(`Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`);
4532
- if (!["asc", "desc"].includes(c.dir)) throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
4533
- 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`);
4889
+ ].includes(value);
4890
+ };
4891
+ function validate(cursor) {
4892
+ if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
4893
+ const c = cursor;
4894
+ const sort = c.sort;
4895
+ const dir = c.dir;
4896
+ const hash$1 = c.hash;
4897
+ if (typeof sort !== "string" || !isSort(sort)) throw new Error(`Invalid sort field: ${String(sort)}. Must be one of: rate, maturity, expiry, amount`);
4898
+ if (typeof dir !== "string" || !["asc", "desc"].includes(dir)) throw new Error(`Invalid direction: ${String(dir)}. Must be one of: asc, desc`);
4899
+ 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`);
4534
4900
  const validation = {
4535
4901
  rate: {
4536
4902
  field: "rate",
@@ -4547,24 +4913,30 @@ function validate(cursor) {
4547
4913
  maturity: {
4548
4914
  field: "maturity",
4549
4915
  type: "number",
4550
- validator: (val) => val > 0,
4916
+ min: 1,
4551
4917
  error: "positive number"
4552
4918
  },
4553
4919
  expiry: {
4554
4920
  field: "expiry",
4555
4921
  type: "number",
4556
- validator: (val) => val > 0,
4922
+ min: 1,
4557
4923
  error: "positive number"
4558
4924
  }
4559
- }[c.sort];
4560
- if (!validation) throw new Error(`Invalid sort field: ${c.sort}`);
4925
+ }[sort];
4926
+ if (!validation) throw new Error(`Invalid sort field: ${sort}`);
4561
4927
  const fieldValue = c[validation.field];
4562
- if (!fieldValue) throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
4563
- if (typeof fieldValue !== validation.type) throw new Error(`${c.sort} sort requires '${validation.field}' field of type ${validation.type}`);
4564
- if (validation.pattern && !validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
4565
- if (validation.validator && !validation.validator(fieldValue)) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
4566
- if (c.page !== void 0) {
4567
- if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) throw new Error("Invalid page: must be a positive integer");
4928
+ if (fieldValue === void 0 || fieldValue === null) throw new Error(`${sort} sort requires '${validation.field}' field to be present`);
4929
+ if (validation.type === "string") {
4930
+ if (typeof fieldValue !== "string") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
4931
+ if (!validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
4932
+ }
4933
+ if (validation.type === "number") {
4934
+ if (typeof fieldValue !== "number") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
4935
+ if (fieldValue < validation.min) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
4936
+ }
4937
+ const page = c.page;
4938
+ if (page !== void 0) {
4939
+ if (typeof page !== "number" || !Number.isInteger(page) || page < 1) throw new Error("Invalid page: must be a positive integer");
4568
4940
  }
4569
4941
  return true;
4570
4942
  }
@@ -4680,21 +5052,7 @@ const schemas = {
4680
5052
  get_obligations: GetObligationsQueryParams,
4681
5053
  get_obligation: GetObligationParams,
4682
5054
  get_book: GetBookParams,
4683
- validate_offers: zod.object({
4684
- offers: zod.any().refine((val) => val === void 0 || Array.isArray(val), { message: "'offers' must be an array" }),
4685
- calldata: zod.string().regex(/^0x[a-fA-F0-9]*$/, { message: "'calldata' must be a hex string starting with '0x'" }).optional()
4686
- }).superRefine((val, ctx) => {
4687
- const hasOffers = val.offers !== void 0;
4688
- const hasCalldata = val.calldata !== void 0;
4689
- if (hasOffers && hasCalldata) ctx.addIssue({
4690
- code: "custom",
4691
- message: "Request body must contain either 'offers' or 'calldata', not both"
4692
- });
4693
- if (!hasOffers && !hasCalldata) ctx.addIssue({
4694
- code: "custom",
4695
- message: "Request body must contain either 'offers' array or 'calldata' hex string"
4696
- });
4697
- })
5055
+ validate_offers: zod.object({ offers: zod.array(zod.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict()
4698
5056
  };
4699
5057
  function parse(action, query) {
4700
5058
  return schemas[action].parse(query);
@@ -4777,7 +5135,7 @@ async function getDocsHtml({ gatekeeper }) {
4777
5135
  async function getHealth(db) {
4778
5136
  const logger = getLogger();
4779
5137
  try {
4780
- const status$1 = await create$11({ db }).getStatus();
5138
+ const status$1 = await create$14({ db }).getStatus();
4781
5139
  return success({ data: toSnakeCase$1({ status: status$1 }) });
4782
5140
  } catch (err) {
4783
5141
  logger.error({
@@ -4792,7 +5150,7 @@ async function getHealth(db) {
4792
5150
  async function getHealthChains(db, healthClients) {
4793
5151
  const logger = getLogger();
4794
5152
  try {
4795
- const chains$3 = await create$11({
5153
+ const chains$3 = await create$14({
4796
5154
  db,
4797
5155
  healthClients
4798
5156
  }).getChains();
@@ -4815,7 +5173,7 @@ async function getHealthChains(db, healthClients) {
4815
5173
  async function getHealthCollectors(db) {
4816
5174
  const logger = getLogger();
4817
5175
  try {
4818
- const collectors$1 = await create$11({ db }).getCollectors();
5176
+ const collectors$1 = await create$14({ db }).getCollectors();
4819
5177
  return success({ data: collectors$1.map(({ name, chainId, blockNumber, updatedAt, lag, status: status$1 }) => toSnakeCase$1({
4820
5178
  name,
4821
5179
  chainId,
@@ -4916,8 +5274,10 @@ async function getOffers$1(queryParameters, db) {
4916
5274
  cursor: query.cursor,
4917
5275
  limit: query.limit
4918
5276
  });
5277
+ const hashes = offers$1.map((offer) => offer.hash);
5278
+ const attestationMap = await db.trees.getAttestations(hashes);
4919
5279
  return success({
4920
- data: offers$1.map(from$2),
5280
+ data: offers$1.map((offer) => from$2(offer, attestationMap.get(offer.hash.toLowerCase()))),
4921
5281
  cursor: nextCursor ?? null
4922
5282
  });
4923
5283
  } catch (err) {
@@ -4937,56 +5297,49 @@ async function validateOffers(body, gatekeeper) {
4937
5297
  const logger = getLogger();
4938
5298
  const result = safeParse("validate_offers", body, (issue) => issue.message);
4939
5299
  if (!result.success) return failure(new BadRequestError(result.error.issues[0]?.message ?? "Invalid request body"));
4940
- const { offers: rawOffers, calldata } = result.data;
4941
- const results = [];
5300
+ const { offers: rawOffers } = result.data;
4942
5301
  const parsedOffers = [];
4943
- const parsedOfferIndices = [];
4944
- const hasOffers = rawOffers !== void 0;
4945
- if (calldata !== void 0) try {
4946
- const tree = decode$2(calldata);
4947
- for (const [i, offer] of tree.offers.entries()) {
4948
- parsedOffers.push(offer);
4949
- parsedOfferIndices.push(i);
4950
- }
4951
- } catch (err) {
4952
- const message = err instanceof Error ? err.message : String(err);
4953
- return failure(new BadRequestError(`Failed to decode calldata: ${message}`));
4954
- }
4955
- if (hasOffers) for (let i = 0; i < rawOffers.length; i++) {
5302
+ const offerIndexByHash = /* @__PURE__ */ new Map();
5303
+ for (let i = 0; i < rawOffers.length; i++) {
4956
5304
  const rawOffer = rawOffers[i];
4957
5305
  try {
4958
5306
  const offer = fromSnakeCase$1(rawOffer);
4959
- parsedOffers.push(offer);
4960
- parsedOfferIndices.push(i);
5307
+ if (!offerIndexByHash.has(offer.hash)) {
5308
+ offerIndexByHash.set(offer.hash, i);
5309
+ parsedOffers.push(offer);
5310
+ }
4961
5311
  } catch (err) {
4962
5312
  let message = err instanceof Error ? err.message : String(err);
4963
5313
  if (err instanceof InvalidOfferError) message = err.formattedMessage;
4964
- results[i] = {
4965
- offer_hash: rawOffer?.hash ?? "unknown",
4966
- valid: false,
4967
- rule: "parse_error",
4968
- message
4969
- };
5314
+ return failure(new BadRequestError(`Offer at index ${i} failed to parse: ${message}`));
4970
5315
  }
4971
5316
  }
4972
- if (parsedOffers.length > 0) try {
4973
- const { valid, issues } = await gatekeeper.isAllowed(parsedOffers);
4974
- for (const offer of valid) {
4975
- const originalIndex = parsedOfferIndices[parsedOffers.indexOf(offer)];
4976
- if (originalIndex !== void 0) results[originalIndex] = {
4977
- offer_hash: offer.hash,
4978
- valid: true
4979
- };
4980
- }
4981
- for (const issue of issues) {
4982
- const originalIndex = parsedOfferIndices[parsedOffers.indexOf(issue.item)];
4983
- if (originalIndex !== void 0) results[originalIndex] = {
4984
- offer_hash: issue.item.hash,
4985
- valid: false,
4986
- rule: issue.ruleName,
4987
- message: issue.message
4988
- };
5317
+ try {
5318
+ const { issues } = await gatekeeper.isAllowed(parsedOffers);
5319
+ if (issues.length > 0) {
5320
+ const mappedIssues = issues.map((issue) => {
5321
+ const index$1 = offerIndexByHash.get(issue.item.hash);
5322
+ if (index$1 === void 0) return null;
5323
+ return {
5324
+ index: index$1,
5325
+ rule: issue.ruleName,
5326
+ message: issue.message
5327
+ };
5328
+ }).filter((issue) => issue !== null);
5329
+ return success({
5330
+ data: { issues: mappedIssues },
5331
+ cursor: null
5332
+ });
4989
5333
  }
5334
+ const tree = from$12(parsedOffers);
5335
+ const payload = encodeUnsigned(tree);
5336
+ return success({
5337
+ data: {
5338
+ payload,
5339
+ root: tree.root
5340
+ },
5341
+ cursor: null
5342
+ });
4990
5343
  } catch (err) {
4991
5344
  logger.error({
4992
5345
  err,
@@ -4996,11 +5349,6 @@ async function validateOffers(body, gatekeeper) {
4996
5349
  });
4997
5350
  return failure(err);
4998
5351
  }
4999
- const orderedResults = results.filter((r) => r !== void 0);
5000
- return success({
5001
- data: orderedResults,
5002
- cursor: null
5003
- });
5004
5352
  }
5005
5353
 
5006
5354
  //#endregion
@@ -5022,13 +5370,13 @@ var Controllers_exports = /* @__PURE__ */ __export({
5022
5370
  //#region src/api/Api.ts
5023
5371
  function from$1(config) {
5024
5372
  const { db, gatekeeper, port } = config;
5025
- return create$10({
5373
+ return create$13({
5026
5374
  port,
5027
5375
  db,
5028
5376
  gatekeeper
5029
5377
  });
5030
5378
  }
5031
- function create$10(params) {
5379
+ function create$13(params) {
5032
5380
  return { serve: () => serve(params) };
5033
5381
  }
5034
5382
  /**
@@ -5123,7 +5471,7 @@ var RouterApi_exports = /* @__PURE__ */ __export({
5123
5471
  OpenApi: () => OpenApi,
5124
5472
  RouterStatusResponse: () => RouterStatusResponse,
5125
5473
  ValidateController: () => ValidateController,
5126
- create: () => create$10,
5474
+ create: () => create$13,
5127
5475
  from: () => from$1,
5128
5476
  parse: () => parse,
5129
5477
  safeParse: () => safeParse
@@ -5188,24 +5536,28 @@ async function getOffers(apiClient, parameters) {
5188
5536
  throw new HttpGetApiFailedError(`GET request returned ${response.status}`, { details: JSON.stringify(error) });
5189
5537
  }
5190
5538
  const offers$1 = data?.data.map((item) => {
5191
- const { signature, ...rest } = item;
5192
- return fromSnakeCase$1({
5193
- ...rest,
5194
- offering: item.offering,
5195
- maturity: from$14(item.maturity),
5196
- loan_token: item.loan_token,
5197
- collaterals: item.collaterals.map((collateral) => ({
5198
- asset: collateral.asset,
5199
- oracle: collateral.oracle,
5200
- lltv: collateral.lltv
5201
- })),
5202
- callback: {
5203
- ...item.callback,
5204
- address: item.callback.address,
5205
- data: item.callback.data
5206
- },
5207
- ...signature !== null ? { signature: item.signature } : void 0
5208
- });
5539
+ const { root, proof, signature, ...rest } = item;
5540
+ return {
5541
+ ...fromSnakeCase$1({
5542
+ ...rest,
5543
+ offering: item.offering,
5544
+ maturity: from$14(item.maturity),
5545
+ loan_token: item.loan_token,
5546
+ collaterals: item.collaterals.map((collateral) => ({
5547
+ asset: collateral.asset,
5548
+ oracle: collateral.oracle,
5549
+ lltv: collateral.lltv
5550
+ })),
5551
+ callback: {
5552
+ ...item.callback,
5553
+ address: item.callback.address,
5554
+ data: item.callback.data
5555
+ },
5556
+ signature: signature?.toLowerCase()
5557
+ }),
5558
+ root: root?.toLowerCase() || void 0,
5559
+ proof: proof?.map((p) => p.toLowerCase()) || void 0
5560
+ };
5209
5561
  }) ?? [];
5210
5562
  return {
5211
5563
  cursor: data?.cursor ?? null,
@@ -5299,15 +5651,19 @@ var schema_exports = /* @__PURE__ */ __export({
5299
5651
  collectors: () => collectors,
5300
5652
  consumedEvents: () => consumedEvents,
5301
5653
  groups: () => groups,
5654
+ lots: () => lots,
5655
+ merklePaths: () => merklePaths,
5302
5656
  obligationCollateralsV2: () => obligationCollateralsV2,
5303
5657
  obligations: () => obligations,
5304
5658
  offers: () => offers,
5305
5659
  offersCallbacks: () => offersCallbacks,
5660
+ offsets: () => offsets,
5306
5661
  oracles: () => oracles,
5307
5662
  positionTypes: () => positionTypes,
5308
5663
  positions: () => positions,
5309
5664
  status: () => status,
5310
5665
  transfers: () => transfers,
5666
+ trees: () => trees,
5311
5667
  validations: () => validations
5312
5668
  });
5313
5669
  const s = (0, drizzle_orm_pg_core.pgSchema)(VERSION);
@@ -5325,6 +5681,10 @@ var EnumTableName = /* @__PURE__ */ function(EnumTableName$1) {
5325
5681
  EnumTableName$1["VALIDATIONS"] = "validations";
5326
5682
  EnumTableName$1["COLLECTORS"] = "collectors";
5327
5683
  EnumTableName$1["CHAINS"] = "chains";
5684
+ EnumTableName$1["LOTS"] = "lots";
5685
+ EnumTableName$1["OFFSETS"] = "offsets";
5686
+ EnumTableName$1["TREES"] = "trees";
5687
+ EnumTableName$1["MERKLE_PATHS"] = "merkle_paths";
5328
5688
  return EnumTableName$1;
5329
5689
  }(EnumTableName || {});
5330
5690
  const TABLE_NAMES = Object.values(EnumTableName);
@@ -5487,6 +5847,86 @@ const callbacks = s.table(EnumTableName.CALLBACKS, {
5487
5847
  ],
5488
5848
  name: "callbacks_positions_fk"
5489
5849
  }).onDelete("cascade")]);
5850
+ const lots = s.table(EnumTableName.LOTS, {
5851
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
5852
+ user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
5853
+ contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
5854
+ group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
5855
+ lower: (0, drizzle_orm_pg_core.numeric)("lower", {
5856
+ precision: 78,
5857
+ scale: 0
5858
+ }).notNull(),
5859
+ upper: (0, drizzle_orm_pg_core.numeric)("upper", {
5860
+ precision: 78,
5861
+ scale: 0
5862
+ }).notNull()
5863
+ }, (table) => [
5864
+ (0, drizzle_orm_pg_core.primaryKey)({
5865
+ columns: [
5866
+ table.chainId,
5867
+ table.user,
5868
+ table.contract,
5869
+ table.group
5870
+ ],
5871
+ name: "lots_pk"
5872
+ }),
5873
+ (0, drizzle_orm_pg_core.foreignKey)({
5874
+ columns: [
5875
+ table.chainId,
5876
+ table.contract,
5877
+ table.user
5878
+ ],
5879
+ foreignColumns: [
5880
+ positions.chainId,
5881
+ positions.contract,
5882
+ positions.user
5883
+ ],
5884
+ name: "lots_positions_fk"
5885
+ }).onDelete("cascade"),
5886
+ (0, drizzle_orm_pg_core.foreignKey)({
5887
+ columns: [
5888
+ table.chainId,
5889
+ table.user,
5890
+ table.group
5891
+ ],
5892
+ foreignColumns: [
5893
+ groups.chainId,
5894
+ groups.maker,
5895
+ groups.group
5896
+ ],
5897
+ name: "lots_groups_fk"
5898
+ }).onDelete("cascade")
5899
+ ]);
5900
+ const offsets = s.table(EnumTableName.OFFSETS, {
5901
+ chainId: (0, drizzle_orm_pg_core.bigint)("chain_id", { mode: "number" }).$type().notNull(),
5902
+ user: (0, drizzle_orm_pg_core.varchar)("user", { length: 42 }).notNull(),
5903
+ contract: (0, drizzle_orm_pg_core.varchar)("contract", { length: 42 }).notNull(),
5904
+ group: (0, drizzle_orm_pg_core.varchar)("group", { length: 66 }).notNull(),
5905
+ value: (0, drizzle_orm_pg_core.numeric)("value", {
5906
+ precision: 78,
5907
+ scale: 0
5908
+ }).notNull()
5909
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({
5910
+ columns: [
5911
+ table.chainId,
5912
+ table.user,
5913
+ table.contract,
5914
+ table.group
5915
+ ],
5916
+ name: "offsets_pk"
5917
+ }), (0, drizzle_orm_pg_core.foreignKey)({
5918
+ columns: [
5919
+ table.chainId,
5920
+ table.contract,
5921
+ table.user
5922
+ ],
5923
+ foreignColumns: [
5924
+ positions.chainId,
5925
+ positions.contract,
5926
+ positions.user
5927
+ ],
5928
+ name: "offsets_positions_fk"
5929
+ }).onDelete("cascade")]);
5490
5930
  const PositionTypes = s.enum("position_type", Object.values(Type));
5491
5931
  const positionTypes = s.table("position_types", {
5492
5932
  id: (0, drizzle_orm_pg_core.serial)("id").primaryKey(),
@@ -5582,6 +6022,17 @@ const chains$1 = s.table(EnumTableName.CHAINS, {
5582
6022
  }).default("0").notNull(),
5583
6023
  updatedAt: (0, drizzle_orm_pg_core.timestamp)("updated_at").defaultNow().notNull()
5584
6024
  }, (table) => [(0, drizzle_orm_pg_core.uniqueIndex)("chains_id_epoch_idx").on(table.chainId, table.epoch)]);
6025
+ const trees = s.table(EnumTableName.TREES, {
6026
+ root: (0, drizzle_orm_pg_core.varchar)("root", { length: 66 }).primaryKey(),
6027
+ rootSignature: (0, drizzle_orm_pg_core.varchar)("root_signature", { length: 132 }).notNull(),
6028
+ createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
6029
+ });
6030
+ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
6031
+ offerHash: (0, drizzle_orm_pg_core.varchar)("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
6032
+ treeRoot: (0, drizzle_orm_pg_core.varchar)("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
6033
+ proofNodes: (0, drizzle_orm_pg_core.text)("proof_nodes").notNull(),
6034
+ createdAt: (0, drizzle_orm_pg_core.timestamp)("created_at").defaultNow().notNull()
6035
+ }, (table) => [(0, drizzle_orm_pg_core.index)("merkle_paths_tree_root_idx").on(table.treeRoot)]);
5585
6036
 
5586
6037
  //#endregion
5587
6038
  //#region src/database/drizzle/index.ts
@@ -5596,15 +6047,19 @@ var drizzle_exports = /* @__PURE__ */ __export({
5596
6047
  collectors: () => collectors,
5597
6048
  consumedEvents: () => consumedEvents,
5598
6049
  groups: () => groups,
6050
+ lots: () => lots,
6051
+ merklePaths: () => merklePaths,
5599
6052
  obligationCollateralsV2: () => obligationCollateralsV2,
5600
6053
  obligations: () => obligations,
5601
6054
  offers: () => offers,
5602
6055
  offersCallbacks: () => offersCallbacks,
6056
+ offsets: () => offsets,
5603
6057
  oracles: () => oracles,
5604
6058
  positionTypes: () => positionTypes,
5605
6059
  positions: () => positions,
5606
6060
  status: () => status,
5607
6061
  transfers: () => transfers,
6062
+ trees: () => trees,
5608
6063
  validations: () => validations
5609
6064
  });
5610
6065
 
@@ -5612,7 +6067,7 @@ var drizzle_exports = /* @__PURE__ */ __export({
5612
6067
  //#region src/database/domains/Book.ts
5613
6068
  const DEFAULT_LIMIT$3 = 100;
5614
6069
  const MAX_TOTAL_OFFERS = 500;
5615
- function create$9(config) {
6070
+ function create$12(config) {
5616
6071
  const db = config.db;
5617
6072
  const logger = getLogger();
5618
6073
  const getOffers$2 = async (parameters) => {
@@ -5631,52 +6086,20 @@ function create$9(config) {
5631
6086
  offers: [],
5632
6087
  nextCursor: null
5633
6088
  };
5634
- const effectiveLimit = Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned);
5635
- const book = [];
5636
- const prices = /* @__PURE__ */ new Map();
5637
- const callbackState = /* @__PURE__ */ new Map();
5638
- const positionState = /* @__PURE__ */ new Map();
5639
- let offerCursor = null;
5640
- let hasMoreOffers = false;
5641
- while (book.length < effectiveLimit) {
5642
- const batchSize = (effectiveLimit - book.length) * 2;
5643
- const { offers: rawOffers, nextCursor: rawNextCursor } = await _getOffersWithCallbackIds(db, {
5644
- obligationId: obligationId$1,
5645
- side,
5646
- now: now$1,
5647
- rateSortDirection,
5648
- cursor: offerCursor,
5649
- limit: batchSize
5650
- });
5651
- if (rawOffers.length === 0) break;
5652
- const newCallbackIds = rawOffers.flatMap((o) => o.callbackIds).filter((id$1) => !callbackState.has(id$1));
5653
- await _updateCallbacksByIds(callbackState, db, newCallbackIds);
5654
- await _updatePositionsByKeys(positionState, db, [...new Set(newCallbackIds.map((id$1) => callbackState.get(id$1)?.positionKey).filter((k) => k !== void 0 && !positionState.has(k)))]);
5655
- await _updatePrices(prices, db, _collectNewOracleAddresses(rawOffers, callbackState, positionState, prices));
5656
- const validOffers = _computeCrossInvalidation(rawOffers, callbackState, positionState, prices);
5657
- let isOfferInPreviousPages = inputCursor === null;
5658
- const cursorRate = inputCursor ? BigInt(inputCursor.rate) : 0n;
5659
- for (const offer of validOffers) {
5660
- if (!isOfferInPreviousPages) if (rateSortDirection === "asc" ? offer.rate > cursorRate : offer.rate < cursorRate) isOfferInPreviousPages = true;
5661
- else if (offer.hash === inputCursor.hash) {
5662
- isOfferInPreviousPages = true;
5663
- continue;
5664
- } else continue;
5665
- book.push(offer);
5666
- if (book.length >= effectiveLimit) {
5667
- hasMoreOffers = true;
5668
- break;
5669
- }
5670
- }
5671
- offerCursor = rawNextCursor;
5672
- if (!offerCursor) break;
5673
- }
5674
- const lastReturnedOffer = book[book.length - 1];
5675
- const newTotalReturned = previouslyReturned + book.length;
6089
+ const { offers: offers$1, hasMore } = await _getOffers(db, {
6090
+ obligationId: obligationId$1,
6091
+ side,
6092
+ now: now$1,
6093
+ rateSortDirection,
6094
+ cursor: inputCursor,
6095
+ limit: Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned)
6096
+ });
6097
+ const lastReturnedOffer = offers$1[offers$1.length - 1];
6098
+ const newTotalReturned = previouslyReturned + offers$1.length;
5676
6099
  const hasHitHardLimit = newTotalReturned >= MAX_TOTAL_OFFERS;
5677
6100
  return {
5678
- offers: book,
5679
- nextCursor: book.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMoreOffers ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
6101
+ offers: offers$1,
6102
+ nextCursor: offers$1.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMore ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
5680
6103
  };
5681
6104
  };
5682
6105
  return {
@@ -5723,8 +6146,8 @@ function create$9(config) {
5723
6146
  getOffers: getOffers$2
5724
6147
  };
5725
6148
  }
5726
- /** Get offers with their callback IDs for a given obligation. */
5727
- async function _getOffersWithCallbackIds(db, params) {
6149
+ /** Get offers with computed takeable based on lot balance. */
6150
+ async function _getOffers(db, params) {
5728
6151
  const { obligationId: obligationId$1, side, now: now$1, rateSortDirection, cursor, limit } = params;
5729
6152
  const raw = await db.execute(drizzle_orm.sql`
5730
6153
  WITH collats AS MATERIALIZED (
@@ -5792,32 +6215,202 @@ async function _getOffersWithCallbackIds(db, params) {
5792
6215
  ORDER BY e.rate ${rateSortDirection === "asc" ? drizzle_orm.sql`ASC` : drizzle_orm.sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
5793
6216
  LIMIT ${limit}
5794
6217
  ),
5795
- with_callbacks AS (
6218
+ -- Compute sum of offsets per position
6219
+ position_offsets AS (
6220
+ SELECT
6221
+ chain_id,
6222
+ "user",
6223
+ contract,
6224
+ SUM(value::numeric) AS total_offset
6225
+ FROM ${offsets}
6226
+ GROUP BY chain_id, "user", contract
6227
+ ),
6228
+ -- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
6229
+ position_consumed AS (
5796
6230
  SELECT
5797
- p.hash, p.obligation_id, p.assets, p.rate, p.maturity, p.expiry, p.start,
5798
- p.nonce, p.buy, p.callback_address, p.callback_data, p.block_number,
5799
- p.group_chain_id, p.group_maker, p.consumed, p.chain_id, p.loan_token,
5800
- COALESCE(ARRAY_AGG(oc.callback_id) FILTER (WHERE oc.callback_id IS NOT NULL), '{}') AS callback_ids
6231
+ l.chain_id,
6232
+ l.contract,
6233
+ l."user",
6234
+ SUM(
6235
+ CASE
6236
+ WHEN wo.assets::numeric > 0
6237
+ THEN COALESCE(g.consumed::numeric, 0) * (l.upper::numeric - l.lower::numeric) / wo.assets::numeric
6238
+ ELSE 0
6239
+ END
6240
+ ) AS consumed
6241
+ FROM ${lots} l
6242
+ JOIN ${groups} g
6243
+ ON g.chain_id = l.chain_id
6244
+ AND LOWER(g.maker) = LOWER(l."user")
6245
+ AND g."group" = l."group"
6246
+ JOIN winners wo
6247
+ ON wo.group_chain_id = g.chain_id
6248
+ AND LOWER(wo.group_maker) = LOWER(g.maker)
6249
+ AND wo.group_group = g."group"
6250
+ GROUP BY l.chain_id, l.contract, l."user"
6251
+ ),
6252
+ -- Compute callback contributions with lot balance
6253
+ callback_contributions AS (
6254
+ SELECT
6255
+ p.hash,
6256
+ p.obligation_id,
6257
+ p.assets,
6258
+ p.rate,
6259
+ p.maturity,
6260
+ p.expiry,
6261
+ p.start,
6262
+ p.nonce,
6263
+ p.buy,
6264
+ p.callback_address,
6265
+ p.callback_data,
6266
+ p.block_number,
6267
+ p.group_chain_id,
6268
+ p.group_maker,
6269
+ p.group_group,
6270
+ p.consumed,
6271
+ p.chain_id,
6272
+ p.loan_token,
6273
+ c.id AS callback_id,
6274
+ c.position_chain_id,
6275
+ c.position_contract,
6276
+ c.position_user,
6277
+ c.amount AS callback_amount,
6278
+ pos.balance AS position_balance,
6279
+ pos.asset AS position_asset,
6280
+ l.lower AS lot_lower,
6281
+ l.upper AS lot_upper,
6282
+ -- Compute lot_balance: min(position_balance + offset + position_consumed - lot.lower, lot.size - lot_consumed)
6283
+ -- lot_consumed is converted from loan token to lot terms: consumed * lot_size / assets
6284
+ GREATEST(0, LEAST(
6285
+ COALESCE(pos.balance::numeric, 0) + COALESCE(pos_offsets.total_offset, 0) + COALESCE(pc.consumed, 0) - COALESCE(l.lower::numeric, 0),
6286
+ (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) -
6287
+ CASE
6288
+ WHEN p.assets::numeric > 0
6289
+ THEN COALESCE(p.consumed::numeric, 0) * (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) / p.assets::numeric
6290
+ ELSE 0
6291
+ END
6292
+ )) AS lot_balance
5801
6293
  FROM paged p
5802
6294
  LEFT JOIN ${offersCallbacks} oc ON oc.offer_hash = p.hash
5803
- GROUP BY p.hash, p.obligation_id, p.assets, p.rate, p.maturity, p.expiry, p.start,
5804
- p.nonce, p.buy, p.callback_address, p.callback_data, p.block_number,
5805
- p.group_chain_id, p.group_maker, p.consumed, p.chain_id, p.loan_token
6295
+ LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
6296
+ LEFT JOIN ${lots} l
6297
+ ON l.chain_id = c.position_chain_id
6298
+ AND LOWER(l.contract) = LOWER(c.position_contract)
6299
+ AND LOWER(l."user") = LOWER(c.position_user)
6300
+ AND l."group" = p.group_group
6301
+ LEFT JOIN ${positions} pos
6302
+ ON pos.chain_id = c.position_chain_id
6303
+ AND LOWER(pos.contract) = LOWER(c.position_contract)
6304
+ AND LOWER(pos."user") = LOWER(c.position_user)
6305
+ LEFT JOIN position_offsets pos_offsets
6306
+ ON pos_offsets.chain_id = c.position_chain_id
6307
+ AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
6308
+ AND LOWER(pos_offsets."user") = LOWER(c.position_user)
6309
+ LEFT JOIN position_consumed pc
6310
+ ON pc.chain_id = c.position_chain_id
6311
+ AND LOWER(pc.contract) = LOWER(c.position_contract)
6312
+ AND LOWER(pc."user") = LOWER(c.position_user)
6313
+ ),
6314
+ -- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
6315
+ callback_loan_contribution AS (
6316
+ SELECT
6317
+ cc.*,
6318
+ CASE
6319
+ -- No lot exists: contribution is 0
6320
+ WHEN cc.lot_lower IS NULL THEN 0
6321
+ -- Loan token position: use lot_balance directly, apply callback limit
6322
+ WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
6323
+ LEAST(
6324
+ cc.lot_balance,
6325
+ COALESCE(cc.callback_amount::numeric, cc.lot_balance)
6326
+ )
6327
+ -- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
6328
+ ELSE
6329
+ (
6330
+ LEAST(
6331
+ cc.lot_balance,
6332
+ COALESCE(cc.callback_amount::numeric, cc.lot_balance)
6333
+ ) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
6334
+ ) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
6335
+ END AS contribution_in_loan
6336
+ FROM callback_contributions cc
6337
+ LEFT JOIN ${obligationCollateralsV2} collat_info
6338
+ ON collat_info.obligation_id = cc.obligation_id
6339
+ AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
6340
+ LEFT JOIN ${oracles} collat_oracle
6341
+ ON collat_oracle.chain_id = collat_info.oracle_chain_id
6342
+ AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
6343
+ ),
6344
+ -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
6345
+ offer_contributions AS (
6346
+ SELECT
6347
+ hash,
6348
+ obligation_id,
6349
+ assets,
6350
+ rate,
6351
+ maturity,
6352
+ expiry,
6353
+ start,
6354
+ nonce,
6355
+ buy,
6356
+ callback_address,
6357
+ callback_data,
6358
+ block_number,
6359
+ group_chain_id,
6360
+ group_maker,
6361
+ consumed,
6362
+ chain_id,
6363
+ loan_token,
6364
+ SUM(contribution_in_loan) AS total_available
6365
+ FROM (
6366
+ -- Take max contribution per position using DISTINCT ON (idiomatic PostgreSQL)
6367
+ SELECT DISTINCT ON (clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user)
6368
+ clc.*
6369
+ FROM callback_loan_contribution clc
6370
+ WHERE clc.callback_id IS NOT NULL
6371
+ ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
6372
+ ) deduped
6373
+ GROUP BY hash, obligation_id, assets, rate, maturity, expiry, start, nonce, buy,
6374
+ callback_address, callback_data, block_number, group_chain_id, group_maker,
6375
+ consumed, chain_id, loan_token
5806
6376
  )
6377
+ -- Final SELECT with inline takeable computation
5807
6378
  SELECT
5808
- wc.hash, wc.group_maker, wc.assets, wc.consumed, wc.rate, wc.maturity, wc.expiry, wc.start,
5809
- wc.nonce, wc.buy, wc.chain_id, wc.loan_token, wc.callback_address, wc.callback_data,
5810
- wc.block_number, wc.callback_ids, c.collaterals
5811
- FROM with_callbacks wc
5812
- LEFT JOIN collats c ON c.obligation_id = wc.obligation_id
6379
+ oc.hash,
6380
+ oc.group_maker,
6381
+ oc.assets,
6382
+ oc.consumed,
6383
+ oc.rate,
6384
+ oc.maturity,
6385
+ oc.expiry,
6386
+ oc.start,
6387
+ oc.nonce,
6388
+ oc.buy,
6389
+ oc.chain_id,
6390
+ oc.loan_token,
6391
+ oc.callback_address,
6392
+ oc.callback_data,
6393
+ oc.block_number,
6394
+ -- takeable = min(assets - consumed, total_available)
6395
+ GREATEST(0, LEAST(
6396
+ oc.assets::numeric - oc.consumed::numeric,
6397
+ COALESCE(oc.total_available, 0)
6398
+ )) AS takeable,
6399
+ c.collaterals
6400
+ FROM offer_contributions oc
6401
+ LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
6402
+ WHERE GREATEST(0, LEAST(
6403
+ oc.assets::numeric - oc.consumed::numeric,
6404
+ COALESCE(oc.total_available, 0)
6405
+ )) > 0
5813
6406
  ORDER BY
5814
- wc.rate ${rateSortDirection === "asc" ? drizzle_orm.sql`ASC` : drizzle_orm.sql`DESC`},
5815
- wc.block_number ASC,
5816
- wc.assets DESC,
5817
- wc.hash ASC;
6407
+ oc.rate ${rateSortDirection === "asc" ? drizzle_orm.sql`ASC` : drizzle_orm.sql`DESC`},
6408
+ oc.block_number ASC,
6409
+ oc.assets DESC,
6410
+ oc.hash ASC;
5818
6411
  `);
5819
- const offers$1 = raw.rows.map((row) => ({
5820
- ...from$11({
6412
+ return {
6413
+ offers: raw.rows.map((row) => from$11({
5821
6414
  offering: row.group_maker,
5822
6415
  assets: BigInt(row.assets),
5823
6416
  rate: BigInt(row.rate),
@@ -5839,165 +6432,12 @@ async function _getOffersWithCallbackIds(db, params) {
5839
6432
  gasLimit: 0n
5840
6433
  },
5841
6434
  consumed: BigInt(row.consumed),
5842
- takeable: 0n,
6435
+ takeable: BigInt(row.takeable.split(".")[0] ?? "0"),
5843
6436
  blockNumber: row.block_number
5844
- }),
5845
- callbackIds: row.callback_ids ?? []
5846
- }));
5847
- let nextCursor = null;
5848
- if (raw.rows.length === limit) {
5849
- const last = raw.rows[raw.rows.length - 1];
5850
- nextCursor = {
5851
- rate: last.rate,
5852
- blockNumber: last.block_number,
5853
- assets: last.assets,
5854
- hash: last.hash
5855
- };
5856
- }
5857
- return {
5858
- offers: offers$1,
5859
- nextCursor
6437
+ })),
6438
+ hasMore: raw.rows.length === limit
5860
6439
  };
5861
6440
  }
5862
- /** Get callbacks by their IDs. */
5863
- async function _updateCallbacksByIds(state, db, ids) {
5864
- if (ids.length === 0) return;
5865
- const raw = await db.execute(drizzle_orm.sql`
5866
- SELECT c.id, c.position_chain_id, c.position_contract, c.position_user, c.amount
5867
- FROM ${callbacks} c
5868
- WHERE c.id IN (${drizzle_orm.sql.join(ids.map((id$1) => drizzle_orm.sql`${id$1}`), drizzle_orm.sql`, `)})
5869
- `);
5870
- for (const row of raw.rows) if (!state.has(row.id)) state.set(row.id, {
5871
- positionKey: _buildPositionKey(row.position_chain_id, row.position_contract, row.position_user),
5872
- amount: row.amount != null ? BigInt(row.amount) : null
5873
- });
5874
- }
5875
- /** Get positions by their composite keys. */
5876
- async function _updatePositionsByKeys(state, db, keys) {
5877
- if (keys.length === 0) return;
5878
- const parsedKeys = keys.map((key) => {
5879
- const parts = key.split(":");
5880
- return {
5881
- chainId: BigInt(parts[0]),
5882
- contract: parts[1],
5883
- user: parts[2]
5884
- };
5885
- });
5886
- const raw = await db.execute(drizzle_orm.sql`
5887
- SELECT p.chain_id, p.contract, p."user", p.balance, p.asset
5888
- FROM ${positions} p
5889
- WHERE (p.chain_id, LOWER(p.contract), LOWER(p."user")) IN (
5890
- ${drizzle_orm.sql.join(parsedKeys.map((k) => drizzle_orm.sql`(${k.chainId}, ${k.contract.toLowerCase()}, ${k.user.toLowerCase()})`), drizzle_orm.sql`, `)}
5891
- )
5892
- `);
5893
- for (const row of raw.rows) {
5894
- const key = _buildPositionKey(row.chain_id, row.contract, row.user);
5895
- if (!state.has(key)) state.set(key, {
5896
- balance: row.balance ? BigInt(row.balance) : 0n,
5897
- remaining: row.balance ? BigInt(row.balance) : 0n,
5898
- asset: row.asset ?? "0x0000000000000000000000000000000000000000"
5899
- });
5900
- }
5901
- }
5902
- /** Get oracle prices by chain_id and address. */
5903
- async function _updatePrices(state, db, oracles$1) {
5904
- if (oracles$1.length === 0) return;
5905
- const raw = await db.execute(drizzle_orm.sql`
5906
- SELECT o.chain_id, o.address, o.price
5907
- FROM ${oracles} o
5908
- WHERE (o.chain_id, LOWER(o.address)) IN (${drizzle_orm.sql.join(oracles$1.map((o) => drizzle_orm.sql`(${o.chainId}, ${o.address.toLowerCase()})`), drizzle_orm.sql`, `)})
5909
- `);
5910
- for (const row of raw.rows) {
5911
- const key = `${row.chain_id}:${row.address.toLowerCase()}`;
5912
- if (!state.has(key)) state.set(key, row.price ? BigInt(row.price) : 0n);
5913
- }
5914
- }
5915
- /** Build a composite position key from its components. */
5916
- function _buildPositionKey(chainId, contract, user) {
5917
- return `${chainId}:${contract.toLowerCase()}:${user.toLowerCase()}`;
5918
- }
5919
- /** Collect oracle addresses that need to be Geted for collateral positions. */
5920
- function _collectNewOracleAddresses(offers$1, callbackState, positionState, prices) {
5921
- const seen = /* @__PURE__ */ new Set();
5922
- const result = [];
5923
- for (const offer of offers$1) for (const callbackId of offer.callbackIds) {
5924
- const callback$1 = callbackState.get(callbackId);
5925
- if (!callback$1) continue;
5926
- const position = positionState.get(callback$1.positionKey);
5927
- if (!position) continue;
5928
- if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) continue;
5929
- const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
5930
- if (collateral) {
5931
- const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
5932
- if (!prices.has(key) && !seen.has(key)) {
5933
- seen.add(key);
5934
- result.push({
5935
- chainId: offer.chainId,
5936
- address: collateral.oracle.toLowerCase()
5937
- });
5938
- }
5939
- }
5940
- }
5941
- return result;
5942
- }
5943
- /**
5944
- * Compute cross-invalidation for a batch of offers.
5945
- * Deducts consumed liquidity from shared positions and returns offers with takeable amounts.
5946
- */
5947
- function _computeCrossInvalidation(offers$1, callbackState, positionState, prices) {
5948
- const result = [];
5949
- for (const offer of offers$1) {
5950
- const contributions = /* @__PURE__ */ new Map();
5951
- for (const callbackId of offer.callbackIds) {
5952
- const callback$1 = callbackState.get(callbackId);
5953
- if (!callback$1) continue;
5954
- const position = positionState.get(callback$1.positionKey);
5955
- if (!position) continue;
5956
- let conversion;
5957
- if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) conversion = null;
5958
- else {
5959
- const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
5960
- if (!collateral) conversion = {
5961
- price: 0n,
5962
- lltv: 0n
5963
- };
5964
- else {
5965
- const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
5966
- conversion = {
5967
- price: prices.get(key) ?? 0n,
5968
- lltv: collateral.lltv
5969
- };
5970
- }
5971
- }
5972
- const availableFromPosition = conversion === null ? position.remaining : Conversion.collateralToLoan(position.remaining, conversion);
5973
- const callbackLimitInLoanTerms = conversion === null || callback$1.amount === null ? callback$1.amount : Conversion.collateralToLoan(callback$1.amount, conversion);
5974
- const callbackAvailable = callbackLimitInLoanTerms === null ? availableFromPosition : min(availableFromPosition, callbackLimitInLoanTerms);
5975
- const existing = contributions.get(callback$1.positionKey);
5976
- if (existing) existing.available = min(availableFromPosition, max$1(existing.available, callbackAvailable));
5977
- else contributions.set(callback$1.positionKey, {
5978
- available: callbackAvailable,
5979
- conversion
5980
- });
5981
- }
5982
- let totalAvailable = 0n;
5983
- for (const [, contrib] of contributions) totalAvailable += contrib.available;
5984
- const takeable = min(offer.assets - offer.consumed, totalAvailable);
5985
- if (takeable <= 0n) continue;
5986
- for (const [key, contrib] of contributions) {
5987
- const position = positionState.get(key);
5988
- const proportionalTakeable = totalAvailable > 0n ? contrib.available * takeable / totalAvailable : 0n;
5989
- const toDeduct = contrib.conversion === null ? proportionalTakeable : Conversion.loanToCollateral(proportionalTakeable, contrib.conversion);
5990
- position.remaining = position.remaining - toDeduct;
5991
- if (position.remaining < 0n) position.remaining = 0n;
5992
- }
5993
- const { callbackIds: _, ...cleanOffer } = offer;
5994
- result.push(from$11({
5995
- ...cleanOffer,
5996
- takeable
5997
- }));
5998
- }
5999
- return result;
6000
- }
6001
6441
  let Cursor;
6002
6442
  (function(_Cursor) {
6003
6443
  function encode$4(offer, totalReturned, now$1, side) {
@@ -6062,7 +6502,7 @@ let LevelCursor;
6062
6502
  //#endregion
6063
6503
  //#region src/database/domains/Chains.ts
6064
6504
  /** Postgres implementation. */
6065
- const create$8 = (config) => {
6505
+ const create$11 = (config) => {
6066
6506
  const db = config.db;
6067
6507
  const logger = getLogger();
6068
6508
  return {
@@ -6118,7 +6558,7 @@ const create$8 = (config) => {
6118
6558
  //#endregion
6119
6559
  //#region src/database/domains/Collectors.ts
6120
6560
  /** Postgres implementation. */
6121
- const create$7 = (config) => {
6561
+ const create$10 = (config) => {
6122
6562
  const db = config.db;
6123
6563
  const logger = getLogger();
6124
6564
  return {
@@ -6211,7 +6651,7 @@ const DEFAULT_BATCH_SIZE$1 = 4e3;
6211
6651
 
6212
6652
  //#endregion
6213
6653
  //#region src/database/domains/Consumed.ts
6214
- function create$6(db) {
6654
+ function create$9(db) {
6215
6655
  return {
6216
6656
  create: async (events) => {
6217
6657
  if (events.length === 0) return;
@@ -6252,6 +6692,51 @@ function create$6(db) {
6252
6692
  };
6253
6693
  }
6254
6694
 
6695
+ //#endregion
6696
+ //#region src/database/domains/Lots.ts
6697
+ function create$8(db) {
6698
+ return {
6699
+ get: async (parameters) => {
6700
+ const { chainId, user, contract, group } = parameters ?? {};
6701
+ const conditions = [];
6702
+ if (chainId !== void 0) conditions.push((0, drizzle_orm.eq)(lots.chainId, chainId));
6703
+ if (user !== void 0) conditions.push((0, drizzle_orm.eq)(lots.user, user.toLowerCase()));
6704
+ if (contract !== void 0) conditions.push((0, drizzle_orm.eq)(lots.contract, contract.toLowerCase()));
6705
+ if (group !== void 0) conditions.push((0, drizzle_orm.eq)(lots.group, group));
6706
+ return (await db.select().from(lots).where(conditions.length > 0 ? (0, drizzle_orm.and)(...conditions) : void 0)).map((row) => ({
6707
+ chainId: row.chainId,
6708
+ user: row.user,
6709
+ contract: row.contract,
6710
+ group: row.group,
6711
+ lower: BigInt(row.lower),
6712
+ upper: BigInt(row.upper)
6713
+ }));
6714
+ },
6715
+ create: async (parameters) => {
6716
+ if (parameters.length === 0) return;
6717
+ const lotsByPositionGroup = /* @__PURE__ */ new Map();
6718
+ for (const offer of parameters) {
6719
+ const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
6720
+ const existing = lotsByPositionGroup.get(key);
6721
+ if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
6722
+ }
6723
+ for (const offer of lotsByPositionGroup.values()) if ((await db.select().from(lots).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(lots.chainId, offer.positionChainId), (0, drizzle_orm.eq)(lots.contract, offer.positionContract.toLowerCase()), (0, drizzle_orm.eq)(lots.user, offer.positionUser.toLowerCase()), (0, drizzle_orm.eq)(lots.group, offer.group))).limit(1)).length === 0) {
6724
+ const maxUpperResult = await db.select({ maxUpper: drizzle_orm.sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(lots.chainId, offer.positionChainId), (0, drizzle_orm.eq)(lots.contract, offer.positionContract.toLowerCase()), (0, drizzle_orm.eq)(lots.user, offer.positionUser.toLowerCase())));
6725
+ const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
6726
+ const newUpper = newLower + offer.size;
6727
+ await db.insert(lots).values({
6728
+ chainId: offer.positionChainId,
6729
+ user: offer.positionUser.toLowerCase(),
6730
+ contract: offer.positionContract.toLowerCase(),
6731
+ group: offer.group,
6732
+ lower: newLower.toString(),
6733
+ upper: newUpper.toString()
6734
+ });
6735
+ }
6736
+ }
6737
+ };
6738
+ }
6739
+
6255
6740
  //#endregion
6256
6741
  //#region src/gatekeeper/Gate.ts
6257
6742
  var Gate_exports = /* @__PURE__ */ __export({
@@ -6365,8 +6850,8 @@ function getCallback(chain, type) {
6365
6850
  * @param address - Callback contract address
6366
6851
  * @returns The callback type when found, otherwise undefined
6367
6852
  */
6368
- function getCallbackType(chain, address) {
6369
- return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
6853
+ function getCallbackType(chain, address$1) {
6854
+ return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address$1?.toLowerCase()))?.type;
6370
6855
  }
6371
6856
  /**
6372
6857
  * Returns the callback addresses for a given chain and callback type, if it exists.
@@ -6480,8 +6965,8 @@ const configs = {
6480
6965
 
6481
6966
  //#endregion
6482
6967
  //#region src/gatekeeper/Gatekeeper.ts
6483
- var Gatekeeper_exports = /* @__PURE__ */ __export({ create: () => create$5 });
6484
- function create$5(parameters) {
6968
+ var Gatekeeper_exports = /* @__PURE__ */ __export({ create: () => create$7 });
6969
+ function create$7(parameters) {
6485
6970
  return {
6486
6971
  rules: parameters.rules,
6487
6972
  isAllowed: async (offers$1) => {
@@ -6499,6 +6984,7 @@ var Rules_exports = /* @__PURE__ */ __export({
6499
6984
  callback: () => callback,
6500
6985
  chains: () => chains,
6501
6986
  maturity: () => maturity,
6987
+ sameMaker: () => sameMaker,
6502
6988
  token: () => token,
6503
6989
  validity: () => validity
6504
6990
  });
@@ -6635,10 +7121,29 @@ const token = ({ assets: assets$1 }) => single("token", "Validates that offer lo
6635
7121
  if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
6636
7122
  if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
6637
7123
  });
7124
+ /**
7125
+ * A batch validation rule that ensures all offers in a tree have the same maker (offering address).
7126
+ * Returns an issue only for the first non-conforming offer.
7127
+ * This rule is signing-agnostic; signer verification is handled at the collector level.
7128
+ */
7129
+ const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker (offering address)", (offers$1) => {
7130
+ const issues = /* @__PURE__ */ new Map();
7131
+ if (offers$1.length === 0) return issues;
7132
+ const firstMaker = offers$1[0].offering.toLowerCase();
7133
+ for (let i = 1; i < offers$1.length; i++) {
7134
+ const offer = offers$1[i];
7135
+ if (offer.offering.toLowerCase() !== firstMaker) {
7136
+ issues.set(i, { message: `Offer has different maker ${offer.offering} than first offer ${offers$1[0].offering}` });
7137
+ return issues;
7138
+ }
7139
+ }
7140
+ return issues;
7141
+ });
6638
7142
 
6639
7143
  //#endregion
6640
7144
  //#region src/gatekeeper/morphoRules.ts
6641
7145
  const morphoRules = (chains$3) => [
7146
+ sameMaker(),
6642
7147
  chains({ chains: chains$3 }),
6643
7148
  maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
6644
7149
  callback({
@@ -6655,7 +7160,7 @@ const morphoRules = (chains$3) => [
6655
7160
  //#endregion
6656
7161
  //#region src/database/domains/Offers.ts
6657
7162
  const DEFAULT_LIMIT$2 = 100;
6658
- function create$4(config) {
7163
+ function create$6(config) {
6659
7164
  const db = config.db;
6660
7165
  return {
6661
7166
  create: async (offers$1) => {
@@ -6814,6 +7319,22 @@ function create$4(config) {
6814
7319
  amount: callback$1.amount
6815
7320
  })));
6816
7321
  for (const batch$2 of batch$1(callbacksRows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(callbacks).values(batch$2).onConflictDoNothing();
7322
+ const lotInfos = [];
7323
+ for (const [offerHash, callbacks$1] of offersCallbacksMap.entries()) {
7324
+ const offer = inserted.find((o) => o.hash === offerHash);
7325
+ if (!offer) continue;
7326
+ for (const callback$1 of callbacks$1) {
7327
+ const isLoanPosition = obligationsMap.get(offer.obligationId)?.loanToken.toLowerCase() === callback$1.asset?.toLowerCase();
7328
+ lotInfos.push({
7329
+ positionChainId: callback$1.chainId,
7330
+ positionContract: callback$1.contract,
7331
+ positionUser: callback$1.user,
7332
+ group: offer.group,
7333
+ size: isLoanPosition ? BigInt(offer.assets) : BigInt(callback$1.amount)
7334
+ });
7335
+ }
7336
+ }
7337
+ if (lotInfos.length > 0) await dbTx.lots.create(lotInfos);
6817
7338
  obligationsMap.clear();
6818
7339
  collateralsMap.clear();
6819
7340
  oraclesMap.clear();
@@ -6996,9 +7517,29 @@ function create$4(config) {
6996
7517
  };
6997
7518
  }
6998
7519
 
7520
+ //#endregion
7521
+ //#region src/database/domains/Offsets.ts
7522
+ function create$5(db) {
7523
+ return { get: async (parameters) => {
7524
+ const { chainId, user, contract, group } = parameters ?? {};
7525
+ const conditions = [];
7526
+ if (chainId !== void 0) conditions.push((0, drizzle_orm.eq)(offsets.chainId, chainId));
7527
+ if (user !== void 0) conditions.push((0, drizzle_orm.eq)(offsets.user, user.toLowerCase()));
7528
+ if (contract !== void 0) conditions.push((0, drizzle_orm.eq)(offsets.contract, contract.toLowerCase()));
7529
+ if (group !== void 0) conditions.push((0, drizzle_orm.eq)(offsets.group, group));
7530
+ return (await db.select().from(offsets).where(conditions.length > 0 ? (0, drizzle_orm.and)(...conditions) : void 0)).map((row) => ({
7531
+ chainId: row.chainId,
7532
+ user: row.user,
7533
+ contract: row.contract,
7534
+ group: row.group,
7535
+ value: BigInt(row.value)
7536
+ }));
7537
+ } };
7538
+ }
7539
+
6999
7540
  //#endregion
7000
7541
  //#region src/database/domains/Oracles.ts
7001
- function create$3(db) {
7542
+ function create$4(db) {
7002
7543
  return {
7003
7544
  get: async ({ chainId }) => {
7004
7545
  return (await db.select({
@@ -7038,7 +7579,7 @@ function create$3(db) {
7038
7579
  //#endregion
7039
7580
  //#region src/database/domains/Positions.ts
7040
7581
  const DEFAULT_LIMIT$1 = 100;
7041
- const create$2 = (db) => {
7582
+ const create$3 = (db) => {
7042
7583
  return {
7043
7584
  upsert: async (positions$1) => {
7044
7585
  const positionsMap = /* @__PURE__ */ new Map();
@@ -7154,7 +7695,7 @@ const create$2 = (db) => {
7154
7695
 
7155
7696
  //#endregion
7156
7697
  //#region src/database/domains/Transfers.ts
7157
- const create$1 = (db) => ({ create: async (transfers$1) => {
7698
+ const create$2 = (db) => ({ create: async (transfers$1) => {
7158
7699
  if (transfers$1.length === 0) return 0;
7159
7700
  return await db.transaction(async (dbTx) => {
7160
7701
  let totalInserted = 0;
@@ -7251,6 +7792,91 @@ const create$1 = (db) => ({ create: async (transfers$1) => {
7251
7792
  });
7252
7793
  } });
7253
7794
 
7795
+ //#endregion
7796
+ //#region src/database/domains/Trees.ts
7797
+ /**
7798
+ * Creates a Trees domain instance for managing merkle tree metadata.
7799
+ *
7800
+ * @param config - Configuration with database instance
7801
+ * @returns TreesDomain instance
7802
+ */
7803
+ function create$1(config) {
7804
+ const db = config.db;
7805
+ return {
7806
+ create: async (trees$1) => {
7807
+ if (trees$1.length === 0) return [];
7808
+ return await db.transaction(async (dbTx) => {
7809
+ const roots = [];
7810
+ for (const { tree, signature } of trees$1) {
7811
+ const root = tree.root.toLowerCase();
7812
+ roots.push(root);
7813
+ await dbTx.insert(trees).values({
7814
+ root,
7815
+ rootSignature: signature.toLowerCase()
7816
+ }).onConflictDoUpdate({
7817
+ target: [trees.root],
7818
+ set: {
7819
+ rootSignature: signature.toLowerCase(),
7820
+ createdAt: drizzle_orm.sql`NOW()`
7821
+ }
7822
+ });
7823
+ await dbTx.offers.create(tree.offers);
7824
+ const pathRows = proofs(tree).map((proof) => ({
7825
+ offerHash: proof.offer.hash.toLowerCase(),
7826
+ treeRoot: root,
7827
+ proofNodes: concatenateProofs(proof.path)
7828
+ }));
7829
+ for (const batch$2 of batch$1(pathRows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(merklePaths).values(batch$2).onConflictDoUpdate({
7830
+ target: [merklePaths.offerHash],
7831
+ set: {
7832
+ treeRoot: drizzle_orm.sql`excluded.tree_root`,
7833
+ proofNodes: drizzle_orm.sql`excluded.proof_nodes`,
7834
+ createdAt: drizzle_orm.sql`NOW()`
7835
+ }
7836
+ });
7837
+ }
7838
+ return roots;
7839
+ });
7840
+ },
7841
+ getAttestations: async (hashes) => {
7842
+ if (hashes.length === 0) return /* @__PURE__ */ new Map();
7843
+ const normalizedHashes = hashes.map((h) => h.toLowerCase());
7844
+ const results = await db.select({
7845
+ offerHash: merklePaths.offerHash,
7846
+ treeRoot: merklePaths.treeRoot,
7847
+ proofNodes: merklePaths.proofNodes,
7848
+ rootSignature: trees.rootSignature
7849
+ }).from(merklePaths).innerJoin(trees, (0, drizzle_orm.eq)(merklePaths.treeRoot, trees.root)).where((0, drizzle_orm.inArray)(merklePaths.offerHash, normalizedHashes));
7850
+ const attestationMap = /* @__PURE__ */ new Map();
7851
+ for (const row of results) attestationMap.set(row.offerHash, {
7852
+ root: row.treeRoot,
7853
+ signature: row.rootSignature,
7854
+ proof: splitProofs(row.proofNodes)
7855
+ });
7856
+ return attestationMap;
7857
+ }
7858
+ };
7859
+ }
7860
+ /**
7861
+ * Concatenates an array of 32-byte hex hashes into a single hex string.
7862
+ * Empty arrays return "0x".
7863
+ */
7864
+ function concatenateProofs(proofs$1) {
7865
+ if (proofs$1.length === 0) return "0x";
7866
+ return `0x${proofs$1.map((p) => p.slice(2)).join("")}`;
7867
+ }
7868
+ /**
7869
+ * Splits a concatenated hex string back into an array of 32-byte hex hashes.
7870
+ * Returns empty array for "0x" or empty string.
7871
+ */
7872
+ function splitProofs(concatenated) {
7873
+ if (!concatenated || concatenated === "0x" || concatenated.length <= 2) return [];
7874
+ const hex$1 = concatenated.slice(2);
7875
+ const proofs$1 = [];
7876
+ for (let i = 0; i < hex$1.length; i += 64) proofs$1.push(`0x${hex$1.slice(i, i + 64)}`);
7877
+ return proofs$1;
7878
+ }
7879
+
7254
7880
  //#endregion
7255
7881
  //#region src/database/domains/Validations.ts
7256
7882
  const DEFAULT_LIMIT = 100;
@@ -7316,15 +7942,18 @@ function create(db) {
7316
7942
  var Database_exports = /* @__PURE__ */ __export({ connect: () => connect$1 });
7317
7943
  function createDomains(core) {
7318
7944
  return {
7319
- book: create$9({ db: core }),
7320
- collectors: create$7({ db: core }),
7321
- offers: create$4({ db: core }),
7322
- chains: create$8({ db: core }),
7323
- consumed: create$6(core),
7324
- oracles: create$3(core),
7945
+ book: create$12({ db: core }),
7946
+ collectors: create$10({ db: core }),
7947
+ offers: create$6({ db: core }),
7948
+ chains: create$11({ db: core }),
7949
+ consumed: create$9(core),
7950
+ lots: create$8(core),
7951
+ offsets: create$5(core),
7952
+ oracles: create$4(core),
7953
+ trees: create$1({ db: core }),
7325
7954
  validations: create(core),
7326
- positions: create$2(core),
7327
- transfers: create$1(core)
7955
+ positions: create$3(core),
7956
+ transfers: create$2(core)
7328
7957
  };
7329
7958
  }
7330
7959
  const AUGMENT_CACHE = /* @__PURE__ */ new WeakMap();
@@ -7359,10 +7988,22 @@ function augmentWithDomains(base) {
7359
7988
  value: dms.consumed,
7360
7989
  enumerable: true
7361
7990
  },
7991
+ lots: {
7992
+ value: dms.lots,
7993
+ enumerable: true
7994
+ },
7995
+ offsets: {
7996
+ value: dms.offsets,
7997
+ enumerable: true
7998
+ },
7362
7999
  oracles: {
7363
8000
  value: dms.oracles,
7364
8001
  enumerable: true
7365
8002
  },
8003
+ trees: {
8004
+ value: dms.trees,
8005
+ enumerable: true
8006
+ },
7366
8007
  validations: {
7367
8008
  value: dms.validations,
7368
8009
  enumerable: true
@@ -7379,6 +8020,7 @@ function augmentWithDomains(base) {
7379
8020
  AUGMENT_CACHE.set(base, wrapped);
7380
8021
  return wrapped;
7381
8022
  }
8023
+ let cachedInMemoryDatabase;
7382
8024
  /**
7383
8025
  * Connect to the database.
7384
8026
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -7401,15 +8043,17 @@ function connect$1(connectionString) {
7401
8043
  clean: async () => await clean(driver$1)
7402
8044
  });
7403
8045
  }
8046
+ if (cachedInMemoryDatabase) return cachedInMemoryDatabase;
7404
8047
  const pool = new __electric_sql_pglite.PGlite();
7405
8048
  const driver = (0, drizzle_orm_pglite.drizzle)(pool, { schema: schema_exports });
7406
8049
  const core = augmentWithDomains(driver);
7407
- return Object.assign(core, {
8050
+ cachedInMemoryDatabase = Object.assign(core, {
7408
8051
  name: "pglite",
7409
8052
  pool,
7410
8053
  applyMigrations: applyMigrations("pglite", driver),
7411
8054
  clean: async () => await clean(driver)
7412
8055
  });
8056
+ return cachedInMemoryDatabase;
7413
8057
  }
7414
8058
  const MIGRATED_DRIVERS = /* @__PURE__ */ new WeakSet();
7415
8059
  function applyMigrations(kind, driver) {
@@ -7613,6 +8257,35 @@ async function postMigrate(driver) {
7613
8257
  REFERENCING OLD TABLE AS deleted_rows
7614
8258
  FOR EACH STATEMENT
7615
8259
  EXECUTE FUNCTION cleanup_orphan_positions();
8260
+ `);
8261
+ await driver.execute(`
8262
+ CREATE OR REPLACE FUNCTION cleanup_orphan_groups()
8263
+ RETURNS TRIGGER AS $$
8264
+ BEGIN
8265
+ DELETE FROM "${VERSION}"."groups" g
8266
+ USING (
8267
+ SELECT DISTINCT group_chain_id, group_maker, group_group
8268
+ FROM deleted_rows
8269
+ ) AS affected
8270
+ WHERE g.chain_id = affected.group_chain_id
8271
+ AND g.maker = affected.group_maker
8272
+ AND g."group" = affected.group_group
8273
+ AND NOT EXISTS (
8274
+ SELECT 1 FROM "${VERSION}"."offers" o
8275
+ WHERE o.group_chain_id = g.chain_id
8276
+ AND o.group_maker = g.maker
8277
+ AND o.group_group = g."group"
8278
+ );
8279
+ RETURN NULL;
8280
+ END;
8281
+ $$ LANGUAGE plpgsql;
8282
+ `);
8283
+ await driver.execute(`
8284
+ CREATE OR REPLACE TRIGGER trg_cleanup_orphan_groups
8285
+ AFTER DELETE ON "${VERSION}"."offers"
8286
+ REFERENCING OLD TABLE AS deleted_rows
8287
+ FOR EACH STATEMENT
8288
+ EXECUTE FUNCTION cleanup_orphan_groups();
7616
8289
  `);
7617
8290
  await driver.execute(`
7618
8291
  CREATE OR REPLACE FUNCTION cleanup_orphan_obligations_and_oracles()
@@ -7665,6 +8338,58 @@ async function postMigrate(driver) {
7665
8338
  REFERENCING OLD TABLE AS deleted_rows
7666
8339
  FOR EACH STATEMENT
7667
8340
  EXECUTE FUNCTION cleanup_orphan_obligations_and_oracles();
8341
+ `);
8342
+ await driver.execute(`
8343
+ CREATE OR REPLACE FUNCTION create_offset_on_lot_delete()
8344
+ RETURNS trigger
8345
+ LANGUAGE plpgsql AS $$
8346
+ BEGIN
8347
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
8348
+ VALUES (
8349
+ OLD.chain_id,
8350
+ OLD."user",
8351
+ OLD.contract,
8352
+ OLD."group",
8353
+ OLD.upper::numeric - OLD.lower::numeric
8354
+ )
8355
+ ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
8356
+ RETURN OLD;
8357
+ END;
8358
+ $$;
8359
+ `);
8360
+ await driver.execute(`
8361
+ CREATE OR REPLACE TRIGGER trg_lots_create_offset_before_delete
8362
+ BEFORE DELETE ON "${VERSION}"."lots"
8363
+ FOR EACH ROW
8364
+ EXECUTE FUNCTION create_offset_on_lot_delete();
8365
+ `);
8366
+ await driver.execute(`
8367
+ CREATE OR REPLACE FUNCTION delete_position_if_no_lots()
8368
+ RETURNS trigger
8369
+ LANGUAGE plpgsql AS $$
8370
+ BEGIN
8371
+ -- Check if any lots remain on this position
8372
+ IF NOT EXISTS (
8373
+ SELECT 1 FROM "${VERSION}"."lots" l
8374
+ WHERE l.chain_id = OLD.chain_id
8375
+ AND l.contract = OLD.contract
8376
+ AND l."user" = OLD."user"
8377
+ ) THEN
8378
+ -- No lots remain, delete the position (cascades to offsets)
8379
+ DELETE FROM "${VERSION}"."positions" p
8380
+ WHERE p.chain_id = OLD.chain_id
8381
+ AND p.contract = OLD.contract
8382
+ AND p."user" = OLD."user";
8383
+ END IF;
8384
+ RETURN NULL;
8385
+ END;
8386
+ $$;
8387
+ `);
8388
+ await driver.execute(`
8389
+ CREATE OR REPLACE TRIGGER trg_lots_delete_position_if_empty
8390
+ AFTER DELETE ON "${VERSION}"."lots"
8391
+ FOR EACH ROW
8392
+ EXECUTE FUNCTION delete_position_if_no_lots();
7668
8393
  `);
7669
8394
  });
7670
8395
  }
@@ -7696,22 +8421,24 @@ async function add(config, offers$1) {
7696
8421
  const tree = from$12(offers$1.map((o) => from$11(o)));
7697
8422
  const chainId = await getChainId(config.client);
7698
8423
  for (const offer of tree.offers) if (chainId !== offer.chainId) throw new ChainIdMismatchError(offer.chainId, chainId);
8424
+ const signature = await sign(tree.offers, config.client);
8425
+ const encoded = await encode$2(tree, signature);
7699
8426
  try {
7700
8427
  return await config.client.sendTransaction({
7701
8428
  chain: config.client.chain,
7702
8429
  account: config.client.account,
7703
8430
  to: config.mempoolAddress,
7704
- data: encode$2(tree)
8431
+ data: encoded
7705
8432
  });
7706
8433
  } catch (error) {
7707
8434
  throw new ViemClientError(error instanceof Error ? error.message : "Unknown error");
7708
8435
  }
7709
8436
  }
7710
8437
  async function* get(config, parameters) {
7711
- const { loanToken, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE } = {} } = parameters || {};
8438
+ const { loanToken, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE } = {} } = parameters || {};
7712
8439
  yield* streamOffers(config, {
7713
8440
  loanToken,
7714
- order: order$1,
8441
+ order,
7715
8442
  blockNumberGte,
7716
8443
  blockNumberLte,
7717
8444
  options: {
@@ -7733,7 +8460,7 @@ const getChainId = async (client) => {
7733
8460
  return chainId;
7734
8461
  };
7735
8462
  async function* streamOffers(config, parameters) {
7736
- const { loanToken, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE, blockWindow = config.blockWindow } = {} } = parameters;
8463
+ const { loanToken, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE, blockWindow = config.blockWindow } = {} } = parameters;
7737
8464
  const stream = streamLogs({
7738
8465
  client: config.client.extend(viem.publicActions),
7739
8466
  contractAddress: config.mempoolAddress,
@@ -7750,13 +8477,13 @@ async function* streamOffers(config, parameters) {
7750
8477
  },
7751
8478
  blockNumberGte,
7752
8479
  blockNumberLte,
7753
- order: order$1,
8480
+ order,
7754
8481
  options: {
7755
8482
  maxBatchSize,
7756
8483
  blockWindow
7757
8484
  }
7758
8485
  });
7759
- let blockNumber = order$1 === "asc" ? blockNumberGte : blockNumberLte;
8486
+ let blockNumber = order === "asc" ? blockNumberGte : blockNumberLte;
7760
8487
  for await (const { logs, blockNumber: newBlockNumber } of stream) {
7761
8488
  blockNumber = newBlockNumber;
7762
8489
  if (logs.length === 0) continue;
@@ -7765,7 +8492,7 @@ async function* streamOffers(config, parameters) {
7765
8492
  if (!log) continue;
7766
8493
  const [payload] = (0, viem.decodeAbiParameters)([{ type: "bytes" }], log.data);
7767
8494
  try {
7768
- const tree = decode$2(payload);
8495
+ const { tree } = await decode$2(payload);
7769
8496
  for (const offer of tree.offers) {
7770
8497
  if (loanToken && offer.loanToken.toLowerCase() !== loanToken.toLowerCase()) continue;
7771
8498
  offers$1.push({