@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.
@@ -1,7 +1,7 @@
1
1
  import { t as __export } from "./chunk-jass6xSI.mjs";
2
2
  import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { bytesToHex, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, isAddress, isHex, keccak256, maxUint256, parseAbi, parseEventLogs, publicActions, stringify, zeroAddress } from "viem";
4
+ import { bytesToHex, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashMessage, hashTypedData, hexToBytes, isAddress, isHex, keccak256, maxUint256, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, zeroAddress } from "viem";
5
5
  import { SpanStatusCode, trace } from "@opentelemetry/api";
6
6
  import "@opentelemetry/exporter-trace-otlp-proto";
7
7
  import "@opentelemetry/id-generator-aws-xray";
@@ -13,7 +13,6 @@ import "@opentelemetry/resources";
13
13
  import "@opentelemetry/sdk-trace-node";
14
14
  import "@opentelemetry/semantic-conventions";
15
15
  import { anvil, base, mainnet } from "viem/chains";
16
- import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
17
16
  import * as z$1 from "zod";
18
17
  import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
19
18
  import { gzip, ungzip } from "pako";
@@ -408,6 +407,96 @@ function poll(fn, { interval }) {
408
407
  return unwatch;
409
408
  }
410
409
 
410
+ //#endregion
411
+ //#region src/utils/Random.ts
412
+ var Random_exports = /* @__PURE__ */ __export({
413
+ address: () => address,
414
+ bool: () => bool,
415
+ bytes: () => bytes,
416
+ float: () => float,
417
+ hex: () => hex,
418
+ int: () => int,
419
+ seed: () => seed,
420
+ withSeed: () => withSeed
421
+ });
422
+ let currentRng = Math.random;
423
+ const FNV_OFFSET_BASIS = 2166136261;
424
+ const FNV_PRIME = 16777619;
425
+ const hashSeed = (seed$1) => {
426
+ let hash$1 = FNV_OFFSET_BASIS;
427
+ for (let i = 0; i < seed$1.length; i += 1) {
428
+ hash$1 ^= seed$1.charCodeAt(i);
429
+ hash$1 = Math.imul(hash$1, FNV_PRIME);
430
+ }
431
+ return hash$1 >>> 0;
432
+ };
433
+ const createSeededRng = (seed$1) => {
434
+ let state = hashSeed(seed$1);
435
+ return () => {
436
+ state += 1831565813;
437
+ let t = Math.imul(state ^ state >>> 15, state | 1);
438
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
439
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
440
+ };
441
+ };
442
+ /**
443
+ * Runs a function with a deterministic RNG derived from the given seed.
444
+ */
445
+ function withSeed(seed$1, fn) {
446
+ const previous = currentRng;
447
+ currentRng = createSeededRng(seed$1);
448
+ try {
449
+ return fn();
450
+ } finally {
451
+ currentRng = previous;
452
+ }
453
+ }
454
+ /**
455
+ * Seeds the global RNG for deterministic test runs.
456
+ */
457
+ function seed(seed$1) {
458
+ currentRng = createSeededRng(seed$1);
459
+ }
460
+ /**
461
+ * Returns a deterministic random float in [0, 1).
462
+ */
463
+ function float() {
464
+ return currentRng();
465
+ }
466
+ /**
467
+ * Returns a deterministic random integer in [min, maxExclusive).
468
+ */
469
+ function int(maxExclusive, min$1 = 0) {
470
+ return Math.floor(float() * (maxExclusive - min$1)) + min$1;
471
+ }
472
+ /**
473
+ * Returns a deterministic random boolean.
474
+ */
475
+ function bool(probability = .5) {
476
+ return float() < probability;
477
+ }
478
+ /**
479
+ * Returns deterministic random bytes.
480
+ */
481
+ function bytes(length) {
482
+ const output = new Uint8Array(length);
483
+ for (let i = 0; i < length; i += 1) output[i] = int(256);
484
+ return output;
485
+ }
486
+ /**
487
+ * Returns a deterministic random hex string for the given byte length.
488
+ */
489
+ function hex(byteLength) {
490
+ const output = bytes(byteLength);
491
+ return `0x${Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
492
+ }
493
+ /**
494
+ * Returns a deterministic random address.
495
+ */
496
+ function address() {
497
+ return hex(20);
498
+ }
499
+
411
500
  //#endregion
412
501
  //#region src/utils/time.ts
413
502
  var time_exports = /* @__PURE__ */ __export({
@@ -425,6 +514,7 @@ function max() {
425
514
  //#region src/utils/index.ts
426
515
  var utils_exports = /* @__PURE__ */ __export({
427
516
  BaseError: () => BaseError,
517
+ Random: () => Random_exports,
428
518
  ReorgError: () => ReorgError,
429
519
  Time: () => time_exports,
430
520
  batch: () => batch$1,
@@ -442,7 +532,7 @@ var utils_exports = /* @__PURE__ */ __export({
442
532
 
443
533
  //#endregion
444
534
  //#region src/indexer/collectors/Admin.ts
445
- function create$14(parameters) {
535
+ function create$17(parameters) {
446
536
  const collector = "admin";
447
537
  const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
448
538
  const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
@@ -691,8 +781,8 @@ const names = [
691
781
  "positions",
692
782
  "prices"
693
783
  ];
694
- function create$13({ name, collect, client, db, options }) {
695
- const admin = create$14({
784
+ function create$16({ name, collect, client, db, options }) {
785
+ const admin = create$17({
696
786
  client,
697
787
  db,
698
788
  options
@@ -742,7 +832,10 @@ function create$13({ name, collect, client, db, options }) {
742
832
  };
743
833
  });
744
834
  if (done) iterator = null;
745
- else yield blockNumber;
835
+ else {
836
+ lastBlockNumber = blockNumber;
837
+ yield blockNumber;
838
+ }
746
839
  } catch (err) {
747
840
  const isError = err instanceof Error;
748
841
  logger.error({
@@ -954,8 +1047,12 @@ function decode$3(type, data) {
954
1047
  }
955
1048
  function encode$3(type, data) {
956
1049
  switch (type) {
957
- case CallbackType.BuyVaultV1Callback: return encodeBuyVaultV1Callback(data);
958
- case CallbackType.SellERC20Callback: return encodeSellERC20Callback(data);
1050
+ case CallbackType.BuyVaultV1Callback:
1051
+ if (!("vaults" in data)) throw new Error("Invalid callback data");
1052
+ return encodeBuyVaultV1Callback(data);
1053
+ case CallbackType.SellERC20Callback:
1054
+ if (!("collaterals" in data)) throw new Error("Invalid callback data");
1055
+ return encodeSellERC20Callback(data);
959
1056
  default: throw new Error("Invalid callback type");
960
1057
  }
961
1058
  }
@@ -1152,22 +1249,22 @@ const DEFAULT_BATCH_SIZE$2 = 2500;
1152
1249
  const MAX_BLOCK_WINDOW = 1e4;
1153
1250
  const DEFAULT_BLOCK_WINDOW = 8e3;
1154
1251
  async function* streamLogs(parameters) {
1155
- const { client, contractAddress, event, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$2, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
1252
+ const { client, contractAddress, event, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE$2, blockWindow = DEFAULT_BLOCK_WINDOW } = {} } = parameters;
1156
1253
  if (maxBatchSize > MAX_BATCH_SIZE) throw new InvalidBatchSizeError(maxBatchSize);
1157
1254
  if (blockWindow > MAX_BLOCK_WINDOW) throw new InvalidBlockWindowError(blockWindow);
1158
- if (order$1 === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
1255
+ if (order === "asc" && blockNumberGte === void 0) throw new MissingBlockNumberError();
1159
1256
  const latestBlock = (await getBlock(client, {
1160
1257
  blockTag: "latest",
1161
1258
  includeTransactions: false
1162
1259
  })).number;
1163
1260
  let toBlock = 0n;
1164
- if (order$1 === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
1165
- if (order$1 === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
1261
+ if (order === "asc") toBlock = min(BigInt(blockNumberGte) + BigInt(blockWindow), blockNumberLte ? BigInt(blockNumberLte) : latestBlock);
1262
+ if (order === "desc") toBlock = blockNumberLte === void 0 ? latestBlock : min(BigInt(blockNumberLte), latestBlock);
1166
1263
  let fromBlock = 0n;
1167
- if (order$1 === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
1168
- if (order$1 === "desc") fromBlock = max$1(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
1169
- if (order$1 === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
1170
- if (order$1 === "desc") fromBlock = max$1(fromBlock, toBlock - BigInt(blockWindow));
1264
+ if (order === "asc") fromBlock = min(BigInt(blockNumberGte), latestBlock);
1265
+ if (order === "desc") fromBlock = max$1(BigInt(blockNumberGte || toBlock - BigInt(blockWindow)), 0n);
1266
+ if (order === "asc") toBlock = min(toBlock, fromBlock + BigInt(blockWindow));
1267
+ if (order === "desc") fromBlock = max$1(fromBlock, toBlock - BigInt(blockWindow));
1171
1268
  if (fromBlock > toBlock) throw new InvalidBlockRangeError(fromBlock, toBlock);
1172
1269
  let streaming = true;
1173
1270
  while (streaming) {
@@ -1177,29 +1274,29 @@ async function* streamLogs(parameters) {
1177
1274
  fromBlock,
1178
1275
  toBlock
1179
1276
  });
1180
- streaming = order$1 === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
1277
+ streaming = order === "asc" ? toBlock < (blockNumberLte || latestBlock) : fromBlock > (blockNumberGte || 0n);
1181
1278
  if (logs.length === 0 && !streaming) break;
1182
1279
  if (logs.length === 0 && streaming) yield {
1183
1280
  logs: [],
1184
- blockNumber: order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
1281
+ blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
1185
1282
  };
1186
1283
  logs.sort((a, b) => {
1187
- if (a.blockNumber !== b.blockNumber) return order$1 === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
1188
- if (a.transactionIndex !== b.transactionIndex) return order$1 === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
1189
- return order$1 === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
1284
+ if (a.blockNumber !== b.blockNumber) return order === "asc" ? Number(a.blockNumber - b.blockNumber) : Number(b.blockNumber - a.blockNumber);
1285
+ if (a.transactionIndex !== b.transactionIndex) return order === "asc" ? a.transactionIndex - b.transactionIndex : b.transactionIndex - a.transactionIndex;
1286
+ return order === "asc" ? a.logIndex - b.logIndex : b.logIndex - a.logIndex;
1190
1287
  });
1191
1288
  for (const logBatch of batch$1(logs, maxBatchSize)) yield {
1192
1289
  logs: logBatch,
1193
- blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
1290
+ blockNumber: logBatch.length === maxBatchSize ? Number(logBatch[logBatch.length - 1]?.blockNumber) : order === "asc" ? Number(toBlock) : Number(fromBlock)
1194
1291
  };
1195
- if (order$1 === "asc") {
1292
+ if (order === "asc") {
1196
1293
  const upperBound = BigInt(blockNumberLte || latestBlock);
1197
1294
  const nextFromBlock = min(BigInt(toBlock) + 1n, upperBound);
1198
1295
  const nextToBlock = min(toBlock + BigInt(blockWindow) + 1n, upperBound);
1199
1296
  fromBlock = nextFromBlock;
1200
1297
  toBlock = nextToBlock;
1201
1298
  }
1202
- if (order$1 === "desc") {
1299
+ if (order === "desc") {
1203
1300
  const lowerBound = BigInt(blockNumberGte || 0);
1204
1301
  const nextToBlock = max$1(fromBlock - 1n, lowerBound);
1205
1302
  const nextFromBlock = max$1(fromBlock - BigInt(blockWindow) - 1n, lowerBound);
@@ -1209,7 +1306,7 @@ async function* streamLogs(parameters) {
1209
1306
  }
1210
1307
  yield {
1211
1308
  logs: [],
1212
- blockNumber: order$1 === "asc" ? Number(toBlock) : Number(fromBlock)
1309
+ blockNumber: order === "asc" ? Number(toBlock) : Number(fromBlock)
1213
1310
  };
1214
1311
  }
1215
1312
  var InvalidBlockRangeError = class extends BaseError {
@@ -1358,8 +1455,8 @@ const from$15 = (parameters) => {
1358
1455
  */
1359
1456
  function random$3() {
1360
1457
  return from$15({
1361
- asset: privateKeyToAccount(generatePrivateKey()).address,
1362
- oracle: privateKeyToAccount(generatePrivateKey()).address,
1458
+ asset: address(),
1459
+ oracle: address(),
1363
1460
  lltv: .965
1364
1461
  });
1365
1462
  }
@@ -1774,12 +1871,8 @@ function id(obligation) {
1774
1871
  function random$2() {
1775
1872
  return from$13({
1776
1873
  chainId: 1,
1777
- loanToken: privateKeyToAccount(generatePrivateKey()).address,
1778
- collaterals: [from$15({
1779
- asset: privateKeyToAccount(generatePrivateKey()).address,
1780
- oracle: privateKeyToAccount(generatePrivateKey()).address,
1781
- lltv: .965
1782
- })],
1874
+ loanToken: address(),
1875
+ collaterals: [random$3()],
1783
1876
  maturity: from$14("end_of_next_quarter")
1784
1877
  });
1785
1878
  }
@@ -1799,101 +1892,249 @@ var CollateralsAreNotSortedError = class extends BaseError {
1799
1892
  //#endregion
1800
1893
  //#region src/core/Tree.ts
1801
1894
  var Tree_exports = /* @__PURE__ */ __export({
1895
+ DecodeError: () => DecodeError,
1896
+ EncodeError: () => EncodeError,
1897
+ TreeError: () => TreeError,
1802
1898
  VERSION: () => VERSION$1,
1803
1899
  decode: () => decode$2,
1804
1900
  encode: () => encode$2,
1805
- from: () => from$12
1901
+ encodeUnsigned: () => encodeUnsigned,
1902
+ from: () => from$12,
1903
+ proofs: () => proofs
1806
1904
  });
1807
1905
  const VERSION$1 = 1;
1906
+ const normalizeHash = (hash$1) => hash$1.toLowerCase();
1808
1907
  /**
1809
1908
  * Builds a Merkle tree from a list of offers.
1810
1909
  *
1811
1910
  * Leaves are the offer `hash` values as `bytes32` and are deterministically
1812
- * ordered in ascending lexicographic order so that the resulting root is stable
1813
- * regardless of the input order.
1911
+ * ordered following the StandardMerkleTree leaf ordering so that the resulting
1912
+ * root is stable regardless of the input order.
1814
1913
  *
1815
1914
  * @param offers - Offers to include in the tree.
1816
1915
  * @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
1916
+ * @throws {TreeError} If tree building fails due to offer inconsistencies.
1817
1917
  */
1818
1918
  const from$12 = (offers$1) => {
1819
- const leaves = order(offers$1).map((offer) => {
1820
- return [offer.hash];
1821
- });
1919
+ const leaves = offers$1.map((offer) => [offer.hash]);
1822
1920
  const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
1823
- return Object.assign(tree, { offers: offers$1 });
1921
+ const orderedOffers = orderOffers(tree, offers$1);
1922
+ return Object.assign(tree, { offers: orderedOffers });
1824
1923
  };
1825
- const byHashAsc = (a, b) => a.localeCompare(b);
1826
- const order = (offers$1) => {
1827
- return offers$1.sort((a, b) => byHashAsc(a.hash, b.hash));
1924
+ const orderOffers = (tree, offers$1) => {
1925
+ const offerByHash = /* @__PURE__ */ new Map();
1926
+ for (const offer of offers$1) offerByHash.set(normalizeHash(offer.hash), offer);
1927
+ const entries = tree.dump().values.map((value) => {
1928
+ const hash$1 = normalizeHash(value.value[0]);
1929
+ const offer = offerByHash.get(hash$1);
1930
+ if (!offer) throw new TreeError(`missing offer for leaf ${hash$1}`);
1931
+ return {
1932
+ offer,
1933
+ treeIndex: value.treeIndex
1934
+ };
1935
+ });
1936
+ entries.sort((a, b) => b.treeIndex - a.treeIndex);
1937
+ return entries.map((item) => item.offer);
1828
1938
  };
1829
1939
  /**
1830
- * Encodes an `Tree` into a Hex string with a version byte prefix and gzipped payload.
1940
+ * Generates merkle proofs for all offers in a tree.
1831
1941
  *
1832
- * - Layout: `0x{vv}{zip...}` where `{vv}` is one-byte version as two hex chars.
1833
- * - Payload is gzip(JSON.stringify([root, ...offers])) with bigint stringified.
1942
+ * Each proof allows independent verification that an offer is included in the tree
1943
+ * without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
1834
1944
  *
1835
- * @param tree - The offer Merkle tree to encode.
1836
- * @returns Hex string starting with `0x{vv}` followed by gzipped payload bytes.
1837
- * @throws Error if the given `root` does not match the offers.
1945
+ * @param tree - The {@link Tree} to generate proofs for.
1946
+ * @returns Array of proofs - {@link Proof}
1838
1947
  */
1839
- const encode$2 = (tree) => {
1840
- assertRoot(tree.root, tree.offers);
1841
- const offersPayload = tree.offers.map((offer) => ({
1842
- offering: offer.offering,
1843
- assets: offer.assets.toString(),
1844
- rate: offer.rate.toString(),
1845
- maturity: Number(offer.maturity),
1846
- expiry: Number(offer.expiry),
1847
- start: Number(offer.start),
1848
- nonce: offer.nonce.toString(),
1849
- buy: offer.buy,
1850
- chainId: offer.chainId,
1851
- loanToken: offer.loanToken,
1852
- collaterals: offer.collaterals.map((c) => ({
1853
- asset: c.asset,
1854
- oracle: c.oracle,
1855
- lltv: c.lltv.toString()
1856
- })),
1857
- callback: {
1858
- address: offer.callback.address,
1859
- data: offer.callback.data,
1860
- gasLimit: offer.callback.gasLimit.toString()
1861
- },
1862
- signature: offer.signature,
1863
- hash: offer.hash
1864
- }));
1865
- const compressed = gzip(JSON.stringify([tree.root, ...offersPayload]));
1866
- const encoded = new Uint8Array(1 + compressed.length);
1867
- if (VERSION$1 > 255) throw new Error(`Version overflow: ${VERSION$1}`);
1868
- encoded[0] = VERSION$1;
1869
- encoded.set(compressed, 1);
1948
+ const proofs = (tree) => {
1949
+ return tree.offers.map((offer) => {
1950
+ return {
1951
+ offer,
1952
+ path: tree.getProof([offer.hash])
1953
+ };
1954
+ });
1955
+ };
1956
+ const assertHex = (value, expectedBytes, name) => {
1957
+ if (typeof value !== "string" || !isHex(value)) throw new DecodeError(`${name} is not a valid hex string`);
1958
+ if (hexToBytes(value).length !== expectedBytes) throw new DecodeError(`${name}: expected ${expectedBytes} bytes`);
1959
+ };
1960
+ const verifySignatureAndRecoverAddress = async (params) => {
1961
+ const { root, signature } = params;
1962
+ assertHex(signature, 65, "signature");
1963
+ const hash$1 = hashMessage({ raw: root });
1964
+ try {
1965
+ return await recoverAddress({
1966
+ hash: hash$1,
1967
+ signature
1968
+ });
1969
+ } catch {
1970
+ throw new DecodeError("signature recovery failed");
1971
+ }
1972
+ };
1973
+ /**
1974
+ * Encodes a merkle tree with signature into hex calldata for onchain broadcast.
1975
+ *
1976
+ * Layout: `0x{vv}{gzip([...offers])}{root}{signature}` where:
1977
+ * - `{vv}`: 1-byte version (currently 0x01)
1978
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
1979
+ * - `{root}`: 32-byte merkle root
1980
+ * - `{signature}`: 65-byte EIP-191 signature over raw root bytes
1981
+ *
1982
+ * Validates signature authenticity and root integrity before encoding.
1983
+ *
1984
+ * @example
1985
+ * ```typescript
1986
+ * const tree = Tree.from(offers);
1987
+ * const signature = await wallet.signMessage({ message: { raw: tree.root } });
1988
+ * const calldata = await Tree.encode(tree, signature);
1989
+ * await broadcast(calldata);
1990
+ * ```
1991
+ *
1992
+ * @example
1993
+ * Manual construction (for advanced users):
1994
+ * ```typescript
1995
+ * const tree = Tree.from(offers);
1996
+ * const compressed = gzip(JSON.stringify(tree.offers.map(Offer.serialize)));
1997
+ * const partial = `0x01${bytesToHex(compressed)}${tree.root.slice(2)}`;
1998
+ * const signature = await wallet.signMessage({ message: { raw: tree.root } });
1999
+ * const calldata = `${partial}${signature.slice(2)}`;
2000
+ * ```
2001
+ *
2002
+ * @param tree - Merkle tree of offers
2003
+ * @param signature - EIP-191 signature over raw root bytes
2004
+ * @returns Hex-encoded calldata ready for onchain broadcast
2005
+ * @throws {EncodeError} If signature verification fails or root mismatch
2006
+ */
2007
+ const encode$2 = async (tree, signature) => {
2008
+ validateTreeForEncoding(tree);
2009
+ await verifySignatureAndRecoverAddress({
2010
+ root: tree.root,
2011
+ signature
2012
+ });
2013
+ const unsigned = encodeUnsignedBytes(tree);
2014
+ const sigBytes = hexToBytes(signature);
2015
+ const encoded = new Uint8Array(unsigned.length + sigBytes.length);
2016
+ encoded.set(unsigned, 0);
2017
+ encoded.set(sigBytes, unsigned.length);
1870
2018
  return bytesToHex(encoded);
1871
2019
  };
1872
- const assertRoot = (root, offers$1) => {
1873
- const tree = from$12(offers$1);
1874
- if (root !== tree.root) throw new Error(`Invalid root: expected ${tree.root}, got ${root}`);
2020
+ /**
2021
+ * Encodes a merkle tree without a signature into hex payload for client-side signing.
2022
+ *
2023
+ * Layout: `0x{vv}{gzip([...offers])}{root}` where:
2024
+ * - `{vv}`: 1-byte version (currently 0x01)
2025
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
2026
+ * - `{root}`: 32-byte merkle root
2027
+ *
2028
+ * Validates root integrity before encoding.
2029
+ *
2030
+ * @param tree - Merkle tree of offers
2031
+ * @returns Hex-encoded unsigned payload
2032
+ * @throws {EncodeError} If root mismatch
2033
+ */
2034
+ const encodeUnsigned = (tree) => {
2035
+ validateTreeForEncoding(tree);
2036
+ return bytesToHex(encodeUnsignedBytes(tree));
2037
+ };
2038
+ const validateTreeForEncoding = (tree) => {
2039
+ if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
2040
+ const computed = from$12(tree.offers);
2041
+ if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
2042
+ };
2043
+ const encodeUnsignedBytes = (tree) => {
2044
+ const offersPayload = tree.offers.map(serialize);
2045
+ const compressed = gzip(JSON.stringify(offersPayload));
2046
+ const rootBytes = hexToBytes(tree.root);
2047
+ const encoded = new Uint8Array(1 + compressed.length + 32);
2048
+ encoded[0] = VERSION$1;
2049
+ encoded.set(compressed, 1);
2050
+ encoded.set(rootBytes, 1 + compressed.length);
2051
+ return encoded;
1875
2052
  };
1876
2053
  /**
1877
- * Decodes a Hex string produced by {@link encode} back into an `Tree`.
2054
+ * Decodes hex calldata into a validated merkle tree.
1878
2055
  *
1879
- * - Ensures the first byte version matches {@link VERSION}.
1880
- * - Decompresses with gunzip, parses JSON, validates offers, and re-checks the root.
2056
+ * Validates signature before decompression for fail-fast rejection of invalid payloads.
2057
+ * Returns the tree with separately validated signature and recovered signer address.
1881
2058
  *
1882
- * @param encoded - Hex string in the form `0x{vv}{zip...}`.
1883
- * @returns A validated `Tree` rebuilt from the offers.
1884
- * @throws Error if the version is invalid or the root does not match the offers.
2059
+ * Validation order:
2060
+ * 1. Version check
2061
+ * 2. Signature verification (fail-fast, before decompression)
2062
+ * 3. Decompression (only if signature valid)
2063
+ * 4. Root verification (computed from offers vs embedded root)
2064
+ *
2065
+ * @example
2066
+ * ```typescript
2067
+ * const { tree, signature, signer } = await Tree.decode(calldata);
2068
+ * console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
2069
+ * ```
2070
+ *
2071
+ * @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
2072
+ * @returns Validated tree, signature, and recovered signer address
2073
+ * @throws {DecodeError} If version invalid, signature invalid, or root mismatch
2074
+ */
2075
+ const decode$2 = async (encoded) => {
2076
+ const bytes$1 = hexToBytes(encoded);
2077
+ if (bytes$1.length < 98) throw new DecodeError("payload too short");
2078
+ const version = bytes$1[0];
2079
+ if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
2080
+ const signature = bytesToHex(bytes$1.slice(-65));
2081
+ const root = bytesToHex(bytes$1.slice(-97, -65));
2082
+ assertHex(root, 32, "root");
2083
+ assertHex(signature, 65, "signature");
2084
+ const signer = await verifySignatureAndRecoverAddress({
2085
+ root,
2086
+ signature
2087
+ });
2088
+ const compressed = bytes$1.slice(1, -97);
2089
+ let decoded;
2090
+ try {
2091
+ decoded = ungzip(compressed, { to: "string" });
2092
+ } catch {
2093
+ throw new DecodeError("decompression failed");
2094
+ }
2095
+ let rawOffers;
2096
+ try {
2097
+ rawOffers = JSON.parse(decoded);
2098
+ } catch {
2099
+ throw new DecodeError("JSON parse failed");
2100
+ }
2101
+ const tree = from$12(rawOffers.map((o) => OfferSchema().parse(o)));
2102
+ if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
2103
+ return {
2104
+ tree,
2105
+ signature,
2106
+ signer
2107
+ };
2108
+ };
2109
+ /**
2110
+ * Error thrown during tree building operations.
2111
+ * Indicates structural issues with the tree (missing offers, inconsistent state).
2112
+ */
2113
+ var TreeError = class extends BaseError {
2114
+ name = "Tree.TreeError";
2115
+ constructor(reason) {
2116
+ super(`Tree error: ${reason}`);
2117
+ }
2118
+ };
2119
+ /**
2120
+ * Error thrown during tree encoding.
2121
+ * Indicates validation failures (signature, root mismatch, mixed makers).
2122
+ */
2123
+ var EncodeError = class extends BaseError {
2124
+ name = "Tree.EncodeError";
2125
+ constructor(reason) {
2126
+ super(`Failed to encode tree: ${reason}`);
2127
+ }
2128
+ };
2129
+ /**
2130
+ * Error thrown during tree decoding.
2131
+ * Indicates payload corruption, version mismatch, or validation failures.
1885
2132
  */
1886
- const decode$2 = (encoded) => {
1887
- const bytes = hexToBytes(encoded);
1888
- if (bytes.length < 2) throw new Error("Invalid payload: too short");
1889
- const version = bytes[0];
1890
- if (version !== (VERSION$1 & 255)) throw new Error(`Invalid version: expected ${VERSION$1}, got ${version}`);
1891
- const decoded = ungzip(bytes.slice(1), { to: "string" });
1892
- const data = JSON.parse(decoded);
1893
- const root = data[0];
1894
- const tree = from$12(data.slice(1).map((o) => OfferSchema().parse(o)));
1895
- if (root !== tree.root) throw new Error(`Invalid root: expected ${tree.root}, got ${root}`);
1896
- return tree;
2133
+ var DecodeError = class extends BaseError {
2134
+ name = "Tree.DecodeError";
2135
+ constructor(reason) {
2136
+ super(`Failed to decode tree: ${reason}`);
2137
+ }
1897
2138
  };
1898
2139
 
1899
2140
  //#endregion
@@ -1913,6 +2154,7 @@ var Offer_exports = /* @__PURE__ */ __export({
1913
2154
  hash: () => hash,
1914
2155
  obligationId: () => obligationId,
1915
2156
  random: () => random$1,
2157
+ serialize: () => serialize,
1916
2158
  sign: () => sign,
1917
2159
  signatureMsg: () => signatureMsg,
1918
2160
  toSnakeCase: () => toSnakeCase,
@@ -1993,16 +2235,47 @@ function toSnakeCase(offer) {
1993
2235
  return toSnakeCase$1(offer);
1994
2236
  }
1995
2237
  /**
2238
+ * Serializes an offer for merkle tree encoding.
2239
+ * Converts BigInt fields to strings for JSON compatibility.
2240
+ *
2241
+ * @param offer - Offer to serialize
2242
+ * @returns JSON-serializable offer object
2243
+ */
2244
+ const serialize = (offer) => ({
2245
+ offering: offer.offering,
2246
+ assets: offer.assets.toString(),
2247
+ rate: offer.rate.toString(),
2248
+ maturity: Number(offer.maturity),
2249
+ expiry: Number(offer.expiry),
2250
+ start: Number(offer.start),
2251
+ nonce: offer.nonce.toString(),
2252
+ buy: offer.buy,
2253
+ chainId: offer.chainId,
2254
+ loanToken: offer.loanToken,
2255
+ collaterals: offer.collaterals.map((c) => ({
2256
+ asset: c.asset,
2257
+ oracle: c.oracle,
2258
+ lltv: c.lltv.toString()
2259
+ })),
2260
+ callback: {
2261
+ address: offer.callback.address,
2262
+ data: offer.callback.data,
2263
+ gasLimit: offer.callback.gasLimit.toString()
2264
+ },
2265
+ signature: offer.signature,
2266
+ hash: offer.hash
2267
+ });
2268
+ /**
1996
2269
  * Generates a random Offer.
1997
2270
  * The returned Offer contains randomly generated values.
1998
2271
  * @warning The generated Offer should not be used for production usage.
1999
2272
  * @returns {Offer} A randomly generated Offer object.
2000
2273
  */
2001
2274
  function random$1(config) {
2002
- const chain = config?.chains ? config.chains[Math.floor(Math.random() * config.chains.length)] : chains$2.ethereum;
2003
- const loanToken = config?.loanTokens ? config.loanTokens[Math.floor(Math.random() * config.loanTokens.length)] : privateKeyToAccount(generatePrivateKey()).address;
2004
- const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [privateKeyToAccount(generatePrivateKey()).address];
2005
- const collateralAsset = collateralCandidates[Math.floor(Math.random() * collateralCandidates.length)];
2275
+ const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
2276
+ const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
2277
+ const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
2278
+ const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
2006
2279
  const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
2007
2280
  const maturity$1 = config?.maturity ?? from$14(maturityOption);
2008
2281
  const lltv = from$16(weightedChoice([
@@ -2016,7 +2289,7 @@ function random$1(config) {
2016
2289
  [.965, 4],
2017
2290
  [.98, 2]
2018
2291
  ]));
2019
- const buy = config?.buy !== void 0 ? config.buy : Math.random() > .5;
2292
+ const buy = config?.buy !== void 0 ? config.buy : bool();
2020
2293
  const ONE = 1000000000000000000n;
2021
2294
  const qMin = buy ? 16 : 4;
2022
2295
  const len = (buy ? 32 : 16) - qMin + 1;
@@ -2027,9 +2300,9 @@ function random$1(config) {
2027
2300
  const rate = config?.rate ?? weightedChoice(ratePairs);
2028
2301
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
2029
2302
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2030
- const amountBase = BigInt(100 + Math.floor(Math.random() * 999901));
2303
+ const amountBase = BigInt(100 + int(999901));
2031
2304
  const assetsScaled = config?.assets ?? amountBase * unit;
2032
- const consumed = config?.consumed !== void 0 ? config.consumed : Math.random() < .8 ? 0n : assetsScaled * BigInt(1 + Math.floor(Math.random() * 900)) / 1000n;
2305
+ const consumed = config?.consumed !== void 0 ? config.consumed : float() < .8 ? 0n : assetsScaled * BigInt(1 + int(900)) / 1000n;
2033
2306
  const callbackBySide = (() => {
2034
2307
  if (buy) return {
2035
2308
  address: zeroAddress,
@@ -2048,29 +2321,29 @@ function random$1(config) {
2048
2321
  };
2049
2322
  })();
2050
2323
  return from$11({
2051
- offering: config?.offering ?? privateKeyToAccount(generatePrivateKey()).address,
2324
+ offering: config?.offering ?? address(),
2052
2325
  assets: assetsScaled,
2053
2326
  rate,
2054
2327
  maturity: maturity$1,
2055
2328
  expiry: config?.expiry ?? maturity$1 - 1,
2056
2329
  start: config?.start ?? maturity$1 - 10,
2057
- nonce: BigInt(Math.floor(Math.random() * 1e6)),
2330
+ nonce: BigInt(int(1e6)),
2058
2331
  buy,
2059
2332
  chainId: chain.id,
2060
2333
  loanToken,
2061
- collaterals: config?.collaterals ?? Array.from({ length: Math.floor(Math.random() * 3) + 1 }, () => ({
2334
+ collaterals: config?.collaterals ?? Array.from({ length: int(3) + 1 }, () => ({
2062
2335
  ...random$3(),
2063
2336
  lltv
2064
2337
  })).sort((a, b) => a.asset.localeCompare(b.asset)),
2065
2338
  callback: config?.callback ?? callbackBySide,
2066
2339
  consumed,
2067
2340
  takeable: config?.takeable ?? assetsScaled - consumed,
2068
- blockNumber: config?.blockNumber ?? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
2341
+ blockNumber: config?.blockNumber ?? int(Number.MAX_SAFE_INTEGER)
2069
2342
  });
2070
2343
  }
2071
2344
  const weightedChoice = (pairs) => {
2072
2345
  const total = pairs.reduce((sum, [, weight]) => sum + weight, 0);
2073
- let roll = Math.random() * total;
2346
+ let roll = float() * total;
2074
2347
  for (const [value, weight] of pairs) {
2075
2348
  roll -= weight;
2076
2349
  if (roll < 0) return value;
@@ -2538,8 +2811,8 @@ function fromSnakeCase(snake) {
2538
2811
  function random() {
2539
2812
  return from$8({
2540
2813
  obligationId: id(random$2()),
2541
- ask: { rate: BigInt(Math.floor(Math.random() * 1e6)) },
2542
- bid: { rate: BigInt(Math.floor(Math.random() * 1e6)) }
2814
+ ask: { rate: BigInt(int(1e6)) },
2815
+ bid: { rate: BigInt(int(1e6)) }
2543
2816
  });
2544
2817
  }
2545
2818
  var InvalidQuoteError = class extends BaseError {
@@ -2723,30 +2996,79 @@ async function* collectOffersV2(parameters) {
2723
2996
  });
2724
2997
  for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
2725
2998
  blockNumber = lastStreamBlockNumber;
2726
- const offers$1 = [];
2999
+ const decodedTrees = [];
2727
3000
  for (const log of logs) {
2728
3001
  if (!log) continue;
2729
3002
  const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
2730
3003
  try {
2731
- const tree = decode$2(payload);
2732
- for (const offer of tree.offers) offers$1.push({
3004
+ const { tree, signature, signer } = await decode$2(payload);
3005
+ const signerMismatch = tree.offers.find((offer) => offer.offering.toLowerCase() !== signer.toLowerCase());
3006
+ if (signerMismatch) {
3007
+ logger.debug({
3008
+ msg: "Tree rejected: signer mismatch",
3009
+ reason: "signer_mismatch",
3010
+ signer,
3011
+ offering: signerMismatch.offering,
3012
+ chain_id: client.chain.id
3013
+ });
3014
+ continue;
3015
+ }
3016
+ const offersWithBlock = tree.offers.map((offer) => ({
2733
3017
  ...offer,
2734
3018
  blockNumber: Number(log.blockNumber)
3019
+ }));
3020
+ const treeWithBlock = Object.assign(Object.create(Object.getPrototypeOf(tree)), tree, { offers: offersWithBlock });
3021
+ decodedTrees.push({
3022
+ tree: treeWithBlock,
3023
+ signature,
3024
+ signer,
3025
+ blockNumber: Number(log.blockNumber)
2735
3026
  });
2736
- } catch (_) {}
3027
+ } catch (err) {
3028
+ const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
3029
+ logger.debug({
3030
+ msg: "Tree decode failed",
3031
+ reason,
3032
+ chain_id: client.chain.id,
3033
+ err: err instanceof Error ? err.message : String(err)
3034
+ });
3035
+ }
2737
3036
  }
2738
3037
  await db.transaction(async (dbTx) => {
2739
3038
  const { epoch, blockNumber: latestBlockNumber } = await dbTx.chains.getBlockNumber(client.chain.id);
2740
- let validOffers = [];
2741
- try {
2742
- validOffers = (await gatekeeper.isAllowed(offers$1)).valid.filter((offer) => offer.blockNumber <= latestBlockNumber);
3039
+ const treesToInsert = [];
3040
+ let totalValidOffers = 0;
3041
+ for (const { tree, signature } of decodedTrees) try {
3042
+ const allowedResults = await gatekeeper.isAllowed(tree.offers);
3043
+ const hasBlockWindowViolation = tree.offers.some((offer) => offer.blockNumber > latestBlockNumber);
3044
+ if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
3045
+ if (allowedResults.issues.length > 0) {
3046
+ const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
3047
+ logger.debug({
3048
+ msg: "Tree offers rejected by gatekeeper",
3049
+ reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
3050
+ chain_id: client.chain.id,
3051
+ issues_count: allowedResults.issues.length
3052
+ });
3053
+ } else if (hasBlockWindowViolation) logger.debug({
3054
+ msg: "Tree rejected: offers outside block window",
3055
+ reason: "block_window",
3056
+ chain_id: client.chain.id
3057
+ });
3058
+ continue;
3059
+ }
3060
+ treesToInsert.push({
3061
+ tree,
3062
+ signature
3063
+ });
3064
+ totalValidOffers += tree.offers.length;
2743
3065
  } catch (err) {
2744
3066
  logger.error({
2745
3067
  err,
2746
- msg: "Failed to validate offers"
3068
+ msg: "Failed to validate offers for tree"
2747
3069
  });
2748
3070
  }
2749
- await dbTx.offers.create(validOffers);
3071
+ if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
2750
3072
  try {
2751
3073
  await dbTx.collectors.saveBlockNumber({
2752
3074
  collectorName: collector,
@@ -2754,10 +3076,11 @@ async function* collectOffersV2(parameters) {
2754
3076
  blockNumber,
2755
3077
  epoch
2756
3078
  });
2757
- if (validOffers.length > 0) logger.info({
3079
+ if (totalValidOffers > 0) logger.info({
2758
3080
  msg: `New offers`,
2759
3081
  collector,
2760
- count: validOffers.length,
3082
+ count: totalValidOffers,
3083
+ trees_count: treesToInsert.length,
2761
3084
  chain_id: client.chain.id,
2762
3085
  block_range: [startBlock, lastStreamBlockNumber]
2763
3086
  });
@@ -3400,7 +3723,7 @@ async function* collectPrices(parameters) {
3400
3723
  //#region src/indexer/collectors/CollectorBuilder.ts
3401
3724
  function createBuilder(parameters) {
3402
3725
  const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow } = {} } = parameters;
3403
- const createCollector = (name, collect) => create$13({
3726
+ const createCollector = (name, collect) => create$16({
3404
3727
  name,
3405
3728
  collect,
3406
3729
  client,
@@ -3488,7 +3811,7 @@ const from$6 = (parameters) => {
3488
3811
  //#endregion
3489
3812
  //#region src/indexer/Indexer.ts
3490
3813
  var Indexer_exports = /* @__PURE__ */ __export({
3491
- create: () => create$12,
3814
+ create: () => create$15,
3492
3815
  from: () => from$5
3493
3816
  });
3494
3817
  function from$5(config) {
@@ -3503,7 +3826,7 @@ function from$5(config) {
3503
3826
  retryAttempts,
3504
3827
  retryDelayMs
3505
3828
  });
3506
- return create$12({
3829
+ return create$15({
3507
3830
  db,
3508
3831
  client,
3509
3832
  collectors: [
@@ -3515,7 +3838,7 @@ function from$5(config) {
3515
3838
  interval
3516
3839
  });
3517
3840
  }
3518
- function create$12(params) {
3841
+ function create$15(params) {
3519
3842
  const { db, collectors: collectors$1, interval, client } = params;
3520
3843
  const logger = getLogger();
3521
3844
  const indexerId = `${client.chain.id.toString()}.indexer`;
@@ -3572,12 +3895,12 @@ function create$12(params) {
3572
3895
 
3573
3896
  //#endregion
3574
3897
  //#region src/api/Health.ts
3575
- var Health_exports = /* @__PURE__ */ __export({ create: () => create$11 });
3898
+ var Health_exports = /* @__PURE__ */ __export({ create: () => create$14 });
3576
3899
  const DEFAULT_MAX_ALLOWED_LAG = 5;
3577
3900
  /**
3578
3901
  * Create a health service that exposes collector and chain block numbers.
3579
3902
  */
3580
- function create$11(parameters) {
3903
+ function create$14(parameters) {
3581
3904
  const { db, maxAllowedLag = DEFAULT_MAX_ALLOWED_LAG, healthClients } = parameters;
3582
3905
  const loadSnapshot = async () => {
3583
3906
  const [collectorRows, chainRows, remoteBlockByChainId] = await Promise.all([
@@ -3724,13 +4047,16 @@ var OfferResponse_exports = /* @__PURE__ */ __export({ from: () => from$2 });
3724
4047
  * Creates an `OfferResponse` from an `Offer`.
3725
4048
  * @constructor
3726
4049
  * @param offer - {@link Offer}
4050
+ * @param attestation - {@link Attestation}
3727
4051
  * @returns The created `OfferResponse`. {@link OfferResponse}
3728
4052
  */
3729
- function from$2(offer) {
3730
- const result = toSnakeCase$1(offer);
4053
+ function from$2(offer, attestation) {
4054
+ const { signature: _, ...rest } = toSnakeCase$1(offer);
3731
4055
  return {
3732
- ...result,
3733
- signature: result.signature ?? null
4056
+ ...rest,
4057
+ root: attestation?.root.toLowerCase() ?? null,
4058
+ proof: attestation?.proof.map((p) => p.toLowerCase()) ?? null,
4059
+ signature: attestation?.signature.toLowerCase() ?? null
3734
4060
  };
3735
4061
  }
3736
4062
 
@@ -3868,10 +4194,12 @@ const offerExample = {
3868
4194
  data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000",
3869
4195
  gas_limit: "500000"
3870
4196
  },
3871
- signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400",
3872
4197
  consumed: "0",
3873
4198
  takeable: "369216000000000000000000",
3874
- block_number: 0xa7495128adfb1
4199
+ block_number: 0xa7495128adfb1,
4200
+ root: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
4201
+ proof: ["0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"],
4202
+ signature: "0x1234567890123456789012345678901234567890123456789012345678901234123456789012345678901234567890123456789012345678901234567890123400"
3875
4203
  };
3876
4204
  const collectorsHealthExample = {
3877
4205
  name: "offers",
@@ -4016,6 +4344,16 @@ __decorate([ApiProperty({
4016
4344
  type: "number",
4017
4345
  example: offerExample.block_number
4018
4346
  })], OfferListItemResponse.prototype, "block_number", void 0);
4347
+ __decorate([ApiProperty({
4348
+ type: "string",
4349
+ nullable: true,
4350
+ example: offerExample.root
4351
+ })], OfferListItemResponse.prototype, "root", void 0);
4352
+ __decorate([ApiProperty({
4353
+ type: [String],
4354
+ nullable: true,
4355
+ example: offerExample.proof
4356
+ })], OfferListItemResponse.prototype, "proof", void 0);
4019
4357
  __decorate([ApiProperty({
4020
4358
  type: "string",
4021
4359
  nullable: true,
@@ -4199,44 +4537,61 @@ __decorate([ApiProperty({
4199
4537
  var ValidateOffersRequest = class {};
4200
4538
  __decorate([ApiProperty({
4201
4539
  type: () => [ValidateOfferRequest],
4202
- description: "Array of offers in snake_case format. Mutually exclusive with 'calldata'.",
4203
- required: false
4540
+ description: "Array of offers in snake_case format. Required, non-empty.",
4541
+ required: true
4204
4542
  })], ValidateOffersRequest.prototype, "offers", void 0);
4543
+ var ValidationSuccessDataResponse = class {};
4205
4544
  __decorate([ApiProperty({
4206
4545
  type: "string",
4207
- description: "Encoded tree calldata as a hex string (0x-prefixed). Mutually exclusive with 'offers'.",
4208
- example: "0x01...",
4209
- required: false
4210
- })], ValidateOffersRequest.prototype, "calldata", void 0);
4211
- var ValidateOfferResultResponse = class {};
4546
+ description: "Unsigned payload: version (1B) + gzip(offers) + root (32B).",
4547
+ example: "0x01789c..."
4548
+ })], ValidationSuccessDataResponse.prototype, "payload", void 0);
4212
4549
  __decorate([ApiProperty({
4213
4550
  type: "string",
4214
- example: offerExample.hash
4215
- })], ValidateOfferResultResponse.prototype, "offer_hash", void 0);
4551
+ description: "Merkle tree root to sign with EIP-191.",
4552
+ example: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427"
4553
+ })], ValidationSuccessDataResponse.prototype, "root", void 0);
4554
+ var ValidationSuccessResponse = class extends SuccessResponse {};
4216
4555
  __decorate([ApiProperty({
4217
- type: "boolean",
4218
- example: false
4219
- })], ValidateOfferResultResponse.prototype, "valid", void 0);
4556
+ type: "string",
4557
+ nullable: true,
4558
+ example: null
4559
+ })], ValidationSuccessResponse.prototype, "cursor", void 0);
4560
+ __decorate([ApiProperty({
4561
+ type: () => ValidationSuccessDataResponse,
4562
+ description: "Payload and root for client-side signing."
4563
+ })], ValidationSuccessResponse.prototype, "data", void 0);
4564
+ var ValidationIssueResponse = class {};
4565
+ __decorate([ApiProperty({
4566
+ type: "number",
4567
+ description: "0-indexed position of the failed offer in the request array.",
4568
+ example: 0
4569
+ })], ValidationIssueResponse.prototype, "index", void 0);
4220
4570
  __decorate([ApiProperty({
4221
4571
  type: "string",
4222
- example: "parse_error",
4223
- nullable: true
4224
- })], ValidateOfferResultResponse.prototype, "rule", void 0);
4572
+ description: "Gatekeeper rule name that rejected the offer.",
4573
+ example: "no_buy"
4574
+ })], ValidationIssueResponse.prototype, "rule", void 0);
4225
4575
  __decorate([ApiProperty({
4226
4576
  type: "string",
4227
- example: "Invalid offer. 'offering': invalid address",
4228
- nullable: true
4229
- })], ValidateOfferResultResponse.prototype, "message", void 0);
4230
- var ValidateOffersListResponse = class extends SuccessResponse {};
4577
+ description: "Human-readable rejection reason.",
4578
+ example: "Buy offers are not supported"
4579
+ })], ValidationIssueResponse.prototype, "message", void 0);
4580
+ var ValidationFailureDataResponse = class {};
4581
+ __decorate([ApiProperty({
4582
+ type: () => [ValidationIssueResponse],
4583
+ description: "List of validation issues. Returned when any offer fails validation."
4584
+ })], ValidationFailureDataResponse.prototype, "issues", void 0);
4585
+ var ValidationFailureResponse = class extends SuccessResponse {};
4231
4586
  __decorate([ApiProperty({
4232
4587
  type: "string",
4233
4588
  nullable: true,
4234
4589
  example: null
4235
- })], ValidateOffersListResponse.prototype, "cursor", void 0);
4590
+ })], ValidationFailureResponse.prototype, "cursor", void 0);
4236
4591
  __decorate([ApiProperty({
4237
- type: () => [ValidateOfferResultResponse],
4238
- description: "Validation results for each offer."
4239
- })], ValidateOffersListResponse.prototype, "data", void 0);
4592
+ type: () => ValidationFailureDataResponse,
4593
+ description: "List of validation issues. Returned when any offer fails validation."
4594
+ })], ValidationFailureResponse.prototype, "data", void 0);
4240
4595
  var BookLevelResponse = class {};
4241
4596
  __decorate([ApiProperty({
4242
4597
  type: "string",
@@ -4301,13 +4656,18 @@ __decorate([
4301
4656
  methods: ["post"],
4302
4657
  path: "/v1/validate",
4303
4658
  summary: "Validate offers",
4304
- description: "Validates offers against router validation rules. Returns validation status for each offer. Accepts either an array of offers or encoded calldata (mutually exclusive)."
4659
+ description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
4305
4660
  }),
4306
4661
  ApiBody({ type: ValidateOffersRequest }),
4307
4662
  ApiResponse({
4308
4663
  status: 200,
4309
4664
  description: "Success",
4310
- type: ValidateOffersListResponse
4665
+ type: ValidationSuccessResponse
4666
+ }),
4667
+ ApiResponse({
4668
+ status: 200,
4669
+ description: "Validation issues",
4670
+ type: ValidationFailureResponse
4311
4671
  })
4312
4672
  ], ValidateController.prototype, "validateOffers", null);
4313
4673
  ValidateController = __decorate([ApiTags("Validate"), ApiResponse({
@@ -4476,7 +4836,7 @@ const OpenApi = async (options = {}) => {
4476
4836
  if (options.rules && options.rules.length > 0) {
4477
4837
  const rulesDescription = options.rules.map((rule) => `- **${rule.name}**: ${rule.description}`).join("\n");
4478
4838
  const validatePath = document.paths?.["/v1/validate"];
4479
- 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}`;
4839
+ 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}`;
4480
4840
  }
4481
4841
  return document;
4482
4842
  };
@@ -4488,17 +4848,23 @@ var Cursor_exports = /* @__PURE__ */ __export({
4488
4848
  encode: () => encode,
4489
4849
  validate: () => validate
4490
4850
  });
4491
- function validate(cursor) {
4492
- if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
4493
- const c = cursor;
4494
- if (![
4851
+ const isSort = (value) => {
4852
+ return [
4495
4853
  "rate",
4496
4854
  "maturity",
4497
4855
  "expiry",
4498
4856
  "amount"
4499
- ].includes(c.sort)) throw new Error(`Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`);
4500
- if (!["asc", "desc"].includes(c.dir)) throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
4501
- 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`);
4857
+ ].includes(value);
4858
+ };
4859
+ function validate(cursor) {
4860
+ if (!cursor || typeof cursor !== "object") throw new Error("Cursor must be an object");
4861
+ const c = cursor;
4862
+ const sort = c.sort;
4863
+ const dir = c.dir;
4864
+ const hash$1 = c.hash;
4865
+ if (typeof sort !== "string" || !isSort(sort)) throw new Error(`Invalid sort field: ${String(sort)}. Must be one of: rate, maturity, expiry, amount`);
4866
+ if (typeof dir !== "string" || !["asc", "desc"].includes(dir)) throw new Error(`Invalid direction: ${String(dir)}. Must be one of: asc, desc`);
4867
+ 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`);
4502
4868
  const validation = {
4503
4869
  rate: {
4504
4870
  field: "rate",
@@ -4515,24 +4881,30 @@ function validate(cursor) {
4515
4881
  maturity: {
4516
4882
  field: "maturity",
4517
4883
  type: "number",
4518
- validator: (val) => val > 0,
4884
+ min: 1,
4519
4885
  error: "positive number"
4520
4886
  },
4521
4887
  expiry: {
4522
4888
  field: "expiry",
4523
4889
  type: "number",
4524
- validator: (val) => val > 0,
4890
+ min: 1,
4525
4891
  error: "positive number"
4526
4892
  }
4527
- }[c.sort];
4528
- if (!validation) throw new Error(`Invalid sort field: ${c.sort}`);
4893
+ }[sort];
4894
+ if (!validation) throw new Error(`Invalid sort field: ${sort}`);
4529
4895
  const fieldValue = c[validation.field];
4530
- if (!fieldValue) throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
4531
- if (typeof fieldValue !== validation.type) throw new Error(`${c.sort} sort requires '${validation.field}' field of type ${validation.type}`);
4532
- if (validation.pattern && !validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
4533
- if (validation.validator && !validation.validator(fieldValue)) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
4534
- if (c.page !== void 0) {
4535
- if (typeof c.page !== "number" || !Number.isInteger(c.page) || c.page < 1) throw new Error("Invalid page: must be a positive integer");
4896
+ if (fieldValue === void 0 || fieldValue === null) throw new Error(`${sort} sort requires '${validation.field}' field to be present`);
4897
+ if (validation.type === "string") {
4898
+ if (typeof fieldValue !== "string") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
4899
+ if (!validation.pattern.test(fieldValue)) throw new Error(`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`);
4900
+ }
4901
+ if (validation.type === "number") {
4902
+ if (typeof fieldValue !== "number") throw new Error(`${sort} sort requires '${validation.field}' field of type ${validation.type}`);
4903
+ if (fieldValue < validation.min) throw new Error(`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`);
4904
+ }
4905
+ const page = c.page;
4906
+ if (page !== void 0) {
4907
+ if (typeof page !== "number" || !Number.isInteger(page) || page < 1) throw new Error("Invalid page: must be a positive integer");
4536
4908
  }
4537
4909
  return true;
4538
4910
  }
@@ -4648,21 +5020,7 @@ const schemas = {
4648
5020
  get_obligations: GetObligationsQueryParams,
4649
5021
  get_obligation: GetObligationParams,
4650
5022
  get_book: GetBookParams,
4651
- validate_offers: z$1.object({
4652
- offers: z$1.any().refine((val) => val === void 0 || Array.isArray(val), { message: "'offers' must be an array" }),
4653
- calldata: z$1.string().regex(/^0x[a-fA-F0-9]*$/, { message: "'calldata' must be a hex string starting with '0x'" }).optional()
4654
- }).superRefine((val, ctx) => {
4655
- const hasOffers = val.offers !== void 0;
4656
- const hasCalldata = val.calldata !== void 0;
4657
- if (hasOffers && hasCalldata) ctx.addIssue({
4658
- code: "custom",
4659
- message: "Request body must contain either 'offers' or 'calldata', not both"
4660
- });
4661
- if (!hasOffers && !hasCalldata) ctx.addIssue({
4662
- code: "custom",
4663
- message: "Request body must contain either 'offers' array or 'calldata' hex string"
4664
- });
4665
- })
5023
+ validate_offers: z$1.object({ offers: z$1.array(z$1.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict()
4666
5024
  };
4667
5025
  function parse(action, query) {
4668
5026
  return schemas[action].parse(query);
@@ -4745,7 +5103,7 @@ async function getDocsHtml({ gatekeeper }) {
4745
5103
  async function getHealth(db) {
4746
5104
  const logger = getLogger();
4747
5105
  try {
4748
- const status$1 = await create$11({ db }).getStatus();
5106
+ const status$1 = await create$14({ db }).getStatus();
4749
5107
  return success({ data: toSnakeCase$1({ status: status$1 }) });
4750
5108
  } catch (err) {
4751
5109
  logger.error({
@@ -4760,7 +5118,7 @@ async function getHealth(db) {
4760
5118
  async function getHealthChains(db, healthClients) {
4761
5119
  const logger = getLogger();
4762
5120
  try {
4763
- const chains$3 = await create$11({
5121
+ const chains$3 = await create$14({
4764
5122
  db,
4765
5123
  healthClients
4766
5124
  }).getChains();
@@ -4783,7 +5141,7 @@ async function getHealthChains(db, healthClients) {
4783
5141
  async function getHealthCollectors(db) {
4784
5142
  const logger = getLogger();
4785
5143
  try {
4786
- const collectors$1 = await create$11({ db }).getCollectors();
5144
+ const collectors$1 = await create$14({ db }).getCollectors();
4787
5145
  return success({ data: collectors$1.map(({ name, chainId, blockNumber, updatedAt, lag, status: status$1 }) => toSnakeCase$1({
4788
5146
  name,
4789
5147
  chainId,
@@ -4884,8 +5242,10 @@ async function getOffers$1(queryParameters, db) {
4884
5242
  cursor: query.cursor,
4885
5243
  limit: query.limit
4886
5244
  });
5245
+ const hashes = offers$1.map((offer) => offer.hash);
5246
+ const attestationMap = await db.trees.getAttestations(hashes);
4887
5247
  return success({
4888
- data: offers$1.map(from$2),
5248
+ data: offers$1.map((offer) => from$2(offer, attestationMap.get(offer.hash.toLowerCase()))),
4889
5249
  cursor: nextCursor ?? null
4890
5250
  });
4891
5251
  } catch (err) {
@@ -4905,56 +5265,49 @@ async function validateOffers(body, gatekeeper) {
4905
5265
  const logger = getLogger();
4906
5266
  const result = safeParse("validate_offers", body, (issue) => issue.message);
4907
5267
  if (!result.success) return failure(new BadRequestError(result.error.issues[0]?.message ?? "Invalid request body"));
4908
- const { offers: rawOffers, calldata } = result.data;
4909
- const results = [];
5268
+ const { offers: rawOffers } = result.data;
4910
5269
  const parsedOffers = [];
4911
- const parsedOfferIndices = [];
4912
- const hasOffers = rawOffers !== void 0;
4913
- if (calldata !== void 0) try {
4914
- const tree = decode$2(calldata);
4915
- for (const [i, offer] of tree.offers.entries()) {
4916
- parsedOffers.push(offer);
4917
- parsedOfferIndices.push(i);
4918
- }
4919
- } catch (err) {
4920
- const message = err instanceof Error ? err.message : String(err);
4921
- return failure(new BadRequestError(`Failed to decode calldata: ${message}`));
4922
- }
4923
- if (hasOffers) for (let i = 0; i < rawOffers.length; i++) {
5270
+ const offerIndexByHash = /* @__PURE__ */ new Map();
5271
+ for (let i = 0; i < rawOffers.length; i++) {
4924
5272
  const rawOffer = rawOffers[i];
4925
5273
  try {
4926
5274
  const offer = fromSnakeCase$1(rawOffer);
4927
- parsedOffers.push(offer);
4928
- parsedOfferIndices.push(i);
5275
+ if (!offerIndexByHash.has(offer.hash)) {
5276
+ offerIndexByHash.set(offer.hash, i);
5277
+ parsedOffers.push(offer);
5278
+ }
4929
5279
  } catch (err) {
4930
5280
  let message = err instanceof Error ? err.message : String(err);
4931
5281
  if (err instanceof InvalidOfferError) message = err.formattedMessage;
4932
- results[i] = {
4933
- offer_hash: rawOffer?.hash ?? "unknown",
4934
- valid: false,
4935
- rule: "parse_error",
4936
- message
4937
- };
5282
+ return failure(new BadRequestError(`Offer at index ${i} failed to parse: ${message}`));
4938
5283
  }
4939
5284
  }
4940
- if (parsedOffers.length > 0) try {
4941
- const { valid, issues } = await gatekeeper.isAllowed(parsedOffers);
4942
- for (const offer of valid) {
4943
- const originalIndex = parsedOfferIndices[parsedOffers.indexOf(offer)];
4944
- if (originalIndex !== void 0) results[originalIndex] = {
4945
- offer_hash: offer.hash,
4946
- valid: true
4947
- };
4948
- }
4949
- for (const issue of issues) {
4950
- const originalIndex = parsedOfferIndices[parsedOffers.indexOf(issue.item)];
4951
- if (originalIndex !== void 0) results[originalIndex] = {
4952
- offer_hash: issue.item.hash,
4953
- valid: false,
4954
- rule: issue.ruleName,
4955
- message: issue.message
4956
- };
5285
+ try {
5286
+ const { issues } = await gatekeeper.isAllowed(parsedOffers);
5287
+ if (issues.length > 0) {
5288
+ const mappedIssues = issues.map((issue) => {
5289
+ const index$1 = offerIndexByHash.get(issue.item.hash);
5290
+ if (index$1 === void 0) return null;
5291
+ return {
5292
+ index: index$1,
5293
+ rule: issue.ruleName,
5294
+ message: issue.message
5295
+ };
5296
+ }).filter((issue) => issue !== null);
5297
+ return success({
5298
+ data: { issues: mappedIssues },
5299
+ cursor: null
5300
+ });
4957
5301
  }
5302
+ const tree = from$12(parsedOffers);
5303
+ const payload = encodeUnsigned(tree);
5304
+ return success({
5305
+ data: {
5306
+ payload,
5307
+ root: tree.root
5308
+ },
5309
+ cursor: null
5310
+ });
4958
5311
  } catch (err) {
4959
5312
  logger.error({
4960
5313
  err,
@@ -4964,11 +5317,6 @@ async function validateOffers(body, gatekeeper) {
4964
5317
  });
4965
5318
  return failure(err);
4966
5319
  }
4967
- const orderedResults = results.filter((r) => r !== void 0);
4968
- return success({
4969
- data: orderedResults,
4970
- cursor: null
4971
- });
4972
5320
  }
4973
5321
 
4974
5322
  //#endregion
@@ -4990,13 +5338,13 @@ var Controllers_exports = /* @__PURE__ */ __export({
4990
5338
  //#region src/api/Api.ts
4991
5339
  function from$1(config) {
4992
5340
  const { db, gatekeeper, port } = config;
4993
- return create$10({
5341
+ return create$13({
4994
5342
  port,
4995
5343
  db,
4996
5344
  gatekeeper
4997
5345
  });
4998
5346
  }
4999
- function create$10(params) {
5347
+ function create$13(params) {
5000
5348
  return { serve: () => serve$1(params) };
5001
5349
  }
5002
5350
  /**
@@ -5091,7 +5439,7 @@ var RouterApi_exports = /* @__PURE__ */ __export({
5091
5439
  OpenApi: () => OpenApi,
5092
5440
  RouterStatusResponse: () => RouterStatusResponse,
5093
5441
  ValidateController: () => ValidateController,
5094
- create: () => create$10,
5442
+ create: () => create$13,
5095
5443
  from: () => from$1,
5096
5444
  parse: () => parse,
5097
5445
  safeParse: () => safeParse
@@ -5156,24 +5504,28 @@ async function getOffers(apiClient, parameters) {
5156
5504
  throw new HttpGetApiFailedError(`GET request returned ${response.status}`, { details: JSON.stringify(error) });
5157
5505
  }
5158
5506
  const offers$1 = data?.data.map((item) => {
5159
- const { signature, ...rest } = item;
5160
- return fromSnakeCase$1({
5161
- ...rest,
5162
- offering: item.offering,
5163
- maturity: from$14(item.maturity),
5164
- loan_token: item.loan_token,
5165
- collaterals: item.collaterals.map((collateral) => ({
5166
- asset: collateral.asset,
5167
- oracle: collateral.oracle,
5168
- lltv: collateral.lltv
5169
- })),
5170
- callback: {
5171
- ...item.callback,
5172
- address: item.callback.address,
5173
- data: item.callback.data
5174
- },
5175
- ...signature !== null ? { signature: item.signature } : void 0
5176
- });
5507
+ const { root, proof, signature, ...rest } = item;
5508
+ return {
5509
+ ...fromSnakeCase$1({
5510
+ ...rest,
5511
+ offering: item.offering,
5512
+ maturity: from$14(item.maturity),
5513
+ loan_token: item.loan_token,
5514
+ collaterals: item.collaterals.map((collateral) => ({
5515
+ asset: collateral.asset,
5516
+ oracle: collateral.oracle,
5517
+ lltv: collateral.lltv
5518
+ })),
5519
+ callback: {
5520
+ ...item.callback,
5521
+ address: item.callback.address,
5522
+ data: item.callback.data
5523
+ },
5524
+ signature: signature?.toLowerCase()
5525
+ }),
5526
+ root: root?.toLowerCase() || void 0,
5527
+ proof: proof?.map((p) => p.toLowerCase()) || void 0
5528
+ };
5177
5529
  }) ?? [];
5178
5530
  return {
5179
5531
  cursor: data?.cursor ?? null,
@@ -5267,15 +5619,19 @@ var schema_exports = /* @__PURE__ */ __export({
5267
5619
  collectors: () => collectors,
5268
5620
  consumedEvents: () => consumedEvents,
5269
5621
  groups: () => groups,
5622
+ lots: () => lots,
5623
+ merklePaths: () => merklePaths,
5270
5624
  obligationCollateralsV2: () => obligationCollateralsV2,
5271
5625
  obligations: () => obligations,
5272
5626
  offers: () => offers,
5273
5627
  offersCallbacks: () => offersCallbacks,
5628
+ offsets: () => offsets,
5274
5629
  oracles: () => oracles,
5275
5630
  positionTypes: () => positionTypes,
5276
5631
  positions: () => positions,
5277
5632
  status: () => status,
5278
5633
  transfers: () => transfers,
5634
+ trees: () => trees,
5279
5635
  validations: () => validations
5280
5636
  });
5281
5637
  const s = pgSchema(VERSION);
@@ -5293,6 +5649,10 @@ var EnumTableName = /* @__PURE__ */ function(EnumTableName$1) {
5293
5649
  EnumTableName$1["VALIDATIONS"] = "validations";
5294
5650
  EnumTableName$1["COLLECTORS"] = "collectors";
5295
5651
  EnumTableName$1["CHAINS"] = "chains";
5652
+ EnumTableName$1["LOTS"] = "lots";
5653
+ EnumTableName$1["OFFSETS"] = "offsets";
5654
+ EnumTableName$1["TREES"] = "trees";
5655
+ EnumTableName$1["MERKLE_PATHS"] = "merkle_paths";
5296
5656
  return EnumTableName$1;
5297
5657
  }(EnumTableName || {});
5298
5658
  const TABLE_NAMES = Object.values(EnumTableName);
@@ -5455,6 +5815,86 @@ const callbacks = s.table(EnumTableName.CALLBACKS, {
5455
5815
  ],
5456
5816
  name: "callbacks_positions_fk"
5457
5817
  }).onDelete("cascade")]);
5818
+ const lots = s.table(EnumTableName.LOTS, {
5819
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5820
+ user: varchar("user", { length: 42 }).notNull(),
5821
+ contract: varchar("contract", { length: 42 }).notNull(),
5822
+ group: varchar("group", { length: 66 }).notNull(),
5823
+ lower: numeric("lower", {
5824
+ precision: 78,
5825
+ scale: 0
5826
+ }).notNull(),
5827
+ upper: numeric("upper", {
5828
+ precision: 78,
5829
+ scale: 0
5830
+ }).notNull()
5831
+ }, (table) => [
5832
+ primaryKey({
5833
+ columns: [
5834
+ table.chainId,
5835
+ table.user,
5836
+ table.contract,
5837
+ table.group
5838
+ ],
5839
+ name: "lots_pk"
5840
+ }),
5841
+ foreignKey({
5842
+ columns: [
5843
+ table.chainId,
5844
+ table.contract,
5845
+ table.user
5846
+ ],
5847
+ foreignColumns: [
5848
+ positions.chainId,
5849
+ positions.contract,
5850
+ positions.user
5851
+ ],
5852
+ name: "lots_positions_fk"
5853
+ }).onDelete("cascade"),
5854
+ foreignKey({
5855
+ columns: [
5856
+ table.chainId,
5857
+ table.user,
5858
+ table.group
5859
+ ],
5860
+ foreignColumns: [
5861
+ groups.chainId,
5862
+ groups.maker,
5863
+ groups.group
5864
+ ],
5865
+ name: "lots_groups_fk"
5866
+ }).onDelete("cascade")
5867
+ ]);
5868
+ const offsets = s.table(EnumTableName.OFFSETS, {
5869
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5870
+ user: varchar("user", { length: 42 }).notNull(),
5871
+ contract: varchar("contract", { length: 42 }).notNull(),
5872
+ group: varchar("group", { length: 66 }).notNull(),
5873
+ value: numeric("value", {
5874
+ precision: 78,
5875
+ scale: 0
5876
+ }).notNull()
5877
+ }, (table) => [primaryKey({
5878
+ columns: [
5879
+ table.chainId,
5880
+ table.user,
5881
+ table.contract,
5882
+ table.group
5883
+ ],
5884
+ name: "offsets_pk"
5885
+ }), foreignKey({
5886
+ columns: [
5887
+ table.chainId,
5888
+ table.contract,
5889
+ table.user
5890
+ ],
5891
+ foreignColumns: [
5892
+ positions.chainId,
5893
+ positions.contract,
5894
+ positions.user
5895
+ ],
5896
+ name: "offsets_positions_fk"
5897
+ }).onDelete("cascade")]);
5458
5898
  const PositionTypes = s.enum("position_type", Object.values(Type));
5459
5899
  const positionTypes = s.table("position_types", {
5460
5900
  id: serial("id").primaryKey(),
@@ -5550,6 +5990,17 @@ const chains$1 = s.table(EnumTableName.CHAINS, {
5550
5990
  }).default("0").notNull(),
5551
5991
  updatedAt: timestamp("updated_at").defaultNow().notNull()
5552
5992
  }, (table) => [uniqueIndex("chains_id_epoch_idx").on(table.chainId, table.epoch)]);
5993
+ const trees = s.table(EnumTableName.TREES, {
5994
+ root: varchar("root", { length: 66 }).primaryKey(),
5995
+ rootSignature: varchar("root_signature", { length: 132 }).notNull(),
5996
+ createdAt: timestamp("created_at").defaultNow().notNull()
5997
+ });
5998
+ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
5999
+ offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
6000
+ treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
6001
+ proofNodes: text("proof_nodes").notNull(),
6002
+ createdAt: timestamp("created_at").defaultNow().notNull()
6003
+ }, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
5553
6004
 
5554
6005
  //#endregion
5555
6006
  //#region src/database/drizzle/index.ts
@@ -5564,15 +6015,19 @@ var drizzle_exports = /* @__PURE__ */ __export({
5564
6015
  collectors: () => collectors,
5565
6016
  consumedEvents: () => consumedEvents,
5566
6017
  groups: () => groups,
6018
+ lots: () => lots,
6019
+ merklePaths: () => merklePaths,
5567
6020
  obligationCollateralsV2: () => obligationCollateralsV2,
5568
6021
  obligations: () => obligations,
5569
6022
  offers: () => offers,
5570
6023
  offersCallbacks: () => offersCallbacks,
6024
+ offsets: () => offsets,
5571
6025
  oracles: () => oracles,
5572
6026
  positionTypes: () => positionTypes,
5573
6027
  positions: () => positions,
5574
6028
  status: () => status,
5575
6029
  transfers: () => transfers,
6030
+ trees: () => trees,
5576
6031
  validations: () => validations
5577
6032
  });
5578
6033
 
@@ -5580,7 +6035,7 @@ var drizzle_exports = /* @__PURE__ */ __export({
5580
6035
  //#region src/database/domains/Book.ts
5581
6036
  const DEFAULT_LIMIT$3 = 100;
5582
6037
  const MAX_TOTAL_OFFERS = 500;
5583
- function create$9(config) {
6038
+ function create$12(config) {
5584
6039
  const db = config.db;
5585
6040
  const logger = getLogger();
5586
6041
  const getOffers$2 = async (parameters) => {
@@ -5599,52 +6054,20 @@ function create$9(config) {
5599
6054
  offers: [],
5600
6055
  nextCursor: null
5601
6056
  };
5602
- const effectiveLimit = Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned);
5603
- const book = [];
5604
- const prices = /* @__PURE__ */ new Map();
5605
- const callbackState = /* @__PURE__ */ new Map();
5606
- const positionState = /* @__PURE__ */ new Map();
5607
- let offerCursor = null;
5608
- let hasMoreOffers = false;
5609
- while (book.length < effectiveLimit) {
5610
- const batchSize = (effectiveLimit - book.length) * 2;
5611
- const { offers: rawOffers, nextCursor: rawNextCursor } = await _getOffersWithCallbackIds(db, {
5612
- obligationId: obligationId$1,
5613
- side,
5614
- now: now$1,
5615
- rateSortDirection,
5616
- cursor: offerCursor,
5617
- limit: batchSize
5618
- });
5619
- if (rawOffers.length === 0) break;
5620
- const newCallbackIds = rawOffers.flatMap((o) => o.callbackIds).filter((id$1) => !callbackState.has(id$1));
5621
- await _updateCallbacksByIds(callbackState, db, newCallbackIds);
5622
- await _updatePositionsByKeys(positionState, db, [...new Set(newCallbackIds.map((id$1) => callbackState.get(id$1)?.positionKey).filter((k) => k !== void 0 && !positionState.has(k)))]);
5623
- await _updatePrices(prices, db, _collectNewOracleAddresses(rawOffers, callbackState, positionState, prices));
5624
- const validOffers = _computeCrossInvalidation(rawOffers, callbackState, positionState, prices);
5625
- let isOfferInPreviousPages = inputCursor === null;
5626
- const cursorRate = inputCursor ? BigInt(inputCursor.rate) : 0n;
5627
- for (const offer of validOffers) {
5628
- if (!isOfferInPreviousPages) if (rateSortDirection === "asc" ? offer.rate > cursorRate : offer.rate < cursorRate) isOfferInPreviousPages = true;
5629
- else if (offer.hash === inputCursor.hash) {
5630
- isOfferInPreviousPages = true;
5631
- continue;
5632
- } else continue;
5633
- book.push(offer);
5634
- if (book.length >= effectiveLimit) {
5635
- hasMoreOffers = true;
5636
- break;
5637
- }
5638
- }
5639
- offerCursor = rawNextCursor;
5640
- if (!offerCursor) break;
5641
- }
5642
- const lastReturnedOffer = book[book.length - 1];
5643
- const newTotalReturned = previouslyReturned + book.length;
6057
+ const { offers: offers$1, hasMore } = await _getOffers(db, {
6058
+ obligationId: obligationId$1,
6059
+ side,
6060
+ now: now$1,
6061
+ rateSortDirection,
6062
+ cursor: inputCursor,
6063
+ limit: Math.min(requestedLimit, MAX_TOTAL_OFFERS - previouslyReturned)
6064
+ });
6065
+ const lastReturnedOffer = offers$1[offers$1.length - 1];
6066
+ const newTotalReturned = previouslyReturned + offers$1.length;
5644
6067
  const hasHitHardLimit = newTotalReturned >= MAX_TOTAL_OFFERS;
5645
6068
  return {
5646
- offers: book,
5647
- nextCursor: book.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMoreOffers ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
6069
+ offers: offers$1,
6070
+ nextCursor: offers$1.length > 0 && lastReturnedOffer && !hasHitHardLimit && hasMore ? Cursor.encode(lastReturnedOffer, newTotalReturned, now$1, side) : null
5648
6071
  };
5649
6072
  };
5650
6073
  return {
@@ -5691,8 +6114,8 @@ function create$9(config) {
5691
6114
  getOffers: getOffers$2
5692
6115
  };
5693
6116
  }
5694
- /** Get offers with their callback IDs for a given obligation. */
5695
- async function _getOffersWithCallbackIds(db, params) {
6117
+ /** Get offers with computed takeable based on lot balance. */
6118
+ async function _getOffers(db, params) {
5696
6119
  const { obligationId: obligationId$1, side, now: now$1, rateSortDirection, cursor, limit } = params;
5697
6120
  const raw = await db.execute(sql`
5698
6121
  WITH collats AS MATERIALIZED (
@@ -5760,32 +6183,202 @@ async function _getOffersWithCallbackIds(db, params) {
5760
6183
  ORDER BY e.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
5761
6184
  LIMIT ${limit}
5762
6185
  ),
5763
- with_callbacks AS (
6186
+ -- Compute sum of offsets per position
6187
+ position_offsets AS (
6188
+ SELECT
6189
+ chain_id,
6190
+ "user",
6191
+ contract,
6192
+ SUM(value::numeric) AS total_offset
6193
+ FROM ${offsets}
6194
+ GROUP BY chain_id, "user", contract
6195
+ ),
6196
+ -- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
6197
+ position_consumed AS (
5764
6198
  SELECT
5765
- p.hash, p.obligation_id, p.assets, p.rate, p.maturity, p.expiry, p.start,
5766
- p.nonce, p.buy, p.callback_address, p.callback_data, p.block_number,
5767
- p.group_chain_id, p.group_maker, p.consumed, p.chain_id, p.loan_token,
5768
- COALESCE(ARRAY_AGG(oc.callback_id) FILTER (WHERE oc.callback_id IS NOT NULL), '{}') AS callback_ids
6199
+ l.chain_id,
6200
+ l.contract,
6201
+ l."user",
6202
+ SUM(
6203
+ CASE
6204
+ WHEN wo.assets::numeric > 0
6205
+ THEN COALESCE(g.consumed::numeric, 0) * (l.upper::numeric - l.lower::numeric) / wo.assets::numeric
6206
+ ELSE 0
6207
+ END
6208
+ ) AS consumed
6209
+ FROM ${lots} l
6210
+ JOIN ${groups} g
6211
+ ON g.chain_id = l.chain_id
6212
+ AND LOWER(g.maker) = LOWER(l."user")
6213
+ AND g."group" = l."group"
6214
+ JOIN winners wo
6215
+ ON wo.group_chain_id = g.chain_id
6216
+ AND LOWER(wo.group_maker) = LOWER(g.maker)
6217
+ AND wo.group_group = g."group"
6218
+ GROUP BY l.chain_id, l.contract, l."user"
6219
+ ),
6220
+ -- Compute callback contributions with lot balance
6221
+ callback_contributions AS (
6222
+ SELECT
6223
+ p.hash,
6224
+ p.obligation_id,
6225
+ p.assets,
6226
+ p.rate,
6227
+ p.maturity,
6228
+ p.expiry,
6229
+ p.start,
6230
+ p.nonce,
6231
+ p.buy,
6232
+ p.callback_address,
6233
+ p.callback_data,
6234
+ p.block_number,
6235
+ p.group_chain_id,
6236
+ p.group_maker,
6237
+ p.group_group,
6238
+ p.consumed,
6239
+ p.chain_id,
6240
+ p.loan_token,
6241
+ c.id AS callback_id,
6242
+ c.position_chain_id,
6243
+ c.position_contract,
6244
+ c.position_user,
6245
+ c.amount AS callback_amount,
6246
+ pos.balance AS position_balance,
6247
+ pos.asset AS position_asset,
6248
+ l.lower AS lot_lower,
6249
+ l.upper AS lot_upper,
6250
+ -- Compute lot_balance: min(position_balance + offset + position_consumed - lot.lower, lot.size - lot_consumed)
6251
+ -- lot_consumed is converted from loan token to lot terms: consumed * lot_size / assets
6252
+ GREATEST(0, LEAST(
6253
+ COALESCE(pos.balance::numeric, 0) + COALESCE(pos_offsets.total_offset, 0) + COALESCE(pc.consumed, 0) - COALESCE(l.lower::numeric, 0),
6254
+ (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) -
6255
+ CASE
6256
+ WHEN p.assets::numeric > 0
6257
+ THEN COALESCE(p.consumed::numeric, 0) * (COALESCE(l.upper::numeric, 0) - COALESCE(l.lower::numeric, 0)) / p.assets::numeric
6258
+ ELSE 0
6259
+ END
6260
+ )) AS lot_balance
5769
6261
  FROM paged p
5770
6262
  LEFT JOIN ${offersCallbacks} oc ON oc.offer_hash = p.hash
5771
- GROUP BY p.hash, p.obligation_id, p.assets, p.rate, p.maturity, p.expiry, p.start,
5772
- p.nonce, p.buy, p.callback_address, p.callback_data, p.block_number,
5773
- p.group_chain_id, p.group_maker, p.consumed, p.chain_id, p.loan_token
6263
+ LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
6264
+ LEFT JOIN ${lots} l
6265
+ ON l.chain_id = c.position_chain_id
6266
+ AND LOWER(l.contract) = LOWER(c.position_contract)
6267
+ AND LOWER(l."user") = LOWER(c.position_user)
6268
+ AND l."group" = p.group_group
6269
+ LEFT JOIN ${positions} pos
6270
+ ON pos.chain_id = c.position_chain_id
6271
+ AND LOWER(pos.contract) = LOWER(c.position_contract)
6272
+ AND LOWER(pos."user") = LOWER(c.position_user)
6273
+ LEFT JOIN position_offsets pos_offsets
6274
+ ON pos_offsets.chain_id = c.position_chain_id
6275
+ AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
6276
+ AND LOWER(pos_offsets."user") = LOWER(c.position_user)
6277
+ LEFT JOIN position_consumed pc
6278
+ ON pc.chain_id = c.position_chain_id
6279
+ AND LOWER(pc.contract) = LOWER(c.position_contract)
6280
+ AND LOWER(pc."user") = LOWER(c.position_user)
6281
+ ),
6282
+ -- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
6283
+ callback_loan_contribution AS (
6284
+ SELECT
6285
+ cc.*,
6286
+ CASE
6287
+ -- No lot exists: contribution is 0
6288
+ WHEN cc.lot_lower IS NULL THEN 0
6289
+ -- Loan token position: use lot_balance directly, apply callback limit
6290
+ WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
6291
+ LEAST(
6292
+ cc.lot_balance,
6293
+ COALESCE(cc.callback_amount::numeric, cc.lot_balance)
6294
+ )
6295
+ -- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
6296
+ ELSE
6297
+ (
6298
+ LEAST(
6299
+ cc.lot_balance,
6300
+ COALESCE(cc.callback_amount::numeric, cc.lot_balance)
6301
+ ) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
6302
+ ) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
6303
+ END AS contribution_in_loan
6304
+ FROM callback_contributions cc
6305
+ LEFT JOIN ${obligationCollateralsV2} collat_info
6306
+ ON collat_info.obligation_id = cc.obligation_id
6307
+ AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
6308
+ LEFT JOIN ${oracles} collat_oracle
6309
+ ON collat_oracle.chain_id = collat_info.oracle_chain_id
6310
+ AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
6311
+ ),
6312
+ -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
6313
+ offer_contributions AS (
6314
+ SELECT
6315
+ hash,
6316
+ obligation_id,
6317
+ assets,
6318
+ rate,
6319
+ maturity,
6320
+ expiry,
6321
+ start,
6322
+ nonce,
6323
+ buy,
6324
+ callback_address,
6325
+ callback_data,
6326
+ block_number,
6327
+ group_chain_id,
6328
+ group_maker,
6329
+ consumed,
6330
+ chain_id,
6331
+ loan_token,
6332
+ SUM(contribution_in_loan) AS total_available
6333
+ FROM (
6334
+ -- Take max contribution per position using DISTINCT ON (idiomatic PostgreSQL)
6335
+ SELECT DISTINCT ON (clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user)
6336
+ clc.*
6337
+ FROM callback_loan_contribution clc
6338
+ WHERE clc.callback_id IS NOT NULL
6339
+ ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
6340
+ ) deduped
6341
+ GROUP BY hash, obligation_id, assets, rate, maturity, expiry, start, nonce, buy,
6342
+ callback_address, callback_data, block_number, group_chain_id, group_maker,
6343
+ consumed, chain_id, loan_token
5774
6344
  )
6345
+ -- Final SELECT with inline takeable computation
5775
6346
  SELECT
5776
- wc.hash, wc.group_maker, wc.assets, wc.consumed, wc.rate, wc.maturity, wc.expiry, wc.start,
5777
- wc.nonce, wc.buy, wc.chain_id, wc.loan_token, wc.callback_address, wc.callback_data,
5778
- wc.block_number, wc.callback_ids, c.collaterals
5779
- FROM with_callbacks wc
5780
- LEFT JOIN collats c ON c.obligation_id = wc.obligation_id
6347
+ oc.hash,
6348
+ oc.group_maker,
6349
+ oc.assets,
6350
+ oc.consumed,
6351
+ oc.rate,
6352
+ oc.maturity,
6353
+ oc.expiry,
6354
+ oc.start,
6355
+ oc.nonce,
6356
+ oc.buy,
6357
+ oc.chain_id,
6358
+ oc.loan_token,
6359
+ oc.callback_address,
6360
+ oc.callback_data,
6361
+ oc.block_number,
6362
+ -- takeable = min(assets - consumed, total_available)
6363
+ GREATEST(0, LEAST(
6364
+ oc.assets::numeric - oc.consumed::numeric,
6365
+ COALESCE(oc.total_available, 0)
6366
+ )) AS takeable,
6367
+ c.collaterals
6368
+ FROM offer_contributions oc
6369
+ LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
6370
+ WHERE GREATEST(0, LEAST(
6371
+ oc.assets::numeric - oc.consumed::numeric,
6372
+ COALESCE(oc.total_available, 0)
6373
+ )) > 0
5781
6374
  ORDER BY
5782
- wc.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`},
5783
- wc.block_number ASC,
5784
- wc.assets DESC,
5785
- wc.hash ASC;
6375
+ oc.rate ${rateSortDirection === "asc" ? sql`ASC` : sql`DESC`},
6376
+ oc.block_number ASC,
6377
+ oc.assets DESC,
6378
+ oc.hash ASC;
5786
6379
  `);
5787
- const offers$1 = raw.rows.map((row) => ({
5788
- ...from$11({
6380
+ return {
6381
+ offers: raw.rows.map((row) => from$11({
5789
6382
  offering: row.group_maker,
5790
6383
  assets: BigInt(row.assets),
5791
6384
  rate: BigInt(row.rate),
@@ -5807,165 +6400,12 @@ async function _getOffersWithCallbackIds(db, params) {
5807
6400
  gasLimit: 0n
5808
6401
  },
5809
6402
  consumed: BigInt(row.consumed),
5810
- takeable: 0n,
6403
+ takeable: BigInt(row.takeable.split(".")[0] ?? "0"),
5811
6404
  blockNumber: row.block_number
5812
- }),
5813
- callbackIds: row.callback_ids ?? []
5814
- }));
5815
- let nextCursor = null;
5816
- if (raw.rows.length === limit) {
5817
- const last = raw.rows[raw.rows.length - 1];
5818
- nextCursor = {
5819
- rate: last.rate,
5820
- blockNumber: last.block_number,
5821
- assets: last.assets,
5822
- hash: last.hash
5823
- };
5824
- }
5825
- return {
5826
- offers: offers$1,
5827
- nextCursor
6405
+ })),
6406
+ hasMore: raw.rows.length === limit
5828
6407
  };
5829
6408
  }
5830
- /** Get callbacks by their IDs. */
5831
- async function _updateCallbacksByIds(state, db, ids) {
5832
- if (ids.length === 0) return;
5833
- const raw = await db.execute(sql`
5834
- SELECT c.id, c.position_chain_id, c.position_contract, c.position_user, c.amount
5835
- FROM ${callbacks} c
5836
- WHERE c.id IN (${sql.join(ids.map((id$1) => sql`${id$1}`), sql`, `)})
5837
- `);
5838
- for (const row of raw.rows) if (!state.has(row.id)) state.set(row.id, {
5839
- positionKey: _buildPositionKey(row.position_chain_id, row.position_contract, row.position_user),
5840
- amount: row.amount != null ? BigInt(row.amount) : null
5841
- });
5842
- }
5843
- /** Get positions by their composite keys. */
5844
- async function _updatePositionsByKeys(state, db, keys) {
5845
- if (keys.length === 0) return;
5846
- const parsedKeys = keys.map((key) => {
5847
- const parts = key.split(":");
5848
- return {
5849
- chainId: BigInt(parts[0]),
5850
- contract: parts[1],
5851
- user: parts[2]
5852
- };
5853
- });
5854
- const raw = await db.execute(sql`
5855
- SELECT p.chain_id, p.contract, p."user", p.balance, p.asset
5856
- FROM ${positions} p
5857
- WHERE (p.chain_id, LOWER(p.contract), LOWER(p."user")) IN (
5858
- ${sql.join(parsedKeys.map((k) => sql`(${k.chainId}, ${k.contract.toLowerCase()}, ${k.user.toLowerCase()})`), sql`, `)}
5859
- )
5860
- `);
5861
- for (const row of raw.rows) {
5862
- const key = _buildPositionKey(row.chain_id, row.contract, row.user);
5863
- if (!state.has(key)) state.set(key, {
5864
- balance: row.balance ? BigInt(row.balance) : 0n,
5865
- remaining: row.balance ? BigInt(row.balance) : 0n,
5866
- asset: row.asset ?? "0x0000000000000000000000000000000000000000"
5867
- });
5868
- }
5869
- }
5870
- /** Get oracle prices by chain_id and address. */
5871
- async function _updatePrices(state, db, oracles$1) {
5872
- if (oracles$1.length === 0) return;
5873
- const raw = await db.execute(sql`
5874
- SELECT o.chain_id, o.address, o.price
5875
- FROM ${oracles} o
5876
- WHERE (o.chain_id, LOWER(o.address)) IN (${sql.join(oracles$1.map((o) => sql`(${o.chainId}, ${o.address.toLowerCase()})`), sql`, `)})
5877
- `);
5878
- for (const row of raw.rows) {
5879
- const key = `${row.chain_id}:${row.address.toLowerCase()}`;
5880
- if (!state.has(key)) state.set(key, row.price ? BigInt(row.price) : 0n);
5881
- }
5882
- }
5883
- /** Build a composite position key from its components. */
5884
- function _buildPositionKey(chainId, contract, user) {
5885
- return `${chainId}:${contract.toLowerCase()}:${user.toLowerCase()}`;
5886
- }
5887
- /** Collect oracle addresses that need to be Geted for collateral positions. */
5888
- function _collectNewOracleAddresses(offers$1, callbackState, positionState, prices) {
5889
- const seen = /* @__PURE__ */ new Set();
5890
- const result = [];
5891
- for (const offer of offers$1) for (const callbackId of offer.callbackIds) {
5892
- const callback$1 = callbackState.get(callbackId);
5893
- if (!callback$1) continue;
5894
- const position = positionState.get(callback$1.positionKey);
5895
- if (!position) continue;
5896
- if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) continue;
5897
- const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
5898
- if (collateral) {
5899
- const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
5900
- if (!prices.has(key) && !seen.has(key)) {
5901
- seen.add(key);
5902
- result.push({
5903
- chainId: offer.chainId,
5904
- address: collateral.oracle.toLowerCase()
5905
- });
5906
- }
5907
- }
5908
- }
5909
- return result;
5910
- }
5911
- /**
5912
- * Compute cross-invalidation for a batch of offers.
5913
- * Deducts consumed liquidity from shared positions and returns offers with takeable amounts.
5914
- */
5915
- function _computeCrossInvalidation(offers$1, callbackState, positionState, prices) {
5916
- const result = [];
5917
- for (const offer of offers$1) {
5918
- const contributions = /* @__PURE__ */ new Map();
5919
- for (const callbackId of offer.callbackIds) {
5920
- const callback$1 = callbackState.get(callbackId);
5921
- if (!callback$1) continue;
5922
- const position = positionState.get(callback$1.positionKey);
5923
- if (!position) continue;
5924
- let conversion;
5925
- if (position.asset.toLowerCase() === offer.loanToken.toLowerCase()) conversion = null;
5926
- else {
5927
- const collateral = offer.collaterals.find((c) => c.asset.toLowerCase() === position.asset.toLowerCase());
5928
- if (!collateral) conversion = {
5929
- price: 0n,
5930
- lltv: 0n
5931
- };
5932
- else {
5933
- const key = `${offer.chainId}:${collateral.oracle.toLowerCase()}`;
5934
- conversion = {
5935
- price: prices.get(key) ?? 0n,
5936
- lltv: collateral.lltv
5937
- };
5938
- }
5939
- }
5940
- const availableFromPosition = conversion === null ? position.remaining : Conversion.collateralToLoan(position.remaining, conversion);
5941
- const callbackLimitInLoanTerms = conversion === null || callback$1.amount === null ? callback$1.amount : Conversion.collateralToLoan(callback$1.amount, conversion);
5942
- const callbackAvailable = callbackLimitInLoanTerms === null ? availableFromPosition : min(availableFromPosition, callbackLimitInLoanTerms);
5943
- const existing = contributions.get(callback$1.positionKey);
5944
- if (existing) existing.available = min(availableFromPosition, max$1(existing.available, callbackAvailable));
5945
- else contributions.set(callback$1.positionKey, {
5946
- available: callbackAvailable,
5947
- conversion
5948
- });
5949
- }
5950
- let totalAvailable = 0n;
5951
- for (const [, contrib] of contributions) totalAvailable += contrib.available;
5952
- const takeable = min(offer.assets - offer.consumed, totalAvailable);
5953
- if (takeable <= 0n) continue;
5954
- for (const [key, contrib] of contributions) {
5955
- const position = positionState.get(key);
5956
- const proportionalTakeable = totalAvailable > 0n ? contrib.available * takeable / totalAvailable : 0n;
5957
- const toDeduct = contrib.conversion === null ? proportionalTakeable : Conversion.loanToCollateral(proportionalTakeable, contrib.conversion);
5958
- position.remaining = position.remaining - toDeduct;
5959
- if (position.remaining < 0n) position.remaining = 0n;
5960
- }
5961
- const { callbackIds: _, ...cleanOffer } = offer;
5962
- result.push(from$11({
5963
- ...cleanOffer,
5964
- takeable
5965
- }));
5966
- }
5967
- return result;
5968
- }
5969
6409
  let Cursor;
5970
6410
  (function(_Cursor) {
5971
6411
  function encode$4(offer, totalReturned, now$1, side) {
@@ -6030,7 +6470,7 @@ let LevelCursor;
6030
6470
  //#endregion
6031
6471
  //#region src/database/domains/Chains.ts
6032
6472
  /** Postgres implementation. */
6033
- const create$8 = (config) => {
6473
+ const create$11 = (config) => {
6034
6474
  const db = config.db;
6035
6475
  const logger = getLogger();
6036
6476
  return {
@@ -6086,7 +6526,7 @@ const create$8 = (config) => {
6086
6526
  //#endregion
6087
6527
  //#region src/database/domains/Collectors.ts
6088
6528
  /** Postgres implementation. */
6089
- const create$7 = (config) => {
6529
+ const create$10 = (config) => {
6090
6530
  const db = config.db;
6091
6531
  const logger = getLogger();
6092
6532
  return {
@@ -6179,7 +6619,7 @@ const DEFAULT_BATCH_SIZE$1 = 4e3;
6179
6619
 
6180
6620
  //#endregion
6181
6621
  //#region src/database/domains/Consumed.ts
6182
- function create$6(db) {
6622
+ function create$9(db) {
6183
6623
  return {
6184
6624
  create: async (events) => {
6185
6625
  if (events.length === 0) return;
@@ -6220,6 +6660,51 @@ function create$6(db) {
6220
6660
  };
6221
6661
  }
6222
6662
 
6663
+ //#endregion
6664
+ //#region src/database/domains/Lots.ts
6665
+ function create$8(db) {
6666
+ return {
6667
+ get: async (parameters) => {
6668
+ const { chainId, user, contract, group } = parameters ?? {};
6669
+ const conditions = [];
6670
+ if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
6671
+ if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
6672
+ if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
6673
+ if (group !== void 0) conditions.push(eq(lots.group, group));
6674
+ return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
6675
+ chainId: row.chainId,
6676
+ user: row.user,
6677
+ contract: row.contract,
6678
+ group: row.group,
6679
+ lower: BigInt(row.lower),
6680
+ upper: BigInt(row.upper)
6681
+ }));
6682
+ },
6683
+ create: async (parameters) => {
6684
+ if (parameters.length === 0) return;
6685
+ const lotsByPositionGroup = /* @__PURE__ */ new Map();
6686
+ for (const offer of parameters) {
6687
+ const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
6688
+ const existing = lotsByPositionGroup.get(key);
6689
+ if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
6690
+ }
6691
+ for (const offer of lotsByPositionGroup.values()) if ((await db.select().from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.group, offer.group))).limit(1)).length === 0) {
6692
+ const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase())));
6693
+ const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
6694
+ const newUpper = newLower + offer.size;
6695
+ await db.insert(lots).values({
6696
+ chainId: offer.positionChainId,
6697
+ user: offer.positionUser.toLowerCase(),
6698
+ contract: offer.positionContract.toLowerCase(),
6699
+ group: offer.group,
6700
+ lower: newLower.toString(),
6701
+ upper: newUpper.toString()
6702
+ });
6703
+ }
6704
+ }
6705
+ };
6706
+ }
6707
+
6223
6708
  //#endregion
6224
6709
  //#region src/gatekeeper/Gate.ts
6225
6710
  var Gate_exports = /* @__PURE__ */ __export({
@@ -6333,8 +6818,8 @@ function getCallback(chain, type) {
6333
6818
  * @param address - Callback contract address
6334
6819
  * @returns The callback type when found, otherwise undefined
6335
6820
  */
6336
- function getCallbackType(chain, address) {
6337
- return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
6821
+ function getCallbackType(chain, address$1) {
6822
+ return configs[chain].callbacks?.find((c) => c.type !== CallbackType.BuyWithEmptyCallback && c.addresses.includes(address$1?.toLowerCase()))?.type;
6338
6823
  }
6339
6824
  /**
6340
6825
  * Returns the callback addresses for a given chain and callback type, if it exists.
@@ -6448,8 +6933,8 @@ const configs = {
6448
6933
 
6449
6934
  //#endregion
6450
6935
  //#region src/gatekeeper/Gatekeeper.ts
6451
- var Gatekeeper_exports = /* @__PURE__ */ __export({ create: () => create$5 });
6452
- function create$5(parameters) {
6936
+ var Gatekeeper_exports = /* @__PURE__ */ __export({ create: () => create$7 });
6937
+ function create$7(parameters) {
6453
6938
  return {
6454
6939
  rules: parameters.rules,
6455
6940
  isAllowed: async (offers$1) => {
@@ -6467,6 +6952,7 @@ var Rules_exports = /* @__PURE__ */ __export({
6467
6952
  callback: () => callback,
6468
6953
  chains: () => chains,
6469
6954
  maturity: () => maturity,
6955
+ sameMaker: () => sameMaker,
6470
6956
  token: () => token,
6471
6957
  validity: () => validity
6472
6958
  });
@@ -6603,10 +7089,29 @@ const token = ({ assets: assets$1 }) => single("token", "Validates that offer lo
6603
7089
  if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
6604
7090
  if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
6605
7091
  });
7092
+ /**
7093
+ * A batch validation rule that ensures all offers in a tree have the same maker (offering address).
7094
+ * Returns an issue only for the first non-conforming offer.
7095
+ * This rule is signing-agnostic; signer verification is handled at the collector level.
7096
+ */
7097
+ const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker (offering address)", (offers$1) => {
7098
+ const issues = /* @__PURE__ */ new Map();
7099
+ if (offers$1.length === 0) return issues;
7100
+ const firstMaker = offers$1[0].offering.toLowerCase();
7101
+ for (let i = 1; i < offers$1.length; i++) {
7102
+ const offer = offers$1[i];
7103
+ if (offer.offering.toLowerCase() !== firstMaker) {
7104
+ issues.set(i, { message: `Offer has different maker ${offer.offering} than first offer ${offers$1[0].offering}` });
7105
+ return issues;
7106
+ }
7107
+ }
7108
+ return issues;
7109
+ });
6606
7110
 
6607
7111
  //#endregion
6608
7112
  //#region src/gatekeeper/morphoRules.ts
6609
7113
  const morphoRules = (chains$3) => [
7114
+ sameMaker(),
6610
7115
  chains({ chains: chains$3 }),
6611
7116
  maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
6612
7117
  callback({
@@ -6623,7 +7128,7 @@ const morphoRules = (chains$3) => [
6623
7128
  //#endregion
6624
7129
  //#region src/database/domains/Offers.ts
6625
7130
  const DEFAULT_LIMIT$2 = 100;
6626
- function create$4(config) {
7131
+ function create$6(config) {
6627
7132
  const db = config.db;
6628
7133
  return {
6629
7134
  create: async (offers$1) => {
@@ -6782,6 +7287,22 @@ function create$4(config) {
6782
7287
  amount: callback$1.amount
6783
7288
  })));
6784
7289
  for (const batch$2 of batch$1(callbacksRows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(callbacks).values(batch$2).onConflictDoNothing();
7290
+ const lotInfos = [];
7291
+ for (const [offerHash, callbacks$1] of offersCallbacksMap.entries()) {
7292
+ const offer = inserted.find((o) => o.hash === offerHash);
7293
+ if (!offer) continue;
7294
+ for (const callback$1 of callbacks$1) {
7295
+ const isLoanPosition = obligationsMap.get(offer.obligationId)?.loanToken.toLowerCase() === callback$1.asset?.toLowerCase();
7296
+ lotInfos.push({
7297
+ positionChainId: callback$1.chainId,
7298
+ positionContract: callback$1.contract,
7299
+ positionUser: callback$1.user,
7300
+ group: offer.group,
7301
+ size: isLoanPosition ? BigInt(offer.assets) : BigInt(callback$1.amount)
7302
+ });
7303
+ }
7304
+ }
7305
+ if (lotInfos.length > 0) await dbTx.lots.create(lotInfos);
6785
7306
  obligationsMap.clear();
6786
7307
  collateralsMap.clear();
6787
7308
  oraclesMap.clear();
@@ -6964,9 +7485,29 @@ function create$4(config) {
6964
7485
  };
6965
7486
  }
6966
7487
 
7488
+ //#endregion
7489
+ //#region src/database/domains/Offsets.ts
7490
+ function create$5(db) {
7491
+ return { get: async (parameters) => {
7492
+ const { chainId, user, contract, group } = parameters ?? {};
7493
+ const conditions = [];
7494
+ if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
7495
+ if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
7496
+ if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
7497
+ if (group !== void 0) conditions.push(eq(offsets.group, group));
7498
+ return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
7499
+ chainId: row.chainId,
7500
+ user: row.user,
7501
+ contract: row.contract,
7502
+ group: row.group,
7503
+ value: BigInt(row.value)
7504
+ }));
7505
+ } };
7506
+ }
7507
+
6967
7508
  //#endregion
6968
7509
  //#region src/database/domains/Oracles.ts
6969
- function create$3(db) {
7510
+ function create$4(db) {
6970
7511
  return {
6971
7512
  get: async ({ chainId }) => {
6972
7513
  return (await db.select({
@@ -7006,7 +7547,7 @@ function create$3(db) {
7006
7547
  //#endregion
7007
7548
  //#region src/database/domains/Positions.ts
7008
7549
  const DEFAULT_LIMIT$1 = 100;
7009
- const create$2 = (db) => {
7550
+ const create$3 = (db) => {
7010
7551
  return {
7011
7552
  upsert: async (positions$1) => {
7012
7553
  const positionsMap = /* @__PURE__ */ new Map();
@@ -7122,7 +7663,7 @@ const create$2 = (db) => {
7122
7663
 
7123
7664
  //#endregion
7124
7665
  //#region src/database/domains/Transfers.ts
7125
- const create$1 = (db) => ({ create: async (transfers$1) => {
7666
+ const create$2 = (db) => ({ create: async (transfers$1) => {
7126
7667
  if (transfers$1.length === 0) return 0;
7127
7668
  return await db.transaction(async (dbTx) => {
7128
7669
  let totalInserted = 0;
@@ -7219,6 +7760,91 @@ const create$1 = (db) => ({ create: async (transfers$1) => {
7219
7760
  });
7220
7761
  } });
7221
7762
 
7763
+ //#endregion
7764
+ //#region src/database/domains/Trees.ts
7765
+ /**
7766
+ * Creates a Trees domain instance for managing merkle tree metadata.
7767
+ *
7768
+ * @param config - Configuration with database instance
7769
+ * @returns TreesDomain instance
7770
+ */
7771
+ function create$1(config) {
7772
+ const db = config.db;
7773
+ return {
7774
+ create: async (trees$1) => {
7775
+ if (trees$1.length === 0) return [];
7776
+ return await db.transaction(async (dbTx) => {
7777
+ const roots = [];
7778
+ for (const { tree, signature } of trees$1) {
7779
+ const root = tree.root.toLowerCase();
7780
+ roots.push(root);
7781
+ await dbTx.insert(trees).values({
7782
+ root,
7783
+ rootSignature: signature.toLowerCase()
7784
+ }).onConflictDoUpdate({
7785
+ target: [trees.root],
7786
+ set: {
7787
+ rootSignature: signature.toLowerCase(),
7788
+ createdAt: sql`NOW()`
7789
+ }
7790
+ });
7791
+ await dbTx.offers.create(tree.offers);
7792
+ const pathRows = proofs(tree).map((proof) => ({
7793
+ offerHash: proof.offer.hash.toLowerCase(),
7794
+ treeRoot: root,
7795
+ proofNodes: concatenateProofs(proof.path)
7796
+ }));
7797
+ for (const batch$2 of batch$1(pathRows, DEFAULT_BATCH_SIZE$1)) await dbTx.insert(merklePaths).values(batch$2).onConflictDoUpdate({
7798
+ target: [merklePaths.offerHash],
7799
+ set: {
7800
+ treeRoot: sql`excluded.tree_root`,
7801
+ proofNodes: sql`excluded.proof_nodes`,
7802
+ createdAt: sql`NOW()`
7803
+ }
7804
+ });
7805
+ }
7806
+ return roots;
7807
+ });
7808
+ },
7809
+ getAttestations: async (hashes) => {
7810
+ if (hashes.length === 0) return /* @__PURE__ */ new Map();
7811
+ const normalizedHashes = hashes.map((h) => h.toLowerCase());
7812
+ const results = await db.select({
7813
+ offerHash: merklePaths.offerHash,
7814
+ treeRoot: merklePaths.treeRoot,
7815
+ proofNodes: merklePaths.proofNodes,
7816
+ rootSignature: trees.rootSignature
7817
+ }).from(merklePaths).innerJoin(trees, eq(merklePaths.treeRoot, trees.root)).where(inArray(merklePaths.offerHash, normalizedHashes));
7818
+ const attestationMap = /* @__PURE__ */ new Map();
7819
+ for (const row of results) attestationMap.set(row.offerHash, {
7820
+ root: row.treeRoot,
7821
+ signature: row.rootSignature,
7822
+ proof: splitProofs(row.proofNodes)
7823
+ });
7824
+ return attestationMap;
7825
+ }
7826
+ };
7827
+ }
7828
+ /**
7829
+ * Concatenates an array of 32-byte hex hashes into a single hex string.
7830
+ * Empty arrays return "0x".
7831
+ */
7832
+ function concatenateProofs(proofs$1) {
7833
+ if (proofs$1.length === 0) return "0x";
7834
+ return `0x${proofs$1.map((p) => p.slice(2)).join("")}`;
7835
+ }
7836
+ /**
7837
+ * Splits a concatenated hex string back into an array of 32-byte hex hashes.
7838
+ * Returns empty array for "0x" or empty string.
7839
+ */
7840
+ function splitProofs(concatenated) {
7841
+ if (!concatenated || concatenated === "0x" || concatenated.length <= 2) return [];
7842
+ const hex$1 = concatenated.slice(2);
7843
+ const proofs$1 = [];
7844
+ for (let i = 0; i < hex$1.length; i += 64) proofs$1.push(`0x${hex$1.slice(i, i + 64)}`);
7845
+ return proofs$1;
7846
+ }
7847
+
7222
7848
  //#endregion
7223
7849
  //#region src/database/domains/Validations.ts
7224
7850
  const DEFAULT_LIMIT = 100;
@@ -7284,15 +7910,18 @@ function create(db) {
7284
7910
  var Database_exports = /* @__PURE__ */ __export({ connect: () => connect$1 });
7285
7911
  function createDomains(core) {
7286
7912
  return {
7287
- book: create$9({ db: core }),
7288
- collectors: create$7({ db: core }),
7289
- offers: create$4({ db: core }),
7290
- chains: create$8({ db: core }),
7291
- consumed: create$6(core),
7292
- oracles: create$3(core),
7913
+ book: create$12({ db: core }),
7914
+ collectors: create$10({ db: core }),
7915
+ offers: create$6({ db: core }),
7916
+ chains: create$11({ db: core }),
7917
+ consumed: create$9(core),
7918
+ lots: create$8(core),
7919
+ offsets: create$5(core),
7920
+ oracles: create$4(core),
7921
+ trees: create$1({ db: core }),
7293
7922
  validations: create(core),
7294
- positions: create$2(core),
7295
- transfers: create$1(core)
7923
+ positions: create$3(core),
7924
+ transfers: create$2(core)
7296
7925
  };
7297
7926
  }
7298
7927
  const AUGMENT_CACHE = /* @__PURE__ */ new WeakMap();
@@ -7327,10 +7956,22 @@ function augmentWithDomains(base$1) {
7327
7956
  value: dms.consumed,
7328
7957
  enumerable: true
7329
7958
  },
7959
+ lots: {
7960
+ value: dms.lots,
7961
+ enumerable: true
7962
+ },
7963
+ offsets: {
7964
+ value: dms.offsets,
7965
+ enumerable: true
7966
+ },
7330
7967
  oracles: {
7331
7968
  value: dms.oracles,
7332
7969
  enumerable: true
7333
7970
  },
7971
+ trees: {
7972
+ value: dms.trees,
7973
+ enumerable: true
7974
+ },
7334
7975
  validations: {
7335
7976
  value: dms.validations,
7336
7977
  enumerable: true
@@ -7347,6 +7988,7 @@ function augmentWithDomains(base$1) {
7347
7988
  AUGMENT_CACHE.set(base$1, wrapped);
7348
7989
  return wrapped;
7349
7990
  }
7991
+ let cachedInMemoryDatabase;
7350
7992
  /**
7351
7993
  * Connect to the database.
7352
7994
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -7369,15 +8011,17 @@ function connect$1(connectionString) {
7369
8011
  clean: async () => await clean(driver$1)
7370
8012
  });
7371
8013
  }
8014
+ if (cachedInMemoryDatabase) return cachedInMemoryDatabase;
7372
8015
  const pool = new PGlite();
7373
8016
  const driver = drizzle$1(pool, { schema: schema_exports });
7374
8017
  const core = augmentWithDomains(driver);
7375
- return Object.assign(core, {
8018
+ cachedInMemoryDatabase = Object.assign(core, {
7376
8019
  name: "pglite",
7377
8020
  pool,
7378
8021
  applyMigrations: applyMigrations("pglite", driver),
7379
8022
  clean: async () => await clean(driver)
7380
8023
  });
8024
+ return cachedInMemoryDatabase;
7381
8025
  }
7382
8026
  const MIGRATED_DRIVERS = /* @__PURE__ */ new WeakSet();
7383
8027
  function applyMigrations(kind, driver) {
@@ -7581,6 +8225,35 @@ async function postMigrate(driver) {
7581
8225
  REFERENCING OLD TABLE AS deleted_rows
7582
8226
  FOR EACH STATEMENT
7583
8227
  EXECUTE FUNCTION cleanup_orphan_positions();
8228
+ `);
8229
+ await driver.execute(`
8230
+ CREATE OR REPLACE FUNCTION cleanup_orphan_groups()
8231
+ RETURNS TRIGGER AS $$
8232
+ BEGIN
8233
+ DELETE FROM "${VERSION}"."groups" g
8234
+ USING (
8235
+ SELECT DISTINCT group_chain_id, group_maker, group_group
8236
+ FROM deleted_rows
8237
+ ) AS affected
8238
+ WHERE g.chain_id = affected.group_chain_id
8239
+ AND g.maker = affected.group_maker
8240
+ AND g."group" = affected.group_group
8241
+ AND NOT EXISTS (
8242
+ SELECT 1 FROM "${VERSION}"."offers" o
8243
+ WHERE o.group_chain_id = g.chain_id
8244
+ AND o.group_maker = g.maker
8245
+ AND o.group_group = g."group"
8246
+ );
8247
+ RETURN NULL;
8248
+ END;
8249
+ $$ LANGUAGE plpgsql;
8250
+ `);
8251
+ await driver.execute(`
8252
+ CREATE OR REPLACE TRIGGER trg_cleanup_orphan_groups
8253
+ AFTER DELETE ON "${VERSION}"."offers"
8254
+ REFERENCING OLD TABLE AS deleted_rows
8255
+ FOR EACH STATEMENT
8256
+ EXECUTE FUNCTION cleanup_orphan_groups();
7584
8257
  `);
7585
8258
  await driver.execute(`
7586
8259
  CREATE OR REPLACE FUNCTION cleanup_orphan_obligations_and_oracles()
@@ -7633,6 +8306,58 @@ async function postMigrate(driver) {
7633
8306
  REFERENCING OLD TABLE AS deleted_rows
7634
8307
  FOR EACH STATEMENT
7635
8308
  EXECUTE FUNCTION cleanup_orphan_obligations_and_oracles();
8309
+ `);
8310
+ await driver.execute(`
8311
+ CREATE OR REPLACE FUNCTION create_offset_on_lot_delete()
8312
+ RETURNS trigger
8313
+ LANGUAGE plpgsql AS $$
8314
+ BEGIN
8315
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
8316
+ VALUES (
8317
+ OLD.chain_id,
8318
+ OLD."user",
8319
+ OLD.contract,
8320
+ OLD."group",
8321
+ OLD.upper::numeric - OLD.lower::numeric
8322
+ )
8323
+ ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
8324
+ RETURN OLD;
8325
+ END;
8326
+ $$;
8327
+ `);
8328
+ await driver.execute(`
8329
+ CREATE OR REPLACE TRIGGER trg_lots_create_offset_before_delete
8330
+ BEFORE DELETE ON "${VERSION}"."lots"
8331
+ FOR EACH ROW
8332
+ EXECUTE FUNCTION create_offset_on_lot_delete();
8333
+ `);
8334
+ await driver.execute(`
8335
+ CREATE OR REPLACE FUNCTION delete_position_if_no_lots()
8336
+ RETURNS trigger
8337
+ LANGUAGE plpgsql AS $$
8338
+ BEGIN
8339
+ -- Check if any lots remain on this position
8340
+ IF NOT EXISTS (
8341
+ SELECT 1 FROM "${VERSION}"."lots" l
8342
+ WHERE l.chain_id = OLD.chain_id
8343
+ AND l.contract = OLD.contract
8344
+ AND l."user" = OLD."user"
8345
+ ) THEN
8346
+ -- No lots remain, delete the position (cascades to offsets)
8347
+ DELETE FROM "${VERSION}"."positions" p
8348
+ WHERE p.chain_id = OLD.chain_id
8349
+ AND p.contract = OLD.contract
8350
+ AND p."user" = OLD."user";
8351
+ END IF;
8352
+ RETURN NULL;
8353
+ END;
8354
+ $$;
8355
+ `);
8356
+ await driver.execute(`
8357
+ CREATE OR REPLACE TRIGGER trg_lots_delete_position_if_empty
8358
+ AFTER DELETE ON "${VERSION}"."lots"
8359
+ FOR EACH ROW
8360
+ EXECUTE FUNCTION delete_position_if_no_lots();
7636
8361
  `);
7637
8362
  });
7638
8363
  }
@@ -7664,22 +8389,24 @@ async function add(config, offers$1) {
7664
8389
  const tree = from$12(offers$1.map((o) => from$11(o)));
7665
8390
  const chainId = await getChainId(config.client);
7666
8391
  for (const offer of tree.offers) if (chainId !== offer.chainId) throw new ChainIdMismatchError(offer.chainId, chainId);
8392
+ const signature = await sign(tree.offers, config.client);
8393
+ const encoded = await encode$2(tree, signature);
7667
8394
  try {
7668
8395
  return await config.client.sendTransaction({
7669
8396
  chain: config.client.chain,
7670
8397
  account: config.client.account,
7671
8398
  to: config.mempoolAddress,
7672
- data: encode$2(tree)
8399
+ data: encoded
7673
8400
  });
7674
8401
  } catch (error) {
7675
8402
  throw new ViemClientError(error instanceof Error ? error.message : "Unknown error");
7676
8403
  }
7677
8404
  }
7678
8405
  async function* get(config, parameters) {
7679
- const { loanToken, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE } = {} } = parameters || {};
8406
+ const { loanToken, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE } = {} } = parameters || {};
7680
8407
  yield* streamOffers(config, {
7681
8408
  loanToken,
7682
- order: order$1,
8409
+ order,
7683
8410
  blockNumberGte,
7684
8411
  blockNumberLte,
7685
8412
  options: {
@@ -7701,7 +8428,7 @@ const getChainId = async (client) => {
7701
8428
  return chainId;
7702
8429
  };
7703
8430
  async function* streamOffers(config, parameters) {
7704
- const { loanToken, blockNumberGte, blockNumberLte, order: order$1 = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE, blockWindow = config.blockWindow } = {} } = parameters;
8431
+ const { loanToken, blockNumberGte, blockNumberLte, order = "desc", options: { maxBatchSize = DEFAULT_BATCH_SIZE, blockWindow = config.blockWindow } = {} } = parameters;
7705
8432
  const stream = streamLogs({
7706
8433
  client: config.client.extend(publicActions),
7707
8434
  contractAddress: config.mempoolAddress,
@@ -7718,13 +8445,13 @@ async function* streamOffers(config, parameters) {
7718
8445
  },
7719
8446
  blockNumberGte,
7720
8447
  blockNumberLte,
7721
- order: order$1,
8448
+ order,
7722
8449
  options: {
7723
8450
  maxBatchSize,
7724
8451
  blockWindow
7725
8452
  }
7726
8453
  });
7727
- let blockNumber = order$1 === "asc" ? blockNumberGte : blockNumberLte;
8454
+ let blockNumber = order === "asc" ? blockNumberGte : blockNumberLte;
7728
8455
  for await (const { logs, blockNumber: newBlockNumber } of stream) {
7729
8456
  blockNumber = newBlockNumber;
7730
8457
  if (logs.length === 0) continue;
@@ -7733,7 +8460,7 @@ async function* streamOffers(config, parameters) {
7733
8460
  if (!log) continue;
7734
8461
  const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
7735
8462
  try {
7736
- const tree = decode$2(payload);
8463
+ const { tree } = await decode$2(payload);
7737
8464
  for (const offer of tree.offers) {
7738
8465
  if (loanToken && offer.loanToken.toLowerCase() !== loanToken.toLowerCase()) continue;
7739
8466
  offers$1.push({