@morpho-dev/router 0.5.0 → 0.6.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.6.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
@@ -1836,7 +1819,7 @@ const Oracle = [{
1836
1819
  * @param chains - Array of chain objects to register.
1837
1820
  * @returns A registry for looking up chains by ID. {@link ChainRegistry}
1838
1821
  */
1839
- function create$16(chains) {
1822
+ function create$19(chains) {
1840
1823
  const byId = /* @__PURE__ */ new Map();
1841
1824
  for (const chain of chains) byId.set(chain.id, chain);
1842
1825
  return {
@@ -2158,196 +2141,6 @@ var CollateralsAreNotSortedError = class extends BaseError {
2158
2141
  }
2159
2142
  };
2160
2143
 
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
2144
  //#endregion
2352
2145
  //#region src/core/Offer.ts
2353
2146
  /** Internal symbol for caching the computed hash. */
@@ -2399,7 +2192,7 @@ const OfferSchema = () => {
2399
2192
  * @param input - The offer to create.
2400
2193
  * @returns The created offer.
2401
2194
  */
2402
- function from$11(input) {
2195
+ function from$12(input) {
2403
2196
  try {
2404
2197
  return OfferSchema().parse(input);
2405
2198
  } catch (error) {
@@ -2413,7 +2206,7 @@ function from$11(input) {
2413
2206
  * @returns The created offer.
2414
2207
  */
2415
2208
  function fromSnakeCase(input) {
2416
- return from$11(fromSnakeCase$1(input));
2209
+ return from$12(fromSnakeCase$1(input));
2417
2210
  }
2418
2211
  /**
2419
2212
  * Converts an offer to a snake case object.
@@ -2507,7 +2300,7 @@ function random(config) {
2507
2300
  })
2508
2301
  };
2509
2302
  })();
2510
- return from$11({
2303
+ return from$12({
2511
2304
  maker: config?.maker ?? address(),
2512
2305
  assets: assetsScaled,
2513
2306
  obligationUnits: config?.obligationUnits ?? 0n,
@@ -2738,7 +2531,7 @@ var InvalidOfferError = class InvalidOfferError extends BaseError {
2738
2531
  * @param data - The data to create the oracle from.
2739
2532
  * @returns The created oracle.
2740
2533
  */
2741
- function from$10(data) {
2534
+ function from$11(data) {
2742
2535
  return {
2743
2536
  chainId: data.chainId,
2744
2537
  address: data.address.toLowerCase(),
@@ -2772,7 +2565,7 @@ let Type = /* @__PURE__ */ function(Type) {
2772
2565
  * @param parameters - {@link from.Parameters}
2773
2566
  * @returns The created Position. {@link from.ReturnType}
2774
2567
  */
2775
- function from$9(parameters) {
2568
+ function from$10(parameters) {
2776
2569
  return {
2777
2570
  chainId: parameters.chainId,
2778
2571
  contract: parameters.contract.toLowerCase(),
@@ -2803,7 +2596,7 @@ const QuoteSchema = z$2.object({
2803
2596
  * const quote = Quote.from({ obligationId: "0x123", ask: { price: 100n }, bid: { price: 100n } });
2804
2597
  * ```
2805
2598
  */
2806
- function from$8(parameters) {
2599
+ function from$9(parameters) {
2807
2600
  try {
2808
2601
  const parsedQuote = QuoteSchema.parse(parameters);
2809
2602
  return {
@@ -2841,7 +2634,7 @@ const WAD = 10n ** 18n;
2841
2634
  * const transfer = Transfer.from({ id: "1", chainId: 1, contract: "0x123", from: "0x456", to: "0x789", value: 100n, blockNumber: 100n });
2842
2635
  * ```
2843
2636
  */
2844
- function from$7(parameters) {
2637
+ function from$8(parameters) {
2845
2638
  return {
2846
2639
  id: parameters.id,
2847
2640
  chainId: parameters.chainId,
@@ -2854,51 +2647,288 @@ function from$7(parameters) {
2854
2647
  }
2855
2648
 
2856
2649
  //#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
- });
2650
+ //#region src/core/Tree.ts
2651
+ const VERSION$1 = 1;
2877
2652
  /**
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.
2653
+ * EIP-712 types for signing the tree root (Root(bytes32 root)).
2881
2654
  */
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
- });
2655
+ const signatureTypes = {
2656
+ EIP712Domain: [{
2657
+ name: "chainId",
2658
+ type: "uint256"
2659
+ }, {
2660
+ name: "verifyingContract",
2661
+ type: "address"
2662
+ }],
2663
+ Root: [{
2664
+ name: "root",
2665
+ type: "bytes32"
2666
+ }]
2667
+ };
2668
+ const normalizeHash = (hash) => hash.toLowerCase();
2888
2669
  /**
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.
2670
+ * Builds a Merkle tree from a list of offers.
2671
+ *
2672
+ * Leaves are the offer `hash` values as `bytes32` and are deterministically
2673
+ * ordered following the StandardMerkleTree leaf ordering so that the resulting
2674
+ * root is stable regardless of the input order.
2675
+ *
2676
+ * @param offers - Offers to include in the tree.
2677
+ * @returns A `StandardMerkleTree` of `bytes32` leaves representing the offers.
2678
+ * @throws {TreeError} If tree building fails due to offer inconsistencies.
2892
2679
  */
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();
2897
- for (let i = 1; i < offers.length; i++) {
2898
- const offer = offers[i];
2899
- if (offer.maker.toLowerCase() !== firstMaker) {
2900
- issues.set(i, { message: `Offer has different maker ${offer.maker} than first offer ${offers[0].maker}` });
2901
- return issues;
2680
+ const from$7 = (offers) => {
2681
+ const leaves = offers.map((offer) => [hash(offer)]);
2682
+ const tree = StandardMerkleTree.of(leaves, ["bytes32"]);
2683
+ const orderedOffers = orderOffers(tree, offers);
2684
+ return Object.assign(tree, { offers: orderedOffers });
2685
+ };
2686
+ const orderOffers = (tree, offers) => {
2687
+ const offerByHash = /* @__PURE__ */ new Map();
2688
+ for (const offer of offers) offerByHash.set(normalizeHash(hash(offer)), offer);
2689
+ const entries = tree.dump().values.map((value) => {
2690
+ const hash = normalizeHash(value.value[0]);
2691
+ const offer = offerByHash.get(hash);
2692
+ if (!offer) throw new TreeError(`missing offer for leaf ${hash}`);
2693
+ return {
2694
+ offer,
2695
+ treeIndex: value.treeIndex
2696
+ };
2697
+ });
2698
+ entries.sort((a, b) => b.treeIndex - a.treeIndex);
2699
+ return entries.map((item) => item.offer);
2700
+ };
2701
+ /**
2702
+ * Generates merkle proofs for all offers in a tree.
2703
+ *
2704
+ * Each proof allows independent verification that an offer is included in the tree
2705
+ * without requiring the full tree. Proofs are ordered by StandardMerkleTree leaf ordering.
2706
+ *
2707
+ * @param tree - The {@link Tree} to generate proofs for.
2708
+ * @returns Array of proofs - {@link Proof}
2709
+ */
2710
+ const proofs = (tree) => {
2711
+ return tree.offers.map((offer) => {
2712
+ return {
2713
+ offer,
2714
+ path: tree.getProof([hash(offer)])
2715
+ };
2716
+ });
2717
+ };
2718
+ const normalizeSignatureDomain = (domain, errorFactory) => {
2719
+ let chainId;
2720
+ try {
2721
+ chainId = typeof domain.chainId === "bigint" ? domain.chainId : BigInt(domain.chainId);
2722
+ } catch {
2723
+ throw errorFactory("invalid chainId");
2724
+ }
2725
+ if (chainId < 0n) throw errorFactory("invalid chainId");
2726
+ if (!isAddress(domain.verifyingContract)) throw errorFactory("invalid verifyingContract");
2727
+ return {
2728
+ chainId,
2729
+ verifyingContract: domain.verifyingContract.toLowerCase()
2730
+ };
2731
+ };
2732
+ const assertHex = (value, expectedBytes, name, errorFactory = (reason) => new DecodeError(reason)) => {
2733
+ if (typeof value !== "string" || !isHex(value)) throw errorFactory(`${name} is not a valid hex string`);
2734
+ if (hexToBytes(value).length !== expectedBytes) throw errorFactory(`${name}: expected ${expectedBytes} bytes`);
2735
+ };
2736
+ const verifySignatureAndRecoverAddress = async (params) => {
2737
+ const { root, signature, domain, errorFactory } = params;
2738
+ assertHex(root, 32, "root", errorFactory);
2739
+ assertHex(signature, 65, "signature", errorFactory);
2740
+ const hash = hashTypedData({
2741
+ domain,
2742
+ types: signatureTypes,
2743
+ primaryType: "Root",
2744
+ message: { root }
2745
+ });
2746
+ try {
2747
+ return await recoverAddress({
2748
+ hash,
2749
+ signature
2750
+ });
2751
+ } catch {
2752
+ throw errorFactory("signature recovery failed");
2753
+ }
2754
+ };
2755
+ /**
2756
+ * Encodes a merkle tree without a signature into hex payload for client-side signing.
2757
+ *
2758
+ * Layout: `0x{vv}{gzip([...offers])}{root}` where:
2759
+ * - `{vv}`: 1-byte version (currently 0x01)
2760
+ * - `{gzip([...offers])}`: gzipped JSON array of serialized offers
2761
+ * - `{root}`: 32-byte merkle root
2762
+ *
2763
+ * Validates root integrity before encoding.
2764
+ *
2765
+ * @param tree - Merkle tree of offers
2766
+ * @returns Hex-encoded unsigned payload
2767
+ * @throws {EncodeError} If root mismatch
2768
+ */
2769
+ const encodeUnsigned = (tree) => {
2770
+ validateTreeForEncoding(tree);
2771
+ return bytesToHex(encodeUnsignedBytes(tree));
2772
+ };
2773
+ const validateTreeForEncoding = (tree, domain) => {
2774
+ if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
2775
+ const computed = from$7(tree.offers);
2776
+ if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
2777
+ if (domain) {
2778
+ const mismatched = tree.offers.find((offer) => BigInt(offer.chainId) !== domain.chainId);
2779
+ if (mismatched) throw new EncodeError(`chainId mismatch: expected ${domain.chainId}, got ${mismatched.chainId}`);
2780
+ }
2781
+ };
2782
+ const encodeUnsignedBytes = (tree) => {
2783
+ const offersPayload = tree.offers.map(serialize);
2784
+ const compressed = gzip(JSON.stringify(offersPayload));
2785
+ const rootBytes = hexToBytes(tree.root);
2786
+ const encoded = new Uint8Array(1 + compressed.length + 32);
2787
+ encoded[0] = VERSION$1;
2788
+ encoded.set(compressed, 1);
2789
+ encoded.set(rootBytes, 1 + compressed.length);
2790
+ return encoded;
2791
+ };
2792
+ /**
2793
+ * Decodes hex calldata into a validated merkle tree.
2794
+ *
2795
+ * Validates signature before decompression for fail-fast rejection of invalid payloads.
2796
+ * Returns the tree with separately validated signature and recovered signer address.
2797
+ *
2798
+ * Validation order:
2799
+ * 1. Version check
2800
+ * 2. Signature verification (fail-fast, before decompression)
2801
+ * 3. Decompression (only if signature valid)
2802
+ * 4. Root verification (computed from offers vs embedded root)
2803
+ *
2804
+ * @example
2805
+ * ```typescript
2806
+ * const { tree, signature, signer } = await Tree.decode(calldata, { chainId, verifyingContract });
2807
+ * console.log(`Tree signed by ${signer} with ${tree.offers.length} offers`);
2808
+ * ```
2809
+ *
2810
+ * @param encoded - Hex calldata in format `0x{vv}{gzip}{root}{signature}`
2811
+ * @param domain - EIP-712 domain with chain id and verifying contract
2812
+ * @returns Validated tree, signature, and recovered signer address
2813
+ * @throws {DecodeError} If version invalid, signature invalid, or root mismatch
2814
+ */
2815
+ const decode = async (encoded, domain) => {
2816
+ const errorFactory = (reason) => new DecodeError(reason);
2817
+ const normalizedDomain = normalizeSignatureDomain(domain, errorFactory);
2818
+ const bytes = hexToBytes(encoded);
2819
+ if (bytes.length < 98) throw new DecodeError("payload too short");
2820
+ const version = bytes[0];
2821
+ if (version !== (VERSION$1 & 255)) throw new DecodeError(`invalid version: expected ${VERSION$1}, got ${version ?? 0}`);
2822
+ const signature = bytesToHex(bytes.slice(-65));
2823
+ const root = bytesToHex(bytes.slice(-97, -65));
2824
+ assertHex(root, 32, "root");
2825
+ assertHex(signature, 65, "signature");
2826
+ const signer = await verifySignatureAndRecoverAddress({
2827
+ root,
2828
+ signature,
2829
+ domain: normalizedDomain,
2830
+ errorFactory
2831
+ });
2832
+ const compressed = bytes.slice(1, -97);
2833
+ let decoded;
2834
+ try {
2835
+ decoded = ungzip(compressed, { to: "string" });
2836
+ } catch {
2837
+ throw new DecodeError("decompression failed");
2838
+ }
2839
+ let rawOffers;
2840
+ try {
2841
+ rawOffers = JSON.parse(decoded);
2842
+ } catch {
2843
+ throw new DecodeError("JSON parse failed");
2844
+ }
2845
+ const tree = from$7(rawOffers.map((o) => OfferSchema().parse(o)));
2846
+ if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
2847
+ const chainIdMismatch = tree.offers.find((offer) => BigInt(offer.chainId) !== normalizedDomain.chainId);
2848
+ if (chainIdMismatch) throw new DecodeError(`chainId mismatch: expected ${normalizedDomain.chainId}, got ${chainIdMismatch.chainId}`);
2849
+ return {
2850
+ tree,
2851
+ signature,
2852
+ signer
2853
+ };
2854
+ };
2855
+ /**
2856
+ * Error thrown during tree building operations.
2857
+ * Indicates structural issues with the tree (missing offers, inconsistent state).
2858
+ */
2859
+ var TreeError = class extends BaseError {
2860
+ name = "Tree.TreeError";
2861
+ constructor(reason) {
2862
+ super(`Tree error: ${reason}`);
2863
+ }
2864
+ };
2865
+ /**
2866
+ * Error thrown during tree encoding.
2867
+ * Indicates validation failures (signature, root mismatch, mixed makers).
2868
+ */
2869
+ var EncodeError = class extends BaseError {
2870
+ name = "Tree.EncodeError";
2871
+ constructor(reason) {
2872
+ super(`Failed to encode tree: ${reason}`);
2873
+ }
2874
+ };
2875
+ /**
2876
+ * Error thrown during tree decoding.
2877
+ * Indicates payload corruption, version mismatch, or validation failures.
2878
+ */
2879
+ var DecodeError = class extends BaseError {
2880
+ name = "Tree.DecodeError";
2881
+ constructor(reason) {
2882
+ super(`Failed to decode tree: ${reason}`);
2883
+ }
2884
+ };
2885
+
2886
+ //#endregion
2887
+ //#region src/core/types.ts
2888
+ const BrandTypeId = Symbol.for("mempool/Brand");
2889
+
2890
+ //#endregion
2891
+ //#region src/gatekeeper/Rules.ts
2892
+ const chains$1 = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
2893
+ const allowedChainIds = chains.map((c) => c.id);
2894
+ if (!allowedChainIds.some((id) => id === offer.chainId)) return { message: `Chain ID ${offer.chainId} is not in the allowed chains (${allowedChainIds.join(", ")})` };
2895
+ });
2896
+ const maturity = ({ maturities }) => single("maturity", `Validates that offer maturity is one of: [${maturities.join(", ")}]`, (offer) => {
2897
+ const allowedMaturities = maturities.map((m) => from$16(m));
2898
+ 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}` };
2899
+ });
2900
+ 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) => {
2901
+ if (isEmptyCallback(offer) && offer.buy && !callbacks?.find((c) => c === Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
2902
+ if (isEmptyCallback(offer) && !offer.buy) return { message: "Sell offers require a non-empty callback." };
2903
+ if (!isEmptyCallback(offer)) {
2904
+ if (!allowedAddresses.includes(offer.callback.address?.toLowerCase())) return { message: `Callback address ${offer.callback.address} is not allowed.` };
2905
+ }
2906
+ });
2907
+ /**
2908
+ * A validation rule that checks if the offer's tokens are allowed for its chain.
2909
+ * @param assetsByChainId - Allowed assets indexed by chain id.
2910
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
2911
+ */
2912
+ const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
2913
+ const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
2914
+ if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
2915
+ if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
2916
+ if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
2917
+ });
2918
+ /**
2919
+ * A batch validation rule that ensures all offers in a tree have the same maker address.
2920
+ * Returns an issue only for the first non-conforming offer.
2921
+ * This rule is signing-agnostic; signer verification is handled at the collector level.
2922
+ */
2923
+ const sameMaker = () => batch("mixed_maker", "Validates that all offers in a batch have the same maker address", (offers) => {
2924
+ const issues = /* @__PURE__ */ new Map();
2925
+ if (offers.length === 0) return issues;
2926
+ const firstMaker = offers[0].maker.toLowerCase();
2927
+ for (let i = 1; i < offers.length; i++) {
2928
+ const offer = offers[i];
2929
+ if (offer.maker.toLowerCase() !== firstMaker) {
2930
+ issues.set(i, { message: `Offer has different maker ${offer.maker} than first offer ${offers[0].maker}` });
2931
+ return issues;
2902
2932
  }
2903
2933
  }
2904
2934
  return issues;
@@ -2935,171 +2965,78 @@ const morphoRules = (chains) => {
2935
2965
  };
2936
2966
 
2937
2967
  //#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
2989
- }
2990
- };
2991
- }
2968
+ //#region src/gatekeeper/ConfigRules.ts
2992
2969
  /**
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.
2970
+ * Build the configured rules (maturities + callback addresses + loan tokens) for the provided chains.
2971
+ * @param chains - Chains to include in the configured rules.
2972
+ * @returns Sorted list of config rules.
2995
2973
  */
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
- }
3012
- }
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;
3027
- }
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;
2974
+ function buildConfigRules(chains) {
2975
+ const rules = [];
2976
+ for (const chain of chains) {
2977
+ const config = configs[chain.name];
2978
+ const maturities = config.maturities ?? [];
2979
+ for (const maturityName of maturities) rules.push({
2980
+ type: "maturity",
2981
+ chain_id: chain.id,
2982
+ name: maturityName,
2983
+ timestamp: from$16(maturityName)
2984
+ });
2985
+ const callbacks = config.callbacks ?? [];
2986
+ for (const callback of callbacks) {
2987
+ if (callback.type === Type$1.BuyWithEmptyCallback) continue;
2988
+ if (!("addresses" in callback)) continue;
2989
+ for (const address of callback.addresses) rules.push({
2990
+ type: "callback",
2991
+ chain_id: chain.id,
2992
+ address: normalizeAddress(address),
2993
+ callback_type: callback.type
2994
+ });
3061
2995
  }
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;
2996
+ const loanTokens = assets[chain.id.toString()] ?? [];
2997
+ for (const address of loanTokens) rules.push({
2998
+ type: "loan_token",
2999
+ chain_id: chain.id,
3000
+ address: normalizeAddress(address)
3001
+ });
3093
3002
  }
3094
- try {
3095
- return stringify(value);
3096
- } catch {
3097
- try {
3098
- return JSON.stringify(value);
3099
- } catch {
3100
- return String(value);
3003
+ rules.sort(compareConfigRules);
3004
+ return rules;
3005
+ }
3006
+ /**
3007
+ * Compute a stable checksum for the provided configured rules.
3008
+ * @param rules - Configured rules to checksum.
3009
+ * @returns MD5 checksum.
3010
+ */
3011
+ function buildConfigRulesChecksum(rules) {
3012
+ const hash = createHash("md5");
3013
+ const orderedRules = [...rules].sort(compareConfigRules);
3014
+ for (const rule of orderedRules) {
3015
+ if (rule.type === "maturity") {
3016
+ hash.update(`maturity:${rule.chain_id}:${rule.name}:${rule.timestamp}\n`);
3017
+ continue;
3018
+ }
3019
+ if (rule.type === "callback") {
3020
+ hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
3021
+ continue;
3101
3022
  }
3023
+ hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
3024
+ }
3025
+ return hash.digest("hex");
3026
+ }
3027
+ function normalizeAddress(address) {
3028
+ return address.toLowerCase();
3029
+ }
3030
+ function compareConfigRules(left, right) {
3031
+ if (left.chain_id !== right.chain_id) return left.chain_id - right.chain_id;
3032
+ if (left.type !== right.type) return left.type.localeCompare(right.type);
3033
+ if (left.type === "maturity" && right.type === "maturity") return left.timestamp - right.timestamp;
3034
+ if (left.type === "callback" && right.type === "callback") {
3035
+ if (left.callback_type !== right.callback_type) return left.callback_type.localeCompare(right.callback_type);
3036
+ return left.address.localeCompare(right.address);
3102
3037
  }
3038
+ if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
3039
+ return 0;
3103
3040
  }
3104
3041
 
3105
3042
  //#endregion
@@ -3229,6 +3166,104 @@ function from$4(input) {
3229
3166
  };
3230
3167
  }
3231
3168
 
3169
+ //#endregion
3170
+ //#region src/api/Controllers/Payload.ts
3171
+ const API_ERROR_CODES = [
3172
+ "VALIDATION_ERROR",
3173
+ "NOT_FOUND",
3174
+ "INTERNAL_SERVER_ERROR",
3175
+ "BAD_REQUEST"
3176
+ ];
3177
+ let STATUS_CODE = /* @__PURE__ */ function(STATUS_CODE) {
3178
+ STATUS_CODE[STATUS_CODE["SUCCESS"] = 200] = "SUCCESS";
3179
+ STATUS_CODE[STATUS_CODE["BAD_REQUEST"] = 400] = "BAD_REQUEST";
3180
+ STATUS_CODE[STATUS_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
3181
+ STATUS_CODE[STATUS_CODE["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
3182
+ return STATUS_CODE;
3183
+ }({});
3184
+ var APIError = class extends Error {
3185
+ constructor(statusCode, message, code, details) {
3186
+ super(message);
3187
+ this.statusCode = statusCode;
3188
+ this.code = code;
3189
+ this.details = details;
3190
+ this.name = "APIError";
3191
+ }
3192
+ };
3193
+ var ValidationError = class extends APIError {
3194
+ constructor(message, details) {
3195
+ super(STATUS_CODE.BAD_REQUEST, message, "VALIDATION_ERROR", details);
3196
+ }
3197
+ };
3198
+ var NotFoundError = class extends APIError {
3199
+ constructor(message) {
3200
+ super(STATUS_CODE.NOT_FOUND, message, "NOT_FOUND");
3201
+ }
3202
+ };
3203
+ var InternalServerError = class extends APIError {
3204
+ constructor(message = "Internal server error") {
3205
+ super(STATUS_CODE.INTERNAL_SERVER_ERROR, message, "INTERNAL_SERVER_ERROR");
3206
+ }
3207
+ };
3208
+ var BadRequestError = class extends APIError {
3209
+ constructor(message = "Invalid JSON format", details) {
3210
+ super(STATUS_CODE.BAD_REQUEST, message, "BAD_REQUEST", details);
3211
+ }
3212
+ };
3213
+ function success(args) {
3214
+ const { data, cursor } = args;
3215
+ return {
3216
+ statusCode: STATUS_CODE.SUCCESS,
3217
+ body: {
3218
+ meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
3219
+ cursor: cursor ?? null,
3220
+ data
3221
+ }
3222
+ };
3223
+ }
3224
+ /**
3225
+ * Generic failure builder. Preserves the concrete status code when the input is an APIError.
3226
+ * If not an APIError, maps to INTERNAL_SERVER_ERROR. Zod & SyntaxError are mapped to BAD_REQUEST.
3227
+ */
3228
+ function failure(err) {
3229
+ if (err instanceof APIError) return handleAPIError(err);
3230
+ if (err instanceof SyntaxError) return handleAPIError(new BadRequestError(err.message));
3231
+ if (err instanceof z$2.ZodError) return handleAPIError(handleZodError(err));
3232
+ return handleAPIError(new InternalServerError());
3233
+ }
3234
+ function handleAPIError(error) {
3235
+ return {
3236
+ statusCode: error.statusCode,
3237
+ body: {
3238
+ meta: { timestamp: (/* @__PURE__ */ new Date()).toISOString() },
3239
+ error: {
3240
+ code: error.code,
3241
+ message: error.message,
3242
+ ...error.details && typeof error.details === "object" ? { details: error.details } : {}
3243
+ }
3244
+ }
3245
+ };
3246
+ }
3247
+ function handleZodError(error) {
3248
+ return new ValidationError("Validation failed", error.issues.map((issue) => {
3249
+ const field = issue.path.join(".");
3250
+ let msg = issue.message;
3251
+ switch (issue.code) {
3252
+ case "invalid_type":
3253
+ if (issue.message.includes("received undefined")) msg = `${field} is required`;
3254
+ break;
3255
+ case "invalid_format":
3256
+ msg = issue.format === "regex" ? issue.message : `${field} has an invalid format`;
3257
+ break;
3258
+ default: break;
3259
+ }
3260
+ return {
3261
+ field,
3262
+ issue: msg
3263
+ };
3264
+ }));
3265
+ }
3266
+
3232
3267
  //#endregion
3233
3268
  //#region \0@oxc-project+runtime@0.110.0/helpers/decorate.js
3234
3269
  function __decorate(decorators, target, key, desc) {
@@ -3321,6 +3356,21 @@ const validateOfferExample = {
3321
3356
  data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
3322
3357
  }
3323
3358
  };
3359
+ const callbackTypesRequestExample = { callbacks: [{
3360
+ chain_id: 1,
3361
+ addresses: [
3362
+ "0x1111111111111111111111111111111111111111",
3363
+ "0x3333333333333333333333333333333333333333",
3364
+ "0x9999999999999999999999999999999999999999"
3365
+ ]
3366
+ }] };
3367
+ const callbackTypesResponseExample = [{
3368
+ chain_id: 1,
3369
+ sell_erc20_callback: ["0x1111111111111111111111111111111111111111"],
3370
+ buy_erc20: ["0x5555555555555555555555555555555555555555"],
3371
+ buy_vault_v1_callback: ["0x3333333333333333333333333333333333333333"],
3372
+ not_supported: ["0x9999999999999999999999999999999999999999"]
3373
+ }];
3324
3374
  const routerStatusExample = {
3325
3375
  status: "live",
3326
3376
  initialized: true,
@@ -3389,6 +3439,55 @@ __decorate([ApiProperty({
3389
3439
  type: "string",
3390
3440
  example: validateOfferExample.callback.data
3391
3441
  })], ValidateCallbackRequest.prototype, "data", void 0);
3442
+ var CallbackTypesChainRequest = class {};
3443
+ __decorate([ApiProperty({
3444
+ type: "number",
3445
+ example: callbackTypesRequestExample.callbacks[0].chain_id
3446
+ })], CallbackTypesChainRequest.prototype, "chain_id", void 0);
3447
+ __decorate([ApiProperty({
3448
+ type: () => [String],
3449
+ example: callbackTypesRequestExample.callbacks[0].addresses
3450
+ })], CallbackTypesChainRequest.prototype, "addresses", void 0);
3451
+ var CallbackTypesRequest = class {};
3452
+ __decorate([ApiProperty({
3453
+ type: () => [CallbackTypesChainRequest],
3454
+ example: callbackTypesRequestExample.callbacks
3455
+ })], CallbackTypesRequest.prototype, "callbacks", void 0);
3456
+ var CallbackTypesChainResponse = class {};
3457
+ __decorate([ApiProperty({
3458
+ type: "number",
3459
+ example: callbackTypesResponseExample[0].chain_id
3460
+ })], CallbackTypesChainResponse.prototype, "chain_id", void 0);
3461
+ __decorate([ApiProperty({
3462
+ type: () => [String],
3463
+ required: false,
3464
+ example: callbackTypesResponseExample[0].buy_vault_v1_callback
3465
+ })], CallbackTypesChainResponse.prototype, "buy_vault_v1_callback", void 0);
3466
+ __decorate([ApiProperty({
3467
+ type: () => [String],
3468
+ required: false,
3469
+ example: callbackTypesResponseExample[0].sell_erc20_callback
3470
+ })], CallbackTypesChainResponse.prototype, "sell_erc20_callback", void 0);
3471
+ __decorate([ApiProperty({
3472
+ type: () => [String],
3473
+ required: false,
3474
+ example: callbackTypesResponseExample[0].buy_erc20
3475
+ })], CallbackTypesChainResponse.prototype, "buy_erc20", void 0);
3476
+ __decorate([ApiProperty({
3477
+ type: () => [String],
3478
+ example: callbackTypesResponseExample[0].not_supported
3479
+ })], CallbackTypesChainResponse.prototype, "not_supported", void 0);
3480
+ var CallbackTypesSuccessResponse = class extends SuccessResponse {};
3481
+ __decorate([ApiProperty({
3482
+ type: "string",
3483
+ nullable: true,
3484
+ example: "maturity:1:1730415600:end_of_next_month"
3485
+ })], CallbackTypesSuccessResponse.prototype, "cursor", void 0);
3486
+ __decorate([ApiProperty({
3487
+ type: () => [CallbackTypesChainResponse],
3488
+ description: "Callback types grouped by chain.",
3489
+ example: callbackTypesResponseExample
3490
+ })], CallbackTypesSuccessResponse.prototype, "data", void 0);
3392
3491
  var AskResponse = class {};
3393
3492
  __decorate([ApiProperty({
3394
3493
  type: "string",
@@ -3924,6 +4023,28 @@ ValidateController = __decorate([ApiTags("Make"), ApiResponse({
3924
4023
  description: "Bad Request",
3925
4024
  type: BadRequestResponse
3926
4025
  })], ValidateController);
4026
+ let CallbacksController = class CallbacksController {
4027
+ async resolveCallbackTypes() {}
4028
+ };
4029
+ __decorate([
4030
+ ApiOperation({
4031
+ methods: ["post"],
4032
+ path: "/v1/callbacks",
4033
+ summary: "Resolve callback types",
4034
+ description: "Returns callback types for callback addresses grouped by chain."
4035
+ }),
4036
+ ApiBody({ type: CallbackTypesRequest }),
4037
+ ApiResponse({
4038
+ status: 200,
4039
+ description: "Success",
4040
+ type: CallbackTypesSuccessResponse
4041
+ })
4042
+ ], CallbacksController.prototype, "resolveCallbackTypes", null);
4043
+ CallbacksController = __decorate([ApiTags("Make"), ApiResponse({
4044
+ status: 400,
4045
+ description: "Bad Request",
4046
+ type: BadRequestResponse
4047
+ })], CallbacksController);
3927
4048
  let OffersController = class OffersController {
3928
4049
  async getOffers() {}
3929
4050
  };
@@ -3992,107 +4113,274 @@ __decorate([
3992
4113
  description: "Returns the aggregated status of the router."
3993
4114
  }),
3994
4115
  ApiQuery({
3995
- name: "strict",
3996
- type: "boolean",
4116
+ name: "strict",
4117
+ type: "boolean",
4118
+ required: false,
4119
+ example: true,
4120
+ description: "Fail the request if initialization is incomplete."
4121
+ }),
4122
+ ApiResponse({
4123
+ status: 200,
4124
+ description: "Success",
4125
+ type: RouterStatusSuccessResponse
4126
+ })
4127
+ ], HealthController.prototype, "getRouterStatus", null);
4128
+ __decorate([
4129
+ ApiOperation({
4130
+ methods: ["get"],
4131
+ path: "/v1/health/collectors",
4132
+ summary: "Retrieve collectors health",
4133
+ description: "Returns the latest block numbers processed by collectors and their sync status."
4134
+ }),
4135
+ ApiQuery({
4136
+ name: "strict",
4137
+ type: "boolean",
4138
+ required: false,
4139
+ example: true,
4140
+ description: "Fail the request if initialization is incomplete."
4141
+ }),
4142
+ ApiResponse({
4143
+ status: 200,
4144
+ description: "Success",
4145
+ type: CollectorsHealthSuccessResponse
4146
+ })
4147
+ ], HealthController.prototype, "getCollectorsHealth", null);
4148
+ __decorate([
4149
+ ApiOperation({
4150
+ methods: ["get"],
4151
+ path: "/v1/health/chains",
4152
+ summary: "Retrieve chains health",
4153
+ description: "Returns the latest block that can be processed by collectors for each chain."
4154
+ }),
4155
+ ApiQuery({
4156
+ name: "strict",
4157
+ type: "boolean",
4158
+ required: false,
4159
+ example: true,
4160
+ description: "Fail the request if initialization is incomplete."
4161
+ }),
4162
+ ApiResponse({
4163
+ status: 200,
4164
+ description: "Success",
4165
+ type: ChainsHealthSuccessResponse
4166
+ })
4167
+ ], HealthController.prototype, "getChainsHealth", null);
4168
+ HealthController = __decorate([ApiTags("System")], HealthController);
4169
+ const configContractsExample = {
4170
+ chain_id: 505050505,
4171
+ address: "0xD946246695A9259F3B33a78629026F61B3Ab40aF",
4172
+ name: "mempool"
4173
+ };
4174
+ const configContractsPayloadExample = [
4175
+ {
4176
+ chain_id: 505050505,
4177
+ address: "0xD946246695A9259F3B33a78629026F61B3Ab40aF",
4178
+ name: "mempool"
4179
+ },
4180
+ {
4181
+ chain_id: 505050505,
4182
+ address: "0x8A409D5D6394fC197c596d4E6E2c35e5d13f8a4d",
4183
+ name: "multicall"
4184
+ },
4185
+ {
4186
+ chain_id: 505050505,
4187
+ address: "0x23DFBc4B8B80C14CC5e25011B8491f268395BAd6",
4188
+ name: "v2"
4189
+ }
4190
+ ];
4191
+ const configRulesMaturityExample = {
4192
+ type: "maturity",
4193
+ chain_id: 1,
4194
+ name: "end_of_next_month",
4195
+ timestamp: 1730415600
4196
+ };
4197
+ const configRulesCallbackExample = {
4198
+ type: "callback",
4199
+ chain_id: 1,
4200
+ address: "0x1111111111111111111111111111111111111111",
4201
+ callback_type: "sell_erc20_callback"
4202
+ };
4203
+ const configRulesLoanTokenExample = {
4204
+ type: "loan_token",
4205
+ chain_id: 1,
4206
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
4207
+ };
4208
+ const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
4209
+ const configRulesPayloadExample = [
4210
+ configRulesMaturityExample,
4211
+ configRulesCallbackExample,
4212
+ configRulesLoanTokenExample
4213
+ ];
4214
+ const configContractNames = [
4215
+ "mempool",
4216
+ "multicall",
4217
+ "v2"
4218
+ ];
4219
+ const configContractsCursorExample = "505050505:0xd946246695a9259f3b33a78629026f61b3ab40af";
4220
+ var ConfigContractResponse = class {};
4221
+ __decorate([ApiProperty({
4222
+ type: "number",
4223
+ example: configContractsExample.chain_id
4224
+ })], ConfigContractResponse.prototype, "chain_id", void 0);
4225
+ __decorate([ApiProperty({
4226
+ type: "string",
4227
+ example: configContractsExample.address
4228
+ })], ConfigContractResponse.prototype, "address", void 0);
4229
+ __decorate([ApiProperty({
4230
+ type: "string",
4231
+ enum: configContractNames,
4232
+ example: configContractsExample.name
4233
+ })], ConfigContractResponse.prototype, "name", void 0);
4234
+ var ConfigContractsSuccessResponse = class extends SuccessResponse {};
4235
+ __decorate([ApiProperty({
4236
+ type: "string",
4237
+ nullable: true,
4238
+ example: null
4239
+ })], ConfigContractsSuccessResponse.prototype, "cursor", void 0);
4240
+ __decorate([ApiProperty({
4241
+ type: () => [ConfigContractResponse],
4242
+ description: "Indexer contract configuration for all indexed chains.",
4243
+ example: configContractsPayloadExample
4244
+ })], ConfigContractsSuccessResponse.prototype, "data", void 0);
4245
+ var ConfigRulesMeta = class {};
4246
+ __decorate([ApiProperty({
4247
+ type: "string",
4248
+ example: timestampExample
4249
+ })], ConfigRulesMeta.prototype, "timestamp", void 0);
4250
+ __decorate([ApiProperty({
4251
+ type: "string",
4252
+ example: configRulesChecksumExample
4253
+ })], ConfigRulesMeta.prototype, "checksum", void 0);
4254
+ var ConfigRulesRuleResponse = class {};
4255
+ __decorate([ApiProperty({
4256
+ type: "string",
4257
+ example: configRulesMaturityExample.type
4258
+ })], ConfigRulesRuleResponse.prototype, "type", void 0);
4259
+ __decorate([ApiProperty({
4260
+ type: "number",
4261
+ example: configRulesMaturityExample.chain_id
4262
+ })], ConfigRulesRuleResponse.prototype, "chain_id", void 0);
4263
+ __decorate([ApiProperty({
4264
+ type: "string",
4265
+ example: configRulesMaturityExample.name,
4266
+ required: false
4267
+ })], ConfigRulesRuleResponse.prototype, "name", void 0);
4268
+ __decorate([ApiProperty({
4269
+ type: "number",
4270
+ example: configRulesMaturityExample.timestamp,
4271
+ required: false
4272
+ })], ConfigRulesRuleResponse.prototype, "timestamp", void 0);
4273
+ __decorate([ApiProperty({
4274
+ type: "string",
4275
+ example: configRulesCallbackExample.address,
4276
+ required: false
4277
+ })], ConfigRulesRuleResponse.prototype, "address", void 0);
4278
+ __decorate([ApiProperty({
4279
+ type: "string",
4280
+ example: configRulesCallbackExample.callback_type,
4281
+ required: false
4282
+ })], ConfigRulesRuleResponse.prototype, "callback_type", void 0);
4283
+ var ConfigRulesSuccessResponse = class {};
4284
+ __decorate([ApiProperty({ type: () => ConfigRulesMeta })], ConfigRulesSuccessResponse.prototype, "meta", void 0);
4285
+ __decorate([ApiProperty({
4286
+ type: "string",
4287
+ nullable: true,
4288
+ example: null
4289
+ })], ConfigRulesSuccessResponse.prototype, "cursor", void 0);
4290
+ __decorate([ApiProperty({
4291
+ type: () => [ConfigRulesRuleResponse],
4292
+ description: "Configured rules returned by the router API.",
4293
+ example: configRulesPayloadExample
4294
+ })], ConfigRulesSuccessResponse.prototype, "data", void 0);
4295
+ let ConfigContractsController = class ConfigContractsController {
4296
+ async getConfigContracts() {}
4297
+ };
4298
+ __decorate([
4299
+ ApiOperation({
4300
+ methods: ["get"],
4301
+ path: "/v1/config/contracts",
4302
+ summary: "Get indexer contract configuration",
4303
+ description: "Returns contract addresses used by indexers (mempool, v2) and multicall for indexed chains."
4304
+ }),
4305
+ ApiQuery({
4306
+ name: "cursor",
4307
+ type: "string",
4308
+ required: false,
4309
+ example: configContractsCursorExample,
4310
+ description: "Pagination cursor in chain_id:address format (lowercase address)."
4311
+ }),
4312
+ ApiQuery({
4313
+ name: "limit",
4314
+ type: "number",
4315
+ required: false,
4316
+ example: 1e3,
4317
+ description: "Maximum number of contracts to return (max 1000)."
4318
+ }),
4319
+ ApiQuery({
4320
+ name: "chains",
4321
+ type: ["number"],
3997
4322
  required: false,
3998
- example: true,
3999
- description: "Fail the request if initialization is incomplete."
4323
+ example: "1,8453",
4324
+ description: "Filter by chain IDs (comma-separated).",
4325
+ style: "form",
4326
+ explode: false
4000
4327
  }),
4001
4328
  ApiResponse({
4002
4329
  status: 200,
4003
4330
  description: "Success",
4004
- type: RouterStatusSuccessResponse
4331
+ type: ConfigContractsSuccessResponse
4005
4332
  })
4006
- ], HealthController.prototype, "getRouterStatus", null);
4333
+ ], ConfigContractsController.prototype, "getConfigContracts", null);
4334
+ ConfigContractsController = __decorate([ApiTags("System")], ConfigContractsController);
4335
+ let ConfigRulesController = class ConfigRulesController {
4336
+ async getConfigRules() {}
4337
+ };
4007
4338
  __decorate([
4008
4339
  ApiOperation({
4009
4340
  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."
4341
+ path: "/v1/config/rules",
4342
+ summary: "Get config rules",
4343
+ description: "Returns configured rules for supported chains."
4013
4344
  }),
4014
4345
  ApiQuery({
4015
- name: "strict",
4016
- type: "boolean",
4346
+ name: "cursor",
4347
+ type: "string",
4017
4348
  required: false,
4018
- example: true,
4019
- description: "Fail the request if initialization is incomplete."
4349
+ example: "maturity:1:1730415600:end_of_next_month",
4350
+ description: "Pagination cursor in type:chain_id:<value> format."
4020
4351
  }),
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."
4352
+ ApiQuery({
4353
+ name: "limit",
4354
+ type: "number",
4355
+ required: false,
4356
+ example: 100,
4357
+ description: "Maximum number of rules to return (max 1000)."
4033
4358
  }),
4034
4359
  ApiQuery({
4035
- name: "strict",
4036
- type: "boolean",
4360
+ name: "types",
4361
+ type: ["string"],
4037
4362
  required: false,
4038
- example: true,
4039
- description: "Fail the request if initialization is incomplete."
4363
+ example: "maturity,loan_token",
4364
+ description: "Filter by rule types (comma-separated).",
4365
+ style: "form",
4366
+ explode: false
4367
+ }),
4368
+ ApiQuery({
4369
+ name: "chains",
4370
+ type: ["number"],
4371
+ required: false,
4372
+ example: "1,8453",
4373
+ description: "Filter by chain IDs (comma-separated).",
4374
+ style: "form",
4375
+ explode: false
4040
4376
  }),
4041
4377
  ApiResponse({
4042
4378
  status: 200,
4043
4379
  description: "Success",
4044
- type: ChainsHealthSuccessResponse
4380
+ type: ConfigRulesSuccessResponse
4045
4381
  })
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);
4382
+ ], ConfigRulesController.prototype, "getConfigRules", null);
4383
+ ConfigRulesController = __decorate([ApiTags("System")], ConfigRulesController);
4096
4384
  let ObligationsController = class ObligationsController {
4097
4385
  async getObligations() {}
4098
4386
  async getObligation() {}
@@ -4221,16 +4509,18 @@ UsersController = __decorate([ApiTags("Make"), ApiResponse({
4221
4509
  description: "Bad Request",
4222
4510
  type: BadRequestResponse
4223
4511
  })], UsersController);
4224
- const OpenApi = async (options = {}) => {
4225
- const document = await generateDocument({
4512
+ const OpenApi = async () => {
4513
+ return await generateDocument({
4226
4514
  controllers: [
4227
4515
  BooksController,
4228
- ConfigController,
4516
+ ConfigContractsController,
4517
+ ConfigRulesController,
4229
4518
  OffersController,
4230
4519
  ObligationsController,
4231
4520
  HealthController,
4232
4521
  UsersController,
4233
- ValidateController
4522
+ ValidateController,
4523
+ CallbacksController
4234
4524
  ],
4235
4525
  document: {
4236
4526
  openapi: "3.1.0",
@@ -4262,12 +4552,6 @@ const OpenApi = async (options = {}) => {
4262
4552
  ]
4263
4553
  }
4264
4554
  });
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
4555
  };
4272
4556
 
4273
4557
  //#endregion
@@ -4291,6 +4575,10 @@ function from$3(position) {
4291
4575
  //#region src/api/Schema/requests.ts
4292
4576
  const MAX_LIMIT = 100;
4293
4577
  const DEFAULT_LIMIT$4 = 20;
4578
+ const CONFIG_RULES_MAX_LIMIT = 1e3;
4579
+ const CONFIG_RULES_DEFAULT_LIMIT = 100;
4580
+ const CONFIG_CONTRACTS_MAX_LIMIT = 1e3;
4581
+ const CONFIG_CONTRACTS_DEFAULT_LIMIT = 1e3;
4294
4582
  /** Validate cursor is a valid base64url-encoded JSON object.
4295
4583
  * Domain layer handles semantic validation of cursor fields. */
4296
4584
  function isValidBase64urlJson(val) {
@@ -4324,6 +4612,43 @@ const PaginationQueryParams = z$2.object({
4324
4612
  example: 10
4325
4613
  })
4326
4614
  });
4615
+ const ConfigRuleTypes = z$2.enum([
4616
+ "maturity",
4617
+ "callback",
4618
+ "loan_token"
4619
+ ]);
4620
+ const GetConfigRulesQueryParams = z$2.object({
4621
+ cursor: z$2.string().regex(/^(maturity|callback|loan_token):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
4622
+ description: "Pagination cursor in type:chain_id:<value> format",
4623
+ example: "maturity:1:1730415600:end_of_next_month"
4624
+ }),
4625
+ 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({
4626
+ description: `Limit maximum: ${CONFIG_RULES_MAX_LIMIT}. Default: ${CONFIG_RULES_DEFAULT_LIMIT}`,
4627
+ example: 100
4628
+ }),
4629
+ types: csvArray(ConfigRuleTypes).meta({
4630
+ description: "Filter by rule types (comma-separated).",
4631
+ example: "maturity,loan_token"
4632
+ }),
4633
+ chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4634
+ description: "Filter by chain IDs (comma-separated).",
4635
+ example: "1,8453"
4636
+ })
4637
+ });
4638
+ const GetConfigContractsQueryParams = z$2.object({
4639
+ 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({
4640
+ description: "Pagination cursor in chain_id:address format (lowercase address).",
4641
+ example: "1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
4642
+ }),
4643
+ 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({
4644
+ description: `Limit maximum: ${CONFIG_CONTRACTS_MAX_LIMIT}. Default: ${CONFIG_CONTRACTS_DEFAULT_LIMIT}`,
4645
+ example: 1e3
4646
+ }),
4647
+ chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4648
+ description: "Filter by chain IDs (comma-separated).",
4649
+ example: "1,8453"
4650
+ })
4651
+ });
4327
4652
  const GetOffersQueryParams = z$2.object({
4328
4653
  ...PaginationQueryParams.shape,
4329
4654
  side: z$2.enum(["buy", "sell"]).optional().meta({
@@ -4426,6 +4751,16 @@ const GetBookParams = z$2.object({
4426
4751
  })
4427
4752
  });
4428
4753
  const ValidateOffersBody = z$2.object({ offers: z$2.array(z$2.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
4754
+ const CallbackTypesBody = z$2.object({ callbacks: z$2.array(z$2.object({
4755
+ chain_id: z$2.number().int().positive().meta({
4756
+ description: "Chain id.",
4757
+ example: 1
4758
+ }),
4759
+ 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({
4760
+ description: "Callback contract addresses.",
4761
+ example: ["0x1111111111111111111111111111111111111111", "0x3333333333333333333333333333333333333333"]
4762
+ })
4763
+ }).strict()) }).strict();
4429
4764
  const GetUserPositionsParams = z$2.object({
4430
4765
  ...PaginationQueryParams.shape,
4431
4766
  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 +4772,204 @@ const schemas = {
4437
4772
  get_health: HealthQueryParams,
4438
4773
  get_health_collectors: HealthQueryParams,
4439
4774
  get_health_chains: HealthQueryParams,
4775
+ get_config_contracts: GetConfigContractsQueryParams,
4776
+ get_config_rules: GetConfigRulesQueryParams,
4440
4777
  get_offers: GetOffersQueryParams,
4441
4778
  get_obligations: GetObligationsQueryParams,
4442
4779
  get_obligation: GetObligationParams,
4443
4780
  get_book: GetBookParams,
4444
4781
  validate_offers: ValidateOffersBody,
4782
+ callback_types: CallbackTypesBody,
4445
4783
  get_user_positions: GetUserPositionsParams
4446
4784
  };
4447
4785
  function safeParse(action, query, error) {
4448
4786
  return schemas[action].safeParse(query, { error });
4449
4787
  }
4450
4788
 
4789
+ //#endregion
4790
+ //#region src/api/Controllers/getConfigRules.ts
4791
+ /**
4792
+ * Returns configured rules for the configured chains.
4793
+ * @param query - Raw query parameters containing filters/cursor/limit.
4794
+ * @param chains - Chains to include in the configured rules.
4795
+ * @returns Config rules response payload. {@link ApiPayload.Payload}
4796
+ */
4797
+ async function getConfigRules(query, chains) {
4798
+ const parsed = safeParse("get_config_rules", query ?? {});
4799
+ if (!parsed.success) return failure(parsed.error);
4800
+ const { cursor, limit, types, chains: chainIds } = parsed.data;
4801
+ const typeFilter = types?.length ? new Set(types) : null;
4802
+ const chainFilter = chainIds?.length ? new Set(chainIds) : null;
4803
+ const filteredRules = buildConfigRules(chains).filter((rule) => {
4804
+ if (chainFilter && !chainFilter.has(rule.chain_id)) return false;
4805
+ if (typeFilter && !typeFilter.has(rule.type)) return false;
4806
+ return true;
4807
+ });
4808
+ const checksum = buildConfigRulesChecksum(filteredRules);
4809
+ let cursorRule = null;
4810
+ if (cursor) try {
4811
+ cursorRule = parseCursor$1(cursor);
4812
+ } catch (err) {
4813
+ return failure(err);
4814
+ }
4815
+ if (cursorRule && typeFilter && !typeFilter.has(cursorRule.type)) return failure(new BadRequestError("Cursor type must match requested rule types"));
4816
+ if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError("Cursor chain_id must match requested chains"));
4817
+ const startIndex = cursorRule ? findStartIndex$1(filteredRules, cursorRule) : 0;
4818
+ const page = filteredRules.slice(startIndex, startIndex + limit);
4819
+ const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
4820
+ const response = success({
4821
+ data: page,
4822
+ cursor: nextCursor
4823
+ });
4824
+ response.body.meta.checksum = checksum;
4825
+ return response;
4826
+ }
4827
+ function formatCursor$1(rule) {
4828
+ if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
4829
+ if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
4830
+ return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
4831
+ }
4832
+ function parseCursor$1(cursor) {
4833
+ const [type, chain, ...rest] = cursor.split(":");
4834
+ if (!type || !chain || rest.length === 0) throw new BadRequestError("Cursor must be in the format type:chain_id:<value>");
4835
+ if (!isConfigRuleType(type)) throw new BadRequestError("Cursor has an invalid rule type");
4836
+ const chain_id = Number.parseInt(chain, 10);
4837
+ if (!Number.isFinite(chain_id)) throw new BadRequestError("Cursor has an invalid chain_id");
4838
+ if (type === "maturity") {
4839
+ const timestampValue = Number.parseInt(rest[0] ?? "", 10);
4840
+ const nameValue = rest.slice(1).join(":");
4841
+ if (!Number.isFinite(timestampValue) || nameValue.length === 0) throw new BadRequestError("Cursor must be in the format maturity:chain_id:timestamp:name");
4842
+ if (!isMaturityType(nameValue)) throw new BadRequestError("Cursor has an invalid maturity name");
4843
+ return {
4844
+ type,
4845
+ chain_id,
4846
+ timestamp: parseMaturity(timestampValue),
4847
+ name: nameValue
4848
+ };
4849
+ }
4850
+ if (type === "callback") {
4851
+ const callbackTypeValue = rest[0] ?? "";
4852
+ const addressValue = rest.slice(1).join(":");
4853
+ if (!callbackTypeValue || !addressValue) throw new BadRequestError("Cursor must be in the format callback:chain_id:callback_type:address");
4854
+ if (!isCallbackType(callbackTypeValue)) throw new BadRequestError("Cursor has an invalid callback type");
4855
+ return {
4856
+ type,
4857
+ chain_id,
4858
+ callback_type: callbackTypeValue,
4859
+ address: parseAddress(addressValue, "Cursor address")
4860
+ };
4861
+ }
4862
+ const addressValue = rest.join(":");
4863
+ if (!addressValue) throw new BadRequestError("Cursor must be in the format loan_token:chain_id:address");
4864
+ return {
4865
+ type,
4866
+ chain_id,
4867
+ address: parseAddress(addressValue, "Cursor address")
4868
+ };
4869
+ }
4870
+ function findStartIndex$1(rules, cursor) {
4871
+ let low = 0;
4872
+ let high = rules.length;
4873
+ while (low < high) {
4874
+ const mid = Math.floor((low + high) / 2);
4875
+ const current = rules[mid];
4876
+ if (compareConfigRules(current, cursor) <= 0) low = mid + 1;
4877
+ else high = mid;
4878
+ }
4879
+ return low;
4880
+ }
4881
+ function parseAddress(address, label) {
4882
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new BadRequestError(`${label} must be a valid 20-byte address`);
4883
+ return address.toLowerCase();
4884
+ }
4885
+ function isConfigRuleType(value) {
4886
+ return value === "maturity" || value === "callback" || value === "loan_token";
4887
+ }
4888
+ function isMaturityType(value) {
4889
+ return Object.values(MaturityType).includes(value);
4890
+ }
4891
+ function parseMaturity(value) {
4892
+ try {
4893
+ return from$16(value);
4894
+ } catch (err) {
4895
+ throw new BadRequestError(err instanceof Error ? err.message : "Invalid maturity timestamp");
4896
+ }
4897
+ }
4898
+ function isCallbackType(value) {
4899
+ if (value === Type$1.BuyWithEmptyCallback) return false;
4900
+ return Object.values(Type$1).includes(value);
4901
+ }
4902
+
4903
+ //#endregion
4904
+ //#region src/logger/Logger.ts
4905
+ const LogLevelValues = [
4906
+ "trace",
4907
+ "debug",
4908
+ "info",
4909
+ "warn",
4910
+ "error",
4911
+ "fatal",
4912
+ "silent"
4913
+ ];
4914
+ function defaultLogger(minLevel, pretty) {
4915
+ const threshold = minLevel ?? process.env.ROUTER_LOG_LEVEL ?? "info";
4916
+ const prettyEnabled = typeof pretty === "boolean" ? pretty : String(process.env.ROUTER_LOG_PRETTY ?? "false").toLowerCase() === "true";
4917
+ const levelIndexByName = LogLevelValues.reduce((acc, lvl, idx) => {
4918
+ acc[lvl] = idx;
4919
+ return acc;
4920
+ }, {});
4921
+ const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
4922
+ const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
4923
+ if (!prettyEnabled) {
4924
+ console[consoleMethod](stringify({
4925
+ level: methodLevel,
4926
+ ...entry
4927
+ }));
4928
+ return;
4929
+ }
4930
+ const { msg, ...rest } = entry;
4931
+ const stack = typeof rest.stack === "string" ? rest.stack : void 0;
4932
+ if (stack) delete rest.stack;
4933
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4934
+ const level = methodLevel.toUpperCase();
4935
+ const extras = Object.entries(rest).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
4936
+ const line = extras.length > 0 ? `${timestamp} [${level}] ${msg} ${extras}` : `${timestamp} [${level}] ${msg}`;
4937
+ console[consoleMethod](line);
4938
+ if (stack) console[consoleMethod](stack);
4939
+ } : () => {};
4940
+ return {
4941
+ trace: wrap("trace", "trace"),
4942
+ debug: wrap("debug", "debug"),
4943
+ info: wrap("info", "info"),
4944
+ warn: wrap("warn", "warn"),
4945
+ error: wrap("error", "error"),
4946
+ fatal: wrap("error", "fatal")
4947
+ };
4948
+ }
4949
+ const loggerContext = new AsyncLocalStorage();
4950
+ function runWithLogger(logger, fn) {
4951
+ return loggerContext.run(logger, fn);
4952
+ }
4953
+ function getLogger() {
4954
+ return loggerContext.getStore() ?? defaultLogger();
4955
+ }
4956
+ function formatValue(value) {
4957
+ if (value === null || value === void 0 || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
4958
+ if (typeof value === "string") {
4959
+ if (value.includes(" ")) return JSON.stringify(value);
4960
+ return value;
4961
+ }
4962
+ try {
4963
+ return stringify(value);
4964
+ } catch {
4965
+ try {
4966
+ return JSON.stringify(value);
4967
+ } catch {
4968
+ return String(value);
4969
+ }
4970
+ }
4971
+ }
4972
+
4451
4973
  //#endregion
4452
4974
  //#region src/api/Controllers/validateOffers.ts
4453
4975
  async function validateOffers(body, gatekeeper) {
@@ -4461,9 +4983,9 @@ async function validateOffers(body, gatekeeper) {
4461
4983
  const rawOffer = rawOffers[i];
4462
4984
  try {
4463
4985
  const offer = fromSnakeCase(rawOffer);
4464
- const hash$2 = hash(offer);
4465
- if (!offerIndexByHash.has(hash$2)) {
4466
- offerIndexByHash.set(hash$2, i);
4986
+ const hash$3 = hash(offer);
4987
+ if (!offerIndexByHash.has(hash$3)) {
4988
+ offerIndexByHash.set(hash$3, i);
4467
4989
  parsedOffers.push(offer);
4468
4990
  }
4469
4991
  } catch (err) {
@@ -4489,7 +5011,7 @@ async function validateOffers(body, gatekeeper) {
4489
5011
  cursor: null
4490
5012
  });
4491
5013
  }
4492
- const tree = from$12(parsedOffers);
5014
+ const tree = from$7(parsedOffers);
4493
5015
  const payload = encodeUnsigned(tree);
4494
5016
  return success({
4495
5017
  data: {
@@ -4509,6 +5031,35 @@ async function validateOffers(body, gatekeeper) {
4509
5031
  }
4510
5032
  }
4511
5033
 
5034
+ //#endregion
5035
+ //#region src/gatekeeper/CallbackTypes.ts
5036
+ /**
5037
+ * Resolve callback types for a list of callback addresses grouped by chain.
5038
+ * @param parameters - Resolve parameters. {@link resolveCallbackTypes.Parameters}
5039
+ * @returns Callback types grouped by chain. {@link resolveCallbackTypes.ReturnType}
5040
+ * @throws If a chain id is unknown.
5041
+ */
5042
+ function resolveCallbackTypes$2(parameters) {
5043
+ const { chains, request } = parameters;
5044
+ const chainsById = new Map(chains.map((chain) => [chain.id, chain]));
5045
+ return request.callbacks.map(({ chain_id, addresses }) => {
5046
+ const chain = chainsById.get(chain_id);
5047
+ if (!chain) throw new Error(`Unknown chain id ${chain_id}`);
5048
+ const buckets = /* @__PURE__ */ new Map();
5049
+ const uniqueAddresses = new Set(addresses.map((address) => address.toLowerCase()));
5050
+ for (const address of uniqueAddresses) {
5051
+ const bucketKey = getCallbackType(chain.name, address) ?? "not_supported";
5052
+ const list = buckets.get(bucketKey) ?? [];
5053
+ list.push(address);
5054
+ buckets.set(bucketKey, list);
5055
+ }
5056
+ const response = { chain_id };
5057
+ for (const [type, list] of buckets.entries()) response[type] = list;
5058
+ if (!response.not_supported) response.not_supported = [];
5059
+ return response;
5060
+ });
5061
+ }
5062
+
4512
5063
  //#endregion
4513
5064
  //#region src/gatekeeper/Service.ts
4514
5065
  /**
@@ -4516,28 +5067,59 @@ async function validateOffers(body, gatekeeper) {
4516
5067
  * @param parameters - App parameters including the {@link Gatekeeper} instance.
4517
5068
  * @returns Hono app exposing gatekeeper endpoints.
4518
5069
  */
5070
+ const CallbackTypesRequestSchema = z$2.object({ callbacks: z$2.array(z$2.object({
5071
+ chain_id: z$2.number(),
5072
+ addresses: z$2.array(z$2.string().regex(/^0x[a-fA-F0-9]{40}$/))
5073
+ })) });
4519
5074
  function createApp(parameters) {
4520
- const { gatekeeper } = parameters;
5075
+ const { gatekeeper, chainRegistry } = parameters;
4521
5076
  const app = new Hono();
4522
5077
  app.post("/v1/validate", async (c) => {
4523
5078
  let body;
4524
5079
  try {
4525
5080
  body = await c.req.json();
4526
5081
  } catch (err) {
4527
- const failure$3 = failure(err);
4528
- return c.json(failure$3.body, failure$3.statusCode);
5082
+ const failure$5 = failure(err);
5083
+ return c.json(failure$5.body, failure$5.statusCode);
4529
5084
  }
4530
5085
  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);
5086
+ const failure$9 = failure(new BadRequestError("Request body must be a JSON object"));
5087
+ return c.json(failure$9.body, failure$9.statusCode);
4533
5088
  }
4534
5089
  const { statusCode, body: payload } = await validateOffers(body, gatekeeper);
4535
5090
  return c.json(payload, statusCode);
4536
5091
  });
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);
5092
+ app.get("/v1/config/rules", async (c) => {
5093
+ const { statusCode, body } = await getConfigRules(c.req.query(), chainRegistry.list());
5094
+ return c.json(body, statusCode);
5095
+ });
5096
+ app.post("/v1/callbacks", async (c) => {
5097
+ let body;
5098
+ try {
5099
+ body = await c.req.json();
5100
+ } catch (err) {
5101
+ const failure$8 = failure(err);
5102
+ return c.json(failure$8.body, failure$8.statusCode);
5103
+ }
5104
+ if (body === null || typeof body !== "object") {
5105
+ const failure$6 = failure(new BadRequestError("Request body must be a JSON object"));
5106
+ return c.json(failure$6.body, failure$6.statusCode);
5107
+ }
5108
+ try {
5109
+ const request = CallbackTypesRequestSchema.parse(body);
5110
+ const chainIds = new Set(chainRegistry.list().map((chain) => chain.id));
5111
+ const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
5112
+ if (unknown) throw new BadRequestError(`Unknown chain id ${unknown.chain_id}`);
5113
+ const data = resolveCallbackTypes$2({
5114
+ chains: chainRegistry.list(),
5115
+ request
5116
+ });
5117
+ const response = success({ data });
5118
+ return c.json(response.body, response.statusCode);
5119
+ } catch (err) {
5120
+ const failure$7 = failure(err);
5121
+ return c.json(failure$7.body, failure$7.statusCode);
5122
+ }
4541
5123
  });
4542
5124
  return app;
4543
5125
  }
@@ -4547,8 +5129,11 @@ function createApp(parameters) {
4547
5129
  * @returns Service handle including base URL and shutdown method. {@link ServiceHandle}
4548
5130
  */
4549
5131
  async function start$1(config) {
4550
- const { gatekeeper, port, hostname } = config;
4551
- const app = createApp({ gatekeeper });
5132
+ const { gatekeeper, chainRegistry, port, hostname } = config;
5133
+ const app = createApp({
5134
+ gatekeeper,
5135
+ chainRegistry
5136
+ });
4552
5137
  let address = null;
4553
5138
  let server;
4554
5139
  await new Promise((resolve) => {
@@ -4748,6 +5333,18 @@ async function* collectOffersV2(parameters) {
4748
5333
  const logger = getLogger();
4749
5334
  let startBlock = blockNumber;
4750
5335
  let reorgDetected = false;
5336
+ if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
5337
+ const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
5338
+ logger.error({
5339
+ msg,
5340
+ chain_id: client.chain.id
5341
+ });
5342
+ throw new Error(msg);
5343
+ }
5344
+ const signatureDomain = {
5345
+ chainId: client.chain.id,
5346
+ verifyingContract: client.chain.custom.morpho.address
5347
+ };
4751
5348
  const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
4752
5349
  const stream = streamLogs({
4753
5350
  client,
@@ -4778,7 +5375,7 @@ async function* collectOffersV2(parameters) {
4778
5375
  if (!log) continue;
4779
5376
  const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
4780
5377
  try {
4781
- const { tree, signature, signer } = await decode(payload);
5378
+ const { tree, signature, signer } = await decode(payload, signatureDomain);
4782
5379
  const signerMismatch = tree.offers.find((offer) => offer.maker.toLowerCase() !== signer.toLowerCase());
4783
5380
  if (signerMismatch) {
4784
5381
  logger.debug({
@@ -4810,6 +5407,7 @@ async function* collectOffersV2(parameters) {
4810
5407
  const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
4811
5408
  const treesToInsert = [];
4812
5409
  let totalValidOffers = 0;
5410
+ const offersWithBlock = [];
4813
5411
  for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
4814
5412
  const allowedResults = await gatekeeper.isAllowed(tree.offers);
4815
5413
  const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
@@ -4831,10 +5429,13 @@ async function* collectOffersV2(parameters) {
4831
5429
  }
4832
5430
  treesToInsert.push({
4833
5431
  tree,
4834
- signature,
4835
- blockNumber: treeBlockNumber
5432
+ signature
4836
5433
  });
4837
5434
  totalValidOffers += tree.offers.length;
5435
+ offersWithBlock.push(...tree.offers.map((offer) => ({
5436
+ offer,
5437
+ blockNumber: treeBlockNumber
5438
+ })));
4838
5439
  } catch (err) {
4839
5440
  const error = err instanceof Error ? err : new Error(String(err));
4840
5441
  logger.error({
@@ -4844,7 +5445,24 @@ async function* collectOffersV2(parameters) {
4844
5445
  });
4845
5446
  throw new Error("Gatekeeper validation failed", { cause: error });
4846
5447
  }
5448
+ const dependencies = buildOfferDependencies$1(offersWithBlock);
5449
+ await dbTx.oracles.upsert(dependencies.oracles);
5450
+ await dbTx.obligations.create(dependencies.obligations);
5451
+ await dbTx.groups.create(dependencies.groups);
5452
+ const insertedHashes = await dbTx.offers.create(dependencies.offerBatches);
4847
5453
  if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
5454
+ const insertedOffers = filterInsertedOffers({
5455
+ offers: offersWithBlock,
5456
+ hashes: insertedHashes
5457
+ });
5458
+ const { callbacks, positions, lots } = await decodeCallbacks({
5459
+ chainId: client.chain.id,
5460
+ gatekeeper,
5461
+ offers: insertedOffers
5462
+ });
5463
+ if (positions.length > 0) await dbTx.positions.upsert(positions);
5464
+ if (callbacks.length > 0) await dbTx.callbacks.upsert(callbacks);
5465
+ if (lots.length > 0) await dbTx.lots.create(lots);
4848
5466
  try {
4849
5467
  await dbTx.blocks.advanceCollector({
4850
5468
  collectorName: collector,
@@ -4897,10 +5515,151 @@ async function* collectOffersV2(parameters) {
4897
5515
  }
4898
5516
  }
4899
5517
  });
4900
- if (reorgDetected) return;
4901
- yield blockNumber;
4902
- startBlock = blockNumber;
5518
+ if (reorgDetected) return;
5519
+ yield blockNumber;
5520
+ startBlock = blockNumber;
5521
+ }
5522
+ }
5523
+ async function decodeCallbacks(parameters) {
5524
+ const { chainId, gatekeeper, offers } = parameters;
5525
+ if (offers.length === 0) return {
5526
+ callbacks: [],
5527
+ positions: [],
5528
+ lots: []
5529
+ };
5530
+ const addresses = offers.filter((entry) => entry.offer.callback.data !== "0x").map((entry) => entry.offer.callback.address);
5531
+ if (addresses.length === 0) return {
5532
+ callbacks: [],
5533
+ positions: [],
5534
+ lots: []
5535
+ };
5536
+ let response;
5537
+ try {
5538
+ response = await gatekeeper.getCallbackTypes({ callbacks: [{
5539
+ chain_id: chainId,
5540
+ addresses
5541
+ }] });
5542
+ } catch (err) {
5543
+ const error = err instanceof Error ? err : new Error(String(err));
5544
+ throw new Error("Failed to resolve callback types", { cause: error });
5545
+ }
5546
+ const entry = response.find((item) => item.chain_id === chainId);
5547
+ const typeByAddress = /* @__PURE__ */ new Map();
5548
+ if (entry) for (const [key, list] of Object.entries(entry)) {
5549
+ if (key === "chain_id" || key === "not_supported") continue;
5550
+ if (!Array.isArray(list)) continue;
5551
+ for (const address of list) typeByAddress.set(address.toLowerCase(), key);
5552
+ }
5553
+ const callbacks = [];
5554
+ const positions = [];
5555
+ const lots = [];
5556
+ for (const { offer, blockNumber: offerBlockNumber } of offers) {
5557
+ if (offer.callback.data === "0x") continue;
5558
+ const callbackType = typeByAddress.get(offer.callback.address.toLowerCase());
5559
+ if (!callbackType) continue;
5560
+ let decoded;
5561
+ try {
5562
+ decoded = decode$1(callbackType, offer.callback.data);
5563
+ } catch (err) {
5564
+ const error = err instanceof Error ? err : new Error(String(err));
5565
+ throw new Error("Failed to decode callback data", { cause: error });
5566
+ }
5567
+ if (decoded.length === 0) continue;
5568
+ const offerHash = hash(offer);
5569
+ const callbackInputs = decoded.map((callback) => ({
5570
+ chainId: offer.chainId,
5571
+ contract: callback.contract,
5572
+ user: offer.maker,
5573
+ amount: callback.amount
5574
+ }));
5575
+ callbacks.push({
5576
+ offerHash,
5577
+ callbacks: callbackInputs
5578
+ });
5579
+ for (const callback of decoded) {
5580
+ const contract = callback.contract;
5581
+ const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
5582
+ const asset = callbackType === Type$1.BuyVaultV1Callback ? void 0 : contract;
5583
+ positions.push(from$10({
5584
+ chainId: offer.chainId,
5585
+ contract,
5586
+ user: offer.maker,
5587
+ type: positionType,
5588
+ asset,
5589
+ blockNumber: offerBlockNumber
5590
+ }));
5591
+ const isLoanPosition = offer.loanToken.toLowerCase() === asset?.toLowerCase();
5592
+ lots.push({
5593
+ positionChainId: offer.chainId,
5594
+ positionContract: contract,
5595
+ positionUser: offer.maker,
5596
+ group: offer.group,
5597
+ size: isLoanPosition ? offer.assets : callback.amount
5598
+ });
5599
+ }
5600
+ }
5601
+ return {
5602
+ callbacks,
5603
+ positions,
5604
+ lots
5605
+ };
5606
+ }
5607
+ function buildOfferDependencies$1(offers) {
5608
+ const obligationsById = /* @__PURE__ */ new Map();
5609
+ const oraclesByKey = /* @__PURE__ */ new Map();
5610
+ const groupsByKey = /* @__PURE__ */ new Map();
5611
+ const offersByBlock = /* @__PURE__ */ new Map();
5612
+ for (const { offer, blockNumber } of offers) {
5613
+ const list = offersByBlock.get(blockNumber) ?? [];
5614
+ list.push(offer);
5615
+ offersByBlock.set(blockNumber, list);
5616
+ const obligationId$2 = obligationId(offer);
5617
+ if (!obligationsById.get(obligationId$2)) obligationsById.set(obligationId$2, from$13({
5618
+ chainId: offer.chainId,
5619
+ loanToken: offer.loanToken,
5620
+ maturity: offer.maturity,
5621
+ collaterals: offer.collaterals
5622
+ }));
5623
+ for (const collateral of offer.collaterals) {
5624
+ const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
5625
+ if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
5626
+ chainId: offer.chainId,
5627
+ address: collateral.oracle,
5628
+ price: null,
5629
+ blockNumber
5630
+ }));
5631
+ }
5632
+ const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
5633
+ if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
5634
+ chainId: offer.chainId,
5635
+ maker: offer.maker,
5636
+ group: offer.group,
5637
+ blockNumber
5638
+ });
5639
+ }
5640
+ return {
5641
+ obligations: Array.from(obligationsById.values()),
5642
+ oracles: Array.from(oraclesByKey.values()),
5643
+ groups: Array.from(groupsByKey.values()),
5644
+ offerBatches: Array.from(offersByBlock.entries()).map(([blockNumber, items]) => ({
5645
+ blockNumber,
5646
+ offers: items
5647
+ }))
5648
+ };
5649
+ }
5650
+ function filterInsertedOffers(parameters) {
5651
+ if (parameters.hashes.length === 0) return [];
5652
+ const inserted = new Set(parameters.hashes.map((hash) => hash.toLowerCase()));
5653
+ const seen = /* @__PURE__ */ new Set();
5654
+ const filtered = [];
5655
+ for (const entry of parameters.offers) {
5656
+ const hash$2 = hash(entry.offer).toLowerCase();
5657
+ if (!inserted.has(hash$2)) continue;
5658
+ if (seen.has(hash$2)) continue;
5659
+ seen.add(hash$2);
5660
+ filtered.push(entry);
4903
5661
  }
5662
+ return filtered;
4904
5663
  }
4905
5664
 
4906
5665
  //#endregion
@@ -5160,7 +5919,7 @@ async function* collectPositions(parameters) {
5160
5919
  });
5161
5920
  continue;
5162
5921
  }
5163
- transfers.push(from$7({
5922
+ transfers.push(from$8({
5164
5923
  id: `${client.chain.id}-${log.blockNumber.toString()}-${log.transactionHash}-${log.logIndex.toString()}`,
5165
5924
  chainId: client.chain.id,
5166
5925
  contract: log.address,
@@ -5499,7 +6258,7 @@ async function* collectPrices(parameters) {
5499
6258
  //#region src/indexer/collectors/CollectorBuilder.ts
5500
6259
  function createBuilder(parameters) {
5501
6260
  const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow, interval } = {} } = parameters;
5502
- const createCollector = (name, collect) => create$13({
6261
+ const createCollector = (name, collect) => create$16({
5503
6262
  name,
5504
6263
  collect,
5505
6264
  client,
@@ -5603,7 +6362,7 @@ function from$1(config) {
5603
6362
  retryAttempts,
5604
6363
  retryDelayMs
5605
6364
  });
5606
- return create$15({
6365
+ return create$18({
5607
6366
  client,
5608
6367
  collectors: [
5609
6368
  offersCollector,
@@ -5613,7 +6372,7 @@ function from$1(config) {
5613
6372
  ]
5614
6373
  });
5615
6374
  }
5616
- function create$15(params) {
6375
+ function create$18(params) {
5617
6376
  const { collectors, client } = params;
5618
6377
  const indexerId = `${client.chain.id.toString()}.indexer`;
5619
6378
  const tracer = getTracer(`router.${indexerId}`);
@@ -5642,7 +6401,7 @@ function create$15(params) {
5642
6401
 
5643
6402
  //#endregion
5644
6403
  //#region src/indexer/collectors/Admin.ts
5645
- function create$14(parameters) {
6404
+ function create$17(parameters) {
5646
6405
  const collector = "admin";
5647
6406
  const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
5648
6407
  const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
@@ -5882,8 +6641,8 @@ const names = [
5882
6641
  "positions",
5883
6642
  "prices"
5884
6643
  ];
5885
- function create$13({ name, collect, client, db, options }) {
5886
- const admin = create$14({
6644
+ function create$16({ name, collect, client, db, options }) {
6645
+ const admin = create$17({
5887
6646
  client,
5888
6647
  db,
5889
6648
  options
@@ -6119,7 +6878,6 @@ const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2,
6119
6878
  oracleChainId: bigint("oracle_chain_id", { mode: "number" }).$type().notNull(),
6120
6879
  oracleAddress: varchar("oracle_address", { length: 42 }).notNull(),
6121
6880
  lltv: bigint("lltv", { mode: "bigint" }).notNull(),
6122
- blockNumber: bigint("block_number", { mode: "number" }).notNull(),
6123
6881
  updatedAt: timestamp("updated_at").defaultNow().notNull()
6124
6882
  }, (table) => [
6125
6883
  primaryKey({
@@ -6415,7 +7173,7 @@ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
6415
7173
  //#endregion
6416
7174
  //#region src/database/domains/Blocks.ts
6417
7175
  /** Postgres implementation. */
6418
- const create$12 = (config) => {
7176
+ const create$15 = (config) => {
6419
7177
  const { db, chainRegistry } = config;
6420
7178
  const getChain = async (chainId) => {
6421
7179
  const rows = await db.select({
@@ -6596,7 +7354,7 @@ const create$12 = (config) => {
6596
7354
  //#region src/database/domains/Book.ts
6597
7355
  const DEFAULT_LIMIT$3 = 100;
6598
7356
  const MAX_TOTAL_OFFERS = 500;
6599
- function create$11(config) {
7357
+ function create$14(config) {
6600
7358
  const db = config.db;
6601
7359
  const logger = getLogger();
6602
7360
  const getOffers = async (parameters) => {
@@ -7060,16 +7818,76 @@ let LevelCursor;
7060
7818
  */
7061
7819
  const DEFAULT_BATCH_SIZE = 4e3;
7062
7820
 
7821
+ //#endregion
7822
+ //#region src/database/domains/Callbacks.ts
7823
+ /**
7824
+ * Create a callbacks domain instance.
7825
+ * @param db - Database core instance.
7826
+ * @returns Callbacks domain. {@link CallbacksDomain}
7827
+ */
7828
+ function create$13(db) {
7829
+ return {
7830
+ upsert: async (inputs) => {
7831
+ if (inputs.length === 0) return;
7832
+ const idCache = /* @__PURE__ */ new Map();
7833
+ const seenCallbackIds = /* @__PURE__ */ new Set();
7834
+ const callbacksRows = [];
7835
+ const offersCallbacksRows = [];
7836
+ const callbackId = (input) => {
7837
+ const preimage = `0x${input.chainId}${input.contract}${input.user}${input.amount.toString()}`.toLowerCase();
7838
+ const id = idCache.get(preimage) ?? keccak256(preimage);
7839
+ idCache.set(preimage, id);
7840
+ return id;
7841
+ };
7842
+ for (const { offerHash, callbacks } of inputs) {
7843
+ const normalizedOfferHash = offerHash.toLowerCase();
7844
+ for (const callback of callbacks) {
7845
+ const normalized = {
7846
+ chainId: callback.chainId,
7847
+ contract: callback.contract.toLowerCase(),
7848
+ user: callback.user.toLowerCase(),
7849
+ amount: callback.amount
7850
+ };
7851
+ const id = callbackId(normalized);
7852
+ offersCallbacksRows.push({
7853
+ offerHash: normalizedOfferHash,
7854
+ callbackId: id
7855
+ });
7856
+ if (seenCallbackIds.has(id)) continue;
7857
+ seenCallbackIds.add(id);
7858
+ callbacksRows.push({
7859
+ id,
7860
+ positionChainId: normalized.chainId,
7861
+ positionContract: normalized.contract,
7862
+ positionUser: normalized.user,
7863
+ amount: normalized.amount.toString()
7864
+ });
7865
+ }
7866
+ }
7867
+ if (offersCallbacksRows.length === 0) return;
7868
+ await db.transaction(async (dbTx) => {
7869
+ for (const batch of batch$1(callbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(callbacks).values(batch).onConflictDoNothing();
7870
+ for (const batch of batch$1(offersCallbacksRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(offersCallbacks).values(batch).onConflictDoNothing();
7871
+ });
7872
+ },
7873
+ delete: async ({ offers }) => {
7874
+ if (offers.length === 0) return 0;
7875
+ const normalized = offers.map((offer) => offer.toLowerCase());
7876
+ return (await db.delete(offersCallbacks).where(inArray(offersCallbacks.offerHash, normalized))).affectedRows;
7877
+ }
7878
+ };
7879
+ }
7880
+
7063
7881
  //#endregion
7064
7882
  //#region src/database/domains/Consumed.ts
7065
- function create$10(db) {
7883
+ function create$12(db) {
7066
7884
  return {
7067
7885
  create: async (events) => {
7068
7886
  if (events.length === 0) return;
7069
- const groups$1 = /* @__PURE__ */ new Map();
7887
+ const groups$2 = /* @__PURE__ */ new Map();
7070
7888
  for (const event of events) {
7071
7889
  const groupId = `${event.chainId}-${event.maker}-${event.group}`.toLowerCase();
7072
- groups$1.set(groupId, {
7890
+ groups$2.set(groupId, {
7073
7891
  chainId: event.chainId,
7074
7892
  maker: event.maker,
7075
7893
  group: event.group,
@@ -7077,7 +7895,7 @@ function create$10(db) {
7077
7895
  });
7078
7896
  }
7079
7897
  await db.transaction(async (dbTx) => {
7080
- const groupsRows = Array.from(groups$1.values()).map((group) => ({
7898
+ const groupsRows = Array.from(groups$2.values()).map((group) => ({
7081
7899
  chainId: group.chainId,
7082
7900
  maker: group.maker.toLowerCase(),
7083
7901
  group: group.group.toLowerCase(),
@@ -7103,9 +7921,30 @@ function create$10(db) {
7103
7921
  };
7104
7922
  }
7105
7923
 
7924
+ //#endregion
7925
+ //#region src/database/domains/Groups.ts
7926
+ /**
7927
+ * Create a groups domain instance.
7928
+ * @param db - Database core instance.
7929
+ * @returns Groups domain. {@link GroupsDomain}
7930
+ */
7931
+ function create$11(db) {
7932
+ return { create: async (groups$1) => {
7933
+ if (groups$1.length === 0) return;
7934
+ const rows = groups$1.map((group) => ({
7935
+ chainId: group.chainId,
7936
+ maker: group.maker.toLowerCase(),
7937
+ group: group.group.toLowerCase(),
7938
+ consumed: (group.consumed ?? 0n).toString(),
7939
+ blockNumber: group.blockNumber
7940
+ }));
7941
+ for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await db.insert(groups).values(batch).onConflictDoNothing();
7942
+ } };
7943
+ }
7944
+
7106
7945
  //#endregion
7107
7946
  //#region src/database/domains/Lots.ts
7108
- function create$9(db) {
7947
+ function create$10(db) {
7109
7948
  return {
7110
7949
  get: async (parameters) => {
7111
7950
  const { chainId, user, contract, group } = parameters ?? {};
@@ -7149,268 +7988,88 @@ function create$9(db) {
7149
7988
  }
7150
7989
 
7151
7990
  //#endregion
7152
- //#region src/gatekeeper/Client.ts
7153
- const DEFAULT_TIMEOUT_MS = 1e4;
7991
+ //#region src/database/domains/Obligations.ts
7154
7992
  /**
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}
7993
+ * Create an obligations domain instance.
7994
+ * @param db - Database core instance.
7995
+ * @returns Obligations domain. {@link ObligationsDomain}
7158
7996
  */
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);
7997
+ function create$9(db) {
7998
+ return { create: async (obligations$1) => {
7999
+ if (obligations$1.length === 0) return;
8000
+ const obligationsById = /* @__PURE__ */ new Map();
8001
+ for (const obligation of obligations$1) {
8002
+ const id$1 = id(obligation).toLowerCase();
8003
+ if (!obligationsById.get(id$1)) obligationsById.set(id$1, obligation);
8004
+ }
7166
8005
  try {
7167
- return await fetchFn(`${baseUrl}${path}`, {
7168
- ...init,
7169
- signal: controller.signal
8006
+ await db.transaction(async (dbTx) => {
8007
+ const obligationRows = obligations$1.map((obligation) => ({
8008
+ obligationId: id(obligation),
8009
+ chainId: obligation.chainId,
8010
+ loanToken: obligation.loanToken.toLowerCase(),
8011
+ maturity: obligation.maturity
8012
+ }));
8013
+ for (const batch of batch$1(obligationRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligations).values(batch).onConflictDoNothing();
8014
+ const collateralRows = obligations$1.flatMap((obligation) => {
8015
+ return obligation.collaterals.map((collateral) => ({
8016
+ obligationId: id(obligation),
8017
+ asset: collateral.asset.toLowerCase(),
8018
+ oracleChainId: obligation.chainId,
8019
+ oracleAddress: collateral.oracle.toLowerCase(),
8020
+ lltv: collateral.lltv
8021
+ }));
8022
+ });
8023
+ for (const batch of batch$1(collateralRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationCollateralsV2).values(batch).onConflictDoNothing();
7170
8024
  });
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
- };
8025
+ } catch (err) {
8026
+ const error = err instanceof Error ? err : new Error(String(err));
8027
+ throw new Error("Obligations.create failed. Ensure oracles exist before inserting obligations.", { cause: error });
7213
8028
  }
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;
8029
+ } };
7235
8030
  }
7236
8031
 
7237
8032
  //#endregion
7238
8033
  //#region src/database/domains/Offers.ts
7239
8034
  const DEFAULT_LIMIT$2 = 100;
7240
8035
  function create$8(config) {
7241
- const { db, chainRegistry } = config;
8036
+ const { db } = config;
7242
8037
  return {
7243
8038
  create: async (batches) => {
7244
8039
  if (batches.length === 0) return [];
7245
- const offersWithBlock = batches.flatMap(({ blockNumber, offers }) => offers.map((offer) => ({
7246
- offer,
8040
+ const offersRows = batches.flatMap(({ blockNumber, offers }) => offers.map((offer) => ({
8041
+ ...serialize(offer),
8042
+ obligationId: obligationId(offer),
8043
+ groupChainId: offer.chainId,
8044
+ groupMaker: offer.maker.toLowerCase(),
8045
+ callbackAddress: offer.callback.address.toLowerCase(),
8046
+ callbackData: offer.callback.data,
7247
8047
  blockNumber
7248
8048
  })));
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
- });
8049
+ if (offersRows.length === 0) return [];
8050
+ try {
8051
+ return await db.transaction(async (dbTx) => {
8052
+ const selectExisting = async (hashes) => {
8053
+ if (hashes.length === 0) return /* @__PURE__ */ new Set();
8054
+ const existing = /* @__PURE__ */ new Set();
8055
+ for (const batch of batch$1(hashes, DEFAULT_BATCH_SIZE)) {
8056
+ const rows = await dbTx.select({ hash: offers.hash }).from(offers).where(inArray(offers.hash, batch));
8057
+ for (const row of rows) existing.add(String(row.hash).toLowerCase());
8058
+ }
8059
+ return existing;
8060
+ };
8061
+ const inserted = [];
8062
+ for (const batch of batch$1(offersRows, DEFAULT_BATCH_SIZE)) {
8063
+ const rows = await dbTx.insert(offers).values(batch).onConflictDoNothing().returning();
8064
+ inserted.push(...rows.map((row) => row.hash));
7274
8065
  }
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
8066
+ const existing = await selectExisting(inserted);
8067
+ return inserted.filter((hash) => existing.has(hash));
7282
8068
  });
8069
+ } catch (err) {
8070
+ const error = err instanceof Error ? err : new Error(String(err));
8071
+ throw new Error("Offers.create failed. Ensure obligations and groups exist before inserting offers.", { cause: error });
7283
8072
  }
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
8073
  },
7415
8074
  get: async (parameters) => {
7416
8075
  const limit = parameters?.limit ?? DEFAULT_LIMIT$2;
@@ -7430,36 +8089,12 @@ function create$8(config) {
7430
8089
  '[]'::jsonb
7431
8090
  )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
7432
8091
  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
8092
  const rows = (await db.select({
7457
8093
  hash: offers.hash,
7458
8094
  maker: offers.groupMaker,
7459
8095
  assets: offers.assets,
7460
8096
  obligationUnits: offers.obligationUnits,
7461
8097
  obligationShares: offers.obligationShares,
7462
- consumed: groups.consumed,
7463
8098
  price: offers.price,
7464
8099
  maturity: offers.maturity,
7465
8100
  expiry: offers.expiry,
@@ -7472,19 +8107,8 @@ function create$8(config) {
7472
8107
  callbackAddress: offers.callbackAddress,
7473
8108
  callbackData: offers.callbackData,
7474
8109
  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) => {
8110
+ blockNumber: offers.blockNumber
8111
+ }).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
8112
  return {
7489
8113
  hash: row.hash,
7490
8114
  maker: row.maker,
@@ -7509,13 +8133,9 @@ function create$8(config) {
7509
8133
  address: row.callbackAddress,
7510
8134
  data: row.callbackData
7511
8135
  },
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"),
8136
+ consumed: 0n,
8137
+ available: 0n,
8138
+ takeable: 0n,
7519
8139
  blockNumber: row.blockNumber
7520
8140
  };
7521
8141
  });
@@ -7597,7 +8217,7 @@ function create$8(config) {
7597
8217
  quote.bid = { price: BigInt(row.price) };
7598
8218
  }
7599
8219
  return Array.from(quotes.entries()).map(([id, quote]) => {
7600
- return from$8({
8220
+ return from$9({
7601
8221
  obligationId: id,
7602
8222
  ask: quote.ask,
7603
8223
  bid: quote.bid
@@ -7639,7 +8259,7 @@ function create$6(db) {
7639
8259
  price: oracles.price,
7640
8260
  blockNumber: oracles.blockNumber,
7641
8261
  chainId: oracles.chainId
7642
- }).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$10({
8262
+ }).from(oracles).where(eq(oracles.chainId, chainId))).map((r) => from$11({
7643
8263
  chainId: r.chainId,
7644
8264
  address: r.address,
7645
8265
  price: r.price,
@@ -7654,12 +8274,15 @@ function create$6(db) {
7654
8274
  price: o.price !== null ? o.price.toString() : null,
7655
8275
  blockNumber: o.blockNumber
7656
8276
  }));
7657
- db.transaction(async (dbTx) => {
8277
+ await db.transaction(async (dbTx) => {
7658
8278
  for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await dbTx.insert(oracles).values(batch).onConflictDoUpdate({
7659
8279
  target: [oracles.chainId, oracles.address],
7660
8280
  set: {
7661
- price: sql`EXCLUDED.price`,
7662
- blockNumber: sql`EXCLUDED.block_number`,
8281
+ price: sql`COALESCE(EXCLUDED.price, ${oracles.price})`,
8282
+ blockNumber: sql`CASE
8283
+ WHEN EXCLUDED.price IS NULL THEN ${oracles.blockNumber}
8284
+ ELSE EXCLUDED.block_number
8285
+ END`,
7663
8286
  updatedAt: sql`NOW()`
7664
8287
  }
7665
8288
  });
@@ -8005,41 +8628,42 @@ function create$3(config) {
8005
8628
  return {
8006
8629
  create: async (trees$1) => {
8007
8630
  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
- });
8631
+ try {
8632
+ return await db.transaction(async (dbTx) => {
8633
+ const roots = [];
8634
+ for (const { tree, signature } of trees$1) {
8635
+ const root = tree.root.toLowerCase();
8636
+ roots.push(root);
8637
+ await dbTx.insert(trees).values({
8638
+ root,
8639
+ rootSignature: signature.toLowerCase()
8640
+ }).onConflictDoUpdate({
8641
+ target: [trees.root],
8642
+ set: {
8643
+ rootSignature: signature.toLowerCase(),
8644
+ createdAt: sql`NOW()`
8645
+ }
8646
+ });
8647
+ const pathRows = proofs(tree).map((proof) => ({
8648
+ offerHash: hash(proof.offer).toLowerCase(),
8649
+ treeRoot: root,
8650
+ proofNodes: concatenateProofs(proof.path)
8651
+ }));
8652
+ for (const batch of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch).onConflictDoUpdate({
8653
+ target: [merklePaths.offerHash],
8654
+ set: {
8655
+ treeRoot: sql`excluded.tree_root`,
8656
+ proofNodes: sql`excluded.proof_nodes`,
8657
+ createdAt: sql`NOW()`
8658
+ }
8659
+ });
8660
+ }
8661
+ return roots;
8662
+ });
8663
+ } catch (err) {
8664
+ const error = err instanceof Error ? err : new Error(String(err));
8665
+ throw new Error("Trees.create failed. Ensure offers exist before inserting merkle paths.", { cause: error });
8666
+ }
8043
8667
  },
8044
8668
  getAttestations: async (hashes) => {
8045
8669
  if (hashes.length === 0) return /* @__PURE__ */ new Map();
@@ -8144,17 +8768,17 @@ function create$2(db) {
8144
8768
  //#region src/database/Database.ts
8145
8769
  function createDomains(core, chainRegistry) {
8146
8770
  return {
8147
- book: create$11({ db: core }),
8148
- blocks: create$12({
8149
- db: core,
8150
- chainRegistry
8151
- }),
8152
- offers: create$8({
8771
+ book: create$14({ db: core }),
8772
+ blocks: create$15({
8153
8773
  db: core,
8154
8774
  chainRegistry
8155
8775
  }),
8156
- consumed: create$10(core),
8157
- lots: create$9(core),
8776
+ callbacks: create$13(core),
8777
+ offers: create$8({ db: core }),
8778
+ consumed: create$12(core),
8779
+ groups: create$11(core),
8780
+ lots: create$10(core),
8781
+ obligations: create$9(core),
8158
8782
  offsets: create$7(core),
8159
8783
  oracles: create$6(core),
8160
8784
  trees: create$3({ db: core }),
@@ -8183,6 +8807,10 @@ function augmentWithDomains(base, chainRegistry) {
8183
8807
  value: dms.blocks,
8184
8808
  enumerable: true
8185
8809
  },
8810
+ callbacks: {
8811
+ value: dms.callbacks,
8812
+ enumerable: true
8813
+ },
8186
8814
  offers: {
8187
8815
  value: dms.offers,
8188
8816
  enumerable: true
@@ -8191,10 +8819,18 @@ function augmentWithDomains(base, chainRegistry) {
8191
8819
  value: dms.consumed,
8192
8820
  enumerable: true
8193
8821
  },
8822
+ groups: {
8823
+ value: dms.groups,
8824
+ enumerable: true
8825
+ },
8194
8826
  lots: {
8195
8827
  value: dms.lots,
8196
8828
  enumerable: true
8197
8829
  },
8830
+ obligations: {
8831
+ value: dms.obligations,
8832
+ enumerable: true
8833
+ },
8198
8834
  offsets: {
8199
8835
  value: dms.offsets,
8200
8836
  enumerable: true
@@ -8636,6 +9272,8 @@ const ApiSchema = z.object({ port: z.number().int().positive() }).strict();
8636
9272
  const GatekeeperSchema = z.object({
8637
9273
  url_env: z.string().min(1).optional(),
8638
9274
  url: z.string().min(1).optional(),
9275
+ origin_secret_env: z.string().min(1).optional(),
9276
+ origin_secret: z.string().min(1).optional(),
8639
9277
  timeout_ms: z.number().int().positive().optional(),
8640
9278
  port: z.number().int().positive().optional()
8641
9279
  }).strict().optional();
@@ -8701,6 +9339,7 @@ function resolveRouterConfig(config, env) {
8701
9339
  api: config.api ? { port: config.api.port } : void 0,
8702
9340
  gatekeeper: {
8703
9341
  url: resolveGatekeeperUrl(config.gatekeeper, env),
9342
+ originSecret: resolveGatekeeperOriginSecret(config.gatekeeper, env),
8704
9343
  timeoutMs: config.gatekeeper?.timeout_ms ?? 1e4,
8705
9344
  port: config.gatekeeper?.port ?? 8082
8706
9345
  },
@@ -8811,6 +9450,7 @@ function createDefaultConfig() {
8811
9450
  api: { port: 8081 },
8812
9451
  gatekeeper: {
8813
9452
  url: void 0,
9453
+ originSecret: void 0,
8814
9454
  timeoutMs: 1e4,
8815
9455
  port: 8082
8816
9456
  },
@@ -8829,6 +9469,11 @@ function resolveGatekeeperUrl(gatekeeper, env) {
8829
9469
  if (gatekeeper.url_env) return readEnvVar(env, gatekeeper.url_env, "Gatekeeper URL");
8830
9470
  return gatekeeper.url;
8831
9471
  }
9472
+ function resolveGatekeeperOriginSecret(gatekeeper, env) {
9473
+ if (!gatekeeper) return void 0;
9474
+ if (gatekeeper.origin_secret_env) return readEnvVar(env, gatekeeper.origin_secret_env, "Gatekeeper origin secret");
9475
+ return gatekeeper.origin_secret;
9476
+ }
8832
9477
 
8833
9478
  //#endregion
8834
9479
  //#region src/cli/commands/RouterCmd.ts
@@ -8851,7 +9496,7 @@ var RouterCmd = class RouterCmd extends Command {
8851
9496
  const configPath = resolveConfigPath(options.configFile);
8852
9497
  const config = configPath !== null ? loadRouterConfig(configPath) : createDefaultConfig();
8853
9498
  const logger = defaultLogger(config.logging.level, config.logging.pretty);
8854
- const chainRegistry = create$16(Object.values(config.chains).map((entry) => entry.chain));
9499
+ const chainRegistry = create$19(Object.values(config.chains).map((entry) => entry.chain));
8855
9500
  const clients = (config.indexer?.chains ?? []).map((name) => {
8856
9501
  const chainConfig = config.chains[name];
8857
9502
  if (!chainConfig) throw new Error(`Indexer chain ${name} is not defined under [chains].`);
@@ -8899,9 +9544,10 @@ const gatekeeperCmd = new RouterCmd("gatekeeper");
8899
9544
  gatekeeperCmd.description("Start Gatekeeper validation service.").action(async (opts) => {
8900
9545
  const { gatekeeper: gatekeeperConfig, chainRegistry, logger } = opts;
8901
9546
  await runWithLogger(logger, async () => {
8902
- const gatekeeperCore = create$17({ rules: morphoRules(chainRegistry.list()) });
9547
+ const gatekeeperCore = create$20({ rules: morphoRules(chainRegistry.list()) });
8903
9548
  const handle = await start$1({
8904
9549
  gatekeeper: gatekeeperCore,
9550
+ chainRegistry,
8905
9551
  port: gatekeeperConfig?.port ?? 8082
8906
9552
  });
8907
9553
  logger.info({
@@ -8952,23 +9598,99 @@ async function getBook(params, db) {
8952
9598
  return failure(err);
8953
9599
  }
8954
9600
  }
8955
-
8956
- //#endregion
8957
- //#region src/api/Controllers/getConfig.ts
8958
- /**
8959
- * Returns the configuration for all chains the router is indexing.
8960
- * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
8961
- * @returns The configuration for all chains the router is indexing. {@link ApiPayload.Payload<ChainConfig[]>}
8962
- */
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)
8969
- });
8970
- configs.sort((a, b) => a.chain_id - b.chain_id);
8971
- return success({ data: configs });
9601
+
9602
+ //#endregion
9603
+ //#region src/api/Controllers/getConfigContracts.ts
9604
+ /**
9605
+ * Returns contract addresses used by indexers (mempool, v2) plus multicall per chain.
9606
+ * @param query - Raw query parameters containing optional chain filters.
9607
+ * @param chainRegistry - The chain registry instance. {@link ChainRegistry.ChainRegistry}
9608
+ * @returns The indexer contract configuration. {@link ApiPayload.Payload<ConfigContract[]>}
9609
+ */
9610
+ async function getConfigContracts(query, chainRegistry) {
9611
+ const parsed = safeParse("get_config_contracts", query ?? {});
9612
+ if (!parsed.success) return failure(parsed.error);
9613
+ const { chains: chainsFilter, cursor, limit } = parsed.data;
9614
+ const chainFilter = chainsFilter?.length ? new Set(chainsFilter) : null;
9615
+ const contracts = [];
9616
+ const seenAddresses = /* @__PURE__ */ new Set();
9617
+ for (const chain of chainRegistry.list()) {
9618
+ if (chainFilter && !chainFilter.has(chain.id)) continue;
9619
+ const mempool = chain.custom?.mempool?.address;
9620
+ if (!mempool) return failure(new InternalServerError(`Missing mempool address for chain ${chain.id}.`));
9621
+ const multicall = chain.contracts?.multicall3?.address;
9622
+ if (!multicall) return failure(new InternalServerError(`Missing multicall3 address for chain ${chain.id}.`));
9623
+ const v2 = chain.custom?.morpho?.address;
9624
+ if (!v2) return failure(new InternalServerError(`Missing morpho address for chain ${chain.id}.`));
9625
+ const chainContracts = [
9626
+ {
9627
+ chain_id: chain.id,
9628
+ name: "mempool",
9629
+ address: mempool
9630
+ },
9631
+ {
9632
+ chain_id: chain.id,
9633
+ name: "multicall",
9634
+ address: multicall
9635
+ },
9636
+ {
9637
+ chain_id: chain.id,
9638
+ name: "v2",
9639
+ address: v2
9640
+ }
9641
+ ];
9642
+ for (const contract of chainContracts) {
9643
+ const cursorKey = `${contract.chain_id}:${contract.address.toLowerCase()}`;
9644
+ if (seenAddresses.has(cursorKey)) return failure(new InternalServerError(`Duplicate contract address ${contract.address} for chain ${chain.id}.`));
9645
+ seenAddresses.add(cursorKey);
9646
+ contracts.push(contract);
9647
+ }
9648
+ }
9649
+ contracts.sort((a, b) => {
9650
+ if (a.chain_id !== b.chain_id) return a.chain_id - b.chain_id;
9651
+ const addressCompare = a.address.toLowerCase().localeCompare(b.address.toLowerCase());
9652
+ if (addressCompare !== 0) return addressCompare;
9653
+ return a.name.localeCompare(b.name);
9654
+ });
9655
+ let cursorContract = null;
9656
+ if (cursor) try {
9657
+ cursorContract = parseCursor(cursor);
9658
+ } catch (err) {
9659
+ return failure(err);
9660
+ }
9661
+ const startIndex = cursorContract ? findStartIndex(contracts, cursorContract) : 0;
9662
+ const page = contracts.slice(startIndex, startIndex + limit);
9663
+ const nextCursor = startIndex + limit < contracts.length && page.length > 0 ? formatCursor(page.at(-1)) : null;
9664
+ return success({
9665
+ data: page,
9666
+ cursor: nextCursor
9667
+ });
9668
+ }
9669
+ function parseCursor(cursor) {
9670
+ const [chain, address] = cursor.split(":", 2);
9671
+ if (!chain || !address) throw new BadRequestError("Cursor must be in the format chain_id:0x...");
9672
+ return {
9673
+ chain_id: Number.parseInt(chain, 10),
9674
+ address: address.toLowerCase()
9675
+ };
9676
+ }
9677
+ function formatCursor(contract) {
9678
+ return `${contract.chain_id}:${contract.address.toLowerCase()}`;
9679
+ }
9680
+ function findStartIndex(contracts, cursor) {
9681
+ let low = 0;
9682
+ let high = contracts.length;
9683
+ while (low < high) {
9684
+ const mid = Math.floor((low + high) / 2);
9685
+ const current = contracts[mid];
9686
+ if (compareContract(current, cursor) <= 0) low = mid + 1;
9687
+ else high = mid;
9688
+ }
9689
+ return low;
9690
+ }
9691
+ function compareContract(contract, cursor) {
9692
+ if (contract.chain_id !== cursor.chain_id) return contract.chain_id - cursor.chain_id;
9693
+ return contract.address.toLowerCase().localeCompare(cursor.address.toLowerCase());
8972
9694
  }
8973
9695
 
8974
9696
  //#endregion
@@ -8980,28 +9702,19 @@ const __dirname = (() => {
8980
9702
  return process.cwd();
8981
9703
  }
8982
9704
  })();
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
9705
  /**
8991
9706
  * Build the OpenAPI document for the router.
8992
- * @param parameters - Includes a {@link RulesProvider} to fetch gatekeeper rules.
8993
9707
  * @returns OpenAPI document. {@link OpenAPIDocument}
8994
9708
  */
8995
- async function getSwaggerJson({ gatekeeper }) {
8996
- return OpenApi({ rules: await getGatekeeperRules(gatekeeper) });
9709
+ async function getSwaggerJson() {
9710
+ return OpenApi();
8997
9711
  }
8998
9712
  /**
8999
9713
  * Render the API documentation HTML page.
9000
- * @param parameters - Includes a {@link RulesProvider} to fetch gatekeeper rules.
9001
9714
  * @returns HTML page as string.
9002
9715
  */
9003
- async function getDocsHtml({ gatekeeper }) {
9004
- const spec = await OpenApi({ rules: await getGatekeeperRules(gatekeeper) });
9716
+ async function getDocsHtml() {
9717
+ const spec = await OpenApi();
9005
9718
  return `<!DOCTYPE html>
9006
9719
  <html>
9007
9720
  <head>
@@ -9347,13 +10060,127 @@ async function getObligations(queryParameters, db) {
9347
10060
 
9348
10061
  //#endregion
9349
10062
  //#region src/api/Controllers/getOffers.ts
10063
+ /**
10064
+ * Query offers with computed consumed/available/takeable values.
10065
+ * @param db - The database client. {@link Database.Core}
10066
+ * @param parameters - {@link GetOffersQueryParams}
10067
+ * @returns The offers with pagination cursor.
10068
+ */
10069
+ async function getOffersQuery(db, parameters) {
10070
+ const limit = parameters?.limit ?? DEFAULT_LIMIT$2;
10071
+ const cursor = parameters?.cursor;
10072
+ const maker = parameters?.maker;
10073
+ if (cursor !== null && cursor !== void 0) {
10074
+ if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
10075
+ }
10076
+ const collateralsLateral = db.select({ collaterals: sql`COALESCE(
10077
+ jsonb_agg(
10078
+ jsonb_build_object(
10079
+ 'asset', ${obligationCollateralsV2.asset},
10080
+ 'oracle', ${oracles.address},
10081
+ 'lltv', ${obligationCollateralsV2.lltv}
10082
+ )
10083
+ ),
10084
+ '[]'::jsonb
10085
+ )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
10086
+ AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
10087
+ const availableLateral = db.select({ available: sql`COALESCE(SUM(
10088
+ CASE
10089
+ -- If asset is null, position available is 0
10090
+ WHEN ${positions.asset} IS NULL THEN 0
10091
+
10092
+ -- Position asset matches loan token: no conversion needed
10093
+ WHEN ${positions.asset} = ${obligations.loanToken} THEN
10094
+ CASE
10095
+ WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
10096
+ ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
10097
+ END
10098
+
10099
+ -- Position asset is collateral: apply oracle price * lltv
10100
+ -- Formula: balance * price / 1e36 * lltv / 1e18
10101
+ ELSE
10102
+ (CASE
10103
+ WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
10104
+ ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
10105
+ END)
10106
+ * COALESCE(${oracles.price}, 0)::numeric / 1e36
10107
+ * COALESCE(${obligationCollateralsV2.lltv}, 0)::numeric / 1e18
10108
+ END
10109
+ ), 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");
10110
+ const rows = (await db.select({
10111
+ hash: offers.hash,
10112
+ maker: offers.groupMaker,
10113
+ assets: offers.assets,
10114
+ obligationUnits: offers.obligationUnits,
10115
+ obligationShares: offers.obligationShares,
10116
+ consumed: groups.consumed,
10117
+ price: offers.price,
10118
+ maturity: offers.maturity,
10119
+ expiry: offers.expiry,
10120
+ start: offers.start,
10121
+ group: offers.group,
10122
+ session: offers.session,
10123
+ buy: offers.buy,
10124
+ chainId: obligations.chainId,
10125
+ loanToken: obligations.loanToken,
10126
+ callbackAddress: offers.callbackAddress,
10127
+ callbackData: offers.callbackData,
10128
+ collaterals: collateralsLateral.collaterals,
10129
+ blockNumber: offers.blockNumber,
10130
+ available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
10131
+ takeable: sql`FLOOR(GREATEST(
10132
+ 0,
10133
+ LEAST(
10134
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
10135
+ COALESCE(${availableLateral.available}::numeric, 0)
10136
+ )
10137
+ ))`.as("takeable")
10138
+ }).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(
10139
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
10140
+ COALESCE(${availableLateral.available}::numeric, 0)
10141
+ )) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
10142
+ return {
10143
+ hash: row.hash,
10144
+ maker: row.maker,
10145
+ assets: BigInt(row.assets),
10146
+ obligationUnits: BigInt(row.obligationUnits),
10147
+ obligationShares: BigInt(row.obligationShares),
10148
+ price: BigInt(row.price),
10149
+ maturity: from$16(row.maturity),
10150
+ expiry: row.expiry,
10151
+ start: row.start,
10152
+ group: row.group,
10153
+ session: row.session,
10154
+ buy: row.buy,
10155
+ chainId: row.chainId,
10156
+ loanToken: row.loanToken,
10157
+ collaterals: row.collaterals.map((c) => ({
10158
+ asset: c.asset,
10159
+ oracle: c.oracle,
10160
+ lltv: BigInt(c.lltv)
10161
+ })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
10162
+ callback: {
10163
+ address: row.callbackAddress,
10164
+ data: row.callbackData
10165
+ },
10166
+ consumed: BigInt(row.consumed),
10167
+ available: BigInt(String(row.available ?? "0").split(".")[0] ?? "0"),
10168
+ takeable: BigInt(String(row.takeable ?? "0").split(".")[0] ?? "0"),
10169
+ blockNumber: row.blockNumber
10170
+ };
10171
+ });
10172
+ return {
10173
+ rows,
10174
+ nextCursor: rows.length === limit ? rows[rows.length - 1].hash : null
10175
+ };
10176
+ }
9350
10177
  async function getOffers(queryParameters, db) {
9351
10178
  const logger = getLogger();
9352
10179
  const result = safeParse("get_offers", queryParameters, (issue) => issue.message);
9353
10180
  if (!result.success) return failure(result.error);
9354
10181
  const query = result.data;
9355
10182
  try {
9356
- const { rows, nextCursor } = query.maker ? await db.offers.get({
10183
+ const { rows, nextCursor } = query.maker ? await getOffersQuery(db, {
9357
10184
  maker: query.maker,
9358
10185
  cursor: query.cursor,
9359
10186
  limit: query.limit
@@ -9421,6 +10248,35 @@ async function getUserPositions(queryParameters, db) {
9421
10248
  }
9422
10249
  }
9423
10250
 
10251
+ //#endregion
10252
+ //#region src/api/Controllers/resolveCallbackTypes.ts
10253
+ /**
10254
+ * Resolve callback types for a list of callback addresses grouped by chain.
10255
+ * @param body - Request body with callback addresses. {@link CallbackTypesRequest}
10256
+ * @param chains - Chains to resolve callback types against. {@link Chain.Chain}
10257
+ * @returns Callback types grouped by chain. {@link CallbackTypesPayload}
10258
+ */
10259
+ async function resolveCallbackTypes$1(body, chains) {
10260
+ const result = safeParse("callback_types", body, (issue) => issue.message);
10261
+ if (!result.success) return failure(result.error);
10262
+ const request = result.data;
10263
+ const chainIds = new Set(chains.map((chain) => chain.id));
10264
+ const unknown = request.callbacks.find((entry) => !chainIds.has(entry.chain_id));
10265
+ if (unknown) return failure(new BadRequestError(`Unknown chain id ${unknown.chain_id}`));
10266
+ try {
10267
+ const data = resolveCallbackTypes$2({
10268
+ chains,
10269
+ request
10270
+ });
10271
+ return success({
10272
+ data,
10273
+ cursor: null
10274
+ });
10275
+ } catch (err) {
10276
+ return failure(err);
10277
+ }
10278
+ }
10279
+
9424
10280
  //#endregion
9425
10281
  //#region src/api/Api.ts
9426
10282
  function from(config) {
@@ -9494,6 +10350,21 @@ function serve$1(parameters) {
9494
10350
  return c.json(failure$1.body, failure$1.statusCode);
9495
10351
  }
9496
10352
  });
10353
+ app.post("/v1/callbacks", async (c) => {
10354
+ let body;
10355
+ try {
10356
+ body = await c.req.json();
10357
+ } catch (err) {
10358
+ const failure$3 = failure(err);
10359
+ return c.json(failure$3.body, failure$3.statusCode);
10360
+ }
10361
+ if (body === null || typeof body !== "object") {
10362
+ const failure$2 = failure(new BadRequestError("Request body must be a JSON object"));
10363
+ return c.json(failure$2.body, failure$2.statusCode);
10364
+ }
10365
+ const { statusCode, body: responseBody } = await resolveCallbackTypes$1(body, chainRegistry.list());
10366
+ return c.json(responseBody, statusCode);
10367
+ });
9497
10368
  app.get("/v1/users/:userAddress/positions", async (c) => {
9498
10369
  const query = c.req.query();
9499
10370
  const { statusCode, body } = await getUserPositions({
@@ -9515,12 +10386,21 @@ function serve$1(parameters) {
9515
10386
  const { statusCode, body } = await getHealthChains(c.req.query(), db, void 0, chainRegistry);
9516
10387
  return c.json(body, statusCode);
9517
10388
  });
9518
- app.get("/v1/config", async (c) => {
9519
- const { statusCode, body } = await getConfig(chainRegistry);
10389
+ app.get("/v1/config/contracts", async (c) => {
10390
+ const { statusCode, body } = await getConfigContracts(c.req.query(), chainRegistry);
9520
10391
  return c.json(body, statusCode);
9521
10392
  });
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));
10393
+ app.get("/v1/config/rules", async (c) => {
10394
+ try {
10395
+ const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
10396
+ return c.json(body, statusCode);
10397
+ } catch (err) {
10398
+ const failure$4 = failure(err);
10399
+ return c.json(failure$4.body, failure$4.statusCode);
10400
+ }
10401
+ });
10402
+ app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
10403
+ app.get("/docs/api", async (c) => c.html(await getDocsHtml(), 200));
9524
10404
  app.get("/docs", async (c) => c.html(await getIntegratorDocsHtml(), 200));
9525
10405
  serve({
9526
10406
  fetch: app.fetch,
@@ -9528,6 +10408,120 @@ function serve$1(parameters) {
9528
10408
  });
9529
10409
  }
9530
10410
 
10411
+ //#endregion
10412
+ //#region src/gatekeeper/Client.ts
10413
+ const DEFAULT_TIMEOUT_MS = 1e4;
10414
+ /**
10415
+ * Create an HTTP client for a gatekeeper service.
10416
+ * @param config - Gatekeeper client configuration. {@link ClientConfig}
10417
+ * @returns An HTTP-backed gatekeeper client. {@link GatekeeperClient}
10418
+ */
10419
+ function createHttpClient(config) {
10420
+ const fetchFn = config.fetchFn ?? fetch;
10421
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10422
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
10423
+ const baseHeaders = config.originSecret ? { "x-origin-verify": config.originSecret } : void 0;
10424
+ const request = async (path, init) => {
10425
+ const controller = new AbortController();
10426
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
10427
+ try {
10428
+ return await fetchFn(`${baseUrl}${path}`, {
10429
+ ...init,
10430
+ headers: mergeHeaders(baseHeaders, init.headers),
10431
+ signal: controller.signal
10432
+ });
10433
+ } finally {
10434
+ clearTimeout(timeout);
10435
+ }
10436
+ };
10437
+ const validate = async (body) => {
10438
+ const response = await request("/v1/validate", {
10439
+ method: "POST",
10440
+ headers: { "content-type": "application/json" },
10441
+ body: JSON.stringify(body)
10442
+ });
10443
+ const json = await response.json();
10444
+ return {
10445
+ statusCode: response.status,
10446
+ body: json
10447
+ };
10448
+ };
10449
+ const getConfigRules = async (query) => {
10450
+ const params = new URLSearchParams();
10451
+ if (query?.cursor) params.set("cursor", query.cursor);
10452
+ if (query?.limit !== void 0) params.set("limit", query.limit.toString());
10453
+ if (query?.types !== void 0) {
10454
+ const typesValue = Array.isArray(query.types) ? query.types.join(",") : query.types;
10455
+ if (typesValue.length > 0) params.set("types", typesValue);
10456
+ }
10457
+ const response = await request(params.size > 0 ? `/v1/config/rules?${params.toString()}` : "/v1/config/rules", { method: "GET" });
10458
+ const json = await response.json();
10459
+ return {
10460
+ statusCode: response.status,
10461
+ body: json
10462
+ };
10463
+ };
10464
+ const isAllowed = async (offers) => {
10465
+ const { statusCode, body } = await validate({ offers: offers.map((offer) => toSnakeCase(offer)) });
10466
+ if (statusCode !== 200) {
10467
+ const errorMessage = extractErrorMessage(body);
10468
+ throw new Error(`Gatekeeper validation failed: ${errorMessage ?? `status ${statusCode}`}`);
10469
+ }
10470
+ const data = body.data;
10471
+ if (!data || typeof data !== "object") throw new Error("Gatekeeper validation response is invalid.");
10472
+ if ("issues" in data) {
10473
+ const issues = data.issues.map((issue) => ({
10474
+ ruleName: issue.rule,
10475
+ message: issue.message,
10476
+ item: offers[issue.index]
10477
+ }));
10478
+ const invalidIndices = new Set(data.issues.map((issue) => issue.index));
10479
+ return {
10480
+ valid: offers.filter((_, index) => !invalidIndices.has(index)),
10481
+ issues
10482
+ };
10483
+ }
10484
+ if (!("payload" in data) || !("root" in data)) throw new Error("Gatekeeper validation response is missing payload data.");
10485
+ return {
10486
+ valid: offers.slice(),
10487
+ issues: []
10488
+ };
10489
+ };
10490
+ const getCallbackTypes = async (requestPayload) => {
10491
+ const response = await request("/v1/callbacks", {
10492
+ method: "POST",
10493
+ headers: { "content-type": "application/json" },
10494
+ body: JSON.stringify(requestPayload)
10495
+ });
10496
+ const json = await response.json();
10497
+ if (!response.ok) throw new Error(`Gatekeeper callbacks request failed: ${extractErrorMessage(json) ?? response.statusText}`);
10498
+ if (!("data" in json) || !Array.isArray(json.data)) throw new Error("Gatekeeper callbacks response is invalid.");
10499
+ return json.data;
10500
+ };
10501
+ return {
10502
+ baseUrl,
10503
+ validate,
10504
+ getConfigRules,
10505
+ isAllowed,
10506
+ getCallbackTypes
10507
+ };
10508
+ }
10509
+ function mergeHeaders(base, extra) {
10510
+ if (!base && !extra) return void 0;
10511
+ const merged = new Headers(base ?? void 0);
10512
+ if (extra) for (const [key, value] of new Headers(extra).entries()) merged.set(key, value);
10513
+ return merged;
10514
+ }
10515
+ function normalizeBaseUrl(url) {
10516
+ return url.trim().replace(/\/+$/, "");
10517
+ }
10518
+ function extractErrorMessage(payload) {
10519
+ if (!payload || typeof payload !== "object") return void 0;
10520
+ const error = payload.error;
10521
+ if (!error || typeof error !== "object") return void 0;
10522
+ return typeof error.message === "string" ? error.message : void 0;
10523
+ }
10524
+
9531
10525
  //#endregion
9532
10526
  //#region src/cli/commands/start.ts
9533
10527
  const startCmd = new RouterCmd("start");
@@ -9538,9 +10532,10 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
9538
10532
  let gatekeeperUrl = gatekeeperConfig?.url;
9539
10533
  let gatekeeperHandle = null;
9540
10534
  if (!gatekeeperUrl) {
9541
- const gatekeeperCore = create$17({ rules: morphoRules(chainRegistry.list()) });
10535
+ const gatekeeperCore = create$20({ rules: morphoRules(chainRegistry.list()) });
9542
10536
  gatekeeperHandle = await start$1({
9543
10537
  gatekeeper: gatekeeperCore,
10538
+ chainRegistry,
9544
10539
  port: gatekeeperConfig?.port ?? 8082
9545
10540
  });
9546
10541
  gatekeeperUrl = gatekeeperHandle.url;
@@ -9552,72 +10547,36 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
9552
10547
  }
9553
10548
  const gatekeeper = createHttpClient({
9554
10549
  baseUrl: gatekeeperUrl ?? "http://localhost:8082",
9555
- timeoutMs: gatekeeperConfig?.timeoutMs
10550
+ timeoutMs: gatekeeperConfig?.timeoutMs,
10551
+ originSecret: gatekeeperConfig?.originSecret
9556
10552
  });
9557
10553
  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
10554
+ const offers = await seedMockOffers({
10555
+ db,
10556
+ gatekeeper,
10557
+ chainRegistry,
10558
+ count: mock
9584
10559
  });
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
10560
  logger.info({
9611
10561
  msg: `Offers seeded`,
9612
- count: mock
10562
+ count: offers.length
9613
10563
  });
9614
10564
  }
9615
10565
  if (file) {
9616
10566
  const offers = await getOffersFromFile(file);
9617
- await db.offers.create([{
9618
- blockNumber: 1,
9619
- offers
9620
- }]);
10567
+ const offerBlockNumber = 1;
10568
+ const positionBlockNumber = offerBlockNumber + 1;
10569
+ await seedOffers({
10570
+ db,
10571
+ offers,
10572
+ blockNumber: offerBlockNumber
10573
+ });
10574
+ await seedOfferAssociations({
10575
+ db,
10576
+ gatekeeper,
10577
+ offers,
10578
+ blockNumber: positionBlockNumber
10579
+ });
9621
10580
  logger.info({
9622
10581
  msg: `Offers seeded from file`,
9623
10582
  count: offers.length,
@@ -9669,10 +10628,59 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
9669
10628
  process.stdin.resume();
9670
10629
  });
9671
10630
  });
10631
+ /**
10632
+ * Seed mock offers and their dependencies for the router API.
10633
+ * @param parameters - Seed parameters. {@link seedMockOffers.Parameters}
10634
+ * @returns The generated mock offers. {@link seedMockOffers.ReturnType}
10635
+ */
10636
+ async function seedMockOffers(parameters) {
10637
+ const { db, gatekeeper, chainRegistry, count } = parameters;
10638
+ if (count <= 0) return [];
10639
+ const configuredChains = chainRegistry.list();
10640
+ const tokens = {
10641
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": 6,
10642
+ "0x6B175474E89094C44Da98b954EedeAC495271d0F": 18,
10643
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": 18,
10644
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": 8,
10645
+ "0x4200000000000000000000000000000000000006": 18,
10646
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 6,
10647
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": 18
10648
+ };
10649
+ const now = Math.floor(Date.now() / 1e3);
10650
+ const offerBlockNumber = 1;
10651
+ const positionBlockNumber = offerBlockNumber + 1;
10652
+ const start = Math.max(0, now - 60);
10653
+ const expiry = now + 3600;
10654
+ const maturity = from$16("end_of_next_month");
10655
+ const oraclePrice = 10n ** 36n;
10656
+ const offers = createMockOffers({
10657
+ count,
10658
+ chains: configuredChains,
10659
+ loanTokens: Object.keys(tokens).map((key) => key),
10660
+ assetsDecimals: tokens,
10661
+ start,
10662
+ expiry,
10663
+ maturity,
10664
+ chainRegistry
10665
+ });
10666
+ await seedOffers({
10667
+ db,
10668
+ offers,
10669
+ blockNumber: offerBlockNumber
10670
+ });
10671
+ await db.oracles.upsert(seedMockOracles(offers, oraclePrice, positionBlockNumber));
10672
+ await seedOfferAssociations({
10673
+ db,
10674
+ gatekeeper,
10675
+ offers,
10676
+ blockNumber: positionBlockNumber
10677
+ });
10678
+ return offers;
10679
+ }
9672
10680
  async function getOffersFromFile(filePath) {
9673
10681
  const content = await readFile(filePath, "utf-8");
9674
10682
  const data = JSON.parse(content);
9675
- return Array.isArray(data) ? data.map(from$11) : [from$11(data)];
10683
+ return Array.isArray(data) ? data.map(from$12) : [from$12(data)];
9676
10684
  }
9677
10685
  function createMockOffers(parameters) {
9678
10686
  const { count, chains, loanTokens, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
@@ -9687,13 +10695,11 @@ function createMockOffers(parameters) {
9687
10695
  maturity,
9688
10696
  buy
9689
10697
  });
9690
- const chain = chainRegistry.getById(offer.chainId);
9691
- if (!chain) throw new Error(`Missing chain config for id ${offer.chainId}`);
10698
+ if (!chainRegistry.getById(offer.chainId)) throw new Error(`Missing chain config for id ${offer.chainId}`);
9692
10699
  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)`);
10700
+ const callbackAddress = buy ? BUY_CALLBACK_ADDRESS : SELL_CALLBACK_ADDRESS;
9695
10701
  const callbackData = buildMockCallbackData(callbackType, offer);
9696
- return from$11({
10702
+ return from$12({
9697
10703
  ...offer,
9698
10704
  callback: {
9699
10705
  address: callbackAddress,
@@ -9719,7 +10725,7 @@ function seedMockOracles(offers, price, blockNumber) {
9719
10725
  for (const offer of offers) for (const collateral of offer.collaterals) {
9720
10726
  const key = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
9721
10727
  if (oracleMap.has(key)) continue;
9722
- oracleMap.set(key, from$10({
10728
+ oracleMap.set(key, from$11({
9723
10729
  chainId: offer.chainId,
9724
10730
  address: collateral.oracle,
9725
10731
  price: price.toString(),
@@ -9728,6 +10734,158 @@ function seedMockOracles(offers, price, blockNumber) {
9728
10734
  }
9729
10735
  return Array.from(oracleMap.values());
9730
10736
  }
10737
+ const BUY_CALLBACK_ADDRESS = "0x3333333333333333333333333333333333333333";
10738
+ const SELL_CALLBACK_ADDRESS = "0x1111111111111111111111111111111111111111";
10739
+ async function seedOffers(parameters) {
10740
+ const { db, offers, blockNumber } = parameters;
10741
+ if (offers.length === 0) return;
10742
+ const dependencies = buildOfferDependencies({
10743
+ offers,
10744
+ blockNumber
10745
+ });
10746
+ await db.oracles.upsert(dependencies.oracles);
10747
+ await db.obligations.create(dependencies.obligations);
10748
+ await db.groups.create(dependencies.groups);
10749
+ await db.offers.create([{
10750
+ blockNumber,
10751
+ offers
10752
+ }]);
10753
+ }
10754
+ async function seedOfferAssociations(parameters) {
10755
+ const { db, gatekeeper, offers, blockNumber } = parameters;
10756
+ if (offers.length === 0) return;
10757
+ const { callbacks, positions, lots } = buildOfferAssociationsFromOffers({
10758
+ offers,
10759
+ callbackTypes: await resolveCallbackTypes({
10760
+ gatekeeper,
10761
+ offers
10762
+ }),
10763
+ blockNumber
10764
+ });
10765
+ if (positions.length > 0) await db.positions.upsert(positions);
10766
+ if (callbacks.length > 0) await db.callbacks.upsert(callbacks);
10767
+ if (lots.length > 0) await db.lots.create(lots);
10768
+ }
10769
+ function buildOfferDependencies(parameters) {
10770
+ const { offers, blockNumber } = parameters;
10771
+ const obligationsById = /* @__PURE__ */ new Map();
10772
+ const oraclesByKey = /* @__PURE__ */ new Map();
10773
+ const groupsByKey = /* @__PURE__ */ new Map();
10774
+ for (const offer of offers) {
10775
+ const obligationId$1 = obligationId(offer);
10776
+ if (!obligationsById.get(obligationId$1)) obligationsById.set(obligationId$1, from$13({
10777
+ chainId: offer.chainId,
10778
+ loanToken: offer.loanToken,
10779
+ maturity: offer.maturity,
10780
+ collaterals: offer.collaterals
10781
+ }));
10782
+ for (const collateral of offer.collaterals) {
10783
+ const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
10784
+ if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
10785
+ chainId: offer.chainId,
10786
+ address: collateral.oracle,
10787
+ price: null,
10788
+ blockNumber
10789
+ }));
10790
+ }
10791
+ const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
10792
+ if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
10793
+ chainId: offer.chainId,
10794
+ maker: offer.maker,
10795
+ group: offer.group,
10796
+ blockNumber
10797
+ });
10798
+ }
10799
+ return {
10800
+ obligations: Array.from(obligationsById.values()),
10801
+ oracles: Array.from(oraclesByKey.values()),
10802
+ groups: Array.from(groupsByKey.values())
10803
+ };
10804
+ }
10805
+ async function resolveCallbackTypes(parameters) {
10806
+ const { gatekeeper, offers } = parameters;
10807
+ const addressesByChain = /* @__PURE__ */ new Map();
10808
+ for (const offer of offers) {
10809
+ if (offer.callback.data === "0x") continue;
10810
+ const set = addressesByChain.get(offer.chainId) ?? /* @__PURE__ */ new Set();
10811
+ set.add(offer.callback.address.toLowerCase());
10812
+ addressesByChain.set(offer.chainId, set);
10813
+ }
10814
+ if (addressesByChain.size === 0) return /* @__PURE__ */ new Map();
10815
+ const request = { callbacks: Array.from(addressesByChain.entries()).map(([chainId, addresses]) => ({
10816
+ chain_id: chainId,
10817
+ addresses: Array.from(addresses)
10818
+ })) };
10819
+ const response = await gatekeeper.getCallbackTypes(request);
10820
+ const typeByAddress = /* @__PURE__ */ new Map();
10821
+ for (const entry of response) {
10822
+ const chainId = entry.chain_id;
10823
+ for (const [key, list] of Object.entries(entry)) {
10824
+ if (key === "chain_id" || key === "not_supported") continue;
10825
+ if (!Array.isArray(list)) continue;
10826
+ for (const address of list) {
10827
+ const mapKey = `${chainId}-${address.toLowerCase()}`;
10828
+ typeByAddress.set(mapKey, key);
10829
+ }
10830
+ }
10831
+ }
10832
+ return typeByAddress;
10833
+ }
10834
+ function buildOfferAssociationsFromOffers(parameters) {
10835
+ const { offers, callbackTypes, blockNumber } = parameters;
10836
+ const callbacks = [];
10837
+ const positions = [];
10838
+ const lots = [];
10839
+ for (const offer of offers) {
10840
+ if (offer.callback.data === "0x") continue;
10841
+ const key = `${offer.chainId}-${offer.callback.address.toLowerCase()}`;
10842
+ const callbackType = callbackTypes.get(key);
10843
+ if (!callbackType) continue;
10844
+ let decoded;
10845
+ try {
10846
+ decoded = decode$1(callbackType, offer.callback.data);
10847
+ } catch (err) {
10848
+ const error = err instanceof Error ? err : new Error(String(err));
10849
+ throw new Error("Failed to decode callback data", { cause: error });
10850
+ }
10851
+ if (decoded.length === 0) continue;
10852
+ callbacks.push({
10853
+ offerHash: hash(offer),
10854
+ callbacks: decoded.map((callback) => ({
10855
+ chainId: offer.chainId,
10856
+ contract: callback.contract,
10857
+ user: offer.maker,
10858
+ amount: callback.amount
10859
+ }))
10860
+ });
10861
+ for (const callback of decoded) {
10862
+ const positionType = callbackType === Type$1.BuyVaultV1Callback ? Type.VAULT_V1 : Type.ERC20;
10863
+ const positionAsset = callbackType === Type$1.BuyVaultV1Callback ? offer.loanToken : callback.contract;
10864
+ positions.push(from$10({
10865
+ chainId: offer.chainId,
10866
+ contract: callback.contract,
10867
+ user: offer.maker,
10868
+ type: positionType,
10869
+ balance: callback.amount * 2n,
10870
+ asset: positionAsset,
10871
+ blockNumber
10872
+ }));
10873
+ const isLoanPosition = positionAsset.toLowerCase() === offer.loanToken.toLowerCase();
10874
+ lots.push({
10875
+ positionChainId: offer.chainId,
10876
+ positionContract: callback.contract,
10877
+ positionUser: offer.maker,
10878
+ group: offer.group,
10879
+ size: isLoanPosition ? offer.assets : callback.amount
10880
+ });
10881
+ }
10882
+ }
10883
+ return {
10884
+ callbacks,
10885
+ positions,
10886
+ lots
10887
+ };
10888
+ }
9731
10889
 
9732
10890
  //#endregion
9733
10891
  //#region src/cli/cli.ts