@morpho-dev/router 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import os from "node:os";
16
16
  import path, { dirname, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
19
- import { bytesToHex, createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashMessage, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, numberToHex, pad, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, toHex, zeroAddress } from "viem";
19
+ import { bytesToHex, createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, numberToHex, pad, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, toHex, zeroAddress } from "viem";
20
20
  import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts";
21
21
  import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
22
22
  import { anvil, base, mainnet } from "viem/chains";
@@ -26,13 +26,13 @@ import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
26
26
  import { gzip, ungzip } from "pako";
27
27
  import { serve } from "@hono/node-server";
28
28
  import { Hono } from "hono";
29
- import { AsyncLocalStorage } from "node:async_hooks";
29
+ import crypto, { createHash } from "node:crypto";
30
30
  import { z as z$1 } from "zod/v4";
31
31
  import "reflect-metadata";
32
32
  import { generateDocument } from "openapi-metadata";
33
33
  import { ApiBody, ApiOperation, ApiParam, ApiProperty, ApiQuery, ApiResponse, ApiTags } from "openapi-metadata/decorators";
34
+ import { AsyncLocalStorage } from "node:async_hooks";
34
35
  import dotenv from "dotenv";
35
- import crypto from "node:crypto";
36
36
  import { PGlite } from "@electric-sql/pglite";
37
37
  import { drizzle } from "drizzle-orm/node-postgres";
38
38
  import { migrate } from "drizzle-orm/node-postgres/migrator";
@@ -152,7 +152,7 @@ function startActiveSpan(tracer, name, fn) {
152
152
  //#endregion
153
153
  //#region package.json
154
154
  var name = "@morpho-dev/router";
155
- var version = "0.5.0";
155
+ var version = "0.7.0";
156
156
  var description = "Router package for Morpho protocol";
157
157
 
158
158
  //#endregion
@@ -1469,20 +1469,14 @@ async function run(parameters) {
1469
1469
  * @param parameters - Gatekeeper parameters. {@link GatekeeperParameters}
1470
1470
  * @returns Gatekeeper instance. {@link Gatekeeper}
1471
1471
  */
1472
- function create$17(parameters) {
1472
+ function create$20(parameters) {
1473
1473
  const { rules } = parameters;
1474
- return {
1475
- isAllowed: async (offers) => {
1476
- return await run({
1477
- items: offers,
1478
- rules
1479
- });
1480
- },
1481
- getRules: async () => rules.map((rule) => ({
1482
- name: rule.name,
1483
- description: rule.description
1484
- }))
1485
- };
1474
+ return { isAllowed: async (offers) => {
1475
+ return await run({
1476
+ items: offers,
1477
+ rules
1478
+ });
1479
+ } };
1486
1480
  }
1487
1481
 
1488
1482
  //#endregion
@@ -1689,17 +1683,6 @@ function getCallbackType(chain, address) {
1689
1683
  return configs[chain].callbacks?.find((c) => c.type !== Type$1.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
1690
1684
  }
1691
1685
  /**
1692
- * Returns the callback addresses for a given chain and callback type, if it exists.
1693
- * @param chain - Chain name for which to read the validation configuration
1694
- * @param type - Callback type to retrieve
1695
- * @returns The matching callback addresses or an empty array if not configured
1696
- */
1697
- function getCallbackTypeAddresses(chain, type) {
1698
- if (type === Type$1.BuyWithEmptyCallback) return [];
1699
- const match = configs[chain].callbacks?.find((c) => c.type === type);
1700
- return match && "addresses" in match ? match.addresses : [];
1701
- }
1702
- /**
1703
1686
  * Returns the list of allowed non-empty callback addresses for a chain.
1704
1687
  *
1705
1688
  * @param chain - Chain name
@@ -1713,13 +1696,18 @@ const assets = {
1713
1696
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1714
1697
  "0x6B175474E89094C44Da98b954EedeAC495271d0F",
1715
1698
  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
1716
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1699
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1700
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
1701
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
1717
1702
  ],
1718
1703
  [ChainId.BASE.toString()]: [
1719
1704
  "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
1720
1705
  "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
1721
1706
  "0x4200000000000000000000000000000000000006",
1722
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1707
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1708
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
1709
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
1710
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
1723
1711
  ],
1724
1712
  [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
1725
1713
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
@@ -1735,6 +1723,43 @@ const assets = {
1735
1723
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1736
1724
  ]
1737
1725
  };
1726
+ const oracles$1 = {
1727
+ [ChainId.ETHEREUM.toString()]: [
1728
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
1729
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
1730
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
1731
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
1732
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
1733
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
1734
+ ],
1735
+ [ChainId.BASE.toString()]: [
1736
+ "0xD09048c8B568Dbf5f189302beA26c9edABFC4858",
1737
+ "0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4",
1738
+ "0x05D2618404668D725B66c0f32B39e4EC15B393dC",
1739
+ "0xE1bb8E5b4930eC9FeC7f7943FCF6227649F14B37",
1740
+ "0x663BECd10daE6C4A3Dcd89F1d76c1174199639B9",
1741
+ "0x10b95702a0ce895972C91e432C4f7E19811D320E",
1742
+ "0x8C87DbD7A0c647A4291592Bc2994dbF95880fE2F",
1743
+ "0x4A11590e5326138B514E08A9B52202D42077Ca65",
1744
+ "0xa54122f0E0766258377Ffe732e454A3248f454F4"
1745
+ ],
1746
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
1747
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
1748
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
1749
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
1750
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
1751
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
1752
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
1753
+ ],
1754
+ [ChainId.ANVIL.toString()]: [
1755
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
1756
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
1757
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
1758
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
1759
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
1760
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
1761
+ ]
1762
+ };
1738
1763
  const configs = {
1739
1764
  ethereum: {
1740
1765
  callbacks: [
@@ -1836,7 +1861,7 @@ const Oracle = [{
1836
1861
  * @param chains - Array of chain objects to register.
1837
1862
  * @returns A registry for looking up chains by ID. {@link ChainRegistry}
1838
1863
  */
1839
- function create$16(chains) {
1864
+ function create$19(chains) {
1840
1865
  const byId = /* @__PURE__ */ new Map();
1841
1866
  for (const chain of chains) byId.set(chain.id, chain);
1842
1867
  return {
@@ -2158,196 +2183,6 @@ var CollateralsAreNotSortedError = class extends BaseError {
2158
2183
  }
2159
2184
  };
2160
2185
 
2161
- //#endregion
2162
- //#region src/core/Tree.ts
2163
- const VERSION$1 = 1;
2164
- const normalizeHash = (hash) => hash.toLowerCase();
2165
- /**
2166
- * Builds a Merkle tree from a list of offers.
2167
- *
2168
- * Leaves are the offer `hash` values as `bytes32` and are deterministically
2169
- * ordered following the StandardMerkleTree leaf ordering so that the resulting
2170
- * root is stable regardless of the input order.
2171
- *
2172
- * @param offers - Offers to include in the tree.
2173
- * @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
2174
- * @throws {TreeError} If tree building fails due to offer inconsistencies.
2175
- */
2176
- const from$12 = (offers) => {
2177
- const leaves = offers.map((offer) => [hash(offer)]);
2178
- const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
2179
- const orderedOffers = orderOffers(tree, offers);
2180
- return Object.assign(tree, { offers: orderedOffers });
2181
- };
2182
- const orderOffers = (tree, offers) => {
2183
- const offerByHash = /* @__PURE__ */ new Map();
2184
- for (const offer of offers) offerByHash.set(normalizeHash(hash(offer)), offer);
2185
- const entries = tree.dump().values.map((value) => {
2186
- const hash = normalizeHash(value.value[0]);
2187
- const offer = offerByHash.get(hash);
2188
- if (!offer) throw new TreeError(`missing offer for leaf ${hash}`);
2189
- return {
2190
- offer,
2191
- treeIndex: value.treeIndex
2192
- };
2193
- });
2194
- entries.sort((a, b) => b.treeIndex - a.treeIndex);
2195
- return entries.map((item) => item.offer);
2196
- };
2197
- /**
2198
- * Generates merkle proofs for all offers in a tree.
2199
- *
2200
- * Each proof allows independent verification that an offer is included in the tree
2201
- * without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
2202
- *
2203
- * @param tree - The {@link Tree} to generate proofs for.
2204
- * @returns Array of proofs - {@link Proof}
2205
- */
2206
- const proofs = (tree) => {
2207
- return tree.offers.map((offer) => {
2208
- return {
2209
- offer,
2210
- path: tree.getProof([hash(offer)])
2211
- };
2212
- });
2213
- };
2214
- const assertHex = (value, expectedBytes, name) => {
2215
- if (typeof value !== "string" || !isHex(value)) throw new DecodeError(`${name} is not a valid hex string`);
2216
- if (hexToBytes(value).length !== expectedBytes) throw new DecodeError(`${name}: expected ${expectedBytes} bytes`);
2217
- };
2218
- const verifySignatureAndRecoverAddress = async (params) => {
2219
- const { root, signature } = params;
2220
- assertHex(signature, 65, "signature");
2221
- const hash = hashMessage({ raw: root });
2222
- try {
2223
- return await recoverAddress({
2224
- hash,
2225
- signature
2226
- });
2227
- } catch {
2228
- throw new DecodeError("signature recovery failed");
2229
- }
2230
- };
2231
- /**
2232
- * Encodes a merkle tree without a signature into hex payload for client-side signing.
2233
- *
2234
- * Layout: `0x{vv}{gzip([...offers])}{root}` where:
2235
- * - `{vv}`: 1-byte version (currently 0x01)
2236
- * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
2237
- * - `{root}`: 32-byte merkle root
2238
- *
2239
- * Validates root integrity before encoding.
2240
- *
2241
- * @param tree - Merkle tree of offers
2242
- * @returns Hex-encoded unsigned payload
2243
- * @throws {EncodeError} If root mismatch
2244
- */
2245
- const encodeUnsigned = (tree) => {
2246
- validateTreeForEncoding(tree);
2247
- return bytesToHex(encodeUnsignedBytes(tree));
2248
- };
2249
- const validateTreeForEncoding = (tree) => {
2250
- if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
2251
- const computed = from$12(tree.offers);
2252
- if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
2253
- };
2254
- const encodeUnsignedBytes = (tree) => {
2255
- const offersPayload = tree.offers.map(serialize);
2256
- const compressed = gzip(JSON.stringify(offersPayload));
2257
- const rootBytes = hexToBytes(tree.root);
2258
- const encoded = new Uint8Array(1 + compressed.length + 32);
2259
- encoded[0] = VERSION$1;
2260
- encoded.set(compressed, 1);
2261
- encoded.set(rootBytes, 1 + compressed.length);
2262
- return encoded;
2263
- };
2264
- /**
2265
- * Decodes hex calldata into a validated merkle tree.
2266
- *
2267
- * Validates signature before decompression for fail-fast rejection of invalid payloads.
2268
- * Returns the tree with separately validated signature and recovered signer address.
2269
- *
2270
- * Validation order:
2271
- * 1. Version check
2272
- * 2. Signature verification (fail-fast, before decompression)
2273
- * 3. Decompression (only if signature valid)
2274
- * 4. Root verification (computed from offers vs embedded root)
2275
- *
2276
- * @example
2277
- * ```typescript
2278
- * const { tree, signature, signer } = await Tree.decode(calldata);
2279
- * console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
2280
- * ```
2281
- *
2282
- * @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
2283
- * @returns Validated tree, signature, and recovered signer address
2284
- * @throws {DecodeError} If version invalid, signature invalid, or root mismatch
2285
- */
2286
- const decode = async (encoded) => {
2287
- const bytes = hexToBytes(encoded);
2288
- if (bytes.length < 98) throw new DecodeError("payload too short");
2289
- const version = bytes[0];
2290
- if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
2291
- const signature = bytesToHex(bytes.slice(-65));
2292
- const root = bytesToHex(bytes.slice(-97, -65));
2293
- assertHex(root, 32, "root");
2294
- assertHex(signature, 65, "signature");
2295
- const signer = await verifySignatureAndRecoverAddress({
2296
- root,
2297
- signature
2298
- });
2299
- const compressed = bytes.slice(1, -97);
2300
- let decoded;
2301
- try {
2302
- decoded = ungzip(compressed, { to: "string" });
2303
- } catch {
2304
- throw new DecodeError("decompression failed");
2305
- }
2306
- let rawOffers;
2307
- try {
2308
- rawOffers = JSON.parse(decoded);
2309
- } catch {
2310
- throw new DecodeError("JSON parse failed");
2311
- }
2312
- const tree = from$12(rawOffers.map((o) => OfferSchema().parse(o)));
2313
- if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
2314
- return {
2315
- tree,
2316
- signature,
2317
- signer
2318
- };
2319
- };
2320
- /**
2321
- * Error thrown during tree building operations.
2322
- * Indicates structural issues with the tree (missing offers, inconsistent state).
2323
- */
2324
- var TreeError = class extends BaseError {
2325
- name = "Tree.TreeError";
2326
- constructor(reason) {
2327
- super(`Tree error: ${reason}`);
2328
- }
2329
- };
2330
- /**
2331
- * Error thrown during tree encoding.
2332
- * Indicates validation failures (signature, root mismatch, mixed makers).
2333
- */
2334
- var EncodeError = class extends BaseError {
2335
- name = "Tree.EncodeError";
2336
- constructor(reason) {
2337
- super(`Failed to encode tree: ${reason}`);
2338
- }
2339
- };
2340
- /**
2341
- * Error thrown during tree decoding.
2342
- * Indicates payload corruption, version mismatch, or validation failures.
2343
- */
2344
- var DecodeError = class extends BaseError {
2345
- name = "Tree.DecodeError";
2346
- constructor(reason) {
2347
- super(`Failed to decode tree: ${reason}`);
2348
- }
2349
- };
2350
-
2351
2186
  //#endregion
2352
2187
  //#region src/core/Offer.ts
2353
2188
  /** Internal symbol for caching the computed hash. */
@@ -2399,7 +2234,7 @@ const OfferSchema = () => {
2399
2234
  * @param input - The offer to create.
2400
2235
  * @returns The created offer.
2401
2236
  */
2402
- function from$11(input) {
2237
+ function from$12(input) {
2403
2238
  try {
2404
2239
  return OfferSchema().parse(input);
2405
2240
  } catch (error) {
@@ -2413,7 +2248,7 @@ function from$11(input) {
2413
2248
  * @returns The created offer.
2414
2249
  */
2415
2250
  function fromSnakeCase(input) {
2416
- return from$11(fromSnakeCase$1(input));
2251
+ return from$12(fromSnakeCase$1(input));
2417
2252
  }
2418
2253
  /**
2419
2254
  * Converts an offer to a snake case object.
@@ -2507,7 +2342,7 @@ function random(config) {
2507
2342
  })
2508
2343
  };
2509
2344
  })();
2510
- return from$11({
2345
+ return from$12({
2511
2346
  maker: config?.maker ?? address(),
2512
2347
  assets: assetsScaled,
2513
2348
  obligationUnits: config?.obligationUnits ?? 0n,
@@ -2738,7 +2573,7 @@ var InvalidOfferError = class InvalidOfferError extends BaseError {
2738
2573
  * @param data - The data to create the oracle from.
2739
2574
  * @returns The created oracle.
2740
2575
  */
2741
- function from$10(data) {
2576
+ function from$11(data) {
2742
2577
  return {
2743
2578
  chainId: data.chainId,
2744
2579
  address: data.address.toLowerCase(),
@@ -2772,7 +2607,7 @@ let Type = /* @__PURE__ */ function(Type) {
2772
2607
  * @param parameters - {@link from.Parameters}
2773
2608
  * @returns The created Position. {@link from.ReturnType}
2774
2609
  */
2775
- function from$9(parameters) {
2610
+ function from$10(parameters) {
2776
2611
  return {
2777
2612
  chainId: parameters.chainId,
2778
2613
  contract: parameters.contract.toLowerCase(),
@@ -2803,7 +2638,7 @@ const QuoteSchema = z$2.object({
2803
2638
  * const quote = Quote.from({ obligationId: "0x123", ask: { price: 100n }, bid: { price: 100n } });
2804
2639
  * ```
2805
2640
  */
2806
- function from$8(parameters) {
2641
+ function from$9(parameters) {
2807
2642
  try {
2808
2643
  const parsedQuote = QuoteSchema.parse(parameters);
2809
2644
  return {
@@ -2841,7 +2676,7 @@ const WAD = 10n ** 18n;
2841
2676
  * const transfer = Transfer.from({ id: "1", chainId: 1, contract: "0x123", from: "0x456", to: "0x789", value: 100n, blockNumber: 100n });
2842
2677
  * ```
2843
2678
  */
2844
- function from$7(parameters) {
2679
+ function from$8(parameters) {
2845
2680
  return {
2846
2681
  id: parameters.id,
2847
2682
  chainId: parameters.chainId,
@@ -2854,46 +2689,293 @@ function from$7(parameters) {
2854
2689
  }
2855
2690
 
2856
2691
  //#endregion
2857
- //#region src/core/types.ts
2858
- const BrandTypeId = Symbol.for("mempool/Brand");
2859
-
2860
- //#endregion
2861
- //#region src/gatekeeper/Rules.ts
2862
- const chains$1 = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
2863
- const allowedChainIds = chains.map((c) => c.id);
2864
- if (!allowedChainIds.some((id) => id === offer.chainId)) return { message: `Chain ID ${offer.chainId} is not in the allowed chains (${allowedChainIds.join(", ")})` };
2865
- });
2866
- const maturity = ({ maturities }) => single("maturity", `Validates that offer maturity is one of: [${maturities.join(", ")}]`, (offer) => {
2867
- const allowedMaturities = maturities.map((m) => from$16(m));
2868
- if (!allowedMaturities.includes(offer.maturity)) return { message: `Maturity must be end of current month (${allowedMaturities[0]}) or end of next month (${allowedMaturities[1]}). Got: ${offer.maturity}` };
2869
- });
2870
- const callback = ({ callbacks, allowedAddresses }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell offers must use a non-empty callback; non-empty callbacks must target one of [${allowedAddresses.map((a) => a.toLowerCase()).join(", ")}]`, (offer) => {
2871
- if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
2872
- if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
2873
- if (!isEmptyCallback(offer)) {
2874
- if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
2875
- }
2876
- });
2692
+ //#region src/core/Tree.ts
2693
+ const VERSION$1 = 1;
2877
2694
  /**
2878
- * A validation rule that checks if the offer's tokens are allowed for its chain.
2879
- * @param assetsByChainId - Allowed assets indexed by chain id.
2880
- * @returns The issue that was found. If the offer is valid, this will be undefined.
2695
+ * EIP-712 types for signing the tree root (Root(bytes32 root)).
2881
2696
  */
2882
- const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
2883
- const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
2884
- if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
2885
- if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
2886
- if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
2887
- });
2697
+ const signatureTypes = {
2698
+ EIP712Domain: [{
2699
+ name: "chainId",
2700
+ type: "uint256"
2701
+ }, {
2702
+ name: "verifyingContract",
2703
+ type: "address"
2704
+ }],
2705
+ Root: [{
2706
+ name: "root",
2707
+ type: "bytes32"
2708
+ }]
2709
+ };
2710
+ const normalizeHash = (hash) => hash.toLowerCase();
2888
2711
  /**
2889
- * A batch validation rule that ensures all offers in a tree have the same maker address.
2890
- * Returns an issue only for the first non-conforming offer.
2891
- * This rule is signing-agnostic; signer verification is handled at the collector level.
2892
- */
2893
- const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker address", (offers) => {
2894
- const issues = /* @__PURE__ */ new Map();
2895
- if (offers.length === 0) return issues;
2896
- const firstMaker = offers[0].maker.toLowerCase();
2712
+ * Builds a Merkle tree from a list of offers.
2713
+ *
2714
+ * Leaves are the offer `hash` values as `bytes32` and are deterministically
2715
+ * ordered following the StandardMerkleTree leaf ordering so that the resulting
2716
+ * root is stable regardless of the input order.
2717
+ *
2718
+ * @param offers - Offers to include in the tree.
2719
+ * @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
2720
+ * @throws {TreeError} If tree building fails due to offer inconsistencies.
2721
+ */
2722
+ const from$7 = (offers) => {
2723
+ const leaves = offers.map((offer) => [hash(offer)]);
2724
+ const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
2725
+ const orderedOffers = orderOffers(tree, offers);
2726
+ return Object.assign(tree, { offers: orderedOffers });
2727
+ };
2728
+ const orderOffers = (tree, offers) => {
2729
+ const offerByHash = /* @__PURE__ */ new Map();
2730
+ for (const offer of offers) offerByHash.set(normalizeHash(hash(offer)), offer);
2731
+ const entries = tree.dump().values.map((value) => {
2732
+ const hash = normalizeHash(value.value[0]);
2733
+ const offer = offerByHash.get(hash);
2734
+ if (!offer) throw new TreeError(`missing offer for leaf ${hash}`);
2735
+ return {
2736
+ offer,
2737
+ treeIndex: value.treeIndex
2738
+ };
2739
+ });
2740
+ entries.sort((a, b) => b.treeIndex - a.treeIndex);
2741
+ return entries.map((item) => item.offer);
2742
+ };
2743
+ /**
2744
+ * Generates merkle proofs for all offers in a tree.
2745
+ *
2746
+ * Each proof allows independent verification that an offer is included in the tree
2747
+ * without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
2748
+ *
2749
+ * @param tree - The {@link Tree} to generate proofs for.
2750
+ * @returns Array of proofs - {@link Proof}
2751
+ */
2752
+ const proofs = (tree) => {
2753
+ return tree.offers.map((offer) => {
2754
+ return {
2755
+ offer,
2756
+ path: tree.getProof([hash(offer)])
2757
+ };
2758
+ });
2759
+ };
2760
+ const normalizeSignatureDomain = (domain, errorFactory) => {
2761
+ let chainId;
2762
+ try {
2763
+ chainId = typeof domain.chainId === "bigint" ? domain.chainId : BigInt(domain.chainId);
2764
+ } catch {
2765
+ throw errorFactory("invalid chainId");
2766
+ }
2767
+ if (chainId < 0n) throw errorFactory("invalid chainId");
2768
+ if (!isAddress(domain.verifyingContract)) throw errorFactory("invalid verifyingContract");
2769
+ return {
2770
+ chainId,
2771
+ verifyingContract: domain.verifyingContract.toLowerCase()
2772
+ };
2773
+ };
2774
+ const assertHex = (value, expectedBytes, name, errorFactory = (reason) => new DecodeError(reason)) => {
2775
+ if (typeof value !== "string" || !isHex(value)) throw errorFactory(`${name} is not a valid hex string`);
2776
+ if (hexToBytes(value).length !== expectedBytes) throw errorFactory(`${name}: expected ${expectedBytes} bytes`);
2777
+ };
2778
+ const verifySignatureAndRecoverAddress = async (params) => {
2779
+ const { root, signature, domain, errorFactory } = params;
2780
+ assertHex(root, 32, "root", errorFactory);
2781
+ assertHex(signature, 65, "signature", errorFactory);
2782
+ const hash = hashTypedData({
2783
+ domain,
2784
+ types: signatureTypes,
2785
+ primaryType: "Root",
2786
+ message: { root }
2787
+ });
2788
+ try {
2789
+ return await recoverAddress({
2790
+ hash,
2791
+ signature
2792
+ });
2793
+ } catch {
2794
+ throw errorFactory("signature recovery failed");
2795
+ }
2796
+ };
2797
+ /**
2798
+ * Encodes a merkle tree without a signature into hex payload for client-side signing.
2799
+ *
2800
+ * Layout: `0x{vv}{gzip([...offers])}{root}` where:
2801
+ * - `{vv}`: 1-byte version (currently 0x01)
2802
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
2803
+ * - `{root}`: 32-byte merkle root
2804
+ *
2805
+ * Validates root integrity before encoding.
2806
+ *
2807
+ * @param tree - Merkle tree of offers
2808
+ * @returns Hex-encoded unsigned payload
2809
+ * @throws {EncodeError} If root mismatch
2810
+ */
2811
+ const encodeUnsigned = (tree) => {
2812
+ validateTreeForEncoding(tree);
2813
+ return bytesToHex(encodeUnsignedBytes(tree));
2814
+ };
2815
+ const validateTreeForEncoding = (tree, domain) => {
2816
+ if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
2817
+ const computed = from$7(tree.offers);
2818
+ if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
2819
+ if (domain) {
2820
+ const mismatched = tree.offers.find((offer) => BigInt(offer.chainId) !== domain.chainId);
2821
+ if (mismatched) throw new EncodeError(`chainId mismatch: expected ${domain.chainId}, got ${mismatched.chainId}`);
2822
+ }
2823
+ };
2824
+ const encodeUnsignedBytes = (tree) => {
2825
+ const offersPayload = tree.offers.map(serialize);
2826
+ const compressed = gzip(JSON.stringify(offersPayload));
2827
+ const rootBytes = hexToBytes(tree.root);
2828
+ const encoded = new Uint8Array(1 + compressed.length + 32);
2829
+ encoded[0] = VERSION$1;
2830
+ encoded.set(compressed, 1);
2831
+ encoded.set(rootBytes, 1 + compressed.length);
2832
+ return encoded;
2833
+ };
2834
+ /**
2835
+ * Decodes hex calldata into a validated merkle tree.
2836
+ *
2837
+ * Validates signature before decompression for fail-fast rejection of invalid payloads.
2838
+ * Returns the tree with separately validated signature and recovered signer address.
2839
+ *
2840
+ * Validation order:
2841
+ * 1. Version check
2842
+ * 2. Signature verification (fail-fast, before decompression)
2843
+ * 3. Decompression (only if signature valid)
2844
+ * 4. Root verification (computed from offers vs embedded root)
2845
+ *
2846
+ * @example
2847
+ * ```typescript
2848
+ * const { tree, signature, signer } = await Tree.decode(calldata, { chainId, verifyingContract });
2849
+ * console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
2850
+ * ```
2851
+ *
2852
+ * @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
2853
+ * @param domain - EIP-712 domain with chain id and verifying contract
2854
+ * @returns Validated tree, signature, and recovered signer address
2855
+ * @throws {DecodeError} If version invalid, signature invalid, or root mismatch
2856
+ */
2857
+ const decode = async (encoded, domain) => {
2858
+ const errorFactory = (reason) => new DecodeError(reason);
2859
+ const normalizedDomain = normalizeSignatureDomain(domain, errorFactory);
2860
+ const bytes = hexToBytes(encoded);
2861
+ if (bytes.length < 98) throw new DecodeError("payload too short");
2862
+ const version = bytes[0];
2863
+ if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
2864
+ const signature = bytesToHex(bytes.slice(-65));
2865
+ const root = bytesToHex(bytes.slice(-97, -65));
2866
+ assertHex(root, 32, "root");
2867
+ assertHex(signature, 65, "signature");
2868
+ const signer = await verifySignatureAndRecoverAddress({
2869
+ root,
2870
+ signature,
2871
+ domain: normalizedDomain,
2872
+ errorFactory
2873
+ });
2874
+ const compressed = bytes.slice(1, -97);
2875
+ let decoded;
2876
+ try {
2877
+ decoded = ungzip(compressed, { to: "string" });
2878
+ } catch {
2879
+ throw new DecodeError("decompression failed");
2880
+ }
2881
+ let rawOffers;
2882
+ try {
2883
+ rawOffers = JSON.parse(decoded);
2884
+ } catch {
2885
+ throw new DecodeError("JSON parse failed");
2886
+ }
2887
+ const tree = from$7(rawOffers.map((o) => OfferSchema().parse(o)));
2888
+ if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
2889
+ const chainIdMismatch = tree.offers.find((offer) => BigInt(offer.chainId) !== normalizedDomain.chainId);
2890
+ if (chainIdMismatch) throw new DecodeError(`chainId mismatch: expected ${normalizedDomain.chainId}, got ${chainIdMismatch.chainId}`);
2891
+ return {
2892
+ tree,
2893
+ signature,
2894
+ signer
2895
+ };
2896
+ };
2897
+ /**
2898
+ * Error thrown during tree building operations.
2899
+ * Indicates structural issues with the tree (missing offers, inconsistent state).
2900
+ */
2901
+ var TreeError = class extends BaseError {
2902
+ name = "Tree.TreeError";
2903
+ constructor(reason) {
2904
+ super(`Tree error: ${reason}`);
2905
+ }
2906
+ };
2907
+ /**
2908
+ * Error thrown during tree encoding.
2909
+ * Indicates validation failures (signature, root mismatch, mixed makers).
2910
+ */
2911
+ var EncodeError = class extends BaseError {
2912
+ name = "Tree.EncodeError";
2913
+ constructor(reason) {
2914
+ super(`Failed to encode tree: ${reason}`);
2915
+ }
2916
+ };
2917
+ /**
2918
+ * Error thrown during tree decoding.
2919
+ * Indicates payload corruption, version mismatch, or validation failures.
2920
+ */
2921
+ var DecodeError = class extends BaseError {
2922
+ name = "Tree.DecodeError";
2923
+ constructor(reason) {
2924
+ super(`Failed to decode tree: ${reason}`);
2925
+ }
2926
+ };
2927
+
2928
+ //#endregion
2929
+ //#region src/core/types.ts
2930
+ const BrandTypeId = Symbol.for("mempool/Brand");
2931
+
2932
+ //#endregion
2933
+ //#region src/gatekeeper/Rules.ts
2934
+ const chains$1 = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
2935
+ const allowedChainIds = chains.map((c) => c.id);
2936
+ if (!allowedChainIds.some((id) => id === offer.chainId)) return { message: `Chain ID ${offer.chainId} is not in the allowed chains (${allowedChainIds.join(", ")})` };
2937
+ });
2938
+ const maturity = ({ maturities }) => single("maturity", `Validates that offer maturity is one of: [${maturities.join(", ")}]`, (offer) => {
2939
+ const allowedMaturities = maturities.map((m) => from$16(m));
2940
+ if (!allowedMaturities.includes(offer.maturity)) return { message: `Maturity must be end of current month (${allowedMaturities[0]}) or end of next month (${allowedMaturities[1]}). Got: ${offer.maturity}` };
2941
+ });
2942
+ const callback = ({ callbacks, allowedAddresses }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell offers must use a non-empty callback; non-empty callbacks must target one of [${allowedAddresses.map((a) => a.toLowerCase()).join(", ")}]`, (offer) => {
2943
+ if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
2944
+ if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
2945
+ if (!isEmptyCallback(offer)) {
2946
+ if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
2947
+ }
2948
+ });
2949
+ /**
2950
+ * A validation rule that checks if the offer's tokens are allowed for its chain.
2951
+ * @param assetsByChainId - Allowed assets indexed by chain id.
2952
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
2953
+ */
2954
+ const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
2955
+ const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
2956
+ if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
2957
+ if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
2958
+ if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
2959
+ });
2960
+ /**
2961
+ * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
2962
+ * @param oraclesByChainId - Allowed oracles indexed by chain id.
2963
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
2964
+ */
2965
+ const oracle = ({ oraclesByChainId }) => single("oracle", "Validates that offer collateral oracles are in the allowed oracle list for the offer chain", (offer) => {
2966
+ const allowedOracles = oraclesByChainId[offer.chainId]?.map((oracle) => oracle.toLowerCase());
2967
+ if (!allowedOracles || allowedOracles.length === 0) return { message: `No allowed oracles for chain ${offer.chainId}` };
2968
+ if (offer.collaterals.some((collateral) => !allowedOracles.includes(collateral.oracle.toLowerCase()))) return { message: "Oracle is not allowed" };
2969
+ });
2970
+ /**
2971
+ * A batch validation rule that ensures all offers in a tree have the same maker address.
2972
+ * Returns an issue only for the first non-conforming offer.
2973
+ * This rule is signing-agnostic; signer verification is handled at the collector level.
2974
+ */
2975
+ const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker address", (offers) => {
2976
+ const issues = /* @__PURE__ */ new Map();
2977
+ if (offers.length === 0) return issues;
2978
+ const firstMaker = offers[0].maker.toLowerCase();
2897
2979
  for (let i = 1; i < offers.length; i++) {
2898
2980
  const offer = offers[i];
2899
2981
  if (offer.maker.toLowerCase() !== firstMaker) {
@@ -2916,7 +2998,11 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
2916
2998
  //#region src/gatekeeper/morphoRules.ts
2917
2999
  const morphoRules = (chains) => {
2918
3000
  const assetsByChainId = {};
2919
- for (const chain of chains) assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
3001
+ const oraclesByChainId = {};
3002
+ for (const chain of chains) {
3003
+ assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
3004
+ oraclesByChainId[chain.id] = oracles$1[chain.id.toString()] ?? [];
3005
+ }
2920
3006
  return [
2921
3007
  sameMaker(),
2922
3008
  amountMutualExclusivity(),
@@ -2930,176 +3016,95 @@ const morphoRules = (chains) => {
2930
3016
  ],
2931
3017
  allowedAddresses: chains.flatMap((c) => getCallbackAddresses(c.name))
2932
3018
  }),
2933
- token({ assetsByChainId })
3019
+ token({ assetsByChainId }),
3020
+ oracle({ oraclesByChainId })
2934
3021
  ];
2935
3022
  };
2936
3023
 
2937
3024
  //#endregion
2938
- //#region src/api/Controllers/Payload.ts
2939
- const API_ERROR_CODES = [
2940
- "VALIDATION_ERROR",
2941
- "NOT_FOUND",
2942
- "INTERNAL_SERVER_ERROR",
2943
- "BAD_REQUEST"
2944
- ];
2945
- let STATUS_CODE = /* @__PURE__ */ function(STATUS_CODE) {
2946
- STATUS_CODE[STATUS_CODE["SUCCESS"] = 200] = "SUCCESS";
2947
- STATUS_CODE[STATUS_CODE["BAD_REQUEST"] = 400] = "BAD_REQUEST";
2948
- STATUS_CODE[STATUS_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
2949
- STATUS_CODE[STATUS_CODE["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
2950
- return STATUS_CODE;
2951
- }({});
2952
- var APIError = class extends Error {
2953
- constructor(statusCode, message, code, details) {
2954
- super(message);
2955
- this.statusCode = statusCode;
2956
- this.code = code;
2957
- this.details = details;
2958
- this.name = "APIError";
2959
- }
2960
- };
2961
- var ValidationError = class extends APIError {
2962
- constructor(message, details) {
2963
- super(STATUS_CODE.BAD_REQUEST, message, "VALIDATION_ERROR", details);
2964
- }
2965
- };
2966
- var NotFoundError = class extends APIError {
2967
- constructor(message) {
2968
- super(STATUS_CODE.NOT_FOUND, message, "NOT_FOUND");
2969
- }
2970
- };
2971
- var InternalServerError = class extends APIError {
2972
- constructor(message = "Internal server error") {
2973
- super(STATUS_CODE.INTERNAL_SERVER_ERROR, message, "INTERNAL_SERVER_ERROR");
2974
- }
2975
- };
2976
- var BadRequestError = class extends APIError {
2977
- constructor(message = "Invalid JSON format", details) {
2978
- super(STATUS_CODE.BAD_REQUEST, message, "BAD_REQUEST", details);
2979
- }
2980
- };
2981
- function success(args) {
2982
- const { data, cursor } = args;
2983
- return {
2984
- statusCode: STATUS_CODE.SUCCESS,
2985
- body: {
2986
- meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
2987
- cursor: cursor ?? null,
2988
- data
3025
+ //#region src/gatekeeper/ConfigRules.ts
3026
+ /**
3027
+ * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
3028
+ * @param chains - Chains to include in the configured rules.
3029
+ * @returns Sorted list of config rules.
3030
+ */
3031
+ function buildConfigRules(chains) {
3032
+ const rules = [];
3033
+ for (const chain of chains) {
3034
+ const config = configs[chain.name];
3035
+ const maturities = config.maturities ?? [];
3036
+ for (const maturityName of maturities) rules.push({
3037
+ type: "maturity",
3038
+ chain_id: chain.id,
3039
+ name: maturityName,
3040
+ timestamp: from$16(maturityName)
3041
+ });
3042
+ const callbacks = config.callbacks ?? [];
3043
+ for (const callback of callbacks) {
3044
+ if (callback.type === Type$1.BuyWithEmptyCallback) continue;
3045
+ if (!("addresses" in callback)) continue;
3046
+ for (const address of callback.addresses) rules.push({
3047
+ type: "callback",
3048
+ chain_id: chain.id,
3049
+ address: normalizeAddress(address),
3050
+ callback_type: callback.type
3051
+ });
2989
3052
  }
2990
- };
3053
+ const loanTokens = assets[chain.id.toString()] ?? [];
3054
+ for (const address of loanTokens) rules.push({
3055
+ type: "loan_token",
3056
+ chain_id: chain.id,
3057
+ address: normalizeAddress(address)
3058
+ });
3059
+ const oracles = oracles$1[chain.id.toString()] ?? [];
3060
+ for (const address of oracles) rules.push({
3061
+ type: "oracle",
3062
+ chain_id: chain.id,
3063
+ address: normalizeAddress(address)
3064
+ });
3065
+ }
3066
+ rules.sort(compareConfigRules);
3067
+ return rules;
2991
3068
  }
2992
3069
  /**
2993
- * Generic failure builder. Preserves the concrete status code when the input is an APIError.
2994
- * If not an APIError, maps to INTERNAL_SERVER_ERROR. Zod & SyntaxError are mapped to BAD_REQUEST.
3070
+ * Compute a stable checksum for the provided configured rules.
3071
+ * @param rules - Configured rules to checksum.
3072
+ * @returns MD5 checksum.
2995
3073
  */
2996
- function failure(err) {
2997
- if (err instanceof APIError) return handleAPIError(err);
2998
- if (err instanceof SyntaxError) return handleAPIError(new BadRequestError(err.message));
2999
- if (err instanceof z$2.ZodError) return handleAPIError(handleZodError(err));
3000
- return handleAPIError(new InternalServerError());
3001
- }
3002
- function handleAPIError(error) {
3003
- return {
3004
- statusCode: error.statusCode,
3005
- body: {
3006
- meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
3007
- error: {
3008
- code: error.code,
3009
- message: error.message,
3010
- ...error.details && typeof error.details === "object" ? { details: error.details } : {}
3011
- }
3074
+ function buildConfigRulesChecksum(rules) {
3075
+ const hash = createHash("md5");
3076
+ const orderedRules = [...rules].sort(compareConfigRules);
3077
+ for (const rule of orderedRules) {
3078
+ if (rule.type === "maturity") {
3079
+ hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
3080
+ continue;
3012
3081
  }
3013
- };
3014
- }
3015
- function handleZodError(error) {
3016
- return new ValidationError("Validation failed", error.issues.map((issue) => {
3017
- const field = issue.path.join(".");
3018
- let msg = issue.message;
3019
- switch (issue.code) {
3020
- case "invalid_type":
3021
- if (issue.message.includes("received undefined")) msg = `${field} is required`;
3022
- break;
3023
- case "invalid_format":
3024
- msg = issue.format === "regex" ? issue.message : `${field} has an invalid format`;
3025
- break;
3026
- default: break;
3082
+ if (rule.type === "callback") {
3083
+ hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
3084
+ continue;
3027
3085
  }
3028
- return {
3029
- field,
3030
- issue: msg
3031
- };
3032
- }));
3033
- }
3034
-
3035
- //#endregion
3036
- //#region src/logger/Logger.ts
3037
- const LogLevelValues = [
3038
- "trace",
3039
- "debug",
3040
- "info",
3041
- "warn",
3042
- "error",
3043
- "fatal",
3044
- "silent"
3045
- ];
3046
- function defaultLogger(minLevel, pretty) {
3047
- const threshold = minLevel ?? process.env.ROUTER_LOG_LEVEL ?? "info";
3048
- const prettyEnabled = typeof pretty === "boolean" ? pretty : String(process.env.ROUTER_LOG_PRETTY ?? "false").toLowerCase() === "true";
3049
- const levelIndexByName = LogLevelValues.reduce((acc, lvl, idx) => {
3050
- acc[lvl] = idx;
3051
- return acc;
3052
- }, {});
3053
- const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
3054
- const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
3055
- if (!prettyEnabled) {
3056
- console[consoleMethod](stringify({
3057
- level: methodLevel,
3058
- ...entry
3059
- }));
3060
- return;
3086
+ if (rule.type === "oracle") {
3087
+ hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
3088
+ continue;
3061
3089
  }
3062
- const { msg, ...rest } = entry;
3063
- const stack = typeof rest.stack === "string" ? rest.stack : void 0;
3064
- if (stack) delete rest.stack;
3065
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3066
- const level = methodLevel.toUpperCase();
3067
- const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
3068
- const line = extras.length > 0 ? `${timestamp} [${level}] ${msg} ${extras}` : `${timestamp} [${level}] ${msg}`;
3069
- console[consoleMethod](line);
3070
- if (stack) console[consoleMethod](stack);
3071
- } : () => {};
3072
- return {
3073
- trace: wrap("trace", "trace"),
3074
- debug: wrap("debug", "debug"),
3075
- info: wrap("info", "info"),
3076
- warn: wrap("warn", "warn"),
3077
- error: wrap("error", "error"),
3078
- fatal: wrap("error", "fatal")
3079
- };
3080
- }
3081
- const loggerContext = new AsyncLocalStorage();
3082
- function runWithLogger(logger, fn) {
3083
- return loggerContext.run(logger, fn);
3084
- }
3085
- function getLogger() {
3086
- return loggerContext.getStore() ?? defaultLogger();
3087
- }
3088
- function formatValue(value) {
3089
- if (value === null || value === void 0 || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
3090
- if (typeof value === "string") {
3091
- if (value.includes(" ")) return JSON.stringify(value);
3092
- return value;
3090
+ hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
3093
3091
  }
3094
- try {
3095
- return stringify(value);
3096
- } catch {
3097
- try {
3098
- return JSON.stringify(value);
3099
- } catch {
3100
- return String(value);
3101
- }
3092
+ return hash.digest("hex");
3093
+ }
3094
+ function normalizeAddress(address) {
3095
+ return address.toLowerCase();
3096
+ }
3097
+ function compareConfigRules(left, right) {
3098
+ if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
3099
+ if (left.type !== right.type) return left.type.localeCompare(right.type);
3100
+ if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
3101
+ if (left.type === "callback" && right.type === "callback") {
3102
+ if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
3103
+ return left.address.localeCompare(right.address);
3102
3104
  }
3105
+ if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
3106
+ if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
3107
+ return 0;
3103
3108
  }
3104
3109
 
3105
3110
  //#endregion
@@ -3169,65 +3174,163 @@ function from$5(obligation, quote) {
3169
3174
  bid: { price: quote.bid.price.toString() }
3170
3175
  };
3171
3176
  }
3172
-
3173
- //#endregion
3174
- //#region src/api/Schema/OfferResponse.ts
3177
+
3178
+ //#endregion
3179
+ //#region src/api/Schema/OfferResponse.ts
3180
+ /**
3181
+ * Creates an `OfferResponse` matching the Solidity Offer struct layout.
3182
+ * @constructor
3183
+ * @param input - {@link Input}
3184
+ * @returns The created `OfferResponse`. {@link OfferResponse}
3185
+ */
3186
+ function from$4(input) {
3187
+ const base = {
3188
+ offer: {
3189
+ obligation: {
3190
+ loan_token: input.loanToken,
3191
+ collaterals: input.collaterals.map((c) => ({
3192
+ token: c.asset,
3193
+ lltv: c.lltv.toString(),
3194
+ oracle: c.oracle
3195
+ })),
3196
+ maturity: input.maturity
3197
+ },
3198
+ buy: input.buy,
3199
+ maker: input.maker,
3200
+ assets: input.assets.toString(),
3201
+ obligation_units: input.obligationUnits.toString(),
3202
+ obligation_shares: input.obligationShares.toString(),
3203
+ start: input.start,
3204
+ expiry: input.expiry,
3205
+ price: input.price.toString(),
3206
+ group: input.group,
3207
+ session: input.session,
3208
+ callback: input.callback.address,
3209
+ callback_data: input.callback.data
3210
+ },
3211
+ offer_hash: input.hash,
3212
+ obligation_id: id({
3213
+ chainId: input.chainId,
3214
+ loanToken: input.loanToken,
3215
+ collaterals: [...input.collaterals],
3216
+ maturity: input.maturity
3217
+ }),
3218
+ chain_id: input.chainId,
3219
+ consumed: input.consumed.toString(),
3220
+ takeable: input.takeable.toString(),
3221
+ block_number: input.blockNumber
3222
+ };
3223
+ if (!input.proof || !input.root || !input.signature) return {
3224
+ ...base,
3225
+ root: null,
3226
+ proof: null,
3227
+ signature: null
3228
+ };
3229
+ return {
3230
+ ...base,
3231
+ root: input.root.toLowerCase(),
3232
+ proof: input.proof.map((p) => p.toLowerCase()),
3233
+ signature: input.signature.toLowerCase()
3234
+ };
3235
+ }
3236
+
3237
+ //#endregion
3238
+ //#region src/api/Controllers/Payload.ts
3239
+ const API_ERROR_CODES = [
3240
+ "VALIDATION_ERROR",
3241
+ "NOT_FOUND",
3242
+ "INTERNAL_SERVER_ERROR",
3243
+ "BAD_REQUEST"
3244
+ ];
3245
+ let STATUS_CODE = /* @__PURE__ */ function(STATUS_CODE) {
3246
+ STATUS_CODE[STATUS_CODE["SUCCESS"] = 200] = "SUCCESS";
3247
+ STATUS_CODE[STATUS_CODE["BAD_REQUEST"] = 400] = "BAD_REQUEST";
3248
+ STATUS_CODE[STATUS_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
3249
+ STATUS_CODE[STATUS_CODE["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
3250
+ return STATUS_CODE;
3251
+ }({});
3252
+ var APIError = class extends Error {
3253
+ constructor(statusCode, message, code, details) {
3254
+ super(message);
3255
+ this.statusCode = statusCode;
3256
+ this.code = code;
3257
+ this.details = details;
3258
+ this.name = "APIError";
3259
+ }
3260
+ };
3261
+ var ValidationError = class extends APIError {
3262
+ constructor(message, details) {
3263
+ super(STATUS_CODE.BAD_REQUEST, message, "VALIDATION_ERROR", details);
3264
+ }
3265
+ };
3266
+ var NotFoundError = class extends APIError {
3267
+ constructor(message) {
3268
+ super(STATUS_CODE.NOT_FOUND, message, "NOT_FOUND");
3269
+ }
3270
+ };
3271
+ var InternalServerError = class extends APIError {
3272
+ constructor(message = "Internal server error") {
3273
+ super(STATUS_CODE.INTERNAL_SERVER_ERROR, message, "INTERNAL_SERVER_ERROR");
3274
+ }
3275
+ };
3276
+ var BadRequestError = class extends APIError {
3277
+ constructor(message = "Invalid JSON format", details) {
3278
+ super(STATUS_CODE.BAD_REQUEST, message, "BAD_REQUEST", details);
3279
+ }
3280
+ };
3281
+ function success(args) {
3282
+ const { data, cursor } = args;
3283
+ return {
3284
+ statusCode: STATUS_CODE.SUCCESS,
3285
+ body: {
3286
+ meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
3287
+ cursor: cursor ?? null,
3288
+ data
3289
+ }
3290
+ };
3291
+ }
3175
3292
  /**
3176
- * Creates an `OfferResponse` matching the Solidity Offer struct layout.
3177
- * @constructor
3178
- * @param input - {@link Input}
3179
- * @returns The created `OfferResponse`. {@link OfferResponse}
3293
+ * Generic failure builder. Preserves the concrete status code when the input is an APIError.
3294
+ * If not an APIError, maps to INTERNAL_SERVER_ERROR. Zod & SyntaxError are mapped to BAD_REQUEST.
3180
3295
  */
3181
- function from$4(input) {
3182
- const base = {
3183
- offer: {
3184
- obligation: {
3185
- loan_token: input.loanToken,
3186
- collaterals: input.collaterals.map((c) => ({
3187
- token: c.asset,
3188
- lltv: c.lltv.toString(),
3189
- oracle: c.oracle
3190
- })),
3191
- maturity: input.maturity
3192
- },
3193
- buy: input.buy,
3194
- maker: input.maker,
3195
- assets: input.assets.toString(),
3196
- obligation_units: input.obligationUnits.toString(),
3197
- obligation_shares: input.obligationShares.toString(),
3198
- start: input.start,
3199
- expiry: input.expiry,
3200
- price: input.price.toString(),
3201
- group: input.group,
3202
- session: input.session,
3203
- callback: input.callback.address,
3204
- callback_data: input.callback.data
3205
- },
3206
- offer_hash: input.hash,
3207
- obligation_id: id({
3208
- chainId: input.chainId,
3209
- loanToken: input.loanToken,
3210
- collaterals: [...input.collaterals],
3211
- maturity: input.maturity
3212
- }),
3213
- chain_id: input.chainId,
3214
- consumed: input.consumed.toString(),
3215
- takeable: input.takeable.toString(),
3216
- block_number: input.blockNumber
3217
- };
3218
- if (!input.proof || !input.root || !input.signature) return {
3219
- ...base,
3220
- root: null,
3221
- proof: null,
3222
- signature: null
3223
- };
3296
+ function failure(err) {
3297
+ if (err instanceof APIError) return handleAPIError(err);
3298
+ if (err instanceof SyntaxError) return handleAPIError(new BadRequestError(err.message));
3299
+ if (err instanceof z$2.ZodError) return handleAPIError(handleZodError(err));
3300
+ return handleAPIError(new InternalServerError());
3301
+ }
3302
+ function handleAPIError(error) {
3224
3303
  return {
3225
- ...base,
3226
- root: input.root.toLowerCase(),
3227
- proof: input.proof.map((p) => p.toLowerCase()),
3228
- signature: input.signature.toLowerCase()
3304
+ statusCode: error.statusCode,
3305
+ body: {
3306
+ meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
3307
+ error: {
3308
+ code: error.code,
3309
+ message: error.message,
3310
+ ...error.details && typeof error.details === "object" ? { details: error.details } : {}
3311
+ }
3312
+ }
3229
3313
  };
3230
3314
  }
3315
+ function handleZodError(error) {
3316
+ return new ValidationError("Validation failed", error.issues.map((issue) => {
3317
+ const field = issue.path.join(".");
3318
+ let msg = issue.message;
3319
+ switch (issue.code) {
3320
+ case "invalid_type":
3321
+ if (issue.message.includes("received undefined")) msg = `${field} is required`;
3322
+ break;
3323
+ case "invalid_format":
3324
+ msg = issue.format === "regex" ? issue.message : `${field} has an invalid format`;
3325
+ break;
3326
+ default: break;
3327
+ }
3328
+ return {
3329
+ field,
3330
+ issue: msg
3331
+ };
3332
+ }));
3333
+ }
3231
3334
 
3232
3335
  //#endregion
3233
3336
  //#region \0@oxc-project+runtime@0.110.0/helpers/decorate.js
@@ -3321,6 +3424,21 @@ const validateOfferExample = {
3321
3424
  data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
3322
3425
  }
3323
3426
  };
3427
+ const callbackTypesRequestExample = { callbacks: [{
3428
+ chain_id: 1,
3429
+ addresses: [
3430
+ "0x1111111111111111111111111111111111111111",
3431
+ "0x3333333333333333333333333333333333333333",
3432
+ "0x9999999999999999999999999999999999999999"
3433
+ ]
3434
+ }] };
3435
+ const callbackTypesResponseExample = [{
3436
+ chain_id: 1,
3437
+ sell_erc20_callback: ["0x1111111111111111111111111111111111111111"],
3438
+ buy_erc20: ["0x5555555555555555555555555555555555555555"],
3439
+ buy_vault_v1_callback: ["0x3333333333333333333333333333333333333333"],
3440
+ not_supported: ["0x9999999999999999999999999999999999999999"]
3441
+ }];
3324
3442
  const routerStatusExample = {
3325
3443
  status: "live",
3326
3444
  initialized: true,
@@ -3389,6 +3507,55 @@ __decorate([ApiProperty({
3389
3507
  type: "string",
3390
3508
  example: validateOfferExample.callback.data
3391
3509
  })], ValidateCallbackRequest.prototype, "data", void 0);
3510
+ var CallbackTypesChainRequest = class {};
3511
+ __decorate([ApiProperty({
3512
+ type: "number",
3513
+ example: callbackTypesRequestExample.callbacks[0].chain_id
3514
+ })], CallbackTypesChainRequest.prototype, "chain_id", void 0);
3515
+ __decorate([ApiProperty({
3516
+ type: () => [String],
3517
+ example: callbackTypesRequestExample.callbacks[0].addresses
3518
+ })], CallbackTypesChainRequest.prototype, "addresses", void 0);
3519
+ var CallbackTypesRequest = class {};
3520
+ __decorate([ApiProperty({
3521
+ type: () => [CallbackTypesChainRequest],
3522
+ example: callbackTypesRequestExample.callbacks
3523
+ })], CallbackTypesRequest.prototype, "callbacks", void 0);
3524
+ var CallbackTypesChainResponse = class {};
3525
+ __decorate([ApiProperty({
3526
+ type: "number",
3527
+ example: callbackTypesResponseExample[0].chain_id
3528
+ })], CallbackTypesChainResponse.prototype, "chain_id", void 0);
3529
+ __decorate([ApiProperty({
3530
+ type: () => [String],
3531
+ required: false,
3532
+ example: callbackTypesResponseExample[0].buy_vault_v1_callback
3533
+ })], CallbackTypesChainResponse.prototype, "buy_vault_v1_callback", void 0);
3534
+ __decorate([ApiProperty({
3535
+ type: () => [String],
3536
+ required: false,
3537
+ example: callbackTypesResponseExample[0].sell_erc20_callback
3538
+ })], CallbackTypesChainResponse.prototype, "sell_erc20_callback", void 0);
3539
+ __decorate([ApiProperty({
3540
+ type: () => [String],
3541
+ required: false,
3542
+ example: callbackTypesResponseExample[0].buy_erc20
3543
+ })], CallbackTypesChainResponse.prototype, "buy_erc20", void 0);
3544
+ __decorate([ApiProperty({
3545
+ type: () => [String],
3546
+ example: callbackTypesResponseExample[0].not_supported
3547
+ })], CallbackTypesChainResponse.prototype, "not_supported", void 0);
3548
+ var CallbackTypesSuccessResponse = class extends SuccessResponse {};
3549
+ __decorate([ApiProperty({
3550
+ type: "string",
3551
+ nullable: true,
3552
+ example: "maturity:1:1730415600:end_of_next_month"
3553
+ })], CallbackTypesSuccessResponse.prototype, "cursor", void 0);
3554
+ __decorate([ApiProperty({
3555
+ type: () => [CallbackTypesChainResponse],
3556
+ description: "Callback types grouped by chain.",
3557
+ example: callbackTypesResponseExample
3558
+ })], CallbackTypesSuccessResponse.prototype, "data", void 0);
3392
3559
  var AskResponse = class {};
3393
3560
  __decorate([ApiProperty({
3394
3561
  type: "string",
@@ -3553,7 +3720,8 @@ var OfferListResponse = class extends SuccessResponse {};
3553
3720
  __decorate([ApiProperty({
3554
3721
  type: "string",
3555
3722
  nullable: true,
3556
- example: offerCursorExample
3723
+ example: offerCursorExample,
3724
+ description: "Pagination cursor. Offer hash (0x...) for maker queries; base64url-encoded cursor for obligation queries."
3557
3725
  })], OfferListResponse.prototype, "cursor", void 0);
3558
3726
  __decorate([ApiProperty({
3559
3727
  type: () => [OfferListItemResponse],
@@ -3924,6 +4092,28 @@ ValidateController = __decorate([ApiTags("Make"), ApiResponse({
3924
4092
  description: "Bad Request",
3925
4093
  type: BadRequestResponse
3926
4094
  })], ValidateController);
4095
+ let CallbacksController = class CallbacksController {
4096
+ async resolveCallbackTypes() {}
4097
+ };
4098
+ __decorate([
4099
+ ApiOperation({
4100
+ methods: ["post"],
4101
+ path: "/v1/callbacks",
4102
+ summary: "Resolve callback types",
4103
+ description: "Returns callback types for callback addresses grouped by chain."
4104
+ }),
4105
+ ApiBody({ type: CallbackTypesRequest }),
4106
+ ApiResponse({
4107
+ status: 200,
4108
+ description: "Success",
4109
+ type: CallbackTypesSuccessResponse
4110
+ })
4111
+ ], CallbacksController.prototype, "resolveCallbackTypes", null);
4112
+ CallbacksController = __decorate([ApiTags("Make"), ApiResponse({
4113
+ status: 400,
4114
+ description: "Bad Request",
4115
+ type: BadRequestResponse
4116
+ })], CallbacksController);
3927
4117
  let OffersController = class OffersController {
3928
4118
  async getOffers() {}
3929
4119
  };
@@ -3995,104 +4185,277 @@ __decorate([
3995
4185
  name: "strict",
3996
4186
  type: "boolean",
3997
4187
  required: false,
3998
- example: true,
3999
- description: "Fail the request if initialization is incomplete."
4188
+ example: true,
4189
+ description: "Fail the request if initialization is incomplete."
4190
+ }),
4191
+ ApiResponse({
4192
+ status: 200,
4193
+ description: "Success",
4194
+ type: RouterStatusSuccessResponse
4195
+ })
4196
+ ], HealthController.prototype, "getRouterStatus", null);
4197
+ __decorate([
4198
+ ApiOperation({
4199
+ methods: ["get"],
4200
+ path: "/v1/health/collectors",
4201
+ summary: "Retrieve collectors health",
4202
+ description: "Returns the latest block numbers processed by collectors and their sync status."
4203
+ }),
4204
+ ApiQuery({
4205
+ name: "strict",
4206
+ type: "boolean",
4207
+ required: false,
4208
+ example: true,
4209
+ description: "Fail the request if initialization is incomplete."
4210
+ }),
4211
+ ApiResponse({
4212
+ status: 200,
4213
+ description: "Success",
4214
+ type: CollectorsHealthSuccessResponse
4215
+ })
4216
+ ], HealthController.prototype, "getCollectorsHealth", null);
4217
+ __decorate([
4218
+ ApiOperation({
4219
+ methods: ["get"],
4220
+ path: "/v1/health/chains",
4221
+ summary: "Retrieve chains health",
4222
+ description: "Returns the latest block that can be processed by collectors for each chain."
4223
+ }),
4224
+ ApiQuery({
4225
+ name: "strict",
4226
+ type: "boolean",
4227
+ required: false,
4228
+ example: true,
4229
+ description: "Fail the request if initialization is incomplete."
4230
+ }),
4231
+ ApiResponse({
4232
+ status: 200,
4233
+ description: "Success",
4234
+ type: ChainsHealthSuccessResponse
4235
+ })
4236
+ ], HealthController.prototype, "getChainsHealth", null);
4237
+ HealthController = __decorate([ApiTags("System")], HealthController);
4238
+ const configContractsExample = {
4239
+ chain_id: 505050505,
4240
+ address: "0xD946246695A9259F3B33a78629026F61B3Ab40aF",
4241
+ name: "mempool"
4242
+ };
4243
+ const configContractsPayloadExample = [
4244
+ {
4245
+ chain_id: 505050505,
4246
+ address: "0xD946246695A9259F3B33a78629026F61B3Ab40aF",
4247
+ name: "mempool"
4248
+ },
4249
+ {
4250
+ chain_id: 505050505,
4251
+ address: "0x8A409D5D6394fC197c596d4E6E2c35e5d13f8a4d",
4252
+ name: "multicall"
4253
+ },
4254
+ {
4255
+ chain_id: 505050505,
4256
+ address: "0x23DFBc4B8B80C14CC5e25011B8491f268395BAd6",
4257
+ name: "v2"
4258
+ }
4259
+ ];
4260
+ const configRulesMaturityExample = {
4261
+ type: "maturity",
4262
+ chain_id: 1,
4263
+ name: "end_of_next_month",
4264
+ timestamp: 1730415600
4265
+ };
4266
+ const configRulesCallbackExample = {
4267
+ type: "callback",
4268
+ chain_id: 1,
4269
+ address: "0x1111111111111111111111111111111111111111",
4270
+ callback_type: "sell_erc20_callback"
4271
+ };
4272
+ const configRulesLoanTokenExample = {
4273
+ type: "loan_token",
4274
+ chain_id: 1,
4275
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
4276
+ };
4277
+ const configRulesOracleExample = {
4278
+ type: "oracle",
4279
+ chain_id: 1,
4280
+ address: "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83"
4281
+ };
4282
+ const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
4283
+ const configRulesPayloadExample = [
4284
+ configRulesMaturityExample,
4285
+ configRulesCallbackExample,
4286
+ configRulesLoanTokenExample,
4287
+ configRulesOracleExample
4288
+ ];
4289
+ const configContractNames = [
4290
+ "mempool",
4291
+ "multicall",
4292
+ "v2"
4293
+ ];
4294
+ const configContractsCursorExample = "505050505:0xd946246695a9259f3b33a78629026f61b3ab40af";
4295
+ var ConfigContractResponse = class {};
4296
+ __decorate([ApiProperty({
4297
+ type: "number",
4298
+ example: configContractsExample.chain_id
4299
+ })], ConfigContractResponse.prototype, "chain_id", void 0);
4300
+ __decorate([ApiProperty({
4301
+ type: "string",
4302
+ example: configContractsExample.address
4303
+ })], ConfigContractResponse.prototype, "address", void 0);
4304
+ __decorate([ApiProperty({
4305
+ type: "string",
4306
+ enum: configContractNames,
4307
+ example: configContractsExample.name
4308
+ })], ConfigContractResponse.prototype, "name", void 0);
4309
+ var ConfigContractsSuccessResponse = class extends SuccessResponse {};
4310
+ __decorate([ApiProperty({
4311
+ type: "string",
4312
+ nullable: true,
4313
+ example: null
4314
+ })], ConfigContractsSuccessResponse.prototype, "cursor", void 0);
4315
+ __decorate([ApiProperty({
4316
+ type: () => [ConfigContractResponse],
4317
+ description: "Indexer contract configuration for all indexed chains.",
4318
+ example: configContractsPayloadExample
4319
+ })], ConfigContractsSuccessResponse.prototype, "data", void 0);
4320
+ var ConfigRulesMeta = class {};
4321
+ __decorate([ApiProperty({
4322
+ type: "string",
4323
+ example: timestampExample
4324
+ })], ConfigRulesMeta.prototype, "timestamp", void 0);
4325
+ __decorate([ApiProperty({
4326
+ type: "string",
4327
+ example: configRulesChecksumExample
4328
+ })], ConfigRulesMeta.prototype, "checksum", void 0);
4329
+ var ConfigRulesRuleResponse = class {};
4330
+ __decorate([ApiProperty({
4331
+ type: "string",
4332
+ example: configRulesMaturityExample.type
4333
+ })], ConfigRulesRuleResponse.prototype, "type", void 0);
4334
+ __decorate([ApiProperty({
4335
+ type: "number",
4336
+ example: configRulesMaturityExample.chain_id
4337
+ })], ConfigRulesRuleResponse.prototype, "chain_id", void 0);
4338
+ __decorate([ApiProperty({
4339
+ type: "string",
4340
+ example: configRulesMaturityExample.name,
4341
+ required: false
4342
+ })], ConfigRulesRuleResponse.prototype, "name", void 0);
4343
+ __decorate([ApiProperty({
4344
+ type: "number",
4345
+ example: configRulesMaturityExample.timestamp,
4346
+ required: false
4347
+ })], ConfigRulesRuleResponse.prototype, "timestamp", void 0);
4348
+ __decorate([ApiProperty({
4349
+ type: "string",
4350
+ example: configRulesCallbackExample.address,
4351
+ required: false
4352
+ })], ConfigRulesRuleResponse.prototype, "address", void 0);
4353
+ __decorate([ApiProperty({
4354
+ type: "string",
4355
+ example: configRulesCallbackExample.callback_type,
4356
+ required: false
4357
+ })], ConfigRulesRuleResponse.prototype, "callback_type", void 0);
4358
+ var ConfigRulesSuccessResponse = class {};
4359
+ __decorate([ApiProperty({ type: () => ConfigRulesMeta })], ConfigRulesSuccessResponse.prototype, "meta", void 0);
4360
+ __decorate([ApiProperty({
4361
+ type: "string",
4362
+ nullable: true,
4363
+ example: null
4364
+ })], ConfigRulesSuccessResponse.prototype, "cursor", void 0);
4365
+ __decorate([ApiProperty({
4366
+ type: () => [ConfigRulesRuleResponse],
4367
+ description: "Configured rules returned by the router API.",
4368
+ example: configRulesPayloadExample
4369
+ })], ConfigRulesSuccessResponse.prototype, "data", void 0);
4370
+ let ConfigContractsController = class ConfigContractsController {
4371
+ async getConfigContracts() {}
4372
+ };
4373
+ __decorate([
4374
+ ApiOperation({
4375
+ methods: ["get"],
4376
+ path: "/v1/config/contracts",
4377
+ summary: "Get indexer contract configuration",
4378
+ description: "Returns contract addresses used by indexers (mempool, v2) and multicall for indexed chains."
4379
+ }),
4380
+ ApiQuery({
4381
+ name: "cursor",
4382
+ type: "string",
4383
+ required: false,
4384
+ example: configContractsCursorExample,
4385
+ description: "Pagination cursor in chain_id:address format (lowercase address)."
4386
+ }),
4387
+ ApiQuery({
4388
+ name: "limit",
4389
+ type: "number",
4390
+ required: false,
4391
+ example: 1e3,
4392
+ description: "Maximum number of contracts to return (max 1000)."
4393
+ }),
4394
+ ApiQuery({
4395
+ name: "chains",
4396
+ type: ["number"],
4397
+ required: false,
4398
+ example: "1,8453",
4399
+ description: "Filter by chain IDs (comma-separated).",
4400
+ style: "form",
4401
+ explode: false
4000
4402
  }),
4001
4403
  ApiResponse({
4002
4404
  status: 200,
4003
4405
  description: "Success",
4004
- type: RouterStatusSuccessResponse
4406
+ type: ConfigContractsSuccessResponse
4005
4407
  })
4006
- ], HealthController.prototype, "getRouterStatus", null);
4408
+ ], ConfigContractsController.prototype, "getConfigContracts", null);
4409
+ ConfigContractsController = __decorate([ApiTags("System")], ConfigContractsController);
4410
+ let ConfigRulesController = class ConfigRulesController {
4411
+ async getConfigRules() {}
4412
+ };
4007
4413
  __decorate([
4008
4414
  ApiOperation({
4009
4415
  methods: ["get"],
4010
- path: "/v1/health/collectors",
4011
- summary: "Retrieve collectors health",
4012
- description: "Returns the latest block numbers processed by collectors and their sync status."
4416
+ path: "/v1/config/rules",
4417
+ summary: "Get config rules",
4418
+ description: "Returns configured rules for supported chains."
4013
4419
  }),
4014
4420
  ApiQuery({
4015
- name: "strict",
4016
- type: "boolean",
4421
+ name: "cursor",
4422
+ type: "string",
4017
4423
  required: false,
4018
- example: true,
4019
- description: "Fail the request if initialization is incomplete."
4424
+ example: "maturity:1:1730415600:end_of_next_month",
4425
+ description: "Pagination cursor in type:chain_id:<value> format."
4020
4426
  }),
4021
- ApiResponse({
4022
- status: 200,
4023
- description: "Success",
4024
- type: CollectorsHealthSuccessResponse
4025
- })
4026
- ], HealthController.prototype, "getCollectorsHealth", null);
4027
- __decorate([
4028
- ApiOperation({
4029
- methods: ["get"],
4030
- path: "/v1/health/chains",
4031
- summary: "Retrieve chains health",
4032
- description: "Returns the latest block that can be processed by collectors for each chain."
4427
+ ApiQuery({
4428
+ name: "limit",
4429
+ type: "number",
4430
+ required: false,
4431
+ example: 100,
4432
+ description: "Maximum number of rules to return (max 1000)."
4033
4433
  }),
4034
4434
  ApiQuery({
4035
- name: "strict",
4036
- type: "boolean",
4435
+ name: "types",
4436
+ type: ["string"],
4037
4437
  required: false,
4038
- example: true,
4039
- description: "Fail the request if initialization is incomplete."
4438
+ example: "maturity,loan_token,oracle",
4439
+ description: "Filter by rule types (comma-separated).",
4440
+ style: "form",
4441
+ explode: false
4442
+ }),
4443
+ ApiQuery({
4444
+ name: "chains",
4445
+ type: ["number"],
4446
+ required: false,
4447
+ example: "1,8453",
4448
+ description: "Filter by chain IDs (comma-separated).",
4449
+ style: "form",
4450
+ explode: false
4040
4451
  }),
4041
4452
  ApiResponse({
4042
4453
  status: 200,
4043
4454
  description: "Success",
4044
- type: ChainsHealthSuccessResponse
4455
+ type: ConfigRulesSuccessResponse
4045
4456
  })
4046
- ], HealthController.prototype, "getChainsHealth", null);
4047
- HealthController = __decorate([ApiTags("System")], HealthController);
4048
- const callbacksExample = [Type$1.BuyWithEmptyCallback];
4049
- const chainConfigExample = {
4050
- chain_id: 505050505,
4051
- contracts: { mempool: "0xD946246695A9259F3B33a78629026F61B3Ab40aF" },
4052
- callbacks: callbacksExample
4053
- };
4054
- var ConfigContractsResponse = class {};
4055
- __decorate([ApiProperty({
4056
- type: "string",
4057
- example: chainConfigExample.contracts.mempool
4058
- })], ConfigContractsResponse.prototype, "mempool", void 0);
4059
- var ConfigDataResponse = class {};
4060
- __decorate([ApiProperty({
4061
- type: "number",
4062
- example: chainConfigExample.chain_id
4063
- })], ConfigDataResponse.prototype, "chain_id", void 0);
4064
- __decorate([ApiProperty({ type: () => ConfigContractsResponse })], ConfigDataResponse.prototype, "contracts", void 0);
4065
- __decorate([ApiProperty({
4066
- type: () => [String],
4067
- enum: Object.values(Type$1),
4068
- description: "Supported callback types for this chain.",
4069
- example: callbacksExample
4070
- })], ConfigDataResponse.prototype, "callbacks", void 0);
4071
- var ConfigSuccessResponse = class extends SuccessResponse {};
4072
- __decorate([ApiProperty({
4073
- type: "string",
4074
- nullable: true,
4075
- example: null
4076
- })], ConfigSuccessResponse.prototype, "cursor", void 0);
4077
- __decorate([ApiProperty({
4078
- type: () => [ConfigDataResponse],
4079
- description: "Array of chain configurations for all indexed chains.",
4080
- example: [chainConfigExample]
4081
- })], ConfigSuccessResponse.prototype, "data", void 0);
4082
- let ConfigController = class ConfigController {
4083
- async getConfig() {}
4084
- };
4085
- __decorate([ApiOperation({
4086
- methods: ["get"],
4087
- path: "/v1/config",
4088
- summary: "Get router configuration",
4089
- description: "Returns chain configurations including contract addresses and supported callback types."
4090
- }), ApiResponse({
4091
- status: 200,
4092
- description: "Success",
4093
- type: ConfigSuccessResponse
4094
- })], ConfigController.prototype, "getConfig", null);
4095
- ConfigController = __decorate([ApiTags("System")], ConfigController);
4457
+ ], ConfigRulesController.prototype, "getConfigRules", null);
4458
+ ConfigRulesController = __decorate([ApiTags("System")], ConfigRulesController);
4096
4459
  let ObligationsController = class ObligationsController {
4097
4460
  async getObligations() {}
4098
4461
  async getObligation() {}
@@ -4221,16 +4584,18 @@ UsersController = __decorate([ApiTags("Make"), ApiResponse({
4221
4584
  description: "Bad Request",
4222
4585
  type: BadRequestResponse
4223
4586
  })], UsersController);
4224
- const OpenApi = async (options = {}) => {
4225
- const document = await generateDocument({
4587
+ const OpenApi = async () => {
4588
+ return await generateDocument({
4226
4589
  controllers: [
4227
4590
  BooksController,
4228
- ConfigController,
4591
+ ConfigContractsController,
4592
+ ConfigRulesController,
4229
4593
  OffersController,
4230
4594
  ObligationsController,
4231
4595
  HealthController,
4232
4596
  UsersController,
4233
- ValidateController
4597
+ ValidateController,
4598
+ CallbacksController
4234
4599
  ],
4235
4600
  document: {
4236
4601
  openapi: "3.1.0",
@@ -4262,12 +4627,6 @@ const OpenApi = async (options = {}) => {
4262
4627
  ]
4263
4628
  }
4264
4629
  });
4265
- if (options.rules && options.rules.length > 0) {
4266
- const rulesDescription = options.rules.map((rule) => `- **${rule.name}**: ${rule.description}`).join("\n");
4267
- const validatePath = document.paths?.["/v1/validate"];
4268
- 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}`;
4269
- }
4270
- return document;
4271
4630
  };
4272
4631
 
4273
4632
  //#endregion
@@ -4291,6 +4650,10 @@ function from$3(position) {
4291
4650
  //#region src/api/Schema/requests.ts
4292
4651
  const MAX_LIMIT = 100;
4293
4652
  const DEFAULT_LIMIT$4 = 20;
4653
+ const CONFIG_RULES_MAX_LIMIT = 1e3;
4654
+ const CONFIG_RULES_DEFAULT_LIMIT = 100;
4655
+ const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
4656
+ const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
4294
4657
  /** Validate cursor is a valid base64url-encoded JSON object.
4295
4658
  * Domain layer handles semantic validation of cursor fields. */
4296
4659
  function isValidBase64urlJson(val) {
@@ -4302,6 +4665,9 @@ function isValidBase64urlJson(val) {
4302
4665
  return false;
4303
4666
  }
4304
4667
  }
4668
+ function isValidOfferHashCursor(val) {
4669
+ return /^0x[a-f0-9]{64}$/i.test(val);
4670
+ }
4305
4671
  const csvArray = (schema) => z$2.preprocess((value) => {
4306
4672
  if (value === void 0) return void 0;
4307
4673
  if (Array.isArray(value)) {
@@ -4324,8 +4690,49 @@ const PaginationQueryParams = z$2.object({
4324
4690
  example: 10
4325
4691
  })
4326
4692
  });
4327
- const GetOffersQueryParams = z$2.object({
4328
- ...PaginationQueryParams.shape,
4693
+ const ConfigRuleTypes = z$2.enum([
4694
+ "maturity",
4695
+ "callback",
4696
+ "loan_token",
4697
+ "oracle"
4698
+ ]);
4699
+ const GetConfigRulesQueryParams = z$2.object({
4700
+ cursor: z$2.string().regex(/^(maturity|callback|loan_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
4701
+ description: "Pagination cursor in type:chain_id:<value> format",
4702
+ example: "maturity:1:1730415600:end_of_next_month"
4703
+ }),
4704
+ limit: z$2.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$2.number().max(CONFIG_RULES_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_RULES_MAX_LIMIT}` })).optional().default(CONFIG_RULES_DEFAULT_LIMIT).meta({
4705
+ description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
4706
+ example: 100
4707
+ }),
4708
+ types: csvArray(ConfigRuleTypes).meta({
4709
+ description: "Filter by rule types (comma-separated).",
4710
+ example: "maturity,loan_token,oracle"
4711
+ }),
4712
+ chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4713
+ description: "Filter by chain IDs (comma-separated).",
4714
+ example: "1,8453"
4715
+ })
4716
+ });
4717
+ const GetConfigContractsQueryParams = z$2.object({
4718
+ cursor: z$2.string().regex(/^[1-9]\d*:0x[a-fA-F0-9]{40}$/, { message: "Cursor must be in the format chain_id:0x..." }).optional().meta({
4719
+ description: "Pagination cursor in chain_id:address format (lowercase address).",
4720
+ example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
4721
+ }),
4722
+ limit: z$2.string().regex(/^[1-9]\d*$/, { message: "Limit must be a positive integer" }).transform((val) => Number.parseInt(val, 10)).pipe(z$2.number().max(CONFIG_CONTRACTS_MAX_LIMIT, { message: `Limit cannot exceed ${CONFIG_CONTRACTS_MAX_LIMIT}` })).optional().default(CONFIG_CONTRACTS_DEFAULT_LIMIT).meta({
4723
+ description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
4724
+ example: 1e3
4725
+ }),
4726
+ chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4727
+ description: "Filter by chain IDs (comma-separated).",
4728
+ example: "1,8453"
4729
+ })
4730
+ });
4731
+ const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend({
4732
+ cursor: z$2.string().optional().meta({
4733
+ description: "Pagination cursor. Use offer hash (0x...) for maker queries, base64url for obligation queries.",
4734
+ example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
4735
+ }),
4329
4736
  side: z$2.enum(["buy", "sell"]).optional().meta({
4330
4737
  description: "Side of the offer. Required when using obligation_id.",
4331
4738
  example: "buy"
@@ -4349,11 +4756,29 @@ const GetOffersQueryParams = z$2.object({
4349
4756
  });
4350
4757
  return;
4351
4758
  }
4352
- if (hasMaker) return;
4759
+ if (hasMaker) {
4760
+ if (val.cursor !== void 0 && !isValidOfferHashCursor(val.cursor)) ctx.addIssue({
4761
+ code: "custom",
4762
+ path: ["cursor"],
4763
+ message: "Cursor must be a 32-byte hex offer hash when filtering by maker"
4764
+ });
4765
+ return;
4766
+ }
4353
4767
  if (!hasObligation || !hasSide) ctx.addIssue({
4354
4768
  code: "custom",
4355
4769
  message: "Must provide either maker or both obligation_id and side"
4356
4770
  });
4771
+ if (val.cursor !== void 0 && !isValidBase64urlJson(val.cursor)) ctx.addIssue({
4772
+ code: "custom",
4773
+ path: ["cursor"],
4774
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
4775
+ });
4776
+ }).transform((val) => {
4777
+ if (val.maker && val.cursor) return {
4778
+ ...val,
4779
+ cursor: val.cursor.toLowerCase()
4780
+ };
4781
+ return val;
4357
4782
  });
4358
4783
  const GetObligationsQueryParams = z$2.object({
4359
4784
  ...PaginationQueryParams.shape,
@@ -4426,6 +4851,16 @@ const GetBookParams = z$2.object({
4426
4851
  })
4427
4852
  });
4428
4853
  const ValidateOffersBody = z$2.object({ offers: z$2.array(z$2.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
4854
+ const CallbackTypesBody = z$2.object({ callbacks: z$2.array(z$2.object({
4855
+ chain_id: z$2.number().int().positive().meta({
4856
+ description: "Chain id.",
4857
+ example: 1
4858
+ }),
4859
+ addresses: z$2.array(z$2.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "Callback address must be a valid 20-byte address" }).transform((val) => val.toLowerCase())).meta({
4860
+ description: "Callback contract addresses.",
4861
+ example: ["0x1111111111111111111111111111111111111111", "0x3333333333333333333333333333333333333333"]
4862
+ })
4863
+ }).strict()) }).strict();
4429
4864
  const GetUserPositionsParams = z$2.object({
4430
4865
  ...PaginationQueryParams.shape,
4431
4866
  user_address: z$2.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
@@ -4437,17 +4872,208 @@ const schemas = {
4437
4872
  get_health: HealthQueryParams,
4438
4873
  get_health_collectors: HealthQueryParams,
4439
4874
  get_health_chains: HealthQueryParams,
4875
+ get_config_contracts: GetConfigContractsQueryParams,
4876
+ get_config_rules: GetConfigRulesQueryParams,
4440
4877
  get_offers: GetOffersQueryParams,
4441
4878
  get_obligations: GetObligationsQueryParams,
4442
4879
  get_obligation: GetObligationParams,
4443
4880
  get_book: GetBookParams,
4444
4881
  validate_offers: ValidateOffersBody,
4882
+ callback_types: CallbackTypesBody,
4445
4883
  get_user_positions: GetUserPositionsParams
4446
4884
  };
4447
4885
  function safeParse(action, query, error) {
4448
4886
  return schemas[action].safeParse(query, { error });
4449
4887
  }
4450
4888
 
4889
+ //#endregion
4890
+ //#region src/api/Controllers/getConfigRules.ts
4891
+ /**
4892
+ * Returns configured rules for the configured chains.
4893
+ * @param query - Raw query parameters containing filters/cursor/limit.
4894
+ * @param chains - Chains to include in the configured rules.
4895
+ * @returns Config rules response payload. {@link ApiPayload.Payload}
4896
+ */
4897
+ async function getConfigRules(query, chains) {
4898
+ const parsed = safeParse("get_config_rules", query ?? {});
4899
+ if (!parsed.success) return failure(parsed.error);
4900
+ const { cursor, limit, types, chains: chainIds } = parsed.data;
4901
+ const typeFilter = types?.length ? new Set(types) : null;
4902
+ const chainFilter = chainIds?.length ? new Set(chainIds) : null;
4903
+ const filteredRules = buildConfigRules(chains).filter((rule) => {
4904
+ if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
4905
+ if (typeFilter && !typeFilter.has(rule.type)) return false;
4906
+ return true;
4907
+ });
4908
+ const checksum = buildConfigRulesChecksum(filteredRules);
4909
+ let cursorRule = null;
4910
+ if (cursor) try {
4911
+ cursorRule = parseCursor$1(cursor);
4912
+ } catch (err) {
4913
+ return failure(err);
4914
+ }
4915
+ if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
4916
+ if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
4917
+ const startIndex = cursorRule ? findStartIndex$1(filteredRules, cursorRule) : 0;
4918
+ const page = filteredRules.slice(startIndex, startIndex + limit);
4919
+ const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
4920
+ const response = success({
4921
+ data: page,
4922
+ cursor: nextCursor
4923
+ });
4924
+ response.body.meta.checksum = checksum;
4925
+ return response;
4926
+ }
4927
+ function formatCursor$1(rule) {
4928
+ if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
4929
+ if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
4930
+ if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
4931
+ return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
4932
+ }
4933
+ function parseCursor$1(cursor) {
4934
+ const [type, chain, ...rest] = cursor.split(":");
4935
+ if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
4936
+ if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
4937
+ const chain_id = Number.parseInt(chain, 10);
4938
+ if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
4939
+ if (type === "maturity") {
4940
+ const timestampValue = Number.parseInt(rest[0] ?? "", 10);
4941
+ const nameValue = rest.slice(1).join(":");
4942
+ if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
4943
+ if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
4944
+ return {
4945
+ type,
4946
+ chain_id,
4947
+ timestamp: parseMaturity(timestampValue),
4948
+ name: nameValue
4949
+ };
4950
+ }
4951
+ if (type === "callback") {
4952
+ const callbackTypeValue = rest[0] ?? "";
4953
+ const addressValue = rest.slice(1).join(":");
4954
+ if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
4955
+ if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
4956
+ return {
4957
+ type,
4958
+ chain_id,
4959
+ callback_type: callbackTypeValue,
4960
+ address: parseAddress(addressValue, "Cursor address")
4961
+ };
4962
+ }
4963
+ if (type === "loan_token" || type === "oracle") {
4964
+ const addressValue = rest.join(":");
4965
+ if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
4966
+ return {
4967
+ type,
4968
+ chain_id,
4969
+ address: parseAddress(addressValue, "Cursor address")
4970
+ };
4971
+ }
4972
+ throw new BadRequestError("Cursor has an invalid rule type");
4973
+ }
4974
+ function findStartIndex$1(rules, cursor) {
4975
+ let low = 0;
4976
+ let high = rules.length;
4977
+ while (low < high) {
4978
+ const mid = Math.floor((low + high) / 2);
4979
+ const current = rules[mid];
4980
+ if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
4981
+ else high = mid;
4982
+ }
4983
+ return low;
4984
+ }
4985
+ function parseAddress(address, label) {
4986
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
4987
+ return address.toLowerCase();
4988
+ }
4989
+ function isConfigRuleType(value) {
4990
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
4991
+ }
4992
+ function isMaturityType(value) {
4993
+ return Object.values(MaturityType).includes(value);
4994
+ }
4995
+ function parseMaturity(value) {
4996
+ try {
4997
+ return from$16(value);
4998
+ } catch (err) {
4999
+ throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
5000
+ }
5001
+ }
5002
+ function isCallbackType(value) {
5003
+ if (value === Type$1.BuyWithEmptyCallback) return false;
5004
+ return Object.values(Type$1).includes(value);
5005
+ }
5006
+
5007
+ //#endregion
5008
+ //#region src/logger/Logger.ts
5009
+ const LogLevelValues = [
5010
+ "trace",
5011
+ "debug",
5012
+ "info",
5013
+ "warn",
5014
+ "error",
5015
+ "fatal",
5016
+ "silent"
5017
+ ];
5018
+ function defaultLogger(minLevel, pretty) {
5019
+ const threshold = minLevel ?? process.env.ROUTER_LOG_LEVEL ?? "info";
5020
+ const prettyEnabled = typeof pretty === "boolean" ? pretty : String(process.env.ROUTER_LOG_PRETTY ?? "false").toLowerCase() === "true";
5021
+ const levelIndexByName = LogLevelValues.reduce((acc, lvl, idx) => {
5022
+ acc[lvl] = idx;
5023
+ return acc;
5024
+ }, {});
5025
+ const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
5026
+ const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
5027
+ if (!prettyEnabled) {
5028
+ console[consoleMethod](stringify({
5029
+ level: methodLevel,
5030
+ ...entry
5031
+ }));
5032
+ return;
5033
+ }
5034
+ const { msg, ...rest } = entry;
5035
+ const stack = typeof rest.stack === "string" ? rest.stack : void 0;
5036
+ if (stack) delete rest.stack;
5037
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5038
+ const level = methodLevel.toUpperCase();
5039
+ const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
5040
+ const line = extras.length > 0 ? `${timestamp} [${level}] ${msg} ${extras}` : `${timestamp} [${level}] ${msg}`;
5041
+ console[consoleMethod](line);
5042
+ if (stack) console[consoleMethod](stack);
5043
+ } : () => {};
5044
+ return {
5045
+ trace: wrap("trace", "trace"),
5046
+ debug: wrap("debug", "debug"),
5047
+ info: wrap("info", "info"),
5048
+ warn: wrap("warn", "warn"),
5049
+ error: wrap("error", "error"),
5050
+ fatal: wrap("error", "fatal")
5051
+ };
5052
+ }
5053
+ const loggerContext = new AsyncLocalStorage();
5054
+ function runWithLogger(logger, fn) {
5055
+ return loggerContext.run(logger, fn);
5056
+ }
5057
+ function getLogger() {
5058
+ return loggerContext.getStore() ?? defaultLogger();
5059
+ }
5060
+ function formatValue(value) {
5061
+ if (value === null || value === void 0 || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
5062
+ if (typeof value === "string") {
5063
+ if (value.includes(" ")) return JSON.stringify(value);
5064
+ return value;
5065
+ }
5066
+ try {
5067
+ return stringify(value);
5068
+ } catch {
5069
+ try {
5070
+ return JSON.stringify(value);
5071
+ } catch {
5072
+ return String(value);
5073
+ }
5074
+ }
5075
+ }
5076
+
4451
5077
  //#endregion
4452
5078
  //#region src/api/Controllers/validateOffers.ts
4453
5079
  async function validateOffers(body, gatekeeper) {
@@ -4461,9 +5087,9 @@ async function validateOffers(body, gatekeeper) {
4461
5087
  const rawOffer = rawOffers[i];
4462
5088
  try {
4463
5089
  const offer = fromSnakeCase(rawOffer);
4464
- const hash$2 = hash(offer);
4465
- if (!offerIndexByHash.has(hash$2)) {
4466
- offerIndexByHash.set(hash$2, i);
5090
+ const hash$3 = hash(offer);
5091
+ if (!offerIndexByHash.has(hash$3)) {
5092
+ offerIndexByHash.set(hash$3, i);
4467
5093
  parsedOffers.push(offer);
4468
5094
  }
4469
5095
  } catch (err) {
@@ -4489,7 +5115,7 @@ async function validateOffers(body, gatekeeper) {
4489
5115
  cursor: null
4490
5116
  });
4491
5117
  }
4492
- const tree = from$12(parsedOffers);
5118
+ const tree = from$7(parsedOffers);
4493
5119
  const payload = encodeUnsigned(tree);
4494
5120
  return success({
4495
5121
  data: {
@@ -4509,6 +5135,35 @@ async function validateOffers(body, gatekeeper) {
4509
5135
  }
4510
5136
  }
4511
5137
 
5138
+ //#endregion
5139
+ //#region src/gatekeeper/CallbackTypes.ts
5140
+ /**
5141
+ * Resolve callback types for a list of callback addresses grouped by chain.
5142
+ * @param parameters - Resolve parameters. {@link resolveCallbackTypes.Parameters}
5143
+ * @returns Callback types grouped by chain. {@link resolveCallbackTypes.ReturnType}
5144
+ * @throws If a chain id is unknown.
5145
+ */
5146
+ function resolveCallbackTypes$2(parameters) {
5147
+ const { chains, request } = parameters;
5148
+ const chainsById = new Map(chains.map((chain) => [chain.id, chain]));
5149
+ return request.callbacks.map(({ chain_id, addresses }) => {
5150
+ const chain = chainsById.get(chain_id);
5151
+ if (!chain) throw new Error(`Unknown chain id ${chain_id}`);
5152
+ const buckets = /* @__PURE__ */ new Map();
5153
+ const uniqueAddresses = new Set(addresses.map((address) => address.toLowerCase()));
5154
+ for (const address of uniqueAddresses) {
5155
+ const bucketKey = getCallbackType(chain.name, address) ?? "not_supported";
5156
+ const list = buckets.get(bucketKey) ?? [];
5157
+ list.push(address);
5158
+ buckets.set(bucketKey, list);
5159
+ }
5160
+ const response = { chain_id };
5161
+ for (const [type, list] of buckets.entries()) response[type] = list;
5162
+ if (!response.not_supported) response.not_supported = [];
5163
+ return response;
5164
+ });
5165
+ }
5166
+
4512
5167
  //#endregion
4513
5168
  //#region src/gatekeeper/Service.ts
4514
5169
  /**
@@ -4516,28 +5171,59 @@ async function validateOffers(body, gatekeeper) {
4516
5171
  * @param parameters - App parameters including the {@link Gatekeeper} instance.
4517
5172
  * @returns Hono app exposing gatekeeper endpoints.
4518
5173
  */
5174
+ const CallbackTypesRequestSchema = z$2.object({ callbacks: z$2.array(z$2.object({
5175
+ chain_id: z$2.number(),
5176
+ addresses: z$2.array(z$2.string().regex(/^0x[a-fA-F0-9]{40}$/))
5177
+ })) });
4519
5178
  function createApp(parameters) {
4520
- const { gatekeeper } = parameters;
5179
+ const { gatekeeper, chainRegistry } = parameters;
4521
5180
  const app = new Hono();
4522
5181
  app.post("/v1/validate", async (c) => {
4523
5182
  let body;
4524
5183
  try {
4525
5184
  body = await c.req.json();
4526
5185
  } catch (err) {
4527
- const failure$3 = failure(err);
4528
- return c.json(failure$3.body, failure$3.statusCode);
5186
+ const failure$5 = failure(err);
5187
+ return c.json(failure$5.body, failure$5.statusCode);
4529
5188
  }
4530
5189
  if (body === null || typeof body !== "object") {
4531
- const failure$2 = failure(new BadRequestError("Request body must be a JSON object"));
4532
- return c.json(failure$2.body, failure$2.statusCode);
5190
+ const failure$9 = failure(new BadRequestError("Request body must be a JSON object"));
5191
+ return c.json(failure$9.body, failure$9.statusCode);
4533
5192
  }
4534
5193
  const { statusCode, body: payload } = await validateOffers(body, gatekeeper);
4535
5194
  return c.json(payload, statusCode);
4536
5195
  });
4537
- app.get("/v1/rules", async (c) => {
4538
- const rules = await gatekeeper.getRules();
4539
- const response = success({ data: rules });
4540
- return c.json(response.body, response.statusCode);
5196
+ app.get("/v1/config/rules", async (c) => {
5197
+ const { statusCode, body } = await getConfigRules(c.req.query(), chainRegistry.list());
5198
+ return c.json(body, statusCode);
5199
+ });
5200
+ app.post("/v1/callbacks", async (c) => {
5201
+ let body;
5202
+ try {
5203
+ body = await c.req.json();
5204
+ } catch (err) {
5205
+ const failure$8 = failure(err);
5206
+ return c.json(failure$8.body, failure$8.statusCode);
5207
+ }
5208
+ if (body === null || typeof body !== "object") {
5209
+ const failure$6 = failure(new BadRequestError("Request body must be a JSON object"));
5210
+ return c.json(failure$6.body, failure$6.statusCode);
5211
+ }
5212
+ try {
5213
+ const request = CallbackTypesRequestSchema.parse(body);
5214
+ const chainIds = new Set(chainRegistry.list().map((chain) => chain.id));
5215
+ const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
5216
+ if (unknown) throw new BadRequestError(`Unknown chain id ${unknown.chain_id}`);
5217
+ const data = resolveCallbackTypes$2({
5218
+ chains: chainRegistry.list(),
5219
+ request
5220
+ });
5221
+ const response = success({ data });
5222
+ return c.json(response.body, response.statusCode);
5223
+ } catch (err) {
5224
+ const failure$7 = failure(err);
5225
+ return c.json(failure$7.body, failure$7.statusCode);
5226
+ }
4541
5227
  });
4542
5228
  return app;
4543
5229
  }
@@ -4547,8 +5233,11 @@ function createApp(parameters) {
4547
5233
  * @returns Service handle including base URL and shutdown method. {@link ServiceHandle}
4548
5234
  */
4549
5235
  async function start$1(config) {
4550
- const { gatekeeper, port, hostname } = config;
4551
- const app = createApp({ gatekeeper });
5236
+ const { gatekeeper, chainRegistry, port, hostname } = config;
5237
+ const app = createApp({
5238
+ gatekeeper,
5239
+ chainRegistry
5240
+ });
4552
5241
  let address = null;
4553
5242
  let server;
4554
5243
  await new Promise((resolve) => {
@@ -4748,6 +5437,18 @@ async function* collectOffersV2(parameters) {
4748
5437
  const logger = getLogger();
4749
5438
  let startBlock = blockNumber;
4750
5439
  let reorgDetected = false;
5440
+ if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
5441
+ const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
5442
+ logger.error({
5443
+ msg,
5444
+ chain_id: client.chain.id
5445
+ });
5446
+ throw new Error(msg);
5447
+ }
5448
+ const signatureDomain = {
5449
+ chainId: client.chain.id,
5450
+ verifyingContract: client.chain.custom.morpho.address
5451
+ };
4751
5452
  const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
4752
5453
  const stream = streamLogs({
4753
5454
  client,
@@ -4778,7 +5479,7 @@ async function* collectOffersV2(parameters) {
4778
5479
  if (!log) continue;
4779
5480
  const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
4780
5481
  try {
4781
- const { tree, signature, signer } = await decode(payload);
5482
+ const { tree, signature, signer } = await decode(payload, signatureDomain);
4782
5483
  const signerMismatch = tree.offers.find((offer) => offer.maker.toLowerCase() !== signer.toLowerCase());
4783
5484
  if (signerMismatch) {
4784
5485
  logger.debug({
@@ -4810,6 +5511,7 @@ async function* collectOffersV2(parameters) {
4810
5511
  const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
4811
5512
  const treesToInsert = [];
4812
5513
  let totalValidOffers = 0;
5514
+ const offersWithBlock = [];
4813
5515
  for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
4814
5516
  const allowedResults = await gatekeeper.isAllowed(tree.offers);
4815
5517
  const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
@@ -4831,10 +5533,13 @@ async function* collectOffersV2(parameters) {
4831
5533
  }
4832
5534
  treesToInsert.push({
4833
5535
  tree,
4834
- signature,
4835
- blockNumber: treeBlockNumber
5536
+ signature
4836
5537
  });
4837
5538
  totalValidOffers += tree.offers.length;
5539
+ offersWithBlock.push(...tree.offers.map((offer) => ({
5540
+ offer,
5541
+ blockNumber: treeBlockNumber
5542
+ })));
4838
5543
  } catch (err) {
4839
5544
  const error = err instanceof Error ? err : new Error(String(err));
4840
5545
  logger.error({
@@ -4844,7 +5549,24 @@ async function* collectOffersV2(parameters) {
4844
5549
  });
4845
5550
  throw new Error("Gatekeeper validation failed", { cause: error });
4846
5551
  }
5552
+ const dependencies = buildOfferDependencies$1(offersWithBlock);
5553
+ await dbTx.oracles.upsert(dependencies.oracles);
5554
+ await dbTx.obligations.create(dependencies.obligations);
5555
+ await dbTx.groups.create(dependencies.groups);
5556
+ const insertedHashes = await dbTx.offers.create(dependencies.offerBatches);
4847
5557
  if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
5558
+ const insertedOffers = filterInsertedOffers({
5559
+ offers: offersWithBlock,
5560
+ hashes: insertedHashes
5561
+ });
5562
+ const { callbacks, positions, lots } = await decodeCallbacks({
5563
+ chainId: client.chain.id,
5564
+ gatekeeper,
5565
+ offers: insertedOffers
5566
+ });
5567
+ if (positions.length > 0) await dbTx.positions.upsert(positions);
5568
+ if (callbacks.length > 0) await dbTx.callbacks.upsert(callbacks);
5569
+ if (lots.length > 0) await dbTx.lots.create(lots);
4848
5570
  try {
4849
5571
  await dbTx.blocks.advanceCollector({
4850
5572
  collectorName: collector,
@@ -4897,10 +5619,151 @@ async function* collectOffersV2(parameters) {
4897
5619
  }
4898
5620
  }
4899
5621
  });
4900
- if (reorgDetected) return;
4901
- yield blockNumber;
4902
- startBlock = blockNumber;
5622
+ if (reorgDetected) return;
5623
+ yield blockNumber;
5624
+ startBlock = blockNumber;
5625
+ }
5626
+ }
5627
+ async function decodeCallbacks(parameters) {
5628
+ const { chainId, gatekeeper, offers } = parameters;
5629
+ if (offers.length === 0) return {
5630
+ callbacks: [],
5631
+ positions: [],
5632
+ lots: []
5633
+ };
5634
+ const addresses = offers.filter((entry) => entry.offer.callback.data !== "0x").map((entry) => entry.offer.callback.address);
5635
+ if (addresses.length === 0) return {
5636
+ callbacks: [],
5637
+ positions: [],
5638
+ lots: []
5639
+ };
5640
+ let response;
5641
+ try {
5642
+ response = await gatekeeper.getCallbackTypes({ callbacks: [{
5643
+ chain_id: chainId,
5644
+ addresses
5645
+ }] });
5646
+ } catch (err) {
5647
+ const error = err instanceof Error ? err : new Error(String(err));
5648
+ throw new Error("Failed to resolve callback types", { cause: error });
5649
+ }
5650
+ const entry = response.find((item) => item.chain_id === chainId);
5651
+ const typeByAddress = /* @__PURE__ */ new Map();
5652
+ if (entry) for (const [key, list] of Object.entries(entry)) {
5653
+ if (key === "chain_id" || key === "not_supported") continue;
5654
+ if (!Array.isArray(list)) continue;
5655
+ for (const address of list) typeByAddress.set(address.toLowerCase(), key);
5656
+ }
5657
+ const callbacks = [];
5658
+ const positions = [];
5659
+ const lots = [];
5660
+ for (const { offer, blockNumber: offerBlockNumber } of offers) {
5661
+ if (offer.callback.data === "0x") continue;
5662
+ const callbackType = typeByAddress.get(offer.callback.address.toLowerCase());
5663
+ if (!callbackType) continue;
5664
+ let decoded;
5665
+ try {
5666
+ decoded = decode$1(callbackType, offer.callback.data);
5667
+ } catch (err) {
5668
+ const error = err instanceof Error ? err : new Error(String(err));
5669
+ throw new Error("Failed to decode callback data", { cause: error });
5670
+ }
5671
+ if (decoded.length === 0) continue;
5672
+ const offerHash = hash(offer);
5673
+ const callbackInputs = decoded.map((callback) => ({
5674
+ chainId: offer.chainId,
5675
+ contract: callback.contract,
5676
+ user: offer.maker,
5677
+ amount: callback.amount
5678
+ }));
5679
+ callbacks.push({
5680
+ offerHash,
5681
+ callbacks: callbackInputs
5682
+ });
5683
+ for (const callback of decoded) {
5684
+ const contract = callback.contract;
5685
+ const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
5686
+ const asset = callbackType === Type$1.BuyVaultV1Callback ? void 0 : contract;
5687
+ positions.push(from$10({
5688
+ chainId: offer.chainId,
5689
+ contract,
5690
+ user: offer.maker,
5691
+ type: positionType,
5692
+ asset,
5693
+ blockNumber: offerBlockNumber
5694
+ }));
5695
+ const isLoanPosition = offer.loanToken.toLowerCase() === asset?.toLowerCase();
5696
+ lots.push({
5697
+ positionChainId: offer.chainId,
5698
+ positionContract: contract,
5699
+ positionUser: offer.maker,
5700
+ group: offer.group,
5701
+ size: isLoanPosition ? offer.assets : callback.amount
5702
+ });
5703
+ }
5704
+ }
5705
+ return {
5706
+ callbacks,
5707
+ positions,
5708
+ lots
5709
+ };
5710
+ }
5711
+ function buildOfferDependencies$1(offers) {
5712
+ const obligationsById = /* @__PURE__ */ new Map();
5713
+ const oraclesByKey = /* @__PURE__ */ new Map();
5714
+ const groupsByKey = /* @__PURE__ */ new Map();
5715
+ const offersByBlock = /* @__PURE__ */ new Map();
5716
+ for (const { offer, blockNumber } of offers) {
5717
+ const list = offersByBlock.get(blockNumber) ?? [];
5718
+ list.push(offer);
5719
+ offersByBlock.set(blockNumber, list);
5720
+ const obligationId$2 = obligationId(offer);
5721
+ if (!obligationsById.get(obligationId$2)) obligationsById.set(obligationId$2, from$13({
5722
+ chainId: offer.chainId,
5723
+ loanToken: offer.loanToken,
5724
+ maturity: offer.maturity,
5725
+ collaterals: offer.collaterals
5726
+ }));
5727
+ for (const collateral of offer.collaterals) {
5728
+ const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
5729
+ if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
5730
+ chainId: offer.chainId,
5731
+ address: collateral.oracle,
5732
+ price: null,
5733
+ blockNumber
5734
+ }));
5735
+ }
5736
+ const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
5737
+ if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
5738
+ chainId: offer.chainId,
5739
+ maker: offer.maker,
5740
+ group: offer.group,
5741
+ blockNumber
5742
+ });
5743
+ }
5744
+ return {
5745
+ obligations: Array.from(obligationsById.values()),
5746
+ oracles: Array.from(oraclesByKey.values()),
5747
+ groups: Array.from(groupsByKey.values()),
5748
+ offerBatches: Array.from(offersByBlock.entries()).map(([blockNumber, items]) => ({
5749
+ blockNumber,
5750
+ offers: items
5751
+ }))
5752
+ };
5753
+ }
5754
+ function filterInsertedOffers(parameters) {
5755
+ if (parameters.hashes.length === 0) return [];
5756
+ const inserted = new Set(parameters.hashes.map((hash) => hash.toLowerCase()));
5757
+ const seen = /* @__PURE__ */ new Set();
5758
+ const filtered = [];
5759
+ for (const entry of parameters.offers) {
5760
+ const hash$2 = hash(entry.offer).toLowerCase();
5761
+ if (!inserted.has(hash$2)) continue;
5762
+ if (seen.has(hash$2)) continue;
5763
+ seen.add(hash$2);
5764
+ filtered.push(entry);
4903
5765
  }
5766
+ return filtered;
4904
5767
  }
4905
5768
 
4906
5769
  //#endregion
@@ -5160,7 +6023,7 @@ async function* collectPositions(parameters) {
5160
6023
  });
5161
6024
  continue;
5162
6025
  }
5163
- transfers.push(from$7({
6026
+ transfers.push(from$8({
5164
6027
  id: `${client.chain.id}-${log.blockNumber.toString()}-${log.transactionHash}-${log.logIndex.toString()}`,
5165
6028
  chainId: client.chain.id,
5166
6029
  contract: log.address,
@@ -5499,7 +6362,7 @@ async function* collectPrices(parameters) {
5499
6362
  //#region src/indexer/collectors/CollectorBuilder.ts
5500
6363
  function createBuilder(parameters) {
5501
6364
  const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow, interval } = {} } = parameters;
5502
- const createCollector = (name, collect) => create$13({
6365
+ const createCollector = (name, collect) => create$16({
5503
6366
  name,
5504
6367
  collect,
5505
6368
  client,
@@ -5603,7 +6466,7 @@ function from$1(config) {
5603
6466
  retryAttempts,
5604
6467
  retryDelayMs
5605
6468
  });
5606
- return create$15({
6469
+ return create$18({
5607
6470
  client,
5608
6471
  collectors: [
5609
6472
  offersCollector,
@@ -5613,7 +6476,7 @@ function from$1(config) {
5613
6476
  ]
5614
6477
  });
5615
6478
  }
5616
- function create$15(params) {
6479
+ function create$18(params) {
5617
6480
  const { collectors, client } = params;
5618
6481
  const indexerId = `${client.chain.id.toString()}.indexer`;
5619
6482
  const tracer = getTracer(`router.${indexerId}`);
@@ -5642,7 +6505,7 @@ function create$15(params) {
5642
6505
 
5643
6506
  //#endregion
5644
6507
  //#region src/indexer/collectors/Admin.ts
5645
- function create$14(parameters) {
6508
+ function create$17(parameters) {
5646
6509
  const collector = "admin";
5647
6510
  const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
5648
6511
  const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
@@ -5882,8 +6745,8 @@ const names = [
5882
6745
  "positions",
5883
6746
  "prices"
5884
6747
  ];
5885
- function create$13({ name, collect, client, db, options }) {
5886
- const admin = create$14({
6748
+ function create$16({ name, collect, client, db, options }) {
6749
+ const admin = create$17({
5887
6750
  client,
5888
6751
  db,
5889
6752
  options
@@ -6119,7 +6982,6 @@ const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2,
6119
6982
  oracleChainId: bigint("oracle_chain_id", { mode: "number" }).$type().notNull(),
6120
6983
  oracleAddress: varchar("oracle_address", { length: 42 }).notNull(),
6121
6984
  lltv: bigint("lltv", { mode: "bigint" }).notNull(),
6122
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
6123
6985
  updatedAt: timestamp("updated_at").defaultNow().notNull()
6124
6986
  }, (table) => [
6125
6987
  primaryKey({
@@ -6415,7 +7277,7 @@ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
6415
7277
  //#endregion
6416
7278
  //#region src/database/domains/Blocks.ts
6417
7279
  /** Postgres implementation. */
6418
- const create$12 = (config) => {
7280
+ const create$15 = (config) => {
6419
7281
  const { db, chainRegistry } = config;
6420
7282
  const getChain = async (chainId) => {
6421
7283
  const rows = await db.select({
@@ -6596,7 +7458,7 @@ const create$12 = (config) => {
6596
7458
  //#region src/database/domains/Book.ts
6597
7459
  const DEFAULT_LIMIT$3 = 100;
6598
7460
  const MAX_TOTAL_OFFERS = 500;
6599
- function create$11(config) {
7461
+ function create$14(config) {
6600
7462
  const db = config.db;
6601
7463
  const logger = getLogger();
6602
7464
  const getOffers = async (parameters) => {
@@ -7060,16 +7922,76 @@ let LevelCursor;
7060
7922
  */
7061
7923
  const DEFAULT_BATCH_SIZE = 4e3;
7062
7924
 
7925
+ //#endregion
7926
+ //#region src/database/domains/Callbacks.ts
7927
+ /**
7928
+ * Create a callbacks domain instance.
7929
+ * @param db - Database core instance.
7930
+ * @returns Callbacks domain. {@link CallbacksDomain}
7931
+ */
7932
+ function create$13(db) {
7933
+ return {
7934
+ upsert: async (inputs) => {
7935
+ if (inputs.length === 0) return;
7936
+ const idCache = /* @__PURE__ */ new Map();
7937
+ const seenCallbackIds = /* @__PURE__ */ new Set();
7938
+ const callbacksRows = [];
7939
+ const offersCallbacksRows = [];
7940
+ const callbackId = (input) => {
7941
+ const preimage = `0x${input.chainId}${input.contract}${input.user}${input.amount.toString()}`.toLowerCase();
7942
+ const id = idCache.get(preimage) ?? keccak256(preimage);
7943
+ idCache.set(preimage, id);
7944
+ return id;
7945
+ };
7946
+ for (const { offerHash, callbacks } of inputs) {
7947
+ const normalizedOfferHash = offerHash.toLowerCase();
7948
+ for (const callback of callbacks) {
7949
+ const normalized = {
7950
+ chainId: callback.chainId,
7951
+ contract: callback.contract.toLowerCase(),
7952
+ user: callback.user.toLowerCase(),
7953
+ amount: callback.amount
7954
+ };
7955
+ const id = callbackId(normalized);
7956
+ offersCallbacksRows.push({
7957
+ offerHash: normalizedOfferHash,
7958
+ callbackId: id
7959
+ });
7960
+ if (seenCallbackIds.has(id)) continue;
7961
+ seenCallbackIds.add(id);
7962
+ callbacksRows.push({
7963
+ id,
7964
+ positionChainId: normalized.chainId,
7965
+ positionContract: normalized.contract,
7966
+ positionUser: normalized.user,
7967
+ amount: normalized.amount.toString()
7968
+ });
7969
+ }
7970
+ }
7971
+ if (offersCallbacksRows.length === 0) return;
7972
+ await db.transaction(async (dbTx) => {
7973
+ for (const batch of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch).onConflictDoNothing();
7974
+ for (const batch of batch$1(offersCallbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(offersCallbacks).values(batch).onConflictDoNothing();
7975
+ });
7976
+ },
7977
+ delete: async ({ offers }) => {
7978
+ if (offers.length === 0) return 0;
7979
+ const normalized = offers.map((offer) => offer.toLowerCase());
7980
+ return (await db.delete(offersCallbacks).where(inArray(offersCallbacks.offerHash, normalized))).affectedRows;
7981
+ }
7982
+ };
7983
+ }
7984
+
7063
7985
  //#endregion
7064
7986
  //#region src/database/domains/Consumed.ts
7065
- function create$10(db) {
7987
+ function create$12(db) {
7066
7988
  return {
7067
7989
  create: async (events) => {
7068
7990
  if (events.length === 0) return;
7069
- const groups$1 = /* @__PURE__ */ new Map();
7991
+ const groups$2 = /* @__PURE__ */ new Map();
7070
7992
  for (const event of events) {
7071
7993
  const groupId = `${event.chainId}-${event.maker}-${event.group}`.toLowerCase();
7072
- groups$1.set(groupId, {
7994
+ groups$2.set(groupId, {
7073
7995
  chainId: event.chainId,
7074
7996
  maker: event.maker,
7075
7997
  group: event.group,
@@ -7077,7 +7999,7 @@ function create$10(db) {
7077
7999
  });
7078
8000
  }
7079
8001
  await db.transaction(async (dbTx) => {
7080
- const groupsRows = Array.from(groups$1.values()).map((group) => ({
8002
+ const groupsRows = Array.from(groups$2.values()).map((group) => ({
7081
8003
  chainId: group.chainId,
7082
8004
  maker: group.maker.toLowerCase(),
7083
8005
  group: group.group.toLowerCase(),
@@ -7103,9 +8025,30 @@ function create$10(db) {
7103
8025
  };
7104
8026
  }
7105
8027
 
8028
+ //#endregion
8029
+ //#region src/database/domains/Groups.ts
8030
+ /**
8031
+ * Create a groups domain instance.
8032
+ * @param db - Database core instance.
8033
+ * @returns Groups domain. {@link GroupsDomain}
8034
+ */
8035
+ function create$11(db) {
8036
+ return { create: async (groups$1) => {
8037
+ if (groups$1.length === 0) return;
8038
+ const rows = groups$1.map((group) => ({
8039
+ chainId: group.chainId,
8040
+ maker: group.maker.toLowerCase(),
8041
+ group: group.group.toLowerCase(),
8042
+ consumed: (group.consumed ?? 0n).toString(),
8043
+ blockNumber: group.blockNumber
8044
+ }));
8045
+ for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await db.insert(groups).values(batch).onConflictDoNothing();
8046
+ } };
8047
+ }
8048
+
7106
8049
  //#endregion
7107
8050
  //#region src/database/domains/Lots.ts
7108
- function create$9(db) {
8051
+ function create$10(db) {
7109
8052
  return {
7110
8053
  get: async (parameters) => {
7111
8054
  const { chainId, user, contract, group } = parameters ?? {};
@@ -7149,268 +8092,88 @@ function create$9(db) {
7149
8092
  }
7150
8093
 
7151
8094
  //#endregion
7152
- //#region src/gatekeeper/Client.ts
7153
- const DEFAULT_TIMEOUT_MS = 1e4;
8095
+ //#region src/database/domains/Obligations.ts
7154
8096
  /**
7155
- * Create an HTTP client for a gatekeeper service.
7156
- * @param config - Gatekeeper client configuration. {@link ClientConfig}
7157
- * @returns An HTTP-backed gatekeeper client. {@link GatekeeperClient}
8097
+ * Create an obligations domain instance.
8098
+ * @param db - Database core instance.
8099
+ * @returns Obligations domain. {@link ObligationsDomain}
7158
8100
  */
7159
- function createHttpClient(config) {
7160
- const fetchFn = config.fetchFn ?? fetch;
7161
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7162
- const baseUrl = normalizeBaseUrl(config.baseUrl);
7163
- const request = async (path, init) => {
7164
- const controller = new AbortController();
7165
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
8101
+ function create$9(db) {
8102
+ return { create: async (obligations$1) => {
8103
+ if (obligations$1.length === 0) return;
8104
+ const obligationsById = /* @__PURE__ */ new Map();
8105
+ for (const obligation of obligations$1) {
8106
+ const id$1 = id(obligation).toLowerCase();
8107
+ if (!obligationsById.get(id$1)) obligationsById.set(id$1, obligation);
8108
+ }
7166
8109
  try {
7167
- return await fetchFn(`${baseUrl}${path}`, {
7168
- ...init,
7169
- signal: controller.signal
8110
+ await db.transaction(async (dbTx) => {
8111
+ const obligationRows = obligations$1.map((obligation) => ({
8112
+ obligationId: id(obligation),
8113
+ chainId: obligation.chainId,
8114
+ loanToken: obligation.loanToken.toLowerCase(),
8115
+ maturity: obligation.maturity
8116
+ }));
8117
+ for (const batch of batch$1(obligationRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligations).values(batch).onConflictDoNothing();
8118
+ const collateralRows = obligations$1.flatMap((obligation) => {
8119
+ return obligation.collaterals.map((collateral) => ({
8120
+ obligationId: id(obligation),
8121
+ asset: collateral.asset.toLowerCase(),
8122
+ oracleChainId: obligation.chainId,
8123
+ oracleAddress: collateral.oracle.toLowerCase(),
8124
+ lltv: collateral.lltv
8125
+ }));
8126
+ });
8127
+ for (const batch of batch$1(collateralRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationCollateralsV2).values(batch).onConflictDoNothing();
7170
8128
  });
7171
- } finally {
7172
- clearTimeout(timeout);
7173
- }
7174
- };
7175
- const validate = async (body) => {
7176
- const response = await request("/v1/validate", {
7177
- method: "POST",
7178
- headers: { "content-type": "application/json" },
7179
- body: JSON.stringify(body)
7180
- });
7181
- const json = await response.json();
7182
- return {
7183
- statusCode: response.status,
7184
- body: json
7185
- };
7186
- };
7187
- const getRules = async () => {
7188
- const response = await request("/v1/rules", { method: "GET" });
7189
- const json = await response.json();
7190
- if (!response.ok) throw new Error(`Gatekeeper rules request failed: ${extractErrorMessage(json) ?? response.statusText}`);
7191
- if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper rules response is invalid.");
7192
- return json.data;
7193
- };
7194
- const isAllowed = async (offers) => {
7195
- const { statusCode, body } = await validate({ offers: offers.map((offer) => toSnakeCase(offer)) });
7196
- if (statusCode !== 200) {
7197
- const errorMessage = extractErrorMessage(body);
7198
- throw new Error(`Gatekeeper validation failed: ${errorMessage ?? `status ${statusCode}`}`);
7199
- }
7200
- const data = body.data;
7201
- if (!data || typeof data !== "object") throw new Error("Gatekeeper validation response is invalid.");
7202
- if ("issues" in data) {
7203
- const issues = data.issues.map((issue) => ({
7204
- ruleName: issue.rule,
7205
- message: issue.message,
7206
- item: offers[issue.index]
7207
- }));
7208
- const invalidIndices = new Set(data.issues.map((issue) => issue.index));
7209
- return {
7210
- valid: offers.filter((_, index) => !invalidIndices.has(index)),
7211
- issues
7212
- };
8129
+ } catch (err) {
8130
+ const error = err instanceof Error ? err : new Error(String(err));
8131
+ throw new Error("Obligations.create failed. Ensure oracles exist before inserting obligations.", { cause: error });
7213
8132
  }
7214
- if (!("payload" in data) || !("root" in data)) throw new Error("Gatekeeper validation response is missing payload data.");
7215
- return {
7216
- valid: offers.slice(),
7217
- issues: []
7218
- };
7219
- };
7220
- return {
7221
- baseUrl,
7222
- validate,
7223
- isAllowed,
7224
- getRules
7225
- };
7226
- }
7227
- function normalizeBaseUrl(url) {
7228
- return url.trim().replace(/\/+$/, "");
7229
- }
7230
- function extractErrorMessage(payload) {
7231
- if (!payload || typeof payload !== "object") return void 0;
7232
- const error = payload.error;
7233
- if (!error || typeof error !== "object") return void 0;
7234
- return typeof error.message === "string" ? error.message : void 0;
8133
+ } };
7235
8134
  }
7236
8135
 
7237
8136
  //#endregion
7238
8137
  //#region src/database/domains/Offers.ts
7239
8138
  const DEFAULT_LIMIT$2 = 100;
7240
8139
  function create$8(config) {
7241
- const { db, chainRegistry } = config;
8140
+ const { db } = config;
7242
8141
  return {
7243
8142
  create: async (batches) => {
7244
8143
  if (batches.length === 0) return [];
7245
- const offersWithBlock = batches.flatMap(({ blockNumber, offers }) => offers.map((offer) => ({
7246
- offer,
8144
+ const offersRows = batches.flatMap(({ blockNumber, offers }) => offers.map((offer) => ({
8145
+ ...serialize(offer),
8146
+ obligationId: obligationId(offer),
8147
+ groupChainId: offer.chainId,
8148
+ groupMaker: offer.maker.toLowerCase(),
8149
+ callbackAddress: offer.callback.address.toLowerCase(),
8150
+ callbackData: offer.callback.data,
7247
8151
  blockNumber
7248
8152
  })));
7249
- if (offersWithBlock.length === 0) return [];
7250
- const obligationsMap = /* @__PURE__ */ new Map();
7251
- const collateralsMap = /* @__PURE__ */ new Map();
7252
- const oraclesMap = /* @__PURE__ */ new Map();
7253
- const groupsMap = /* @__PURE__ */ new Map();
7254
- for (const { offer, blockNumber } of offersWithBlock) {
7255
- const obligationId$1 = obligationId(offer);
7256
- if (!obligationsMap.has(obligationId$1)) {
7257
- obligationsMap.set(obligationId$1, from$13({
7258
- chainId: offer.chainId,
7259
- loanToken: offer.loanToken,
7260
- maturity: offer.maturity,
7261
- collaterals: offer.collaterals
7262
- }));
7263
- collateralsMap.set(obligationId$1, {
7264
- collaterals: [...offer.collaterals],
7265
- blockNumber
7266
- });
7267
- for (const collateral of offer.collaterals) {
7268
- const oracleId = `${offer.chainId}-${collateral.oracle.toLowerCase()}`.toLowerCase();
7269
- if (!oraclesMap.has(oracleId)) oraclesMap.set(oracleId, {
7270
- chainId: offer.chainId,
7271
- address: collateral.oracle,
7272
- blockNumber
7273
- });
8153
+ if (offersRows.length === 0) return [];
8154
+ try {
8155
+ return await db.transaction(async (dbTx) => {
8156
+ const selectExisting = async (hashes) => {
8157
+ if (hashes.length === 0) return /* @__PURE__ */ new Set();
8158
+ const existing = /* @__PURE__ */ new Set();
8159
+ for (const batch of batch$1(hashes, DEFAULT_BATCH_SIZE)) {
8160
+ const rows = await dbTx.select({ hash: offers.hash }).from(offers).where(inArray(offers.hash, batch));
8161
+ for (const row of rows) existing.add(String(row.hash).toLowerCase());
8162
+ }
8163
+ return existing;
8164
+ };
8165
+ const inserted = [];
8166
+ for (const batch of batch$1(offersRows, DEFAULT_BATCH_SIZE)) {
8167
+ const rows = await dbTx.insert(offers).values(batch).onConflictDoNothing().returning();
8168
+ inserted.push(...rows.map((row) => row.hash));
7274
8169
  }
7275
- }
7276
- const groupId = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
7277
- if (!groupsMap.has(groupId)) groupsMap.set(groupId, {
7278
- chainId: offer.chainId,
7279
- maker: offer.maker,
7280
- group: offer.group,
7281
- blockNumber
8170
+ const existing = await selectExisting(inserted);
8171
+ return inserted.filter((hash) => existing.has(hash));
7282
8172
  });
8173
+ } catch (err) {
8174
+ const error = err instanceof Error ? err : new Error(String(err));
8175
+ throw new Error("Offers.create failed. Ensure obligations and groups exist before inserting offers.", { cause: error });
7283
8176
  }
7284
- return await db.transaction(async (dbTx) => {
7285
- const obligationsRows = Array.from(obligationsMap.entries()).map(([obligationId, obligation]) => ({
7286
- obligationId,
7287
- chainId: obligation.chainId,
7288
- loanToken: obligation.loanToken.toLowerCase(),
7289
- maturity: obligation.maturity
7290
- }));
7291
- for (const batch of batch$1(obligationsRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligations).values(batch).onConflictDoNothing();
7292
- const oraclesRows = Array.from(oraclesMap.values()).map((oracle) => ({
7293
- chainId: oracle.chainId,
7294
- address: oracle.address.toLowerCase(),
7295
- blockNumber: oracle.blockNumber
7296
- }));
7297
- for (const batch of batch$1(oraclesRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(oracles).values(batch).onConflictDoNothing();
7298
- const collateralsRows = Array.from(collateralsMap.entries()).flatMap(([obligationId, items]) => items.collaterals.map((collateral) => ({
7299
- obligationId,
7300
- asset: collateral.asset.toLowerCase(),
7301
- oracleChainId: obligationsMap.get(obligationId).chainId,
7302
- oracleAddress: collateral.oracle.toLowerCase(),
7303
- lltv: collateral.lltv,
7304
- blockNumber: items.blockNumber
7305
- })));
7306
- for (const batch of batch$1(collateralsRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationCollateralsV2).values(batch).onConflictDoNothing();
7307
- const groupsRows = Array.from(groupsMap.values()).map((group) => ({
7308
- chainId: group.chainId,
7309
- maker: group.maker.toLowerCase(),
7310
- group: group.group.toLowerCase(),
7311
- consumed: "0",
7312
- blockNumber: group.blockNumber
7313
- }));
7314
- for (const batch of batch$1(groupsRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(groups).values(batch).onConflictDoNothing();
7315
- const offersRows = offersWithBlock.map(({ offer, blockNumber }) => ({
7316
- ...serialize(offer),
7317
- obligationId: obligationId(offer),
7318
- groupChainId: offer.chainId,
7319
- groupMaker: offer.maker.toLowerCase(),
7320
- callbackAddress: offer.callback.address.toLowerCase(),
7321
- callbackData: offer.callback.data,
7322
- blockNumber
7323
- }));
7324
- const inserted = [];
7325
- for (const batch of batch$1(offersRows, DEFAULT_BATCH_SIZE)) {
7326
- const result = await dbTx.insert(offers).values(batch).onConflictDoNothing().returning();
7327
- inserted.push(...result);
7328
- }
7329
- if (inserted.length === 0) return [];
7330
- const idCached = /* @__PURE__ */ new Map();
7331
- const id = (params) => {
7332
- const preimage = `0x${params.chainId}${params.contract}${params.user}${params.amount}`.toLowerCase();
7333
- const id = idCached.get(preimage) ?? keccak256(preimage);
7334
- idCached.set(preimage, id);
7335
- return id;
7336
- };
7337
- const offersCallbacksMap = /* @__PURE__ */ new Map();
7338
- for (const offer of inserted) {
7339
- if (offer.callbackData === "0x") continue;
7340
- const user = offer.groupMaker.toLowerCase();
7341
- if (!offersCallbacksMap.has(offer.hash)) offersCallbacksMap.set(offer.hash, []);
7342
- const chain = chainRegistry.getById(offer.groupChainId);
7343
- if (!chain) continue;
7344
- const callbackType = getCallbackType(chain.name, offer.callbackAddress);
7345
- if (!callbackType) continue;
7346
- const callbacks = decode$1(callbackType, offer.callbackData).map((callback) => ({
7347
- chainId: offer.groupChainId,
7348
- contract: callback.contract.toLowerCase(),
7349
- user: user.toLowerCase(),
7350
- amount: callback.amount.toString(),
7351
- type: callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20,
7352
- asset: callbackType === Type$1.BuyVaultV1Callback ? void 0 : callback.contract.toLowerCase(),
7353
- blockNumber: offer.blockNumber
7354
- }));
7355
- try {
7356
- await dbTx.insert(offersCallbacks).values(callbacks.map((callback) => ({
7357
- offerHash: offer.hash,
7358
- callbackId: id(callback)
7359
- }))).onConflictDoNothing();
7360
- offersCallbacksMap.get(offer.hash).push(...callbacks);
7361
- } catch (_) {
7362
- offersCallbacksMap.delete(offer.hash);
7363
- }
7364
- }
7365
- if (offersCallbacksMap.size === 0) {
7366
- obligationsMap.clear();
7367
- collateralsMap.clear();
7368
- oraclesMap.clear();
7369
- groupsMap.clear();
7370
- offersCallbacksMap.clear();
7371
- idCached.clear();
7372
- return inserted.map((offer) => offer.hash);
7373
- }
7374
- await dbTx.positions.upsert(Array.from(offersCallbacksMap.values()).flatMap((callbacks) => callbacks.map((callback) => ({
7375
- chainId: callback.chainId,
7376
- contract: callback.contract,
7377
- user: callback.user,
7378
- type: callback.type,
7379
- asset: callback.asset,
7380
- blockNumber: callback.blockNumber
7381
- }))));
7382
- const callbacksRows = Array.from(offersCallbacksMap.values()).flatMap((callbacks) => callbacks.map((callback) => ({
7383
- id: id(callback),
7384
- positionChainId: callback.chainId,
7385
- positionContract: callback.contract,
7386
- positionUser: callback.user,
7387
- amount: callback.amount
7388
- })));
7389
- for (const batch of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch).onConflictDoNothing();
7390
- const lotInfos = [];
7391
- for (const [offerHash, callbacks] of offersCallbacksMap.entries()) {
7392
- const offer = inserted.find((o) => o.hash === offerHash);
7393
- if (!offer) continue;
7394
- for (const callback of callbacks) {
7395
- const isLoanPosition = obligationsMap.get(offer.obligationId)?.loanToken.toLowerCase() === callback.asset?.toLowerCase();
7396
- lotInfos.push({
7397
- positionChainId: callback.chainId,
7398
- positionContract: callback.contract,
7399
- positionUser: callback.user,
7400
- group: offer.group,
7401
- size: isLoanPosition ? BigInt(offer.assets) : BigInt(callback.amount)
7402
- });
7403
- }
7404
- }
7405
- if (lotInfos.length > 0) await dbTx.lots.create(lotInfos);
7406
- obligationsMap.clear();
7407
- collateralsMap.clear();
7408
- oraclesMap.clear();
7409
- groupsMap.clear();
7410
- offersCallbacksMap.clear();
7411
- idCached.clear();
7412
- return inserted.map((offer) => offer.hash);
7413
- });
7414
8177
  },
7415
8178
  get: async (parameters) => {
7416
8179
  const limit = parameters?.limit ?? DEFAULT_LIMIT$2;
@@ -7430,36 +8193,12 @@ function create$8(config) {
7430
8193
  '[]'::jsonb
7431
8194
  )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7432
8195
  AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7433
- const availableLateral = db.select({ available: sql`COALESCE(SUM(
7434
- CASE
7435
- -- If asset is null, position available is 0
7436
- WHEN ${positions.asset} IS NULL THEN 0
7437
-
7438
- -- Position asset matches loan token: no conversion needed
7439
- WHEN ${positions.asset} = ${obligations.loanToken} THEN
7440
- CASE
7441
- WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7442
- ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7443
- END
7444
-
7445
- -- Position asset is collateral: apply oracle price * lltv
7446
- -- Formula: balance * price / 1e36 * lltv / 1e18
7447
- ELSE
7448
- (CASE
7449
- WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7450
- ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7451
- END)
7452
- * COALESCE(${oracles.price}, 0)::numeric / 1e36
7453
- * COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
7454
- END
7455
- ), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, eq(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, and(eq(callbacks.positionChainId, positions.chainId), eq(callbacks.positionContract, positions.contract), eq(callbacks.positionUser, positions.user))).leftJoin(obligationCollateralsV2, and(eq(obligationCollateralsV2.obligationId, offers.obligationId), eq(obligationCollateralsV2.asset, positions.asset))).leftJoin(oracles, and(eq(oracles.chainId, obligationCollateralsV2.oracleChainId), eq(oracles.address, obligationCollateralsV2.oracleAddress))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
7456
8196
  const rows = (await db.select({
7457
8197
  hash: offers.hash,
7458
8198
  maker: offers.groupMaker,
7459
8199
  assets: offers.assets,
7460
8200
  obligationUnits: offers.obligationUnits,
7461
8201
  obligationShares: offers.obligationShares,
7462
- consumed: groups.consumed,
7463
8202
  price: offers.price,
7464
8203
  maturity: offers.maturity,
7465
8204
  expiry: offers.expiry,
@@ -7472,19 +8211,8 @@ function create$8(config) {
7472
8211
  callbackAddress: offers.callbackAddress,
7473
8212
  callbackData: offers.callbackData,
7474
8213
  collaterals: collateralsLateral.collaterals,
7475
- blockNumber: offers.blockNumber,
7476
- available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
7477
- takeable: sql`FLOOR(GREATEST(
7478
- 0,
7479
- LEAST(
7480
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
7481
- COALESCE(${availableLateral.available}::numeric, 0)
7482
- )
7483
- ))`.as("takeable")
7484
- }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).leftJoinLateral(availableLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, maker === void 0 ? sql`GREATEST(0, LEAST(
7485
- ${offers.assets}::numeric - ${groups.consumed}::numeric,
7486
- COALESCE(${availableLateral.available}::numeric, 0)
7487
- )) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
8214
+ blockNumber: offers.blockNumber
8215
+ }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
7488
8216
  return {
7489
8217
  hash: row.hash,
7490
8218
  maker: row.maker,
@@ -7509,13 +8237,9 @@ function create$8(config) {
7509
8237
  address: row.callbackAddress,
7510
8238
  data: row.callbackData
7511
8239
  },
7512
- state: {
7513
- hash: row.hash,
7514
- blockNumber: row.blockNumber
7515
- },
7516
- consumed: BigInt(row.consumed),
7517
- available: BigInt(String(row.available ?? "0").split(".")[0] ?? "0"),
7518
- takeable: BigInt(String(row.takeable ?? "0").split(".")[0] ?? "0"),
8240
+ consumed: 0n,
8241
+ available: 0n,
8242
+ takeable: 0n,
7519
8243
  blockNumber: row.blockNumber
7520
8244
  };
7521
8245
  });
@@ -7597,7 +8321,7 @@ function create$8(config) {
7597
8321
  quote.bid = { price: BigInt(row.price) };
7598
8322
  }
7599
8323
  return Array.from(quotes.entries()).map(([id, quote]) => {
7600
- return from$8({
8324
+ return from$9({
7601
8325
  obligationId: id,
7602
8326
  ask: quote.ask,
7603
8327
  bid: quote.bid
@@ -7639,27 +8363,30 @@ function create$6(db) {
7639
8363
  price: oracles.price,
7640
8364
  blockNumber: oracles.blockNumber,
7641
8365
  chainId: oracles.chainId
7642
- }).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$10({
8366
+ }).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$11({
7643
8367
  chainId: r.chainId,
7644
8368
  address: r.address,
7645
8369
  price: r.price,
7646
8370
  blockNumber: r.blockNumber
7647
8371
  }));
7648
8372
  },
7649
- upsert: async (oracles$1) => {
7650
- if (oracles$1.length === 0) return;
7651
- const rows = oracles$1.map((o) => ({
8373
+ upsert: async (oracles$2) => {
8374
+ if (oracles$2.length === 0) return;
8375
+ const rows = oracles$2.map((o) => ({
7652
8376
  chainId: o.chainId,
7653
8377
  address: o.address.toLowerCase(),
7654
8378
  price: o.price !== null ? o.price.toString() : null,
7655
8379
  blockNumber: o.blockNumber
7656
8380
  }));
7657
- db.transaction(async (dbTx) => {
8381
+ await db.transaction(async (dbTx) => {
7658
8382
  for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await dbTx.insert(oracles).values(batch).onConflictDoUpdate({
7659
8383
  target: [oracles.chainId, oracles.address],
7660
8384
  set: {
7661
- price: sql`EXCLUDED.price`,
7662
- blockNumber: sql`EXCLUDED.block_number`,
8385
+ price: sql`COALESCE(EXCLUDED.price, ${oracles.price})`,
8386
+ blockNumber: sql`CASE
8387
+ WHEN EXCLUDED.price IS NULL THEN ${oracles.blockNumber}
8388
+ ELSE EXCLUDED.block_number
8389
+ END`,
7663
8390
  updatedAt: sql`NOW()`
7664
8391
  }
7665
8392
  });
@@ -8005,41 +8732,42 @@ function create$3(config) {
8005
8732
  return {
8006
8733
  create: async (trees$1) => {
8007
8734
  if (trees$1.length === 0) return [];
8008
- return await db.transaction(async (dbTx) => {
8009
- const roots = [];
8010
- for (const { tree, signature, blockNumber } of trees$1) {
8011
- const root = tree.root.toLowerCase();
8012
- roots.push(root);
8013
- await dbTx.insert(trees).values({
8014
- root,
8015
- rootSignature: signature.toLowerCase()
8016
- }).onConflictDoUpdate({
8017
- target: [trees.root],
8018
- set: {
8019
- rootSignature: signature.toLowerCase(),
8020
- createdAt: sql`NOW()`
8021
- }
8022
- });
8023
- await dbTx.offers.create([{
8024
- blockNumber,
8025
- offers: tree.offers
8026
- }]);
8027
- const pathRows = proofs(tree).map((proof) => ({
8028
- offerHash: hash(proof.offer).toLowerCase(),
8029
- treeRoot: root,
8030
- proofNodes: concatenateProofs(proof.path)
8031
- }));
8032
- for (const batch of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch).onConflictDoUpdate({
8033
- target: [merklePaths.offerHash],
8034
- set: {
8035
- treeRoot: sql`excluded.tree_root`,
8036
- proofNodes: sql`excluded.proof_nodes`,
8037
- createdAt: sql`NOW()`
8038
- }
8039
- });
8040
- }
8041
- return roots;
8042
- });
8735
+ try {
8736
+ return await db.transaction(async (dbTx) => {
8737
+ const roots = [];
8738
+ for (const { tree, signature } of trees$1) {
8739
+ const root = tree.root.toLowerCase();
8740
+ roots.push(root);
8741
+ await dbTx.insert(trees).values({
8742
+ root,
8743
+ rootSignature: signature.toLowerCase()
8744
+ }).onConflictDoUpdate({
8745
+ target: [trees.root],
8746
+ set: {
8747
+ rootSignature: signature.toLowerCase(),
8748
+ createdAt: sql`NOW()`
8749
+ }
8750
+ });
8751
+ const pathRows = proofs(tree).map((proof) => ({
8752
+ offerHash: hash(proof.offer).toLowerCase(),
8753
+ treeRoot: root,
8754
+ proofNodes: concatenateProofs(proof.path)
8755
+ }));
8756
+ for (const batch of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch).onConflictDoUpdate({
8757
+ target: [merklePaths.offerHash],
8758
+ set: {
8759
+ treeRoot: sql`excluded.tree_root`,
8760
+ proofNodes: sql`excluded.proof_nodes`,
8761
+ createdAt: sql`NOW()`
8762
+ }
8763
+ });
8764
+ }
8765
+ return roots;
8766
+ });
8767
+ } catch (err) {
8768
+ const error = err instanceof Error ? err : new Error(String(err));
8769
+ throw new Error("Trees.create failed. Ensure offers exist before inserting merkle paths.", { cause: error });
8770
+ }
8043
8771
  },
8044
8772
  getAttestations: async (hashes) => {
8045
8773
  if (hashes.length === 0) return /* @__PURE__ */ new Map();
@@ -8144,17 +8872,17 @@ function create$2(db) {
8144
8872
  //#region src/database/Database.ts
8145
8873
  function createDomains(core, chainRegistry) {
8146
8874
  return {
8147
- book: create$11({ db: core }),
8148
- blocks: create$12({
8149
- db: core,
8150
- chainRegistry
8151
- }),
8152
- offers: create$8({
8875
+ book: create$14({ db: core }),
8876
+ blocks: create$15({
8153
8877
  db: core,
8154
8878
  chainRegistry
8155
8879
  }),
8156
- consumed: create$10(core),
8157
- lots: create$9(core),
8880
+ callbacks: create$13(core),
8881
+ offers: create$8({ db: core }),
8882
+ consumed: create$12(core),
8883
+ groups: create$11(core),
8884
+ lots: create$10(core),
8885
+ obligations: create$9(core),
8158
8886
  offsets: create$7(core),
8159
8887
  oracles: create$6(core),
8160
8888
  trees: create$3({ db: core }),
@@ -8183,6 +8911,10 @@ function augmentWithDomains(base, chainRegistry) {
8183
8911
  value: dms.blocks,
8184
8912
  enumerable: true
8185
8913
  },
8914
+ callbacks: {
8915
+ value: dms.callbacks,
8916
+ enumerable: true
8917
+ },
8186
8918
  offers: {
8187
8919
  value: dms.offers,
8188
8920
  enumerable: true
@@ -8191,10 +8923,18 @@ function augmentWithDomains(base, chainRegistry) {
8191
8923
  value: dms.consumed,
8192
8924
  enumerable: true
8193
8925
  },
8926
+ groups: {
8927
+ value: dms.groups,
8928
+ enumerable: true
8929
+ },
8194
8930
  lots: {
8195
8931
  value: dms.lots,
8196
8932
  enumerable: true
8197
8933
  },
8934
+ obligations: {
8935
+ value: dms.obligations,
8936
+ enumerable: true
8937
+ },
8198
8938
  offsets: {
8199
8939
  value: dms.offsets,
8200
8940
  enumerable: true
@@ -8636,6 +9376,8 @@ const ApiSchema = z.object({ port: z.number().int().positive() }).strict();
8636
9376
  const GatekeeperSchema = z.object({
8637
9377
  url_env: z.string().min(1).optional(),
8638
9378
  url: z.string().min(1).optional(),
9379
+ origin_secret_env: z.string().min(1).optional(),
9380
+ origin_secret: z.string().min(1).optional(),
8639
9381
  timeout_ms: z.number().int().positive().optional(),
8640
9382
  port: z.number().int().positive().optional()
8641
9383
  }).strict().optional();
@@ -8701,6 +9443,7 @@ function resolveRouterConfig(config, env) {
8701
9443
  api: config.api ? { port: config.api.port } : void 0,
8702
9444
  gatekeeper: {
8703
9445
  url: resolveGatekeeperUrl(config.gatekeeper, env),
9446
+ originSecret: resolveGatekeeperOriginSecret(config.gatekeeper, env),
8704
9447
  timeoutMs: config.gatekeeper?.timeout_ms ?? 1e4,
8705
9448
  port: config.gatekeeper?.port ?? 8082
8706
9449
  },
@@ -8811,6 +9554,7 @@ function createDefaultConfig() {
8811
9554
  api: { port: 8081 },
8812
9555
  gatekeeper: {
8813
9556
  url: void 0,
9557
+ originSecret: void 0,
8814
9558
  timeoutMs: 1e4,
8815
9559
  port: 8082
8816
9560
  },
@@ -8829,6 +9573,11 @@ function resolveGatekeeperUrl(gatekeeper, env) {
8829
9573
  if (gatekeeper.url_env) return readEnvVar(env, gatekeeper.url_env, "Gatekeeper URL");
8830
9574
  return gatekeeper.url;
8831
9575
  }
9576
+ function resolveGatekeeperOriginSecret(gatekeeper, env) {
9577
+ if (!gatekeeper) return void 0;
9578
+ if (gatekeeper.origin_secret_env) return readEnvVar(env, gatekeeper.origin_secret_env, "Gatekeeper origin secret");
9579
+ return gatekeeper.origin_secret;
9580
+ }
8832
9581
 
8833
9582
  //#endregion
8834
9583
  //#region src/cli/commands/RouterCmd.ts
@@ -8851,7 +9600,7 @@ var RouterCmd = class RouterCmd extends Command {
8851
9600
  const configPath = resolveConfigPath(options.configFile);
8852
9601
  const config = configPath !== null ? loadRouterConfig(configPath) : createDefaultConfig();
8853
9602
  const logger = defaultLogger(config.logging.level, config.logging.pretty);
8854
- const chainRegistry = create$16(Object.values(config.chains).map((entry) => entry.chain));
9603
+ const chainRegistry = create$19(Object.values(config.chains).map((entry) => entry.chain));
8855
9604
  const clients = (config.indexer?.chains ?? []).map((name) => {
8856
9605
  const chainConfig = config.chains[name];
8857
9606
  if (!chainConfig) throw new Error(`Indexer chain ${name} is not defined under [chains].`);
@@ -8899,9 +9648,10 @@ const gatekeeperCmd = new RouterCmd("gatekeeper");
8899
9648
  gatekeeperCmd.description("Start Gatekeeper validation service.").action(async (opts) => {
8900
9649
  const { gatekeeper: gatekeeperConfig, chainRegistry, logger } = opts;
8901
9650
  await runWithLogger(logger, async () => {
8902
- const gatekeeperCore = create$17({ rules: morphoRules(chainRegistry.list()) });
9651
+ const gatekeeperCore = create$20({ rules: morphoRules(chainRegistry.list()) });
8903
9652
  const handle = await start$1({
8904
9653
  gatekeeper: gatekeeperCore,
9654
+ chainRegistry,
8905
9655
  port: gatekeeperConfig?.port ?? 8082
8906
9656
  });
8907
9657
  logger.info({
@@ -8954,21 +9704,97 @@ async function getBook(params, db) {
8954
9704
  }
8955
9705
 
8956
9706
  //#endregion
8957
- //#region src/api/Controllers/getConfig.ts
9707
+ //#region src/api/Controllers/getConfigContracts.ts
8958
9708
  /**
8959
- * Returns the configuration for all chains the router is indexing.
9709
+ * Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
9710
+ * @param query - Raw query parameters containing optional chain filters.
8960
9711
  * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
8961
- * @returns The configuration for all chains the router is indexing. {@link ApiPayload.Payload<ChainConfig[]>}
9712
+ * @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
8962
9713
  */
8963
- async function getConfig(chainRegistry) {
8964
- const configs = [];
8965
- for (const chain of chainRegistry.list()) configs.push({
8966
- chain_id: chain.id,
8967
- contracts: { mempool: chain.custom.mempool.address },
8968
- callbacks: chain.custom.callbacks.map((c) => c.type)
9714
+ async function getConfigContracts(query, chainRegistry) {
9715
+ const parsed = safeParse("get_config_contracts", query ?? {});
9716
+ if (!parsed.success) return failure(parsed.error);
9717
+ const { chains: chainsFilter, cursor, limit } = parsed.data;
9718
+ const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
9719
+ const contracts = [];
9720
+ const seenAddresses = /* @__PURE__ */ new Set();
9721
+ for (const chain of chainRegistry.list()) {
9722
+ if (chainFilter && !chainFilter.has(chain.id)) continue;
9723
+ const mempool = chain.custom?.mempool?.address;
9724
+ if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
9725
+ const multicall = chain.contracts?.multicall3?.address;
9726
+ if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
9727
+ const v2 = chain.custom?.morpho?.address;
9728
+ if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
9729
+ const chainContracts = [
9730
+ {
9731
+ chain_id: chain.id,
9732
+ name: "mempool",
9733
+ address: mempool
9734
+ },
9735
+ {
9736
+ chain_id: chain.id,
9737
+ name: "multicall",
9738
+ address: multicall
9739
+ },
9740
+ {
9741
+ chain_id: chain.id,
9742
+ name: "v2",
9743
+ address: v2
9744
+ }
9745
+ ];
9746
+ for (const contract of chainContracts) {
9747
+ const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
9748
+ if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
9749
+ seenAddresses.add(cursorKey);
9750
+ contracts.push(contract);
9751
+ }
9752
+ }
9753
+ contracts.sort((a, b) => {
9754
+ if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
9755
+ const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
9756
+ if (addressCompare !== 0) return addressCompare;
9757
+ return a.name.localeCompare(b.name);
8969
9758
  });
8970
- configs.sort((a, b) => a.chain_id - b.chain_id);
8971
- return success({ data: configs });
9759
+ let cursorContract = null;
9760
+ if (cursor) try {
9761
+ cursorContract = parseCursor(cursor);
9762
+ } catch (err) {
9763
+ return failure(err);
9764
+ }
9765
+ const startIndex = cursorContract ? findStartIndex(contracts, cursorContract) : 0;
9766
+ const page = contracts.slice(startIndex, startIndex + limit);
9767
+ const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
9768
+ return success({
9769
+ data: page,
9770
+ cursor: nextCursor
9771
+ });
9772
+ }
9773
+ function parseCursor(cursor) {
9774
+ const [chain, address] = cursor.split(":", 2);
9775
+ if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
9776
+ return {
9777
+ chain_id: Number.parseInt(chain, 10),
9778
+ address: address.toLowerCase()
9779
+ };
9780
+ }
9781
+ function formatCursor(contract) {
9782
+ return `${contract.chain_id}:${contract.address.toLowerCase()}`;
9783
+ }
9784
+ function findStartIndex(contracts, cursor) {
9785
+ let low = 0;
9786
+ let high = contracts.length;
9787
+ while (low < high) {
9788
+ const mid = Math.floor((low + high) / 2);
9789
+ const current = contracts[mid];
9790
+ if (compareContract(current, cursor) <= 0) low = mid + 1;
9791
+ else high = mid;
9792
+ }
9793
+ return low;
9794
+ }
9795
+ function compareContract(contract, cursor) {
9796
+ if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
9797
+ return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
8972
9798
  }
8973
9799
 
8974
9800
  //#endregion
@@ -8980,28 +9806,19 @@ const __dirname = (() => {
8980
9806
  return process.cwd();
8981
9807
  }
8982
9808
  })();
8983
- const parse_error_Description = {
8984
- name: "parse_error",
8985
- description: "Returns when an offer fails to parse due to invalid format or missing required fields"
8986
- };
8987
- const getGatekeeperRules = async (gatekeeper) => {
8988
- return [parse_error_Description, ...await gatekeeper.getRules()];
8989
- };
8990
9809
  /**
8991
9810
  * Build the OpenAPI document for the router.
8992
- * @param parameters - Includes a {@link RulesProvider} to fetch gatekeeper rules.
8993
9811
  * @returns OpenAPI document. {@link OpenAPIDocument}
8994
9812
  */
8995
- async function getSwaggerJson({ gatekeeper }) {
8996
- return OpenApi({ rules: await getGatekeeperRules(gatekeeper) });
9813
+ async function getSwaggerJson() {
9814
+ return OpenApi();
8997
9815
  }
8998
9816
  /**
8999
9817
  * Render the API documentation HTML page.
9000
- * @param parameters - Includes a {@link RulesProvider} to fetch gatekeeper rules.
9001
9818
  * @returns HTML page as string.
9002
9819
  */
9003
- async function getDocsHtml({ gatekeeper }) {
9004
- const spec = await OpenApi({ rules: await getGatekeeperRules(gatekeeper) });
9820
+ async function getDocsHtml() {
9821
+ const spec = await OpenApi();
9005
9822
  return `<!DOCTYPE html>
9006
9823
  <html>
9007
9824
  <head>
@@ -9347,13 +10164,127 @@ async function getObligations(queryParameters, db) {
9347
10164
 
9348
10165
  //#endregion
9349
10166
  //#region src/api/Controllers/getOffers.ts
10167
+ /**
10168
+ * Query offers with computed consumed/available/takeable values.
10169
+ * @param db - The database client. {@link Database.Core}
10170
+ * @param parameters - {@link GetOffersQueryParams}
10171
+ * @returns The offers with pagination cursor.
10172
+ */
10173
+ async function getOffersQuery(db, parameters) {
10174
+ const limit = parameters?.limit ?? DEFAULT_LIMIT$2;
10175
+ const cursor = parameters?.cursor;
10176
+ const maker = parameters?.maker;
10177
+ if (cursor !== null && cursor !== void 0) {
10178
+ if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
10179
+ }
10180
+ const collateralsLateral = db.select({ collaterals: sql`COALESCE(
10181
+ jsonb_agg(
10182
+ jsonb_build_object(
10183
+ 'asset', ${obligationCollateralsV2.asset},
10184
+ 'oracle', ${oracles.address},
10185
+ 'lltv', ${obligationCollateralsV2.lltv}
10186
+ )
10187
+ ),
10188
+ '[]'::jsonb
10189
+ )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
10190
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
10191
+ const availableLateral = db.select({ available: sql`COALESCE(SUM(
10192
+ CASE
10193
+ -- If asset is null, position available is 0
10194
+ WHEN ${positions.asset} IS NULL THEN 0
10195
+
10196
+ -- Position asset matches loan token: no conversion needed
10197
+ WHEN ${positions.asset} = ${obligations.loanToken} THEN
10198
+ CASE
10199
+ WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
10200
+ ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
10201
+ END
10202
+
10203
+ -- Position asset is collateral: apply oracle price * lltv
10204
+ -- Formula: balance * price / 1e36 * lltv / 1e18
10205
+ ELSE
10206
+ (CASE
10207
+ WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
10208
+ ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
10209
+ END)
10210
+ * COALESCE(${oracles.price}, 0)::numeric / 1e36
10211
+ * COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
10212
+ END
10213
+ ), 0)`.as("available") }).from(offersCallbacks).innerJoin(callbacks, eq(offersCallbacks.callbackId, callbacks.id)).innerJoin(positions, and(eq(callbacks.positionChainId, positions.chainId), eq(callbacks.positionContract, positions.contract), eq(callbacks.positionUser, positions.user))).leftJoin(obligationCollateralsV2, and(eq(obligationCollateralsV2.obligationId, offers.obligationId), eq(obligationCollateralsV2.asset, positions.asset))).leftJoin(oracles, and(eq(oracles.chainId, obligationCollateralsV2.oracleChainId), eq(oracles.address, obligationCollateralsV2.oracleAddress))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
10214
+ const rows = (await db.select({
10215
+ hash: offers.hash,
10216
+ maker: offers.groupMaker,
10217
+ assets: offers.assets,
10218
+ obligationUnits: offers.obligationUnits,
10219
+ obligationShares: offers.obligationShares,
10220
+ consumed: groups.consumed,
10221
+ price: offers.price,
10222
+ maturity: offers.maturity,
10223
+ expiry: offers.expiry,
10224
+ start: offers.start,
10225
+ group: offers.group,
10226
+ session: offers.session,
10227
+ buy: offers.buy,
10228
+ chainId: obligations.chainId,
10229
+ loanToken: obligations.loanToken,
10230
+ callbackAddress: offers.callbackAddress,
10231
+ callbackData: offers.callbackData,
10232
+ collaterals: collateralsLateral.collaterals,
10233
+ blockNumber: offers.blockNumber,
10234
+ available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
10235
+ takeable: sql`FLOOR(GREATEST(
10236
+ 0,
10237
+ LEAST(
10238
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
10239
+ COALESCE(${availableLateral.available}::numeric, 0)
10240
+ )
10241
+ ))`.as("takeable")
10242
+ }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).leftJoinLateral(availableLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, maker === void 0 ? sql`GREATEST(0, LEAST(
10243
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
10244
+ COALESCE(${availableLateral.available}::numeric, 0)
10245
+ )) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
10246
+ return {
10247
+ hash: row.hash,
10248
+ maker: row.maker,
10249
+ assets: BigInt(row.assets),
10250
+ obligationUnits: BigInt(row.obligationUnits),
10251
+ obligationShares: BigInt(row.obligationShares),
10252
+ price: BigInt(row.price),
10253
+ maturity: from$16(row.maturity),
10254
+ expiry: row.expiry,
10255
+ start: row.start,
10256
+ group: row.group,
10257
+ session: row.session,
10258
+ buy: row.buy,
10259
+ chainId: row.chainId,
10260
+ loanToken: row.loanToken,
10261
+ collaterals: row.collaterals.map((c) => ({
10262
+ asset: c.asset,
10263
+ oracle: c.oracle,
10264
+ lltv: BigInt(c.lltv)
10265
+ })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
10266
+ callback: {
10267
+ address: row.callbackAddress,
10268
+ data: row.callbackData
10269
+ },
10270
+ consumed: BigInt(row.consumed),
10271
+ available: BigInt(String(row.available ?? "0").split(".")[0] ?? "0"),
10272
+ takeable: BigInt(String(row.takeable ?? "0").split(".")[0] ?? "0"),
10273
+ blockNumber: row.blockNumber
10274
+ };
10275
+ });
10276
+ return {
10277
+ rows,
10278
+ nextCursor: rows.length === limit ? rows[rows.length - 1].hash : null
10279
+ };
10280
+ }
9350
10281
  async function getOffers(queryParameters, db) {
9351
10282
  const logger = getLogger();
9352
10283
  const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
9353
10284
  if (!result.success) return failure(result.error);
9354
10285
  const query = result.data;
9355
10286
  try {
9356
- const { rows, nextCursor } = query.maker ? await db.offers.get({
10287
+ const { rows, nextCursor } = query.maker ? await getOffersQuery(db, {
9357
10288
  maker: query.maker,
9358
10289
  cursor: query.cursor,
9359
10290
  limit: query.limit
@@ -9421,6 +10352,35 @@ async function getUserPositions(queryParameters, db) {
9421
10352
  }
9422
10353
  }
9423
10354
 
10355
+ //#endregion
10356
+ //#region src/api/Controllers/resolveCallbackTypes.ts
10357
+ /**
10358
+ * Resolve callback types for a list of callback addresses grouped by chain.
10359
+ * @param body - Request body with callback addresses. {@link CallbackTypesRequest}
10360
+ * @param chains - Chains to resolve callback types against. {@link Chain.Chain}
10361
+ * @returns Callback types grouped by chain. {@link CallbackTypesPayload}
10362
+ */
10363
+ async function resolveCallbackTypes$1(body, chains) {
10364
+ const result = safeParse("callback_types", body, (issue) => issue.message);
10365
+ if (!result.success) return failure(result.error);
10366
+ const request = result.data;
10367
+ const chainIds = new Set(chains.map((chain) => chain.id));
10368
+ const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
10369
+ if (unknown) return failure(new BadRequestError(`Unknown chain id ${unknown.chain_id}`));
10370
+ try {
10371
+ const data = resolveCallbackTypes$2({
10372
+ chains,
10373
+ request
10374
+ });
10375
+ return success({
10376
+ data,
10377
+ cursor: null
10378
+ });
10379
+ } catch (err) {
10380
+ return failure(err);
10381
+ }
10382
+ }
10383
+
9424
10384
  //#endregion
9425
10385
  //#region src/api/Api.ts
9426
10386
  function from(config) {
@@ -9494,6 +10454,21 @@ function serve$1(parameters) {
9494
10454
  return c.json(failure$1.body, failure$1.statusCode);
9495
10455
  }
9496
10456
  });
10457
+ app.post("/v1/callbacks", async (c) => {
10458
+ let body;
10459
+ try {
10460
+ body = await c.req.json();
10461
+ } catch (err) {
10462
+ const failure$3 = failure(err);
10463
+ return c.json(failure$3.body, failure$3.statusCode);
10464
+ }
10465
+ if (body === null || typeof body !== "object") {
10466
+ const failure$2 = failure(new BadRequestError("Request body must be a JSON object"));
10467
+ return c.json(failure$2.body, failure$2.statusCode);
10468
+ }
10469
+ const { statusCode, body: responseBody } = await resolveCallbackTypes$1(body, chainRegistry.list());
10470
+ return c.json(responseBody, statusCode);
10471
+ });
9497
10472
  app.get("/v1/users/:userAddress/positions", async (c) => {
9498
10473
  const query = c.req.query();
9499
10474
  const { statusCode, body } = await getUserPositions({
@@ -9515,12 +10490,21 @@ function serve$1(parameters) {
9515
10490
  const { statusCode, body } = await getHealthChains(c.req.query(), db, void 0, chainRegistry);
9516
10491
  return c.json(body, statusCode);
9517
10492
  });
9518
- app.get("/v1/config", async (c) => {
9519
- const { statusCode, body } = await getConfig(chainRegistry);
10493
+ app.get("/v1/config/contracts", async (c) => {
10494
+ const { statusCode, body } = await getConfigContracts(c.req.query(), chainRegistry);
9520
10495
  return c.json(body, statusCode);
9521
10496
  });
9522
- app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson({ gatekeeper })), 200, { "Content-Type": "application/json; charset=utf-8" }));
9523
- app.get("/docs/api", async (c) => c.html(await getDocsHtml({ gatekeeper }), 200));
10497
+ app.get("/v1/config/rules", async (c) => {
10498
+ try {
10499
+ const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
10500
+ return c.json(body, statusCode);
10501
+ } catch (err) {
10502
+ const failure$4 = failure(err);
10503
+ return c.json(failure$4.body, failure$4.statusCode);
10504
+ }
10505
+ });
10506
+ app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
10507
+ app.get("/docs/api", async (c) => c.html(await getDocsHtml(), 200));
9524
10508
  app.get("/docs", async (c) => c.html(await getIntegratorDocsHtml(), 200));
9525
10509
  serve({
9526
10510
  fetch: app.fetch,
@@ -9528,6 +10512,120 @@ function serve$1(parameters) {
9528
10512
  });
9529
10513
  }
9530
10514
 
10515
+ //#endregion
10516
+ //#region src/gatekeeper/Client.ts
10517
+ const DEFAULT_TIMEOUT_MS = 1e4;
10518
+ /**
10519
+ * Create an HTTP client for a gatekeeper service.
10520
+ * @param config - Gatekeeper client configuration. {@link ClientConfig}
10521
+ * @returns An HTTP-backed gatekeeper client. {@link GatekeeperClient}
10522
+ */
10523
+ function createHttpClient(config) {
10524
+ const fetchFn = config.fetchFn ?? fetch;
10525
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10526
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
10527
+ const baseHeaders = config.originSecret ? { "x-origin-verify": config.originSecret } : void 0;
10528
+ const request = async (path, init) => {
10529
+ const controller = new AbortController();
10530
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
10531
+ try {
10532
+ return await fetchFn(`${baseUrl}${path}`, {
10533
+ ...init,
10534
+ headers: mergeHeaders(baseHeaders, init.headers),
10535
+ signal: controller.signal
10536
+ });
10537
+ } finally {
10538
+ clearTimeout(timeout);
10539
+ }
10540
+ };
10541
+ const validate = async (body) => {
10542
+ const response = await request("/v1/validate", {
10543
+ method: "POST",
10544
+ headers: { "content-type": "application/json" },
10545
+ body: JSON.stringify(body)
10546
+ });
10547
+ const json = await response.json();
10548
+ return {
10549
+ statusCode: response.status,
10550
+ body: json
10551
+ };
10552
+ };
10553
+ const getConfigRules = async (query) => {
10554
+ const params = new URLSearchParams();
10555
+ if (query?.cursor) params.set("cursor", query.cursor);
10556
+ if (query?.limit !== void 0) params.set("limit", query.limit.toString());
10557
+ if (query?.types !== void 0) {
10558
+ const typesValue = Array.isArray(query.types) ? query.types.join(",") : query.types;
10559
+ if (typesValue.length > 0) params.set("types", typesValue);
10560
+ }
10561
+ const response = await request(params.size > 0 ? `/v1/config/rules?${params.toString()}` : "/v1/config/rules", { method: "GET" });
10562
+ const json = await response.json();
10563
+ return {
10564
+ statusCode: response.status,
10565
+ body: json
10566
+ };
10567
+ };
10568
+ const isAllowed = async (offers) => {
10569
+ const { statusCode, body } = await validate({ offers: offers.map((offer) => toSnakeCase(offer)) });
10570
+ if (statusCode !== 200) {
10571
+ const errorMessage = extractErrorMessage(body);
10572
+ throw new Error(`Gatekeeper validation failed: ${errorMessage ?? `status ${statusCode}`}`);
10573
+ }
10574
+ const data = body.data;
10575
+ if (!data || typeof data !== "object") throw new Error("Gatekeeper validation response is invalid.");
10576
+ if ("issues" in data) {
10577
+ const issues = data.issues.map((issue) => ({
10578
+ ruleName: issue.rule,
10579
+ message: issue.message,
10580
+ item: offers[issue.index]
10581
+ }));
10582
+ const invalidIndices = new Set(data.issues.map((issue) => issue.index));
10583
+ return {
10584
+ valid: offers.filter((_, index) => !invalidIndices.has(index)),
10585
+ issues
10586
+ };
10587
+ }
10588
+ if (!("payload" in data) || !("root" in data)) throw new Error("Gatekeeper validation response is missing payload data.");
10589
+ return {
10590
+ valid: offers.slice(),
10591
+ issues: []
10592
+ };
10593
+ };
10594
+ const getCallbackTypes = async (requestPayload) => {
10595
+ const response = await request("/v1/callbacks", {
10596
+ method: "POST",
10597
+ headers: { "content-type": "application/json" },
10598
+ body: JSON.stringify(requestPayload)
10599
+ });
10600
+ const json = await response.json();
10601
+ if (!response.ok) throw new Error(`Gatekeeper callbacks request failed: ${extractErrorMessage(json) ?? response.statusText}`);
10602
+ if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper callbacks response is invalid.");
10603
+ return json.data;
10604
+ };
10605
+ return {
10606
+ baseUrl,
10607
+ validate,
10608
+ getConfigRules,
10609
+ isAllowed,
10610
+ getCallbackTypes
10611
+ };
10612
+ }
10613
+ function mergeHeaders(base, extra) {
10614
+ if (!base && !extra) return void 0;
10615
+ const merged = new Headers(base ?? void 0);
10616
+ if (extra) for (const [key, value] of new Headers(extra).entries()) merged.set(key, value);
10617
+ return merged;
10618
+ }
10619
+ function normalizeBaseUrl(url) {
10620
+ return url.trim().replace(/\/+$/, "");
10621
+ }
10622
+ function extractErrorMessage(payload) {
10623
+ if (!payload || typeof payload !== "object") return void 0;
10624
+ const error = payload.error;
10625
+ if (!error || typeof error !== "object") return void 0;
10626
+ return typeof error.message === "string" ? error.message : void 0;
10627
+ }
10628
+
9531
10629
  //#endregion
9532
10630
  //#region src/cli/commands/start.ts
9533
10631
  const startCmd = new RouterCmd("start");
@@ -9538,9 +10636,10 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
9538
10636
  let gatekeeperUrl = gatekeeperConfig?.url;
9539
10637
  let gatekeeperHandle = null;
9540
10638
  if (!gatekeeperUrl) {
9541
- const gatekeeperCore = create$17({ rules: morphoRules(chainRegistry.list()) });
10639
+ const gatekeeperCore = create$20({ rules: morphoRules(chainRegistry.list()) });
9542
10640
  gatekeeperHandle = await start$1({
9543
10641
  gatekeeper: gatekeeperCore,
10642
+ chainRegistry,
9544
10643
  port: gatekeeperConfig?.port ?? 8082
9545
10644
  });
9546
10645
  gatekeeperUrl = gatekeeperHandle.url;
@@ -9552,72 +10651,36 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
9552
10651
  }
9553
10652
  const gatekeeper = createHttpClient({
9554
10653
  baseUrl: gatekeeperUrl ?? "http://localhost:8082",
9555
- timeoutMs: gatekeeperConfig?.timeoutMs
10654
+ timeoutMs: gatekeeperConfig?.timeoutMs,
10655
+ originSecret: gatekeeperConfig?.originSecret
9556
10656
  });
9557
10657
  if (mock !== void 0 && mock > 0) {
9558
- const configuredChains = chainRegistry.list();
9559
- const tokens = {
9560
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": 6,
9561
- "0x6B175474E89094C44Da98b954EedeAC495271d0F": 18,
9562
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": 18,
9563
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": 8,
9564
- "0x4200000000000000000000000000000000000006": 18,
9565
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 6,
9566
- "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": 18
9567
- };
9568
- const now = Math.floor(Date.now() / 1e3);
9569
- const offerBlockNumber = 1;
9570
- const positionBlockNumber = offerBlockNumber + 1;
9571
- const start = Math.max(0, now - 60);
9572
- const expiry = now + 3600;
9573
- const maturity = from$16("end_of_next_month");
9574
- const oraclePrice = 10n ** 36n;
9575
- const offers = createMockOffers({
9576
- count: mock,
9577
- chains: configuredChains,
9578
- loanTokens: Object.keys(tokens).map((key) => key),
9579
- assetsDecimals: tokens,
9580
- start,
9581
- expiry,
9582
- maturity,
9583
- chainRegistry
10658
+ const offers = await seedMockOffers({
10659
+ db,
10660
+ gatekeeper,
10661
+ chainRegistry,
10662
+ count: mock
9584
10663
  });
9585
- await db.offers.create([{
9586
- blockNumber: offerBlockNumber,
9587
- offers
9588
- }]);
9589
- await db.oracles.upsert(seedMockOracles(offers, oraclePrice, positionBlockNumber));
9590
- for (const offer of offers) {
9591
- if (offer.callback.data === "0x") continue;
9592
- const chain = chainRegistry.getById(offer.chainId);
9593
- if (!chain) continue;
9594
- const callbackType = getCallbackType(chain.name, offer.callback.address);
9595
- if (!callbackType) continue;
9596
- const callbacks = decode$1(callbackType, offer.callback.data);
9597
- for (const cb of callbacks) {
9598
- const positionAsset = callbackType === Type$1.SellERC20Callback ? cb.contract : offer.loanToken;
9599
- await db.positions.upsert([from$9({
9600
- chainId: offer.chainId,
9601
- contract: cb.contract,
9602
- user: offer.maker,
9603
- type: callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20,
9604
- balance: cb.amount * 2n,
9605
- asset: positionAsset,
9606
- blockNumber: positionBlockNumber
9607
- })]);
9608
- }
9609
- }
9610
10664
  logger.info({
9611
10665
  msg: `Offers seeded`,
9612
- count: mock
10666
+ count: offers.length
9613
10667
  });
9614
10668
  }
9615
10669
  if (file) {
9616
10670
  const offers = await getOffersFromFile(file);
9617
- await db.offers.create([{
9618
- blockNumber: 1,
9619
- offers
9620
- }]);
10671
+ const offerBlockNumber = 1;
10672
+ const positionBlockNumber = offerBlockNumber + 1;
10673
+ await seedOffers({
10674
+ db,
10675
+ offers,
10676
+ blockNumber: offerBlockNumber
10677
+ });
10678
+ await seedOfferAssociations({
10679
+ db,
10680
+ gatekeeper,
10681
+ offers,
10682
+ blockNumber: positionBlockNumber
10683
+ });
9621
10684
  logger.info({
9622
10685
  msg: `Offers seeded from file`,
9623
10686
  count: offers.length,
@@ -9669,32 +10732,127 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
9669
10732
  process.stdin.resume();
9670
10733
  });
9671
10734
  });
10735
+ /**
10736
+ * Seed mock offers and their dependencies for the router API.
10737
+ * @param parameters - Seed parameters. {@link seedMockOffers.Parameters}
10738
+ * @returns The generated mock offers. {@link seedMockOffers.ReturnType}
10739
+ */
10740
+ async function seedMockOffers(parameters) {
10741
+ const { db, gatekeeper, chainRegistry, count } = parameters;
10742
+ if (count <= 0) return [];
10743
+ const configuredChains = chainRegistry.list();
10744
+ const assetsDecimals = {
10745
+ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": 6,
10746
+ "0x6b175474e89094c44da98b954eedeac495271d0f": 18,
10747
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": 18,
10748
+ "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": 8,
10749
+ "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c": 6,
10750
+ "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0": 18,
10751
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
10752
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
10753
+ "0x4200000000000000000000000000000000000006": 18,
10754
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
10755
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18,
10756
+ "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42": 6,
10757
+ "0xce79ddb3152d52ff8fe65a4c7e058b035fcb560a": 18
10758
+ };
10759
+ const assetsByChainId = {};
10760
+ const oraclesByChainId = {};
10761
+ for (const chain of configuredChains) {
10762
+ const assets$1 = assets[chain.id.toString()]?.map((asset) => asset.toLowerCase()) ?? [];
10763
+ if (assets$1.length === 0) throw new Error(`No gatekeeper assets configured for chain ${chain.id}`);
10764
+ const missingDecimals = assets$1.filter((asset) => assetsDecimals[asset] === void 0);
10765
+ if (missingDecimals.length > 0) throw new Error(`Missing decimals for assets on chain ${chain.id}: ${missingDecimals.join(", ")}`);
10766
+ assetsByChainId[chain.id] = assets$1;
10767
+ const oracles = oracles$1[chain.id.toString()]?.map((oracle) => oracle.toLowerCase()) ?? [];
10768
+ if (oracles.length === 0) throw new Error(`No gatekeeper oracles configured for chain ${chain.id}`);
10769
+ oraclesByChainId[chain.id] = oracles;
10770
+ }
10771
+ const now = Math.floor(Date.now() / 1e3);
10772
+ const offerBlockNumber = 1;
10773
+ const positionBlockNumber = offerBlockNumber + 1;
10774
+ const start = Math.max(0, now - 60);
10775
+ const expiry = now + 3600;
10776
+ const maturity = from$16("end_of_next_month");
10777
+ const oraclePrice = 10n ** 36n;
10778
+ const offers = createMockOffers({
10779
+ count,
10780
+ chains: configuredChains,
10781
+ assetsByChainId,
10782
+ oraclesByChainId,
10783
+ assetsDecimals,
10784
+ start,
10785
+ expiry,
10786
+ maturity,
10787
+ chainRegistry
10788
+ });
10789
+ await seedOffers({
10790
+ db,
10791
+ offers,
10792
+ blockNumber: offerBlockNumber
10793
+ });
10794
+ await db.oracles.upsert(seedMockOracles(offers, oraclePrice, positionBlockNumber));
10795
+ await seedOfferAssociations({
10796
+ db,
10797
+ gatekeeper,
10798
+ offers,
10799
+ blockNumber: positionBlockNumber
10800
+ });
10801
+ return offers;
10802
+ }
9672
10803
  async function getOffersFromFile(filePath) {
9673
10804
  const content = await readFile(filePath, "utf-8");
9674
10805
  const data = JSON.parse(content);
9675
- return Array.isArray(data) ? data.map(from$11) : [from$11(data)];
10806
+ return Array.isArray(data) ? data.map(from$12) : [from$12(data)];
9676
10807
  }
9677
10808
  function createMockOffers(parameters) {
9678
- const { count, chains, loanTokens, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
10809
+ const { count, chains, assetsByChainId, oraclesByChainId, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
10810
+ if (chains.length === 0) throw new Error("No chains configured for mock offers");
9679
10811
  return Array.from({ length: count }, () => {
9680
10812
  const buy = Math.random() < .5;
10813
+ const chain = chains[Math.floor(Math.random() * chains.length)];
10814
+ const allowedAssets = assetsByChainId[chain.id] ?? [];
10815
+ if (allowedAssets.length === 0) throw new Error(`No allowed assets configured for chain ${chain.id}`);
10816
+ const loanToken = allowedAssets[Math.floor(Math.random() * allowedAssets.length)];
9681
10817
  const offer = random({
9682
- chains,
9683
- loanTokens,
10818
+ chains: [chain],
10819
+ loanTokens: [loanToken],
9684
10820
  assetsDecimals,
9685
10821
  start,
9686
10822
  expiry,
9687
10823
  maturity,
9688
10824
  buy
9689
10825
  });
9690
- const chain = chainRegistry.getById(offer.chainId);
9691
- if (!chain) throw new Error(`Missing chain config for id ${offer.chainId}`);
10826
+ const allowedOracles = oraclesByChainId[chain.id] ?? [];
10827
+ if (allowedOracles.length === 0) throw new Error(`No allowed oracles configured for chain ${chain.id}`);
10828
+ const collateralAssets = allowedAssets.filter((asset) => asset !== loanToken);
10829
+ if (collateralAssets.length === 0) throw new Error(`No collateral assets available for chain ${chain.id}`);
10830
+ const collateralCount = Math.min(collateralAssets.length, 1 + Math.floor(Math.random() * 3));
10831
+ const collateralPool = [...collateralAssets];
10832
+ const collateralSelections = [];
10833
+ while (collateralSelections.length < collateralCount && collateralPool.length > 0) {
10834
+ const index = Math.floor(Math.random() * collateralPool.length);
10835
+ const [value] = collateralPool.splice(index, 1);
10836
+ if (value !== void 0) collateralSelections.push(value);
10837
+ }
10838
+ collateralSelections.sort();
10839
+ const lltv = offer.collaterals[0]?.lltv ?? .86;
10840
+ const collaterals = collateralSelections.map((asset) => from$14({
10841
+ asset,
10842
+ oracle: allowedOracles[Math.floor(Math.random() * allowedOracles.length)],
10843
+ lltv
10844
+ }));
10845
+ if (!chainRegistry.getById(offer.chainId)) throw new Error(`Missing chain config for id ${offer.chainId}`);
9692
10846
  const callbackType = buy ? Type$1.BuyVaultV1Callback : Type$1.SellERC20Callback;
9693
- const callbackAddress = getCallbackTypeAddresses(chain.name, callbackType)[0];
9694
- if (!callbackAddress) throw new Error(`Missing callback addresses for ${chain.name} (${buy ? "buy" : "sell"} offers)`);
9695
- const callbackData = buildMockCallbackData(callbackType, offer);
9696
- return from$11({
10847
+ const callbackAddress = buy ? BUY_CALLBACK_ADDRESS : SELL_CALLBACK_ADDRESS;
10848
+ const callbackData = buildMockCallbackData(callbackType, {
10849
+ ...offer,
10850
+ collaterals
10851
+ });
10852
+ return from$12({
9697
10853
  ...offer,
10854
+ loanToken,
10855
+ collaterals,
9698
10856
  callback: {
9699
10857
  address: callbackAddress,
9700
10858
  data: callbackData
@@ -9719,7 +10877,7 @@ function seedMockOracles(offers, price, blockNumber) {
9719
10877
  for (const offer of offers) for (const collateral of offer.collaterals) {
9720
10878
  const key = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
9721
10879
  if (oracleMap.has(key)) continue;
9722
- oracleMap.set(key, from$10({
10880
+ oracleMap.set(key, from$11({
9723
10881
  chainId: offer.chainId,
9724
10882
  address: collateral.oracle,
9725
10883
  price: price.toString(),
@@ -9728,6 +10886,158 @@ function seedMockOracles(offers, price, blockNumber) {
9728
10886
  }
9729
10887
  return Array.from(oracleMap.values());
9730
10888
  }
10889
+ const BUY_CALLBACK_ADDRESS = "0x3333333333333333333333333333333333333333";
10890
+ const SELL_CALLBACK_ADDRESS = "0x1111111111111111111111111111111111111111";
10891
+ async function seedOffers(parameters) {
10892
+ const { db, offers, blockNumber } = parameters;
10893
+ if (offers.length === 0) return;
10894
+ const dependencies = buildOfferDependencies({
10895
+ offers,
10896
+ blockNumber
10897
+ });
10898
+ await db.oracles.upsert(dependencies.oracles);
10899
+ await db.obligations.create(dependencies.obligations);
10900
+ await db.groups.create(dependencies.groups);
10901
+ await db.offers.create([{
10902
+ blockNumber,
10903
+ offers
10904
+ }]);
10905
+ }
10906
+ async function seedOfferAssociations(parameters) {
10907
+ const { db, gatekeeper, offers, blockNumber } = parameters;
10908
+ if (offers.length === 0) return;
10909
+ const { callbacks, positions, lots } = buildOfferAssociationsFromOffers({
10910
+ offers,
10911
+ callbackTypes: await resolveCallbackTypes({
10912
+ gatekeeper,
10913
+ offers
10914
+ }),
10915
+ blockNumber
10916
+ });
10917
+ if (positions.length > 0) await db.positions.upsert(positions);
10918
+ if (callbacks.length > 0) await db.callbacks.upsert(callbacks);
10919
+ if (lots.length > 0) await db.lots.create(lots);
10920
+ }
10921
+ function buildOfferDependencies(parameters) {
10922
+ const { offers, blockNumber } = parameters;
10923
+ const obligationsById = /* @__PURE__ */ new Map();
10924
+ const oraclesByKey = /* @__PURE__ */ new Map();
10925
+ const groupsByKey = /* @__PURE__ */ new Map();
10926
+ for (const offer of offers) {
10927
+ const obligationId$1 = obligationId(offer);
10928
+ if (!obligationsById.get(obligationId$1)) obligationsById.set(obligationId$1, from$13({
10929
+ chainId: offer.chainId,
10930
+ loanToken: offer.loanToken,
10931
+ maturity: offer.maturity,
10932
+ collaterals: offer.collaterals
10933
+ }));
10934
+ for (const collateral of offer.collaterals) {
10935
+ const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
10936
+ if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
10937
+ chainId: offer.chainId,
10938
+ address: collateral.oracle,
10939
+ price: null,
10940
+ blockNumber
10941
+ }));
10942
+ }
10943
+ const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
10944
+ if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
10945
+ chainId: offer.chainId,
10946
+ maker: offer.maker,
10947
+ group: offer.group,
10948
+ blockNumber
10949
+ });
10950
+ }
10951
+ return {
10952
+ obligations: Array.from(obligationsById.values()),
10953
+ oracles: Array.from(oraclesByKey.values()),
10954
+ groups: Array.from(groupsByKey.values())
10955
+ };
10956
+ }
10957
+ async function resolveCallbackTypes(parameters) {
10958
+ const { gatekeeper, offers } = parameters;
10959
+ const addressesByChain = /* @__PURE__ */ new Map();
10960
+ for (const offer of offers) {
10961
+ if (offer.callback.data === "0x") continue;
10962
+ const set = addressesByChain.get(offer.chainId) ?? /* @__PURE__ */ new Set();
10963
+ set.add(offer.callback.address.toLowerCase());
10964
+ addressesByChain.set(offer.chainId, set);
10965
+ }
10966
+ if (addressesByChain.size === 0) return /* @__PURE__ */ new Map();
10967
+ const request = { callbacks: Array.from(addressesByChain.entries()).map(([chainId, addresses]) => ({
10968
+ chain_id: chainId,
10969
+ addresses: Array.from(addresses)
10970
+ })) };
10971
+ const response = await gatekeeper.getCallbackTypes(request);
10972
+ const typeByAddress = /* @__PURE__ */ new Map();
10973
+ for (const entry of response) {
10974
+ const chainId = entry.chain_id;
10975
+ for (const [key, list] of Object.entries(entry)) {
10976
+ if (key === "chain_id" || key === "not_supported") continue;
10977
+ if (!Array.isArray(list)) continue;
10978
+ for (const address of list) {
10979
+ const mapKey = `${chainId}-${address.toLowerCase()}`;
10980
+ typeByAddress.set(mapKey, key);
10981
+ }
10982
+ }
10983
+ }
10984
+ return typeByAddress;
10985
+ }
10986
+ function buildOfferAssociationsFromOffers(parameters) {
10987
+ const { offers, callbackTypes, blockNumber } = parameters;
10988
+ const callbacks = [];
10989
+ const positions = [];
10990
+ const lots = [];
10991
+ for (const offer of offers) {
10992
+ if (offer.callback.data === "0x") continue;
10993
+ const key = `${offer.chainId}-${offer.callback.address.toLowerCase()}`;
10994
+ const callbackType = callbackTypes.get(key);
10995
+ if (!callbackType) continue;
10996
+ let decoded;
10997
+ try {
10998
+ decoded = decode$1(callbackType, offer.callback.data);
10999
+ } catch (err) {
11000
+ const error = err instanceof Error ? err : new Error(String(err));
11001
+ throw new Error("Failed to decode callback data", { cause: error });
11002
+ }
11003
+ if (decoded.length === 0) continue;
11004
+ callbacks.push({
11005
+ offerHash: hash(offer),
11006
+ callbacks: decoded.map((callback) => ({
11007
+ chainId: offer.chainId,
11008
+ contract: callback.contract,
11009
+ user: offer.maker,
11010
+ amount: callback.amount
11011
+ }))
11012
+ });
11013
+ for (const callback of decoded) {
11014
+ const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
11015
+ const positionAsset = callbackType === Type$1.BuyVaultV1Callback ? offer.loanToken : callback.contract;
11016
+ positions.push(from$10({
11017
+ chainId: offer.chainId,
11018
+ contract: callback.contract,
11019
+ user: offer.maker,
11020
+ type: positionType,
11021
+ balance: callback.amount * 2n,
11022
+ asset: positionAsset,
11023
+ blockNumber
11024
+ }));
11025
+ const isLoanPosition = positionAsset.toLowerCase() === offer.loanToken.toLowerCase();
11026
+ lots.push({
11027
+ positionChainId: offer.chainId,
11028
+ positionContract: callback.contract,
11029
+ positionUser: offer.maker,
11030
+ group: offer.group,
11031
+ size: isLoanPosition ? offer.assets : callback.amount
11032
+ });
11033
+ }
11034
+ }
11035
+ return {
11036
+ callbacks,
11037
+ positions,
11038
+ lots
11039
+ };
11040
+ }
9731
11041
 
9732
11042
  //#endregion
9733
11043
  //#region src/cli/cli.ts