@morpho-dev/router 0.6.0 → 0.7.1

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
@@ -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.6.0";
155
+ var version = "0.7.1";
156
156
  var description = "Router package for Morpho protocol";
157
157
 
158
158
  //#endregion
@@ -1483,73 +1483,10 @@ function create$20(parameters) {
1483
1483
  //#region src/core/Callback.ts
1484
1484
  let Type$1 = /* @__PURE__ */ function(Type) {
1485
1485
  Type["BuyWithEmptyCallback"] = "buy_with_empty_callback";
1486
- Type["BuyERC20"] = "buy_erc20";
1487
- Type["BuyVaultV1Callback"] = "buy_vault_v1_callback";
1488
- Type["SellERC20Callback"] = "sell_erc20_callback";
1486
+ Type["SellWithEmptyCallback"] = "sell_with_empty_callback";
1489
1487
  return Type;
1490
1488
  }({});
1491
1489
  const isEmptyCallback = (offer) => offer.callback.data === "0x";
1492
- function decode$1(type, data) {
1493
- switch (type) {
1494
- case Type$1.BuyERC20: return decodeBuyERC20(data);
1495
- case Type$1.BuyVaultV1Callback: return decodeBuyVaultV1Callback(data);
1496
- case Type$1.SellERC20Callback: return decodeSellERC20Callback(data);
1497
- default: throw new Error("Invalid callback type");
1498
- }
1499
- }
1500
- /**
1501
- * Decodes BuyERC20 callback data into positions.
1502
- * @param data - The ABI-encoded callback data containing token addresses and amounts.
1503
- * @returns Array of positions with contract address and amount.
1504
- * @throws If data is empty, malformed, or arrays have mismatched lengths.
1505
- */
1506
- function decodeBuyERC20(data) {
1507
- if (!data || data === "0x") throw new Error("Empty callback data");
1508
- let tokens;
1509
- let amounts;
1510
- try {
1511
- [tokens, amounts] = decodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], data);
1512
- } catch (_) {
1513
- throw new Error("Invalid BuyERC20 callback data");
1514
- }
1515
- if (tokens.length !== amounts.length) throw new Error("Mismatched array lengths");
1516
- return tokens.map((token, index) => ({
1517
- contract: token,
1518
- amount: amounts[index]
1519
- }));
1520
- }
1521
- function decodeBuyVaultV1Callback(data) {
1522
- if (!data || data === "0x") throw new Error("Empty callback data");
1523
- try {
1524
- const [vaults, amounts] = decodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], data);
1525
- if (vaults.length !== amounts.length) throw new Error("Mismatched array lengths");
1526
- return vaults.map((v, i) => ({
1527
- contract: v,
1528
- amount: amounts[i]
1529
- }));
1530
- } catch (_) {
1531
- throw new Error("Invalid BuyVaultV1Callback callback data");
1532
- }
1533
- }
1534
- function decodeSellERC20Callback(data) {
1535
- if (!data || data === "0x") throw new Error("Empty callback data");
1536
- try {
1537
- const [collaterals, amounts] = decodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], data);
1538
- if (collaterals.length !== amounts.length) throw new Error("Mismatched array lengths");
1539
- return collaterals.map((c, i) => ({
1540
- contract: c,
1541
- amount: amounts[i]
1542
- }));
1543
- } catch (_) {
1544
- throw new Error("Invalid SellERC20Callback callback data");
1545
- }
1546
- }
1547
- function encodeBuyVaultV1Callback(parameters) {
1548
- return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.vaults, parameters.amounts]);
1549
- }
1550
- function encodeSellERC20Callback(parameters) {
1551
- return encodeAbiParameters([{ type: "address[]" }, { type: "uint256[]" }], [parameters.collaterals, parameters.amounts]);
1552
- }
1553
1490
 
1554
1491
  //#endregion
1555
1492
  //#region src/core/Maturity.ts
@@ -1671,38 +1608,23 @@ var InvalidOptionError$1 = class extends BaseError {
1671
1608
 
1672
1609
  //#endregion
1673
1610
  //#region src/gatekeeper/GateConfig.ts
1674
- /**
1675
- * Attempts to infer the configured callback type from a callback address on a chain.
1676
- * Skips the empty callback type as it does not carry addresses.
1677
- *
1678
- * @param chain - Chain name for which to infer the callback type
1679
- * @param address - Callback contract address
1680
- * @returns The callback type when found, otherwise undefined
1681
- */
1682
- function getCallbackType(chain, address) {
1683
- return configs[chain].callbacks?.find((c) => c.type !== Type$1.BuyWithEmptyCallback && c.addresses.includes(address?.toLowerCase()))?.type;
1684
- }
1685
- /**
1686
- * Returns the list of allowed non-empty callback addresses for a chain.
1687
- *
1688
- * @param chain - Chain name
1689
- * @returns Array of allowed callback addresses (lowercased). Empty when none configured
1690
- */
1691
- const getCallbackAddresses = (chain) => {
1692
- return configs[chain].callbacks?.filter((c) => c.type !== Type$1.BuyWithEmptyCallback).flatMap((c) => c.addresses) ?? [];
1693
- };
1694
1611
  const assets = {
1695
1612
  [ChainId.ETHEREUM.toString()]: [
1696
1613
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1697
1614
  "0x6B175474E89094C44Da98b954EedeAC495271d0F",
1698
1615
  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
1699
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1616
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1617
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
1618
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
1700
1619
  ],
1701
1620
  [ChainId.BASE.toString()]: [
1702
1621
  "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
1703
1622
  "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
1704
1623
  "0x4200000000000000000000000000000000000006",
1705
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1624
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1625
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
1626
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
1627
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
1706
1628
  ],
1707
1629
  [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
1708
1630
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
@@ -1718,65 +1640,58 @@ const assets = {
1718
1640
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1719
1641
  ]
1720
1642
  };
1643
+ const oracles$1 = {
1644
+ [ChainId.ETHEREUM.toString()]: [
1645
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
1646
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
1647
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
1648
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
1649
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
1650
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
1651
+ ],
1652
+ [ChainId.BASE.toString()]: [
1653
+ "0xD09048c8B568Dbf5f189302beA26c9edABFC4858",
1654
+ "0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4",
1655
+ "0x05D2618404668D725B66c0f32B39e4EC15B393dC",
1656
+ "0xE1bb8E5b4930eC9FeC7f7943FCF6227649F14B37",
1657
+ "0x663BECd10daE6C4A3Dcd89F1d76c1174199639B9",
1658
+ "0x10b95702a0ce895972C91e432C4f7E19811D320E",
1659
+ "0x8C87DbD7A0c647A4291592Bc2994dbF95880fE2F",
1660
+ "0x4A11590e5326138B514E08A9B52202D42077Ca65",
1661
+ "0xa54122f0E0766258377Ffe732e454A3248f454F4"
1662
+ ],
1663
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
1664
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
1665
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
1666
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
1667
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
1668
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
1669
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
1670
+ ],
1671
+ [ChainId.ANVIL.toString()]: [
1672
+ "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
1673
+ "0x9CB3f4276bcD149b3668e1a645a964bC12877b89",
1674
+ "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2",
1675
+ "0x6Eb9F4128CeBc8B885A4d8562Db1Addf097f7348",
1676
+ "0xbD60A6770b27E084E8617335ddE769241B0e71D8",
1677
+ "0xAe12416c1F21B0698c27fe042D9309C83baC6597"
1678
+ ]
1679
+ };
1721
1680
  const configs = {
1722
1681
  ethereum: {
1723
- callbacks: [
1724
- {
1725
- type: Type$1.BuyVaultV1Callback,
1726
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
1727
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
1728
- },
1729
- {
1730
- type: Type$1.SellERC20Callback,
1731
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
1732
- },
1733
- { type: Type$1.BuyWithEmptyCallback }
1734
- ],
1682
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
1735
1683
  maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
1736
1684
  },
1737
1685
  base: {
1738
- callbacks: [
1739
- {
1740
- type: Type$1.BuyVaultV1Callback,
1741
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
1742
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0xFf62A7c278C62eD665133147129245053Bbf5918"]
1743
- },
1744
- {
1745
- type: Type$1.SellERC20Callback,
1746
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
1747
- },
1748
- { type: Type$1.BuyWithEmptyCallback }
1749
- ],
1686
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
1750
1687
  maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
1751
1688
  },
1752
1689
  "ethereum-virtual-testnet": {
1753
- callbacks: [
1754
- {
1755
- type: Type$1.BuyVaultV1Callback,
1756
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
1757
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
1758
- },
1759
- {
1760
- type: Type$1.SellERC20Callback,
1761
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
1762
- },
1763
- { type: Type$1.BuyWithEmptyCallback }
1764
- ],
1690
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
1765
1691
  maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
1766
1692
  },
1767
1693
  anvil: {
1768
- callbacks: [
1769
- {
1770
- type: Type$1.BuyVaultV1Callback,
1771
- addresses: ["0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444"],
1772
- vaultFactories: ["0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101", "0x1897A8997241C1cD4bD0698647e4EB7213535c24"]
1773
- },
1774
- {
1775
- type: Type$1.SellERC20Callback,
1776
- addresses: ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
1777
- },
1778
- { type: Type$1.BuyWithEmptyCallback }
1779
- ],
1694
+ callbacks: [{ type: Type$1.BuyWithEmptyCallback }, { type: Type$1.SellWithEmptyCallback }],
1780
1695
  maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth]
1781
1696
  }
1782
1697
  };
@@ -2258,7 +2173,7 @@ function random(config) {
2258
2173
  const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
2259
2174
  const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
2260
2175
  const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
2261
- const collateralAsset = collateralCandidates[int(collateralCandidates.length)];
2176
+ collateralCandidates[int(collateralCandidates.length)];
2262
2177
  const maturityOption = weightedChoice([["end_of_month", 1], ["end_of_next_month", 1]]);
2263
2178
  const maturity = config?.maturity ?? from$16(maturityOption);
2264
2179
  const lltv = from$15(weightedChoice([
@@ -2285,21 +2200,10 @@ function random(config) {
2285
2200
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2286
2201
  const amountBase = BigInt(100 + int(999901));
2287
2202
  const assetsScaled = config?.assets ?? amountBase * unit;
2288
- const callbackBySide = (() => {
2289
- if (buy) return {
2290
- address: zeroAddress,
2291
- data: "0x"
2292
- };
2293
- const sellCallbackAddress = "0x3333333333333333333333333333333333333333";
2294
- const amount = assetsScaled * 1000000000000000000000n;
2295
- return {
2296
- address: sellCallbackAddress,
2297
- data: encodeSellERC20Callback({
2298
- collaterals: [collateralAsset],
2299
- amounts: [amount]
2300
- })
2301
- };
2302
- })();
2203
+ const emptyCallback = {
2204
+ address: zeroAddress,
2205
+ data: "0x"
2206
+ };
2303
2207
  return from$12({
2304
2208
  maker: config?.maker ?? address(),
2305
2209
  assets: assetsScaled,
@@ -2318,7 +2222,7 @@ function random(config) {
2318
2222
  ...random$1(),
2319
2223
  lltv
2320
2224
  })).sort((a, b) => a.asset.localeCompare(b.asset)),
2321
- callback: config?.callback ?? callbackBySide
2225
+ callback: config?.callback ?? emptyCallback
2322
2226
  });
2323
2227
  }
2324
2228
  const weightedChoice = (pairs) => {
@@ -2472,6 +2376,94 @@ function obligationId(offer) {
2472
2376
  }));
2473
2377
  }
2474
2378
  /**
2379
+ * ABI for the Take event emitted by the Morpho V2 contract.
2380
+ */
2381
+ const takeEvent = {
2382
+ type: "event",
2383
+ name: "Take",
2384
+ inputs: [
2385
+ {
2386
+ name: "caller",
2387
+ type: "address",
2388
+ indexed: false,
2389
+ internalType: "address"
2390
+ },
2391
+ {
2392
+ name: "id",
2393
+ type: "bytes32",
2394
+ indexed: true,
2395
+ internalType: "bytes32"
2396
+ },
2397
+ {
2398
+ name: "maker",
2399
+ type: "address",
2400
+ indexed: true,
2401
+ internalType: "address"
2402
+ },
2403
+ {
2404
+ name: "taker",
2405
+ type: "address",
2406
+ indexed: true,
2407
+ internalType: "address"
2408
+ },
2409
+ {
2410
+ name: "offerIsBuy",
2411
+ type: "bool",
2412
+ indexed: false,
2413
+ internalType: "bool"
2414
+ },
2415
+ {
2416
+ name: "buyerAssets",
2417
+ type: "uint256",
2418
+ indexed: false,
2419
+ internalType: "uint256"
2420
+ },
2421
+ {
2422
+ name: "sellerAssets",
2423
+ type: "uint256",
2424
+ indexed: false,
2425
+ internalType: "uint256"
2426
+ },
2427
+ {
2428
+ name: "obligationUnits",
2429
+ type: "uint256",
2430
+ indexed: false,
2431
+ internalType: "uint256"
2432
+ },
2433
+ {
2434
+ name: "obligationShares",
2435
+ type: "uint256",
2436
+ indexed: false,
2437
+ internalType: "uint256"
2438
+ },
2439
+ {
2440
+ name: "buyerIsLender",
2441
+ type: "bool",
2442
+ indexed: false,
2443
+ internalType: "bool"
2444
+ },
2445
+ {
2446
+ name: "sellerIsBorrower",
2447
+ type: "bool",
2448
+ indexed: false,
2449
+ internalType: "bool"
2450
+ },
2451
+ {
2452
+ name: "group",
2453
+ type: "bytes32",
2454
+ indexed: false,
2455
+ internalType: "bytes32"
2456
+ },
2457
+ {
2458
+ name: "consumed",
2459
+ type: "uint256",
2460
+ indexed: false,
2461
+ internalType: "uint256"
2462
+ }
2463
+ ],
2464
+ anonymous: false
2465
+ };
2466
+ /**
2475
2467
  * ABI for the Consume event emitted by the Obligation contract.
2476
2468
  */
2477
2469
  const consumedEvent = {
@@ -2897,12 +2889,10 @@ const maturity = ({ maturities }) => single("maturity", `Validates that offer ma
2897
2889
  const allowedMaturities = maturities.map((m) => from$16(m));
2898
2890
  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
2891
  });
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
- }
2892
+ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy empty callback is ${callbacks.includes(Type$1.BuyWithEmptyCallback) ? "allowed" : "not allowed"}; sell empty callback is ${callbacks.includes(Type$1.SellWithEmptyCallback) ? "allowed" : "not allowed"}; non-empty callbacks are rejected`, (offer) => {
2893
+ if (!isEmptyCallback(offer)) return { message: "Non-empty callbacks are not supported." };
2894
+ if (isEmptyCallback(offer) && offer.buy && !callbacks.includes(Type$1.BuyWithEmptyCallback)) return { message: "Buy offers with empty callback not allowed." };
2895
+ if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
2906
2896
  });
2907
2897
  /**
2908
2898
  * A validation rule that checks if the offer's tokens are allowed for its chain.
@@ -2916,6 +2906,16 @@ const token = ({ assetsByChainId }) => single("token", "Validates that offer loa
2916
2906
  if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
2917
2907
  });
2918
2908
  /**
2909
+ * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
2910
+ * @param oraclesByChainId - Allowed oracles indexed by chain id.
2911
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
2912
+ */
2913
+ const oracle = ({ oraclesByChainId }) => single("oracle", "Validates that offer collateral oracles are in the allowed oracle list for the offer chain", (offer) => {
2914
+ const allowedOracles = oraclesByChainId[offer.chainId]?.map((oracle) => oracle.toLowerCase());
2915
+ if (!allowedOracles || allowedOracles.length === 0) return { message: `No allowed oracles for chain ${offer.chainId}` };
2916
+ if (offer.collaterals.some((collateral) => !allowedOracles.includes(collateral.oracle.toLowerCase()))) return { message: "Oracle is not allowed" };
2917
+ });
2918
+ /**
2919
2919
  * A batch validation rule that ensures all offers in a tree have the same maker address.
2920
2920
  * Returns an issue only for the first non-conforming offer.
2921
2921
  * This rule is signing-agnostic; signer verification is handled at the collector level.
@@ -2946,59 +2946,54 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
2946
2946
  //#region src/gatekeeper/morphoRules.ts
2947
2947
  const morphoRules = (chains) => {
2948
2948
  const assetsByChainId = {};
2949
- for (const chain of chains) assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
2949
+ const oraclesByChainId = {};
2950
+ for (const chain of chains) {
2951
+ assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
2952
+ oraclesByChainId[chain.id] = oracles$1[chain.id.toString()] ?? [];
2953
+ }
2950
2954
  return [
2951
2955
  sameMaker(),
2952
2956
  amountMutualExclusivity(),
2953
2957
  chains$1({ chains }),
2954
2958
  maturity({ maturities: [MaturityType.EndOfMonth, MaturityType.EndOfNextMonth] }),
2955
2959
  callback({
2956
- callbacks: [
2957
- Type$1.BuyWithEmptyCallback,
2958
- Type$1.BuyVaultV1Callback,
2959
- Type$1.SellERC20Callback
2960
- ],
2961
- allowedAddresses: chains.flatMap((c) => getCallbackAddresses(c.name))
2960
+ callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
2961
+ allowedAddresses: []
2962
2962
  }),
2963
- token({ assetsByChainId })
2963
+ token({ assetsByChainId }),
2964
+ oracle({ oraclesByChainId })
2964
2965
  ];
2965
2966
  };
2966
2967
 
2967
2968
  //#endregion
2968
2969
  //#region src/gatekeeper/ConfigRules.ts
2969
2970
  /**
2970
- * Build the configured rules (maturities + callback addresses + loan tokens) for the provided chains.
2971
+ * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
2971
2972
  * @param chains - Chains to include in the configured rules.
2972
2973
  * @returns Sorted list of config rules.
2973
2974
  */
2974
2975
  function buildConfigRules(chains) {
2975
2976
  const rules = [];
2976
2977
  for (const chain of chains) {
2977
- const config = configs[chain.name];
2978
- const maturities = config.maturities ?? [];
2978
+ const maturities = configs[chain.name].maturities ?? [];
2979
2979
  for (const maturityName of maturities) rules.push({
2980
2980
  type: "maturity",
2981
2981
  chain_id: chain.id,
2982
2982
  name: maturityName,
2983
2983
  timestamp: from$16(maturityName)
2984
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
- });
2995
- }
2996
2985
  const loanTokens = assets[chain.id.toString()] ?? [];
2997
2986
  for (const address of loanTokens) rules.push({
2998
2987
  type: "loan_token",
2999
2988
  chain_id: chain.id,
3000
2989
  address: normalizeAddress(address)
3001
2990
  });
2991
+ const oracles = oracles$1[chain.id.toString()] ?? [];
2992
+ for (const address of oracles) rules.push({
2993
+ type: "oracle",
2994
+ chain_id: chain.id,
2995
+ address: normalizeAddress(address)
2996
+ });
3002
2997
  }
3003
2998
  rules.sort(compareConfigRules);
3004
2999
  return rules;
@@ -3020,6 +3015,10 @@ function buildConfigRulesChecksum(rules) {
3020
3015
  hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
3021
3016
  continue;
3022
3017
  }
3018
+ if (rule.type === "oracle") {
3019
+ hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
3020
+ continue;
3021
+ }
3023
3022
  hash.update(`loan_token:${rule.chain_id}:${rule.address}\n`);
3024
3023
  }
3025
3024
  return hash.digest("hex");
@@ -3036,6 +3035,7 @@ function compareConfigRules(left, right) {
3036
3035
  return left.address.localeCompare(right.address);
3037
3036
  }
3038
3037
  if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
3038
+ if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
3039
3039
  return 0;
3040
3040
  }
3041
3041
 
@@ -3299,8 +3299,8 @@ const offerExample = {
3299
3299
  price: "2750000000000000000",
3300
3300
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
3301
3301
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
3302
- callback: "0x1111111111111111111111111111111111111111",
3303
- callback_data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
3302
+ callback: "0x0000000000000000000000000000000000000000",
3303
+ callback_data: "0x"
3304
3304
  },
3305
3305
  offer_hash: "0xac4bd8318ec914f89f8af913f162230575b0ac0696a19256bc12138c5cfe1427",
3306
3306
  obligation_id: "0x25690ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9abc",
@@ -3352,25 +3352,10 @@ const validateOfferExample = {
3352
3352
  lltv: "860000000000000000"
3353
3353
  }],
3354
3354
  callback: {
3355
- address: "0x1111111111111111111111111111111111111111",
3356
- data: "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000034cf890db685fc536e05652fb41f02090c3fb751000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000108e644e3ab01184155270aa92a00000000000"
3355
+ address: "0x0000000000000000000000000000000000000000",
3356
+ data: "0x"
3357
3357
  }
3358
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
- }];
3374
3359
  const routerStatusExample = {
3375
3360
  status: "live",
3376
3361
  initialized: true,
@@ -3439,55 +3424,6 @@ __decorate([ApiProperty({
3439
3424
  type: "string",
3440
3425
  example: validateOfferExample.callback.data
3441
3426
  })], 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);
3491
3427
  var AskResponse = class {};
3492
3428
  __decorate([ApiProperty({
3493
3429
  type: "string",
@@ -3652,7 +3588,8 @@ var OfferListResponse = class extends SuccessResponse {};
3652
3588
  __decorate([ApiProperty({
3653
3589
  type: "string",
3654
3590
  nullable: true,
3655
- example: offerCursorExample
3591
+ example: offerCursorExample,
3592
+ description: "Pagination cursor. Offer hash (0x...) for maker queries; base64url-encoded cursor for obligation queries."
3656
3593
  })], OfferListResponse.prototype, "cursor", void 0);
3657
3594
  __decorate([ApiProperty({
3658
3595
  type: () => [OfferListItemResponse],
@@ -4004,7 +3941,7 @@ __decorate([
4004
3941
  methods: ["post"],
4005
3942
  path: "/v1/validate",
4006
3943
  summary: "Validate offers",
4007
- description: "Validates offers against router validation rules. Returns unsigned payload + root on success, or issues only on validation failure."
3944
+ description: "Validates offers against router validation rules. Only empty callbacks (zero address, 0x data) are accepted. Returns unsigned payload + root on success, or issues only on validation failure."
4008
3945
  }),
4009
3946
  ApiBody({ type: ValidateOffersRequest }),
4010
3947
  ApiResponse({
@@ -4023,28 +3960,6 @@ ValidateController = __decorate([ApiTags("Make"), ApiResponse({
4023
3960
  description: "Bad Request",
4024
3961
  type: BadRequestResponse
4025
3962
  })], 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);
4048
3963
  let OffersController = class OffersController {
4049
3964
  async getOffers() {}
4050
3965
  };
@@ -4194,22 +4109,21 @@ const configRulesMaturityExample = {
4194
4109
  name: "end_of_next_month",
4195
4110
  timestamp: 1730415600
4196
4111
  };
4197
- const configRulesCallbackExample = {
4198
- type: "callback",
4199
- chain_id: 1,
4200
- address: "0x1111111111111111111111111111111111111111",
4201
- callback_type: "sell_erc20_callback"
4202
- };
4203
4112
  const configRulesLoanTokenExample = {
4204
4113
  type: "loan_token",
4205
4114
  chain_id: 1,
4206
4115
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
4207
4116
  };
4117
+ const configRulesOracleExample = {
4118
+ type: "oracle",
4119
+ chain_id: 1,
4120
+ address: "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83"
4121
+ };
4208
4122
  const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
4209
4123
  const configRulesPayloadExample = [
4210
4124
  configRulesMaturityExample,
4211
- configRulesCallbackExample,
4212
- configRulesLoanTokenExample
4125
+ configRulesLoanTokenExample,
4126
+ configRulesOracleExample
4213
4127
  ];
4214
4128
  const configContractNames = [
4215
4129
  "mempool",
@@ -4272,14 +4186,9 @@ __decorate([ApiProperty({
4272
4186
  })], ConfigRulesRuleResponse.prototype, "timestamp", void 0);
4273
4187
  __decorate([ApiProperty({
4274
4188
  type: "string",
4275
- example: configRulesCallbackExample.address,
4189
+ example: configRulesLoanTokenExample.address,
4276
4190
  required: false
4277
4191
  })], 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
4192
  var ConfigRulesSuccessResponse = class {};
4284
4193
  __decorate([ApiProperty({ type: () => ConfigRulesMeta })], ConfigRulesSuccessResponse.prototype, "meta", void 0);
4285
4194
  __decorate([ApiProperty({
@@ -4340,7 +4249,7 @@ __decorate([
4340
4249
  methods: ["get"],
4341
4250
  path: "/v1/config/rules",
4342
4251
  summary: "Get config rules",
4343
- description: "Returns configured rules for supported chains."
4252
+ description: "Returns configured rules (maturities, loan tokens, oracles) for supported chains."
4344
4253
  }),
4345
4254
  ApiQuery({
4346
4255
  name: "cursor",
@@ -4360,7 +4269,7 @@ __decorate([
4360
4269
  name: "types",
4361
4270
  type: ["string"],
4362
4271
  required: false,
4363
- example: "maturity,loan_token",
4272
+ example: "maturity,loan_token,oracle",
4364
4273
  description: "Filter by rule types (comma-separated).",
4365
4274
  style: "form",
4366
4275
  explode: false
@@ -4519,8 +4428,7 @@ const OpenApi = async () => {
4519
4428
  ObligationsController,
4520
4429
  HealthController,
4521
4430
  UsersController,
4522
- ValidateController,
4523
- CallbacksController
4431
+ ValidateController
4524
4432
  ],
4525
4433
  document: {
4526
4434
  openapi: "3.1.0",
@@ -4590,6 +4498,9 @@ function isValidBase64urlJson(val) {
4590
4498
  return false;
4591
4499
  }
4592
4500
  }
4501
+ function isValidOfferHashCursor(val) {
4502
+ return /^0x[a-f0-9]{64}$/i.test(val);
4503
+ }
4593
4504
  const csvArray = (schema) => z$2.preprocess((value) => {
4594
4505
  if (value === void 0) return void 0;
4595
4506
  if (Array.isArray(value)) {
@@ -4615,10 +4526,11 @@ const PaginationQueryParams = z$2.object({
4615
4526
  const ConfigRuleTypes = z$2.enum([
4616
4527
  "maturity",
4617
4528
  "callback",
4618
- "loan_token"
4529
+ "loan_token",
4530
+ "oracle"
4619
4531
  ]);
4620
4532
  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({
4533
+ cursor: z$2.string().regex(/^(maturity|callback|loan_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
4622
4534
  description: "Pagination cursor in type:chain_id:<value> format",
4623
4535
  example: "maturity:1:1730415600:end_of_next_month"
4624
4536
  }),
@@ -4628,7 +4540,7 @@ const GetConfigRulesQueryParams = z$2.object({
4628
4540
  }),
4629
4541
  types: csvArray(ConfigRuleTypes).meta({
4630
4542
  description: "Filter by rule types (comma-separated).",
4631
- example: "maturity,loan_token"
4543
+ example: "maturity,loan_token,oracle"
4632
4544
  }),
4633
4545
  chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4634
4546
  description: "Filter by chain IDs (comma-separated).",
@@ -4649,8 +4561,11 @@ const GetConfigContractsQueryParams = z$2.object({
4649
4561
  example: "1,8453"
4650
4562
  })
4651
4563
  });
4652
- const GetOffersQueryParams = z$2.object({
4653
- ...PaginationQueryParams.shape,
4564
+ const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend({
4565
+ cursor: z$2.string().optional().meta({
4566
+ description: "Pagination cursor. Use offer hash (0x...) for maker queries, base64url for obligation queries.",
4567
+ example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
4568
+ }),
4654
4569
  side: z$2.enum(["buy", "sell"]).optional().meta({
4655
4570
  description: "Side of the offer. Required when using obligation_id.",
4656
4571
  example: "buy"
@@ -4674,11 +4589,29 @@ const GetOffersQueryParams = z$2.object({
4674
4589
  });
4675
4590
  return;
4676
4591
  }
4677
- if (hasMaker) return;
4592
+ if (hasMaker) {
4593
+ if (val.cursor !== void 0 && !isValidOfferHashCursor(val.cursor)) ctx.addIssue({
4594
+ code: "custom",
4595
+ path: ["cursor"],
4596
+ message: "Cursor must be a 32-byte hex offer hash when filtering by maker"
4597
+ });
4598
+ return;
4599
+ }
4678
4600
  if (!hasObligation || !hasSide) ctx.addIssue({
4679
4601
  code: "custom",
4680
4602
  message: "Must provide either maker or both obligation_id and side"
4681
4603
  });
4604
+ if (val.cursor !== void 0 && !isValidBase64urlJson(val.cursor)) ctx.addIssue({
4605
+ code: "custom",
4606
+ path: ["cursor"],
4607
+ message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
4608
+ });
4609
+ }).transform((val) => {
4610
+ if (val.maker && val.cursor) return {
4611
+ ...val,
4612
+ cursor: val.cursor.toLowerCase()
4613
+ };
4614
+ return val;
4682
4615
  });
4683
4616
  const GetObligationsQueryParams = z$2.object({
4684
4617
  ...PaginationQueryParams.shape,
@@ -4751,16 +4684,6 @@ const GetBookParams = z$2.object({
4751
4684
  })
4752
4685
  });
4753
4686
  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();
4764
4687
  const GetUserPositionsParams = z$2.object({
4765
4688
  ...PaginationQueryParams.shape,
4766
4689
  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({
@@ -4779,7 +4702,6 @@ const schemas = {
4779
4702
  get_obligation: GetObligationParams,
4780
4703
  get_book: GetBookParams,
4781
4704
  validate_offers: ValidateOffersBody,
4782
- callback_types: CallbackTypesBody,
4783
4705
  get_user_positions: GetUserPositionsParams
4784
4706
  };
4785
4707
  function safeParse(action, query, error) {
@@ -4827,6 +4749,7 @@ async function getConfigRules(query, chains) {
4827
4749
  function formatCursor$1(rule) {
4828
4750
  if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
4829
4751
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
4752
+ if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
4830
4753
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
4831
4754
  }
4832
4755
  function parseCursor$1(cursor) {
@@ -4859,13 +4782,16 @@ function parseCursor$1(cursor) {
4859
4782
  address: parseAddress(addressValue, "Cursor address")
4860
4783
  };
4861
4784
  }
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
- };
4785
+ if (type === "loan_token" || type === "oracle") {
4786
+ const addressValue = rest.join(":");
4787
+ if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
4788
+ return {
4789
+ type,
4790
+ chain_id,
4791
+ address: parseAddress(addressValue, "Cursor address")
4792
+ };
4793
+ }
4794
+ throw new BadRequestError("Cursor has an invalid rule type");
4869
4795
  }
4870
4796
  function findStartIndex$1(rules, cursor) {
4871
4797
  let low = 0;
@@ -4883,7 +4809,7 @@ function parseAddress(address, label) {
4883
4809
  return address.toLowerCase();
4884
4810
  }
4885
4811
  function isConfigRuleType(value) {
4886
- return value === "maturity" || value === "callback" || value === "loan_token";
4812
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
4887
4813
  }
4888
4814
  function isMaturityType(value) {
4889
4815
  return Object.values(MaturityType).includes(value);
@@ -5031,35 +4957,6 @@ async function validateOffers(body, gatekeeper) {
5031
4957
  }
5032
4958
  }
5033
4959
 
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
-
5063
4960
  //#endregion
5064
4961
  //#region src/gatekeeper/Service.ts
5065
4962
  /**
@@ -5067,10 +4964,6 @@ function resolveCallbackTypes$2(parameters) {
5067
4964
  * @param parameters - App parameters including the {@link Gatekeeper} instance.
5068
4965
  * @returns Hono app exposing gatekeeper endpoints.
5069
4966
  */
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
- })) });
5074
4967
  function createApp(parameters) {
5075
4968
  const { gatekeeper, chainRegistry } = parameters;
5076
4969
  const app = new Hono();
@@ -5079,12 +4972,12 @@ function createApp(parameters) {
5079
4972
  try {
5080
4973
  body = await c.req.json();
5081
4974
  } catch (err) {
5082
- const failure$5 = failure(err);
5083
- return c.json(failure$5.body, failure$5.statusCode);
4975
+ const failure$4 = failure(err);
4976
+ return c.json(failure$4.body, failure$4.statusCode);
5084
4977
  }
5085
4978
  if (body === null || typeof body !== "object") {
5086
- const failure$9 = failure(new BadRequestError("Request body must be a JSON object"));
5087
- return c.json(failure$9.body, failure$9.statusCode);
4979
+ const failure$3 = failure(new BadRequestError("Request body must be a JSON object"));
4980
+ return c.json(failure$3.body, failure$3.statusCode);
5088
4981
  }
5089
4982
  const { statusCode, body: payload } = await validateOffers(body, gatekeeper);
5090
4983
  return c.json(payload, statusCode);
@@ -5093,34 +4986,6 @@ function createApp(parameters) {
5093
4986
  const { statusCode, body } = await getConfigRules(c.req.query(), chainRegistry.list());
5094
4987
  return c.json(body, statusCode);
5095
4988
  });
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
- }
5123
- });
5124
4989
  return app;
5125
4990
  }
5126
4991
  /**
@@ -5216,1559 +5081,8 @@ function now() {
5216
5081
  }
5217
5082
 
5218
5083
  //#endregion
5219
- //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
5220
- async function* collectConsumedEvents(parameters) {
5221
- let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
5222
- const logger = getLogger();
5223
- let startBlock = blockNumber;
5224
- let reorgDetected = false;
5225
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5226
- const stream = streamLogs({
5227
- client,
5228
- contractAddress: client.chain.custom.morpho.address,
5229
- event: consumedEvent,
5230
- blockNumberGte: blockNumber,
5231
- blockNumberLte: latestBlockNumberChain,
5232
- order: "asc",
5233
- options: {
5234
- maxBatchSize,
5235
- blockWindow
5236
- }
5237
- });
5238
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
5239
- const parsedLogs = parseEventLogs({
5240
- abi: [consumedEvent],
5241
- logs
5242
- });
5243
- const events = [];
5244
- for (const log of parsedLogs) {
5245
- if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
5246
- logger.debug({
5247
- collector,
5248
- chainId: client.chain.id,
5249
- msg: "Skipping log because it is missing required fields"
5250
- });
5251
- continue;
5252
- }
5253
- events.push({
5254
- id: `${log.blockNumber.toString()}-${log.logIndex.toString()}-${client.chain.id}-${log.transactionHash}`,
5255
- chainId: client.chain.id,
5256
- maker: log.args.user,
5257
- group: log.args.group,
5258
- amount: log.args.amount,
5259
- blockNumber: Number(log.blockNumber)
5260
- });
5261
- }
5262
- await db.transaction(async (dbTx) => {
5263
- try {
5264
- await dbTx.consumed.create(events);
5265
- if (events.length > 0) logger.info({
5266
- msg: `Events indexed`,
5267
- collector,
5268
- count: events.length,
5269
- chain_id: client.chain.id,
5270
- block_range: [startBlock, lastStreamBlockNumber]
5271
- });
5272
- } catch (err) {
5273
- logger.error({
5274
- err,
5275
- msg: "Failed to process offer_consumed events"
5276
- });
5277
- }
5278
- blockNumber = lastStreamBlockNumber;
5279
- try {
5280
- await dbTx.blocks.advanceCollector({
5281
- collectorName: collector,
5282
- chainId: client.chain.id,
5283
- blockNumber,
5284
- epoch
5285
- });
5286
- } catch (_) {
5287
- try {
5288
- const ancestor = await dbTx.blocks.getCollector({
5289
- collectorName: collector,
5290
- chainId: client.chain.id
5291
- });
5292
- blockNumber = ancestor.blockNumber;
5293
- const deleted = await dbTx.consumed.delete({
5294
- chainId: client.chain.id,
5295
- blockNumberGte: blockNumber + 1
5296
- });
5297
- logger.info({
5298
- collector,
5299
- chain_id: client.chain.id,
5300
- msg: `Reorg detected, events deleted`,
5301
- count: deleted,
5302
- block_number: blockNumber
5303
- });
5304
- await dbTx.blocks.advanceCollector({
5305
- collectorName: collector,
5306
- chainId: client.chain.id,
5307
- blockNumber,
5308
- epoch: ancestor.epoch
5309
- });
5310
- reorgDetected = true;
5311
- } catch (err) {
5312
- const msg = "Failed to delete consumed events when handling reorg.";
5313
- logger.error({
5314
- collector,
5315
- chainId: client.chain.id,
5316
- msg,
5317
- err
5318
- });
5319
- throw new Error(msg);
5320
- }
5321
- }
5322
- });
5323
- if (reorgDetected) return;
5324
- yield blockNumber;
5325
- startBlock = blockNumber;
5326
- }
5327
- }
5328
-
5329
- //#endregion
5330
- //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
5331
- async function* collectOffersV2(parameters) {
5332
- let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
5333
- const logger = getLogger();
5334
- let startBlock = blockNumber;
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
- };
5348
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5349
- const stream = streamLogs({
5350
- client,
5351
- contractAddress: client.chain.custom.mempool.address,
5352
- event: {
5353
- type: "event",
5354
- name: "Event",
5355
- inputs: [{
5356
- name: "data",
5357
- type: "bytes",
5358
- indexed: false,
5359
- internalType: "bytes"
5360
- }],
5361
- anonymous: false
5362
- },
5363
- blockNumberGte: blockNumber,
5364
- blockNumberLte: latestBlockNumberChain,
5365
- order: "asc",
5366
- options: {
5367
- maxBatchSize,
5368
- blockWindow
5369
- }
5370
- });
5371
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
5372
- blockNumber = lastStreamBlockNumber;
5373
- const decodedTrees = [];
5374
- for (const log of logs) {
5375
- if (!log) continue;
5376
- const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
5377
- try {
5378
- const { tree, signature, signer } = await decode(payload, signatureDomain);
5379
- const signerMismatch = tree.offers.find((offer) => offer.maker.toLowerCase() !== signer.toLowerCase());
5380
- if (signerMismatch) {
5381
- logger.debug({
5382
- msg: "Tree rejected: signer mismatch",
5383
- reason: "signer_mismatch",
5384
- signer,
5385
- maker: signerMismatch.maker,
5386
- chain_id: client.chain.id
5387
- });
5388
- continue;
5389
- }
5390
- decodedTrees.push({
5391
- tree,
5392
- signature,
5393
- signer,
5394
- blockNumber: Number(log.blockNumber)
5395
- });
5396
- } catch (err) {
5397
- const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
5398
- logger.debug({
5399
- msg: "Tree decode failed",
5400
- reason,
5401
- chain_id: client.chain.id,
5402
- err: err instanceof Error ? err.message : String(err)
5403
- });
5404
- }
5405
- }
5406
- await db.transaction(async (dbTx) => {
5407
- const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
5408
- const treesToInsert = [];
5409
- let totalValidOffers = 0;
5410
- const offersWithBlock = [];
5411
- for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
5412
- const allowedResults = await gatekeeper.isAllowed(tree.offers);
5413
- const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
5414
- if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
5415
- if (allowedResults.issues.length > 0) {
5416
- const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
5417
- logger.debug({
5418
- msg: "Tree offers rejected by gatekeeper",
5419
- reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
5420
- chain_id: client.chain.id,
5421
- issues_count: allowedResults.issues.length
5422
- });
5423
- } else if (hasBlockWindowViolation) logger.debug({
5424
- msg: "Tree rejected: offers outside block window",
5425
- reason: "block_window",
5426
- chain_id: client.chain.id
5427
- });
5428
- continue;
5429
- }
5430
- treesToInsert.push({
5431
- tree,
5432
- signature
5433
- });
5434
- totalValidOffers += tree.offers.length;
5435
- offersWithBlock.push(...tree.offers.map((offer) => ({
5436
- offer,
5437
- blockNumber: treeBlockNumber
5438
- })));
5439
- } catch (err) {
5440
- const error = err instanceof Error ? err : new Error(String(err));
5441
- logger.error({
5442
- err: error,
5443
- msg: "Gatekeeper validation failed",
5444
- chain_id: client.chain.id
5445
- });
5446
- throw new Error("Gatekeeper validation failed", { cause: error });
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);
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);
5466
- try {
5467
- await dbTx.blocks.advanceCollector({
5468
- collectorName: collector,
5469
- chainId: client.chain.id,
5470
- blockNumber,
5471
- epoch
5472
- });
5473
- if (totalValidOffers > 0) logger.info({
5474
- msg: `New offers`,
5475
- collector,
5476
- count: totalValidOffers,
5477
- trees_count: treesToInsert.length,
5478
- chain_id: client.chain.id,
5479
- block_range: [startBlock, lastStreamBlockNumber]
5480
- });
5481
- } catch (_) {
5482
- try {
5483
- const ancestor = await dbTx.blocks.getCollector({
5484
- collectorName: collector,
5485
- chainId: client.chain.id
5486
- });
5487
- blockNumber = ancestor.blockNumber;
5488
- const deleted = await dbTx.offers.delete({
5489
- blockNumberGte: blockNumber + 1,
5490
- chainId: client.chain.id
5491
- });
5492
- logger.info({
5493
- collector,
5494
- chain_id: client.chain.id,
5495
- msg: `Reorg detected, offers deleted`,
5496
- count: deleted,
5497
- block_number: blockNumber
5498
- });
5499
- await dbTx.blocks.advanceCollector({
5500
- collectorName: collector,
5501
- chainId: client.chain.id,
5502
- blockNumber,
5503
- epoch: ancestor.epoch
5504
- });
5505
- reorgDetected = true;
5506
- } catch (err) {
5507
- const msg = "Failed to delete offers when handling reorg.";
5508
- logger.error({
5509
- collector,
5510
- chainId: client.chain.id,
5511
- msg,
5512
- err
5513
- });
5514
- throw new Error(msg);
5515
- }
5516
- }
5517
- });
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);
5661
- }
5662
- return filtered;
5663
- }
5664
-
5665
- //#endregion
5666
- //#region src/indexer/collectors/fetchers/fetchOraclePrices.ts
5667
- /**
5668
- * Fetches prices from multiple oracle contracts using multicall.
5669
- *
5670
- * - Executes `price()` on {@link Abi.Oracle} for each {@link Address}.
5671
- * - Requires a client supporting {@link PublicActions.multicall | multicall}.
5672
- *
5673
- * @param parameters - {@link fetchOraclePrices.Parameters} including the list of oracle
5674
- * {@link Address | addresses}, fetch options, and the multicall-enabled client.
5675
- * @returns {@link fetchOraclePrices.ReturnType} mapping {@link Address} to `bigint` price.
5676
- */
5677
- async function fetchOraclePrices(parameters) {
5678
- const { client, oracles, options } = parameters;
5679
- if (oracles.length === 0) return /* @__PURE__ */ new Map();
5680
- const batchSize = Math.max(1, options?.batchSize ?? 5e3);
5681
- const retryAttempts = Math.max(1, options?.retryAttempts ?? 3);
5682
- const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 50);
5683
- const blockNumber = options?.blockNumber ? BigInt(options.blockNumber) : void 0;
5684
- const out = /* @__PURE__ */ new Map();
5685
- for (const oraclesBatch of batch$1(oracles, batchSize)) {
5686
- const priceCalls = [];
5687
- for (const oracle of oraclesBatch) priceCalls.push({
5688
- address: oracle,
5689
- abi: Oracle,
5690
- functionName: "price",
5691
- args: []
5692
- });
5693
- const prices = await batchMulticall({
5694
- client,
5695
- calls: priceCalls,
5696
- batchSize,
5697
- retryAttempts,
5698
- retryDelayMs,
5699
- blockNumber
5700
- });
5701
- for (let i = 0; i < oraclesBatch.length; i++) {
5702
- const oracle = oraclesBatch[i];
5703
- const price = prices[i];
5704
- out.set(oracle, price);
5705
- }
5706
- }
5707
- return out;
5708
- }
5709
-
5710
- //#endregion
5711
- //#region src/indexer/collectors/fetchers/snapshotERC20Positions.ts
5712
- /**
5713
- * Fetches ERC20 balances for positions at a given block number and returns the positions with the updated balances.
5714
- * @notice This method does not mutate positions and returns a new array.
5715
- *
5716
- * @param parameters - {@link snapshotERC20Positions.Parameters}
5717
- * @returns Positions - {@link snapshotERC20Positions.ReturnType}
5718
- */
5719
- async function snapshotERC20Positions(parameters) {
5720
- const { positions: oldPositions, blockNumber, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
5721
- if (oldPositions.length === 0) return [];
5722
- const calls = [];
5723
- for (const position of oldPositions) calls.push({
5724
- address: position.contract,
5725
- abi: erc20Abi,
5726
- functionName: "balanceOf",
5727
- args: [position.user]
5728
- });
5729
- const balances = await batchMulticall({
5730
- client: parameters.client,
5731
- calls,
5732
- blockNumber: BigInt(blockNumber),
5733
- batchSize: maxBatchSize,
5734
- retryAttempts,
5735
- retryDelayMs
5736
- });
5737
- const positions = [];
5738
- for (let i = 0; i < balances.length; i++) {
5739
- const oldPosition = oldPositions[i];
5740
- if (!oldPosition) continue;
5741
- positions.push({
5742
- ...oldPosition,
5743
- balance: balances[i],
5744
- blockNumber
5745
- });
5746
- }
5747
- return positions;
5748
- }
5749
-
5750
- //#endregion
5751
- //#region src/indexer/collectors/fetchers/snapshotVaultPositions.ts
5752
- /**
5753
- * Fetches vault shares for users at a given block number, converts them to assets and returns the positions with the updated balances.
5754
- * @notice This method does not mutate positions and returns a new array.
5755
- *
5756
- * @param parameters - {@link snapshotVaultPositions.Parameters}
5757
- * @returns Positions - {@link snapshotVaultPositions.ReturnType}
5758
- */
5759
- async function snapshotVaultPositions(parameters) {
5760
- const logger = getLogger();
5761
- const { client, positions: oldPositions, blockNumber, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
5762
- const calls = [];
5763
- const contracts = /* @__PURE__ */ new Map();
5764
- const positions = structuredClone(oldPositions);
5765
- for (const position of positions) {
5766
- calls.push({
5767
- address: position.contract,
5768
- abi: MetaMorpho,
5769
- functionName: "balanceOf",
5770
- args: [position.user],
5771
- convertToAssets: (shares) => {
5772
- const contract = contracts.get(position.contract.toLowerCase());
5773
- if (!contract) return;
5774
- if (contract.decimalsOffset === void 0 || contract.totalAssets === void 0 || contract.totalSupply === void 0) return;
5775
- try {
5776
- position.balance = convertToAssets({
5777
- shares,
5778
- totalAssets: contract.totalAssets,
5779
- totalSupply: contract.totalSupply,
5780
- decimalsOffset: contract.decimalsOffset
5781
- });
5782
- position.asset = contract.asset;
5783
- position.blockNumber = blockNumber;
5784
- } catch (err) {
5785
- if (err instanceof DenominatorIsZeroError) {
5786
- logger.error({
5787
- msg: "Failed to convert shares to assets",
5788
- chain_id: client.chain.id,
5789
- block_number: blockNumber,
5790
- position_contract: position.contract,
5791
- position_user: position.user,
5792
- shares,
5793
- err
5794
- });
5795
- return;
5796
- }
5797
- throw err;
5798
- }
5799
- }
5800
- });
5801
- if (contracts.has(position.contract.toLowerCase())) continue;
5802
- calls.push({
5803
- address: position.contract,
5804
- abi: MetaMorpho,
5805
- functionName: "DECIMALS_OFFSET",
5806
- args: []
5807
- }, {
5808
- address: position.contract,
5809
- abi: MetaMorpho,
5810
- functionName: "totalAssets",
5811
- args: []
5812
- }, {
5813
- address: position.contract,
5814
- abi: MetaMorpho,
5815
- functionName: "totalSupply",
5816
- args: []
5817
- }, {
5818
- address: position.contract,
5819
- abi: MetaMorpho,
5820
- functionName: "asset",
5821
- args: []
5822
- });
5823
- contracts.set(position.contract.toLowerCase(), {
5824
- decimalsOffset: void 0,
5825
- totalAssets: void 0,
5826
- totalSupply: void 0,
5827
- asset: void 0
5828
- });
5829
- }
5830
- const results = await batchMulticall({
5831
- client,
5832
- calls,
5833
- blockNumber: BigInt(blockNumber),
5834
- batchSize: maxBatchSize,
5835
- retryAttempts,
5836
- retryDelayMs
5837
- });
5838
- const convertToAssetsList = [];
5839
- for (let i = 0; i < results.length; i++) {
5840
- const call = calls[i];
5841
- const value = results[i];
5842
- const contract = contracts.get(call.address.toLowerCase());
5843
- if (!contract) continue;
5844
- switch (call.functionName) {
5845
- case "balanceOf":
5846
- convertToAssetsList.push(() => call.convertToAssets(value));
5847
- break;
5848
- case "DECIMALS_OFFSET":
5849
- contract.decimalsOffset = value;
5850
- break;
5851
- case "totalAssets":
5852
- contract.totalAssets = value;
5853
- break;
5854
- case "totalSupply":
5855
- contract.totalSupply = value;
5856
- break;
5857
- case "asset":
5858
- contract.asset = value.toLowerCase();
5859
- break;
5860
- }
5861
- }
5862
- for (const convertToAssets of convertToAssetsList) convertToAssets();
5863
- return positions;
5864
- }
5865
-
5866
- //#endregion
5867
- //#region src/indexer/collectors/CollectFunctions/collectPositions.ts
5868
- async function* collectPositions(parameters) {
5869
- let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500, blockWindow } = {} } = parameters;
5870
- const logger = getLogger();
5871
- let startBlock = blockNumber;
5872
- let reorgDetected = false;
5873
- const TransferEvent = {
5874
- type: "event",
5875
- name: "Transfer",
5876
- inputs: [
5877
- {
5878
- name: "from",
5879
- type: "address",
5880
- indexed: true
5881
- },
5882
- {
5883
- name: "to",
5884
- type: "address",
5885
- indexed: true
5886
- },
5887
- {
5888
- name: "value",
5889
- type: "uint256",
5890
- indexed: false
5891
- }
5892
- ]
5893
- };
5894
- const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5895
- const stream = streamLogs({
5896
- client,
5897
- event: TransferEvent,
5898
- blockNumberGte: blockNumber,
5899
- blockNumberLte: latestBlockNumberChain,
5900
- order: "asc",
5901
- options: {
5902
- maxBatchSize,
5903
- blockWindow
5904
- }
5905
- });
5906
- for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
5907
- blockNumber = lastStreamBlockNumber;
5908
- const parsedLogs = parseEventLogs({
5909
- abi: [TransferEvent],
5910
- logs
5911
- });
5912
- const transfers = [];
5913
- for (const log of parsedLogs) {
5914
- if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
5915
- logger.debug({
5916
- collector,
5917
- chainId: client.chain.id,
5918
- msg: "Skipping log because it is missing required fields"
5919
- });
5920
- continue;
5921
- }
5922
- transfers.push(from$8({
5923
- id: `${client.chain.id}-${log.blockNumber.toString()}-${log.transactionHash}-${log.logIndex.toString()}`,
5924
- chainId: client.chain.id,
5925
- contract: log.address,
5926
- from: log.args.from,
5927
- to: log.args.to,
5928
- value: log.args.value,
5929
- blockNumber: Number(log.blockNumber)
5930
- }));
5931
- }
5932
- const { positions } = await db.positions.get({
5933
- chainId: client.chain.id,
5934
- filled: false
5935
- });
5936
- const newPositions = [];
5937
- try {
5938
- newPositions.push(...(await _snapshot({
5939
- positions,
5940
- blockNumber: latestBlockNumberChain,
5941
- client,
5942
- maxBatchSize,
5943
- retryAttempts,
5944
- retryDelayMs
5945
- })).map((p) => ({
5946
- ...p,
5947
- blockNumber: p.blockNumber + 1
5948
- })));
5949
- } catch (err) {
5950
- logger.error({
5951
- msg: "Failed to snapshot new empty positions",
5952
- collector,
5953
- chain_id: client.chain.id,
5954
- block_number: latestBlockNumberChain,
5955
- err
5956
- });
5957
- yield startBlock;
5958
- return;
5959
- }
5960
- try {
5961
- await db.transaction(async (dbTx) => {
5962
- const insertPositions = async () => {
5963
- if (newPositions.length === 0) return;
5964
- try {
5965
- const count = await dbTx.positions.upsert(newPositions);
5966
- logger.info({
5967
- msg: `New positions`,
5968
- collector,
5969
- count,
5970
- chain_id: client.chain.id,
5971
- block_number: latestBlockNumberChain
5972
- });
5973
- } catch (err) {
5974
- throw new InsertPositionsError(err);
5975
- }
5976
- };
5977
- const insertTransfers = async () => {
5978
- if (transfers.length === 0) return;
5979
- try {
5980
- const created = await dbTx.transfers.create(transfers);
5981
- logger.info({
5982
- msg: `New transfers`,
5983
- collector,
5984
- count: created,
5985
- chain_id: client.chain.id,
5986
- block_range: [startBlock, blockNumber]
5987
- });
5988
- } catch (err) {
5989
- throw new InsertTransfersError(err);
5990
- }
5991
- };
5992
- const saveBlockNumber = async () => {
5993
- try {
5994
- await dbTx.blocks.advanceCollector({
5995
- collectorName: collector,
5996
- chainId: client.chain.id,
5997
- blockNumber,
5998
- epoch
5999
- });
6000
- } catch (_) {
6001
- throw new ReorgError(blockNumber);
6002
- }
6003
- };
6004
- await insertPositions();
6005
- await insertTransfers();
6006
- await saveBlockNumber();
6007
- });
6008
- } catch (err) {
6009
- if (err instanceof ReorgError) {
6010
- logger.info({
6011
- msg: "Reorg detected, positions and transfers insertion aborted",
6012
- collector,
6013
- count: newPositions.length,
6014
- chain_id: client.chain.id,
6015
- block_number: blockNumber
6016
- });
6017
- reorgDetected = true;
6018
- }
6019
- if (err instanceof InsertPositionsError) {
6020
- logger.error({
6021
- msg: "Failed to insert positions",
6022
- collector,
6023
- count: newPositions.length,
6024
- chain_id: client.chain.id,
6025
- block_number: latestBlockNumberChain,
6026
- err
6027
- });
6028
- throw err.cause;
6029
- }
6030
- if (err instanceof InsertTransfersError) {
6031
- logger.error({
6032
- msg: "Failed to insert transfers",
6033
- collector,
6034
- count: transfers.length,
6035
- chain_id: client.chain.id,
6036
- block_number: blockNumber,
6037
- err
6038
- });
6039
- throw err.cause;
6040
- }
6041
- }
6042
- if (!reorgDetected) {
6043
- startBlock = blockNumber;
6044
- if (newPositions.length === 0 && transfers.length === 0 && lastStreamBlockNumber !== latestBlockNumberChain) continue;
6045
- yield blockNumber;
6046
- continue;
6047
- }
6048
- await db.transaction(async (dbTx) => {
6049
- try {
6050
- const ancestor = await dbTx.blocks.getCollector({
6051
- collectorName: collector,
6052
- chainId: client.chain.id
6053
- });
6054
- blockNumber = ancestor.blockNumber;
6055
- const emptied = await dbTx.positions.setEmptyAfter({
6056
- chainId: client.chain.id,
6057
- blockNumber: blockNumber + 1
6058
- });
6059
- logger.info({
6060
- msg: "Reorg detected, positions set to empty",
6061
- collector,
6062
- count: emptied,
6063
- chain_id: client.chain.id,
6064
- block_number_gte: blockNumber + 1
6065
- });
6066
- await dbTx.blocks.advanceCollector({
6067
- collectorName: collector,
6068
- chainId: client.chain.id,
6069
- blockNumber,
6070
- epoch: ancestor.epoch
6071
- });
6072
- } catch (err) {
6073
- const msg = "Failed to revert to ancestor block when handling reorg.";
6074
- logger.error({
6075
- collector,
6076
- chainId: client.chain.id,
6077
- msg,
6078
- err
6079
- });
6080
- throw new Error(msg);
6081
- }
6082
- });
6083
- return;
6084
- }
6085
- }
6086
- /**
6087
- * @internal
6088
- *
6089
- * Snapshots positions and returns the new positions.
6090
- * @param parameters - {@link _snapshot.Parameters}
6091
- * @returns The new positions. {@link _snapshot.ReturnType}
6092
- */
6093
- async function _snapshot(parameters) {
6094
- const { positions, blockNumber, client, maxBatchSize, retryAttempts, retryDelayMs } = parameters;
6095
- const vaultV1Positions = [];
6096
- const erc20Positions = [];
6097
- for (const position of positions) switch (position.type) {
6098
- case Type.VAULT_V1:
6099
- vaultV1Positions.push(position);
6100
- break;
6101
- case Type.ERC20:
6102
- erc20Positions.push(position);
6103
- break;
6104
- default: throw new Error("Invalid position type");
6105
- }
6106
- const promises = [snapshotVaultPositions({
6107
- client,
6108
- positions: vaultV1Positions,
6109
- blockNumber,
6110
- options: {
6111
- maxBatchSize,
6112
- retryAttempts,
6113
- retryDelayMs
6114
- }
6115
- }), snapshotERC20Positions({
6116
- client,
6117
- positions: erc20Positions,
6118
- blockNumber,
6119
- options: {
6120
- maxBatchSize,
6121
- retryAttempts,
6122
- retryDelayMs
6123
- }
6124
- })];
6125
- return (await Promise.all(promises)).flat();
6126
- }
6127
- var InsertPositionsError = class extends BaseError {
6128
- name = "InsertPositionsError";
6129
- constructor(err) {
6130
- super("Failed to insert positions", { cause: err });
6131
- }
6132
- };
6133
- var InsertTransfersError = class extends BaseError {
6134
- name = "InsertTransfersError";
6135
- constructor(err) {
6136
- super("Failed to insert transfers", { cause: err });
6137
- }
6138
- };
6139
-
6140
- //#endregion
6141
- //#region src/indexer/collectors/CollectFunctions/collectPrices.ts
6142
- /**
6143
- * Collects oracle prices from on-chain oracles and persists them.
6144
- *
6145
- * - Reads oracle definitions as {@link Oracle.Oracle}.
6146
- * - Uses chain metadata from {@link Chain.Chain}.
6147
- *
6148
- * @param parameters - {@link collectPrices.Parameters} (extends {@link Collector.CollectParameters})
6149
- * with a client supporting {@link PublicActions.multicall | multicall}.
6150
- * @yields Latest processed block number after each successful update.
6151
- */
6152
- async function* collectPrices(parameters) {
6153
- const { db, collector, client, options: { maxBatchSize = 5e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
6154
- const logger = getLogger();
6155
- let blockNumber = parameters.lastBlockNumber;
6156
- const [oracles, { blockNumber: latestBlockNumberChain, epoch }] = await Promise.all([db.oracles.get({ chainId: client.chain.id }), db.blocks.getChain(client.chain.id)]);
6157
- const updatedOracles = [];
6158
- try {
6159
- const pricesMap = await fetchOraclePrices({
6160
- client,
6161
- oracles: oracles.map((oracle) => oracle.address),
6162
- options: {
6163
- batchSize: maxBatchSize,
6164
- blockNumber: latestBlockNumberChain,
6165
- retryAttempts,
6166
- retryDelayMs
6167
- }
6168
- });
6169
- for (const oracle of oracles) {
6170
- const price = pricesMap.get(oracle.address);
6171
- if (price !== void 0) updatedOracles.push({
6172
- chainId: client.chain.id,
6173
- address: oracle.address,
6174
- price,
6175
- blockNumber: latestBlockNumberChain
6176
- });
6177
- }
6178
- } catch (err) {
6179
- logger.error({
6180
- msg: "Failed to fetch oracle prices",
6181
- collector,
6182
- chain_id: client.chain.id,
6183
- block_number: latestBlockNumberChain,
6184
- err
6185
- });
6186
- yield blockNumber;
6187
- return;
6188
- }
6189
- let reorgDetected = false;
6190
- try {
6191
- await db.transaction(async (dbTx) => {
6192
- if (updatedOracles.length > 0) {
6193
- await dbTx.oracles.upsert(updatedOracles);
6194
- logger.info({
6195
- msg: "Oracle prices updated",
6196
- collector,
6197
- count: updatedOracles.length,
6198
- chain_id: client.chain.id,
6199
- block_number: latestBlockNumberChain
6200
- });
6201
- }
6202
- try {
6203
- await dbTx.blocks.advanceCollector({
6204
- collectorName: collector,
6205
- chainId: client.chain.id,
6206
- blockNumber: latestBlockNumberChain,
6207
- epoch
6208
- });
6209
- } catch (_) {
6210
- throw new ReorgError(latestBlockNumberChain);
6211
- }
6212
- blockNumber = latestBlockNumberChain;
6213
- });
6214
- } catch (err) {
6215
- if (err instanceof ReorgError) {
6216
- logger.info({
6217
- msg: "Reorg detected, prices update aborted",
6218
- collector,
6219
- count: updatedOracles.length,
6220
- chain_id: client.chain.id,
6221
- block_number: latestBlockNumberChain
6222
- });
6223
- reorgDetected = true;
6224
- } else throw new Error("Failed to collect oracle prices", { cause: err });
6225
- }
6226
- if (!reorgDetected) {
6227
- yield blockNumber;
6228
- return;
6229
- }
6230
- await db.transaction(async (dbTx) => {
6231
- try {
6232
- const ancestor = await dbTx.blocks.getCollector({
6233
- collectorName: collector,
6234
- chainId: client.chain.id
6235
- });
6236
- blockNumber = ancestor.blockNumber;
6237
- await dbTx.blocks.advanceCollector({
6238
- collectorName: collector,
6239
- chainId: client.chain.id,
6240
- blockNumber,
6241
- epoch: ancestor.epoch
6242
- });
6243
- } catch (err) {
6244
- const msg = "Failed to revert to ancestor block when handling reorg.";
6245
- logger.error({
6246
- collector,
6247
- chainId: client.chain.id,
6248
- msg,
6249
- err
6250
- });
6251
- throw new Error(msg);
6252
- }
6253
- });
6254
- yield blockNumber;
6255
- }
6256
-
6257
- //#endregion
6258
- //#region src/indexer/collectors/CollectorBuilder.ts
6259
- function createBuilder(parameters) {
6260
- const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow, interval } = {} } = parameters;
6261
- const createCollector = (name, collect) => create$16({
6262
- name,
6263
- collect,
6264
- client,
6265
- db,
6266
- options: {
6267
- maxBlockNumber,
6268
- interval
6269
- }
6270
- });
6271
- return {
6272
- buildOffersCollector: ({ options: { maxBatchSize = 1e3 } = {} }) => {
6273
- return createCollector("offers", (p) => collectOffersV2({
6274
- ...p,
6275
- gatekeeper,
6276
- collector: "offers",
6277
- client,
6278
- options: {
6279
- maxBatchSize,
6280
- blockWindow
6281
- }
6282
- }));
6283
- },
6284
- buildConsumedEventsCollector: ({ options: { maxBatchSize = 1e3 } = {} } = {}) => {
6285
- return createCollector("consumed_events", (p) => collectConsumedEvents({
6286
- ...p,
6287
- collector: "consumed_events",
6288
- options: {
6289
- maxBatchSize,
6290
- blockWindow
6291
- }
6292
- }));
6293
- },
6294
- buildPricesCollector: ({ options: { maxBatchSize = 5e3, retryAttempts, retryDelayMs } = {} } = {}) => {
6295
- return createCollector("prices", (p) => collectPrices({
6296
- ...p,
6297
- collector: "prices",
6298
- options: {
6299
- maxBatchSize,
6300
- retryAttempts,
6301
- retryDelayMs
6302
- }
6303
- }));
6304
- },
6305
- buildPositionsCollector: ({ options: { maxBatchSize = 1e3, retryAttempts, retryDelayMs } = {} } = {}) => {
6306
- return createCollector("positions", (p) => collectPositions({
6307
- ...p,
6308
- collector: "positions",
6309
- options: {
6310
- maxBatchSize,
6311
- retryAttempts,
6312
- retryDelayMs,
6313
- blockWindow
6314
- }
6315
- }));
6316
- }
6317
- };
6318
- }
6319
-
6320
- //#endregion
6321
- //#region src/indexer/collectors/Collectors.ts
6322
- const from$2 = (parameters) => {
6323
- const { client, db, gatekeeper, maxBatchSize, maxBlockNumber, blockWindow, interval, retryAttempts, retryDelayMs } = parameters;
6324
- const collectorBuilder = createBuilder({
6325
- client,
6326
- db,
6327
- gatekeeper,
6328
- options: {
6329
- maxBlockNumber,
6330
- blockWindow,
6331
- interval
6332
- }
6333
- });
6334
- return {
6335
- offersCollector: collectorBuilder.buildOffersCollector({ options: { maxBatchSize } }),
6336
- consumedEventsCollector: collectorBuilder.buildConsumedEventsCollector({ options: { maxBatchSize } }),
6337
- pricesCollector: collectorBuilder.buildPricesCollector({ options: {
6338
- maxBatchSize,
6339
- retryAttempts,
6340
- retryDelayMs
6341
- } }),
6342
- positionsCollector: collectorBuilder.buildPositionsCollector({ options: {
6343
- maxBatchSize,
6344
- retryAttempts,
6345
- retryDelayMs
6346
- } })
6347
- };
6348
- };
6349
-
6350
- //#endregion
6351
- //#region src/indexer/Indexer.ts
6352
- function from$1(config) {
6353
- const { client, gatekeeper, db, interval = 1e4, maxBatchSize = 1e3, maxBlockNumber, blockWindow, retryAttempts, retryDelayMs } = config;
6354
- const { offersCollector, consumedEventsCollector, positionsCollector, pricesCollector } = from$2({
6355
- client,
6356
- db,
6357
- gatekeeper,
6358
- maxBatchSize,
6359
- maxBlockNumber,
6360
- blockWindow,
6361
- interval,
6362
- retryAttempts,
6363
- retryDelayMs
6364
- });
6365
- return create$18({
6366
- client,
6367
- collectors: [
6368
- offersCollector,
6369
- consumedEventsCollector,
6370
- positionsCollector,
6371
- pricesCollector
6372
- ]
6373
- });
6374
- }
6375
- function create$18(params) {
6376
- const { collectors, client } = params;
6377
- const indexerId = `${client.chain.id.toString()}.indexer`;
6378
- const tracer = getTracer(`router.${indexerId}`);
6379
- const iterators = collectors.map((collector) => collector.collect());
6380
- const next = async () => {
6381
- await startActiveSpan(tracer, `${indexerId}.next`, async () => {
6382
- await Promise.all(iterators.map((iterator) => iterator.next()));
6383
- });
6384
- };
6385
- const _return = async () => {
6386
- await Promise.all(iterators.map(async (iterator) => iterator.return()));
6387
- };
6388
- return {
6389
- start: () => {
6390
- const stops = collectors.map((collector) => start(collector));
6391
- return () => {
6392
- stops.forEach((stop) => {
6393
- stop();
6394
- });
6395
- };
6396
- },
6397
- next,
6398
- return: _return
6399
- };
6400
- }
6401
-
6402
- //#endregion
6403
- //#region src/indexer/collectors/Admin.ts
6404
- function create$17(parameters) {
6405
- const collector = "admin";
6406
- const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
6407
- const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
6408
- let finalizedBlock = null;
6409
- let unfinalizedBlocks = [];
6410
- let initialized = false;
6411
- const logger = getLogger();
6412
- let isMaxBlockNumberReached = false;
6413
- let tick = 0;
6414
- return { syncBlock: async () => {
6415
- if (!initialized) {
6416
- await Promise.all(names.map((collectorName) => db.blocks.init({
6417
- chainId: client.chain.id,
6418
- collectorName
6419
- })));
6420
- initialized = true;
6421
- }
6422
- if (isMaxBlockNumberReached) return true;
6423
- const head = await client.getBlock({
6424
- blockTag: "latest",
6425
- includeTransactions: false
6426
- });
6427
- await db.transaction(async (dbTx) => {
6428
- const { epoch, blockNumber: latestSavedBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
6429
- if (maxBlockNumberBI !== void 0 && head.number >= maxBlockNumberBI) {
6430
- logger.info({
6431
- msg: `Head is greater than max block number`,
6432
- collector,
6433
- chainId: client.chain.id,
6434
- block_number: head.number,
6435
- max_block_number: maxBlockNumber
6436
- });
6437
- await dbTx.blocks.handleReorg({
6438
- chainId: client.chain.id,
6439
- blockNumber: maxBlockNumber,
6440
- epoch: epoch + 1n
6441
- });
6442
- isMaxBlockNumberReached = true;
6443
- return isMaxBlockNumberReached;
6444
- }
6445
- finalizedBlock = await fetchFinalizedBlock({
6446
- tick,
6447
- client,
6448
- logger,
6449
- collector,
6450
- unfinalizedBlocks,
6451
- previousFinalizedBlock: finalizedBlock
6452
- });
6453
- tick++;
6454
- let { block: returnedBlock, didReorgHappened, unfinalizedBlocks: newUnfinalizedBlocks } = await reconcile({
6455
- client,
6456
- block: head,
6457
- unfinalizedBlocks,
6458
- finalizedBlock,
6459
- logger,
6460
- collector,
6461
- maxBatchSize
6462
- });
6463
- unfinalizedBlocks = newUnfinalizedBlocks;
6464
- const blockNumber = Number(returnedBlock.number);
6465
- didReorgHappened = didReorgHappened || blockNumber < latestSavedBlockNumber;
6466
- if (didReorgHappened) await dbTx.blocks.handleReorg({
6467
- chainId: client.chain.id,
6468
- blockNumber,
6469
- epoch: epoch + 1n
6470
- });
6471
- else await dbTx.blocks.advanceChain({
6472
- chainId: client.chain.id,
6473
- blockNumber,
6474
- epoch
6475
- });
6476
- });
6477
- return isMaxBlockNumberReached;
6478
- } };
6479
- }
6480
- const commonAncestor = (block, unfinalizedBlocks) => {
6481
- const parent = unfinalizedBlocks.find((b) => b.hash === block.parentHash);
6482
- if (parent) return parent;
6483
- return null;
6484
- };
6485
- const fetchFinalizedBlock = async (parameters) => {
6486
- let { tick, client, logger, collector, unfinalizedBlocks, previousFinalizedBlock } = parameters;
6487
- let finalizedBlock = previousFinalizedBlock;
6488
- if (tick % 20 === 0 || previousFinalizedBlock === null) {
6489
- finalizedBlock = await client.getBlock({
6490
- blockTag: "finalized",
6491
- includeTransactions: false
6492
- });
6493
- if (finalizedBlock === null || finalizedBlock.number === null) {
6494
- const msg = "Failed to get finalized block";
6495
- logger.fatal({
6496
- collector,
6497
- chainId: client.chain.id,
6498
- msg
6499
- });
6500
- throw new Error(msg);
6501
- }
6502
- unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number >= finalizedBlock.number);
6503
- }
6504
- if (finalizedBlock.number === null || finalizedBlock.hash === null || finalizedBlock.parentHash === null) {
6505
- const msg = "Failed to get finalized block";
6506
- logger.fatal({
6507
- collector,
6508
- chainId: client.chain.id,
6509
- msg
6510
- });
6511
- throw new Error(msg);
6512
- }
6513
- return {
6514
- hash: finalizedBlock.hash,
6515
- number: finalizedBlock.number,
6516
- parentHash: finalizedBlock.parentHash
6517
- };
6518
- };
6519
- const reconcile = async (parameters) => {
6520
- let { client, block, unfinalizedBlocks, finalizedBlock, logger, collector, maxBatchSize } = parameters;
6521
- const chain = client.chain;
6522
- if (block.hash === null || block.number === null || block.parentHash === null) throw new Error("Failed to get block");
6523
- const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
6524
- if (latestBlock === void 0) {
6525
- const newBlock = {
6526
- hash: block.hash,
6527
- number: block.number,
6528
- parentHash: block.parentHash
6529
- };
6530
- unfinalizedBlocks.push(newBlock);
6531
- return {
6532
- block: newBlock,
6533
- didReorgHappened: false,
6534
- unfinalizedBlocks
6535
- };
6536
- }
6537
- if (latestBlock.hash === block.hash) return {
6538
- block: latestBlock,
6539
- didReorgHappened: false,
6540
- unfinalizedBlocks
6541
- };
6542
- if (latestBlock.number >= block.number) {
6543
- const ancestor = commonAncestor(block, unfinalizedBlocks) || finalizedBlock;
6544
- logger.info({
6545
- msg: `Reorg detected, latestBlock.number >= block.number`,
6546
- collector,
6547
- chain_id: chain.id,
6548
- ancestor: ancestor.number,
6549
- latest_block_number: latestBlock.number,
6550
- block_number: block.number
6551
- });
6552
- unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number <= ancestor.number);
6553
- return {
6554
- block: ancestor,
6555
- didReorgHappened: true,
6556
- unfinalizedBlocks
6557
- };
6558
- }
6559
- if (latestBlock.number + 1n < block.number) {
6560
- logger.debug({
6561
- collector,
6562
- chain_id: chain.id,
6563
- block_range: [latestBlock.number, block.number],
6564
- msg: `Missing blocks`
6565
- });
6566
- const missingBlockNumbers = (() => {
6567
- const missingBlockNumbers = [];
6568
- let start = latestBlock.number + 1n;
6569
- const threshold = latestBlock.number + BigInt(maxBatchSize) > block.number ? block.number : latestBlock.number + BigInt(maxBatchSize);
6570
- while (start < threshold) {
6571
- missingBlockNumbers.push(start);
6572
- start = start + 1n;
6573
- }
6574
- return missingBlockNumbers;
6575
- })();
6576
- const missingBlocks = await Promise.all(missingBlockNumbers.map((blockNumber) => retry(async () => await client.getBlock({
6577
- blockNumber,
6578
- includeTransactions: false
6579
- }))));
6580
- for (const missingBlock of missingBlocks) {
6581
- const { block: returnedBlock, didReorgHappened, unfinalizedBlocks: newUnfinalizedBlocks } = await reconcile({
6582
- client,
6583
- block: missingBlock,
6584
- unfinalizedBlocks,
6585
- finalizedBlock,
6586
- logger,
6587
- collector,
6588
- maxBatchSize
6589
- });
6590
- if (returnedBlock.number !== missingBlock.number) return {
6591
- block: returnedBlock,
6592
- didReorgHappened,
6593
- unfinalizedBlocks: newUnfinalizedBlocks
6594
- };
6595
- }
6596
- return reconcile({
6597
- client,
6598
- block,
6599
- unfinalizedBlocks,
6600
- finalizedBlock,
6601
- logger,
6602
- collector,
6603
- maxBatchSize
6604
- });
6605
- }
6606
- if (block.parentHash !== latestBlock.hash) {
6607
- const ancestor = commonAncestor(block, unfinalizedBlocks) || finalizedBlock;
6608
- logger.info({
6609
- msg: `Reorg detected, block parent hash !== latest block hash`,
6610
- collector,
6611
- chain_id: chain.id,
6612
- ancestor: ancestor.number,
6613
- latest_block_number: latestBlock.number,
6614
- block_number: block.number
6615
- });
6616
- unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number <= ancestor.number);
6617
- return {
6618
- block: ancestor,
6619
- didReorgHappened: true,
6620
- unfinalizedBlocks
6621
- };
6622
- }
6623
- const newBlock = {
6624
- hash: block.hash,
6625
- number: block.number,
6626
- parentHash: block.parentHash
6627
- };
6628
- unfinalizedBlocks.push(newBlock);
6629
- return {
6630
- block: newBlock,
6631
- didReorgHappened: false,
6632
- unfinalizedBlocks
6633
- };
6634
- };
6635
-
6636
- //#endregion
6637
- //#region src/indexer/collectors/Collector.ts
6638
- const names = [
6639
- "offers",
6640
- "consumed_events",
6641
- "positions",
6642
- "prices"
6643
- ];
6644
- function create$16({ name, collect, client, db, options }) {
6645
- const admin = create$17({
6646
- client,
6647
- db,
6648
- options
6649
- });
6650
- return {
6651
- name,
6652
- chain: client.chain,
6653
- client,
6654
- db,
6655
- interval: options.interval ?? 1e4,
6656
- collect: async function* () {
6657
- const collector = name;
6658
- const chain = client.chain;
6659
- const logger = getLogger();
6660
- const collectorId = `${client.chain.id.toString()}.collector.${collector}`;
6661
- const tracer = getTracer(`router.${collectorId}`);
6662
- logger.info({
6663
- msg: `Collector started`,
6664
- collector,
6665
- chain_id: chain.id
6666
- });
6667
- let iterator = null;
6668
- let lastBlockNumber;
6669
- while (true) try {
6670
- if (iterator === null) iterator = await startActiveSpan(tracer, `${collectorId}.init`, async () => {
6671
- const { collector: collectorBlock } = await db.blocks.init({
6672
- collectorName: name,
6673
- chainId: chain.id
6674
- });
6675
- lastBlockNumber = collectorBlock.blockNumber;
6676
- return collect({
6677
- client,
6678
- collector: name,
6679
- epoch: collectorBlock.epoch,
6680
- lastBlockNumber,
6681
- db
6682
- });
6683
- });
6684
- if (await startActiveSpan(tracer, `${collectorId}.syncBlock`, async (span) => {
6685
- const isMaxBlockNumberReached = await admin.syncBlock();
6686
- span.setAttribute("collector.is_max_block_reached", isMaxBlockNumberReached);
6687
- return isMaxBlockNumberReached;
6688
- }) && options.maxBlockNumber !== void 0 && lastBlockNumber !== void 0 && options.maxBlockNumber === lastBlockNumber) return;
6689
- const { blockNumber, done } = await startActiveSpan(tracer, `${collectorId}.next`, async () => {
6690
- if (iterator === null) throw new Error("Iterator is not initialized");
6691
- const { value: blockNumber, done } = await iterator.next();
6692
- return {
6693
- blockNumber,
6694
- done
6695
- };
6696
- });
6697
- if (done) iterator = null;
6698
- else {
6699
- lastBlockNumber = blockNumber;
6700
- yield blockNumber;
6701
- }
6702
- } catch (err) {
6703
- const isError = err instanceof Error;
6704
- logger.error({
6705
- msg: "Collector error",
6706
- collector,
6707
- chain_id: chain.id,
6708
- error: isError ? err.message : String(err),
6709
- stack: isError ? err.stack : void 0
6710
- });
6711
- }
6712
- }
6713
- };
6714
- }
6715
- /** Start a collector with its own polling cadence based on chain head lag.
6716
- * @param collector - The collector to start.
6717
- * @returns A function to stop the collector.
6718
- */
6719
- function start(collector) {
6720
- let stopped = false;
6721
- const it = collector.collect();
6722
- const logger = getLogger();
6723
- const collectorId = `${collector.chain.id.toString()}.collector.${collector.name}`;
6724
- const tracer = getTracer(`router.${collectorId}`);
6725
- const blocks = collector.db.blocks;
6726
- let initialized = false;
6727
- (async () => {
6728
- while (!stopped) try {
6729
- await startActiveSpan(tracer, `${collectorId}.poll`, async () => {
6730
- if (!initialized) {
6731
- await blocks.init({
6732
- chainId: collector.chain.id,
6733
- collectorName: collector.name
6734
- });
6735
- initialized = true;
6736
- }
6737
- const [block, { blockNumber: collectorBlockNumber }] = await Promise.all([collector.client.getBlock({
6738
- blockTag: "latest",
6739
- includeTransactions: false
6740
- }), blocks.getCollector({
6741
- collectorName: collector.name,
6742
- chainId: collector.chain.id
6743
- })]);
6744
- const delay = Number(block.number) > collectorBlockNumber + 10 ? 250 : collector.interval;
6745
- const waitSpan = tracer.startSpan(`${collectorId}.wait`);
6746
- try {
6747
- await wait(delay);
6748
- } finally {
6749
- waitSpan.end();
6750
- }
6751
- await it.next();
6752
- });
6753
- } catch (err) {
6754
- const error = err instanceof Error ? err : new Error(String(err));
6755
- logger.error({
6756
- msg: "Collector polling error",
6757
- collector: collector.name,
6758
- chain_id: collector.chain.id,
6759
- err: error
6760
- });
6761
- }
6762
- await it.return();
6763
- })();
6764
- return () => {
6765
- stopped = true;
6766
- };
6767
- }
6768
-
6769
- //#endregion
6770
- //#region src/database/drizzle/VERSION.ts
6771
- const VERSION = "router_v1.6";
5084
+ //#region src/database/drizzle/VERSION.ts
5085
+ const VERSION = "router_v1.6";
6772
5086
 
6773
5087
  //#endregion
6774
5088
  //#region src/database/drizzle/schema.ts
@@ -7170,6 +5484,1654 @@ const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
7170
5484
  createdAt: timestamp("created_at").defaultNow().notNull()
7171
5485
  }, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
7172
5486
 
5487
+ //#endregion
5488
+ //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
5489
+ const buildGroupKey = (parameters) => {
5490
+ return `${parameters.chainId}-${parameters.maker.toLowerCase()}-${parameters.group.toLowerCase()}`;
5491
+ };
5492
+ async function* collectConsumedEvents(parameters) {
5493
+ let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
5494
+ const logger = getLogger();
5495
+ let startBlock = blockNumber;
5496
+ let reorgDetected = false;
5497
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5498
+ const stream = streamLogs({
5499
+ client,
5500
+ contractAddress: client.chain.custom.morpho.address,
5501
+ blockNumberGte: blockNumber,
5502
+ blockNumberLte: latestBlockNumberChain,
5503
+ order: "asc",
5504
+ options: {
5505
+ maxBatchSize,
5506
+ blockWindow
5507
+ }
5508
+ });
5509
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
5510
+ const parsedLogs = parseEventLogs({
5511
+ abi: [consumedEvent, takeEvent],
5512
+ logs,
5513
+ strict: false
5514
+ });
5515
+ const normalizedLogs = [];
5516
+ const groups$3 = /* @__PURE__ */ new Map();
5517
+ const eventIds = /* @__PURE__ */ new Set();
5518
+ const recordLog = (log) => {
5519
+ normalizedLogs.push(log);
5520
+ eventIds.add(log.id);
5521
+ const groupKey = buildGroupKey({
5522
+ chainId: log.chainId,
5523
+ maker: log.maker,
5524
+ group: log.group
5525
+ });
5526
+ if (!groups$3.has(groupKey)) groups$3.set(groupKey, {
5527
+ chainId: log.chainId,
5528
+ maker: log.maker.toLowerCase(),
5529
+ group: log.group.toLowerCase()
5530
+ });
5531
+ };
5532
+ for (const rawLog of parsedLogs) {
5533
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
5534
+ logger.debug({
5535
+ collector,
5536
+ chainId: client.chain.id,
5537
+ msg: "Skipping log because it is missing required fields"
5538
+ });
5539
+ continue;
5540
+ }
5541
+ if (rawLog.eventName === consumedEvent.name) {
5542
+ const consumeArgs = rawLog.args;
5543
+ if (consumeArgs.user === void 0 || consumeArgs.group === void 0 || consumeArgs.amount === void 0) {
5544
+ logger.debug({
5545
+ collector,
5546
+ chainId: client.chain.id,
5547
+ msg: "Skipping Consume log because it is missing required args"
5548
+ });
5549
+ continue;
5550
+ }
5551
+ recordLog({
5552
+ kind: "consume",
5553
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
5554
+ chainId: client.chain.id,
5555
+ maker: consumeArgs.user,
5556
+ group: consumeArgs.group,
5557
+ amount: consumeArgs.amount,
5558
+ blockNumber: Number(rawLog.blockNumber)
5559
+ });
5560
+ continue;
5561
+ }
5562
+ if (rawLog.eventName === takeEvent.name) {
5563
+ const takeArgs = rawLog.args;
5564
+ if (takeArgs.maker === void 0 || takeArgs.group === void 0 || takeArgs.consumed === void 0) {
5565
+ logger.debug({
5566
+ collector,
5567
+ chainId: client.chain.id,
5568
+ msg: "Skipping Take log because it is missing required args"
5569
+ });
5570
+ continue;
5571
+ }
5572
+ recordLog({
5573
+ kind: "take",
5574
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
5575
+ chainId: client.chain.id,
5576
+ maker: takeArgs.maker,
5577
+ group: takeArgs.group,
5578
+ consumed: takeArgs.consumed,
5579
+ blockNumber: Number(rawLog.blockNumber)
5580
+ });
5581
+ }
5582
+ }
5583
+ await db.transaction(async (dbTx) => {
5584
+ const existingEventIds = /* @__PURE__ */ new Set();
5585
+ if (eventIds.size > 0) {
5586
+ const ids = Array.from(eventIds);
5587
+ for (let index = 0; index < ids.length; index += 500) {
5588
+ const slice = ids.slice(index, index + 500);
5589
+ const { rows } = await dbTx.execute(sql`
5590
+ SELECT event_id
5591
+ FROM ${consumedEvents}
5592
+ WHERE event_id IN (${sql.join(slice.map((id) => sql`${id}`), sql`,`)});
5593
+ `);
5594
+ for (const row of rows) existingEventIds.add(row.event_id);
5595
+ }
5596
+ }
5597
+ const consumedByGroup = /* @__PURE__ */ new Map();
5598
+ if (groups$3.size > 0) {
5599
+ const groupList = Array.from(groups$3.values());
5600
+ for (let index = 0; index < groupList.length; index += 500) {
5601
+ const slice = groupList.slice(index, index + 500);
5602
+ const { rows } = await dbTx.execute(sql`
5603
+ WITH targets(chain_id, maker, "group") AS (
5604
+ VALUES ${sql.join(slice.map((group) => sql`(${group.chainId}::bigint, ${group.maker.toLowerCase()}::varchar(42), ${group.group.toLowerCase()}::varchar(66))`), sql`,`)}
5605
+ )
5606
+ SELECT
5607
+ targets.chain_id,
5608
+ targets.maker,
5609
+ targets."group",
5610
+ COALESCE(g.consumed, 0)::numeric AS consumed
5611
+ FROM targets
5612
+ LEFT JOIN ${groups} g
5613
+ ON g.chain_id = targets.chain_id
5614
+ AND g.maker = targets.maker
5615
+ AND g."group" = targets."group";
5616
+ `);
5617
+ for (const row of rows) {
5618
+ const groupKey = buildGroupKey({
5619
+ chainId: Number(row.chain_id),
5620
+ maker: row.maker,
5621
+ group: row.group
5622
+ });
5623
+ consumedByGroup.set(groupKey, BigInt(row.consumed ?? "0"));
5624
+ }
5625
+ }
5626
+ }
5627
+ const events = [];
5628
+ for (const log of normalizedLogs) {
5629
+ if (existingEventIds.has(log.id)) continue;
5630
+ const groupKey = buildGroupKey({
5631
+ chainId: log.chainId,
5632
+ maker: log.maker,
5633
+ group: log.group
5634
+ });
5635
+ const previousConsumed = consumedByGroup.get(groupKey) ?? 0n;
5636
+ if (log.kind === "consume") {
5637
+ events.push({
5638
+ id: log.id,
5639
+ chainId: log.chainId,
5640
+ maker: log.maker,
5641
+ group: log.group,
5642
+ amount: log.amount,
5643
+ blockNumber: log.blockNumber
5644
+ });
5645
+ consumedByGroup.set(groupKey, previousConsumed + log.amount);
5646
+ continue;
5647
+ }
5648
+ const delta = log.consumed - previousConsumed;
5649
+ if (delta <= 0n) {
5650
+ logger.debug({
5651
+ collector,
5652
+ chainId: client.chain.id,
5653
+ msg: "Skipping Take log because consumed did not increase",
5654
+ previous_consumed: previousConsumed.toString(),
5655
+ consumed: log.consumed.toString()
5656
+ });
5657
+ continue;
5658
+ }
5659
+ events.push({
5660
+ id: log.id,
5661
+ chainId: log.chainId,
5662
+ maker: log.maker,
5663
+ group: log.group,
5664
+ amount: delta,
5665
+ blockNumber: log.blockNumber
5666
+ });
5667
+ consumedByGroup.set(groupKey, log.consumed);
5668
+ }
5669
+ try {
5670
+ await dbTx.consumed.create(events);
5671
+ if (events.length > 0) logger.info({
5672
+ msg: `Events indexed`,
5673
+ collector,
5674
+ count: events.length,
5675
+ chain_id: client.chain.id,
5676
+ block_range: [startBlock, lastStreamBlockNumber]
5677
+ });
5678
+ } catch (err) {
5679
+ logger.error({
5680
+ err,
5681
+ msg: "Failed to process consumed events"
5682
+ });
5683
+ }
5684
+ blockNumber = lastStreamBlockNumber;
5685
+ try {
5686
+ await dbTx.blocks.advanceCollector({
5687
+ collectorName: collector,
5688
+ chainId: client.chain.id,
5689
+ blockNumber,
5690
+ epoch
5691
+ });
5692
+ } catch (_) {
5693
+ try {
5694
+ const ancestor = await dbTx.blocks.getCollector({
5695
+ collectorName: collector,
5696
+ chainId: client.chain.id
5697
+ });
5698
+ blockNumber = ancestor.blockNumber;
5699
+ const deleted = await dbTx.consumed.delete({
5700
+ chainId: client.chain.id,
5701
+ blockNumberGte: blockNumber + 1
5702
+ });
5703
+ logger.info({
5704
+ collector,
5705
+ chain_id: client.chain.id,
5706
+ msg: `Reorg detected, events deleted`,
5707
+ count: deleted,
5708
+ block_number: blockNumber
5709
+ });
5710
+ await dbTx.blocks.advanceCollector({
5711
+ collectorName: collector,
5712
+ chainId: client.chain.id,
5713
+ blockNumber,
5714
+ epoch: ancestor.epoch
5715
+ });
5716
+ reorgDetected = true;
5717
+ } catch (err) {
5718
+ const msg = "Failed to delete consumed events when handling reorg.";
5719
+ logger.error({
5720
+ collector,
5721
+ chainId: client.chain.id,
5722
+ msg,
5723
+ err
5724
+ });
5725
+ throw new Error(msg, { cause: err });
5726
+ }
5727
+ }
5728
+ });
5729
+ if (reorgDetected) return;
5730
+ yield blockNumber;
5731
+ startBlock = blockNumber;
5732
+ }
5733
+ }
5734
+
5735
+ //#endregion
5736
+ //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
5737
+ async function* collectOffersV2(parameters) {
5738
+ let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
5739
+ const logger = getLogger();
5740
+ let startBlock = blockNumber;
5741
+ let reorgDetected = false;
5742
+ if (client.chain.custom.morpho.address.toLowerCase() === zeroAddress) {
5743
+ const msg = "Morpho V2 address is zero, signature verification will fail. Please set the Morpho V2 address in the chain configuration.";
5744
+ logger.error({
5745
+ msg,
5746
+ chain_id: client.chain.id
5747
+ });
5748
+ throw new Error(msg);
5749
+ }
5750
+ const signatureDomain = {
5751
+ chainId: client.chain.id,
5752
+ verifyingContract: client.chain.custom.morpho.address
5753
+ };
5754
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
5755
+ const stream = streamLogs({
5756
+ client,
5757
+ contractAddress: client.chain.custom.mempool.address,
5758
+ event: {
5759
+ type: "event",
5760
+ name: "Event",
5761
+ inputs: [{
5762
+ name: "data",
5763
+ type: "bytes",
5764
+ indexed: false,
5765
+ internalType: "bytes"
5766
+ }],
5767
+ anonymous: false
5768
+ },
5769
+ blockNumberGte: blockNumber,
5770
+ blockNumberLte: latestBlockNumberChain,
5771
+ order: "asc",
5772
+ options: {
5773
+ maxBatchSize,
5774
+ blockWindow
5775
+ }
5776
+ });
5777
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
5778
+ blockNumber = lastStreamBlockNumber;
5779
+ const decodedTrees = [];
5780
+ for (const log of logs) {
5781
+ if (!log) continue;
5782
+ const [payload] = decodeAbiParameters([{ type: "bytes" }], log.data);
5783
+ try {
5784
+ const { tree, signature, signer } = await decode(payload, signatureDomain);
5785
+ const signerMismatch = tree.offers.find((offer) => offer.maker.toLowerCase() !== signer.toLowerCase());
5786
+ if (signerMismatch) {
5787
+ logger.debug({
5788
+ msg: "Tree rejected: signer mismatch",
5789
+ reason: "signer_mismatch",
5790
+ signer,
5791
+ maker: signerMismatch.maker,
5792
+ chain_id: client.chain.id
5793
+ });
5794
+ continue;
5795
+ }
5796
+ decodedTrees.push({
5797
+ tree,
5798
+ signature,
5799
+ signer,
5800
+ blockNumber: Number(log.blockNumber)
5801
+ });
5802
+ } catch (err) {
5803
+ const reason = err instanceof DecodeError && err.message.includes("signature") ? "invalid_signature" : "decode_failed";
5804
+ logger.debug({
5805
+ msg: "Tree decode failed",
5806
+ reason,
5807
+ chain_id: client.chain.id,
5808
+ err: err instanceof Error ? err.message : String(err)
5809
+ });
5810
+ }
5811
+ }
5812
+ await db.transaction(async (dbTx) => {
5813
+ const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
5814
+ const treesToInsert = [];
5815
+ let totalValidOffers = 0;
5816
+ const offersWithBlock = [];
5817
+ for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
5818
+ const allowedResults = await gatekeeper.isAllowed(tree.offers);
5819
+ const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
5820
+ if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
5821
+ if (allowedResults.issues.length > 0) {
5822
+ const hasMixedMaker = allowedResults.issues.some((i) => i.ruleName === "mixed_maker");
5823
+ logger.debug({
5824
+ msg: "Tree offers rejected by gatekeeper",
5825
+ reason: hasMixedMaker ? "mixed_maker" : "gatekeeper_rejected",
5826
+ chain_id: client.chain.id,
5827
+ issues_count: allowedResults.issues.length
5828
+ });
5829
+ } else if (hasBlockWindowViolation) logger.debug({
5830
+ msg: "Tree rejected: offers outside block window",
5831
+ reason: "block_window",
5832
+ chain_id: client.chain.id
5833
+ });
5834
+ continue;
5835
+ }
5836
+ treesToInsert.push({
5837
+ tree,
5838
+ signature
5839
+ });
5840
+ totalValidOffers += tree.offers.length;
5841
+ offersWithBlock.push(...tree.offers.map((offer) => ({
5842
+ offer,
5843
+ blockNumber: treeBlockNumber
5844
+ })));
5845
+ } catch (err) {
5846
+ const error = err instanceof Error ? err : new Error(String(err));
5847
+ logger.error({
5848
+ err: error,
5849
+ msg: "Gatekeeper validation failed",
5850
+ chain_id: client.chain.id
5851
+ });
5852
+ throw new Error("Gatekeeper validation failed", { cause: error });
5853
+ }
5854
+ const dependencies = buildOfferDependencies$1(offersWithBlock);
5855
+ await dbTx.oracles.upsert(dependencies.oracles);
5856
+ await dbTx.obligations.create(dependencies.obligations);
5857
+ await dbTx.groups.create(dependencies.groups);
5858
+ const insertedHashes = await dbTx.offers.create(dependencies.offerBatches);
5859
+ if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
5860
+ const insertedOffers = filterInsertedOffers({
5861
+ offers: offersWithBlock,
5862
+ hashes: insertedHashes
5863
+ });
5864
+ const { callbacks, positions, lots } = decodeCallbacks({
5865
+ chainId: client.chain.id,
5866
+ offers: insertedOffers
5867
+ });
5868
+ if (positions.length > 0) await dbTx.positions.upsert(positions);
5869
+ if (callbacks.length > 0) await dbTx.callbacks.upsert(callbacks);
5870
+ if (lots.length > 0) await dbTx.lots.create(lots);
5871
+ try {
5872
+ await dbTx.blocks.advanceCollector({
5873
+ collectorName: collector,
5874
+ chainId: client.chain.id,
5875
+ blockNumber,
5876
+ epoch
5877
+ });
5878
+ if (totalValidOffers > 0) logger.info({
5879
+ msg: `New offers`,
5880
+ collector,
5881
+ count: totalValidOffers,
5882
+ trees_count: treesToInsert.length,
5883
+ chain_id: client.chain.id,
5884
+ block_range: [startBlock, lastStreamBlockNumber]
5885
+ });
5886
+ } catch (_) {
5887
+ try {
5888
+ const ancestor = await dbTx.blocks.getCollector({
5889
+ collectorName: collector,
5890
+ chainId: client.chain.id
5891
+ });
5892
+ blockNumber = ancestor.blockNumber;
5893
+ const deleted = await dbTx.offers.delete({
5894
+ blockNumberGte: blockNumber + 1,
5895
+ chainId: client.chain.id
5896
+ });
5897
+ logger.info({
5898
+ collector,
5899
+ chain_id: client.chain.id,
5900
+ msg: `Reorg detected, offers deleted`,
5901
+ count: deleted,
5902
+ block_number: blockNumber
5903
+ });
5904
+ await dbTx.blocks.advanceCollector({
5905
+ collectorName: collector,
5906
+ chainId: client.chain.id,
5907
+ blockNumber,
5908
+ epoch: ancestor.epoch
5909
+ });
5910
+ reorgDetected = true;
5911
+ } catch (err) {
5912
+ const msg = "Failed to delete offers when handling reorg.";
5913
+ logger.error({
5914
+ collector,
5915
+ chainId: client.chain.id,
5916
+ msg,
5917
+ err
5918
+ });
5919
+ throw new Error(msg);
5920
+ }
5921
+ }
5922
+ });
5923
+ if (reorgDetected) return;
5924
+ yield blockNumber;
5925
+ startBlock = blockNumber;
5926
+ }
5927
+ }
5928
+ function decodeCallbacks(parameters) {
5929
+ const { offers } = parameters;
5930
+ if (offers.length === 0) return {
5931
+ callbacks: [],
5932
+ positions: [],
5933
+ lots: []
5934
+ };
5935
+ const callbacks = [];
5936
+ const positions = [];
5937
+ const lots = [];
5938
+ for (const { offer, blockNumber: offerBlockNumber } of offers) {
5939
+ if (!offer.buy) continue;
5940
+ if (!isEmptyCallback(offer)) continue;
5941
+ const loanToken = offer.loanToken.toLowerCase();
5942
+ positions.push(from$10({
5943
+ chainId: offer.chainId,
5944
+ contract: loanToken,
5945
+ user: offer.maker,
5946
+ type: Type.ERC20,
5947
+ asset: loanToken,
5948
+ blockNumber: offerBlockNumber
5949
+ }));
5950
+ lots.push({
5951
+ positionChainId: offer.chainId,
5952
+ positionContract: loanToken,
5953
+ positionUser: offer.maker,
5954
+ group: offer.group,
5955
+ size: offer.assets
5956
+ });
5957
+ callbacks.push({
5958
+ offerHash: hash(offer),
5959
+ callbacks: [{
5960
+ chainId: offer.chainId,
5961
+ contract: loanToken,
5962
+ user: offer.maker,
5963
+ amount: offer.assets
5964
+ }]
5965
+ });
5966
+ }
5967
+ return {
5968
+ callbacks,
5969
+ positions,
5970
+ lots
5971
+ };
5972
+ }
5973
+ function buildOfferDependencies$1(offers) {
5974
+ const obligationsById = /* @__PURE__ */ new Map();
5975
+ const oraclesByKey = /* @__PURE__ */ new Map();
5976
+ const groupsByKey = /* @__PURE__ */ new Map();
5977
+ const offersByBlock = /* @__PURE__ */ new Map();
5978
+ for (const { offer, blockNumber } of offers) {
5979
+ const list = offersByBlock.get(blockNumber) ?? [];
5980
+ list.push(offer);
5981
+ offersByBlock.set(blockNumber, list);
5982
+ const obligationId$2 = obligationId(offer);
5983
+ if (!obligationsById.get(obligationId$2)) obligationsById.set(obligationId$2, from$13({
5984
+ chainId: offer.chainId,
5985
+ loanToken: offer.loanToken,
5986
+ maturity: offer.maturity,
5987
+ collaterals: offer.collaterals
5988
+ }));
5989
+ for (const collateral of offer.collaterals) {
5990
+ const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
5991
+ if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
5992
+ chainId: offer.chainId,
5993
+ address: collateral.oracle,
5994
+ price: null,
5995
+ blockNumber
5996
+ }));
5997
+ }
5998
+ const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
5999
+ if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
6000
+ chainId: offer.chainId,
6001
+ maker: offer.maker,
6002
+ group: offer.group,
6003
+ blockNumber
6004
+ });
6005
+ }
6006
+ return {
6007
+ obligations: Array.from(obligationsById.values()),
6008
+ oracles: Array.from(oraclesByKey.values()),
6009
+ groups: Array.from(groupsByKey.values()),
6010
+ offerBatches: Array.from(offersByBlock.entries()).map(([blockNumber, items]) => ({
6011
+ blockNumber,
6012
+ offers: items
6013
+ }))
6014
+ };
6015
+ }
6016
+ function filterInsertedOffers(parameters) {
6017
+ if (parameters.hashes.length === 0) return [];
6018
+ const inserted = new Set(parameters.hashes.map((hash) => hash.toLowerCase()));
6019
+ const seen = /* @__PURE__ */ new Set();
6020
+ const filtered = [];
6021
+ for (const entry of parameters.offers) {
6022
+ const hash$2 = hash(entry.offer).toLowerCase();
6023
+ if (!inserted.has(hash$2)) continue;
6024
+ if (seen.has(hash$2)) continue;
6025
+ seen.add(hash$2);
6026
+ filtered.push(entry);
6027
+ }
6028
+ return filtered;
6029
+ }
6030
+
6031
+ //#endregion
6032
+ //#region src/indexer/collectors/fetchers/fetchOraclePrices.ts
6033
+ /**
6034
+ * Fetches prices from multiple oracle contracts using multicall.
6035
+ *
6036
+ * - Executes `price()` on {@link Abi.Oracle} for each {@link Address}.
6037
+ * - Requires a client supporting {@link PublicActions.multicall | multicall}.
6038
+ *
6039
+ * @param parameters - {@link fetchOraclePrices.Parameters} including the list of oracle
6040
+ * {@link Address | addresses}, fetch options, and the multicall-enabled client.
6041
+ * @returns {@link fetchOraclePrices.ReturnType} mapping {@link Address} to `bigint` price.
6042
+ */
6043
+ async function fetchOraclePrices(parameters) {
6044
+ const { client, oracles, options } = parameters;
6045
+ if (oracles.length === 0) return /* @__PURE__ */ new Map();
6046
+ const batchSize = Math.max(1, options?.batchSize ?? 5e3);
6047
+ const retryAttempts = Math.max(1, options?.retryAttempts ?? 3);
6048
+ const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 50);
6049
+ const blockNumber = options?.blockNumber ? BigInt(options.blockNumber) : void 0;
6050
+ const out = /* @__PURE__ */ new Map();
6051
+ for (const oraclesBatch of batch$1(oracles, batchSize)) {
6052
+ const priceCalls = [];
6053
+ for (const oracle of oraclesBatch) priceCalls.push({
6054
+ address: oracle,
6055
+ abi: Oracle,
6056
+ functionName: "price",
6057
+ args: []
6058
+ });
6059
+ const prices = await batchMulticall({
6060
+ client,
6061
+ calls: priceCalls,
6062
+ batchSize,
6063
+ retryAttempts,
6064
+ retryDelayMs,
6065
+ blockNumber
6066
+ });
6067
+ for (let i = 0; i < oraclesBatch.length; i++) {
6068
+ const oracle = oraclesBatch[i];
6069
+ const price = prices[i];
6070
+ out.set(oracle, price);
6071
+ }
6072
+ }
6073
+ return out;
6074
+ }
6075
+
6076
+ //#endregion
6077
+ //#region src/indexer/collectors/fetchers/snapshotERC20Positions.ts
6078
+ /**
6079
+ * Fetches ERC20 balances for positions at a given block number and returns the positions with the updated balances.
6080
+ * @notice This method does not mutate positions and returns a new array.
6081
+ *
6082
+ * @param parameters - {@link snapshotERC20Positions.Parameters}
6083
+ * @returns Positions - {@link snapshotERC20Positions.ReturnType}
6084
+ */
6085
+ async function snapshotERC20Positions(parameters) {
6086
+ const { positions: oldPositions, blockNumber, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
6087
+ if (oldPositions.length === 0) return [];
6088
+ const calls = [];
6089
+ for (const position of oldPositions) calls.push({
6090
+ address: position.contract,
6091
+ abi: erc20Abi,
6092
+ functionName: "balanceOf",
6093
+ args: [position.user]
6094
+ });
6095
+ const balances = await batchMulticall({
6096
+ client: parameters.client,
6097
+ calls,
6098
+ blockNumber: BigInt(blockNumber),
6099
+ batchSize: maxBatchSize,
6100
+ retryAttempts,
6101
+ retryDelayMs
6102
+ });
6103
+ const positions = [];
6104
+ for (let i = 0; i < balances.length; i++) {
6105
+ const oldPosition = oldPositions[i];
6106
+ if (!oldPosition) continue;
6107
+ positions.push({
6108
+ ...oldPosition,
6109
+ balance: balances[i],
6110
+ blockNumber
6111
+ });
6112
+ }
6113
+ return positions;
6114
+ }
6115
+
6116
+ //#endregion
6117
+ //#region src/indexer/collectors/fetchers/snapshotVaultPositions.ts
6118
+ /**
6119
+ * Fetches vault shares for users at a given block number, converts them to assets and returns the positions with the updated balances.
6120
+ * @notice This method does not mutate positions and returns a new array.
6121
+ *
6122
+ * @param parameters - {@link snapshotVaultPositions.Parameters}
6123
+ * @returns Positions - {@link snapshotVaultPositions.ReturnType}
6124
+ */
6125
+ async function snapshotVaultPositions(parameters) {
6126
+ const logger = getLogger();
6127
+ const { client, positions: oldPositions, blockNumber, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
6128
+ const calls = [];
6129
+ const contracts = /* @__PURE__ */ new Map();
6130
+ const positions = structuredClone(oldPositions);
6131
+ for (const position of positions) {
6132
+ calls.push({
6133
+ address: position.contract,
6134
+ abi: MetaMorpho,
6135
+ functionName: "balanceOf",
6136
+ args: [position.user],
6137
+ convertToAssets: (shares) => {
6138
+ const contract = contracts.get(position.contract.toLowerCase());
6139
+ if (!contract) return;
6140
+ if (contract.decimalsOffset === void 0 || contract.totalAssets === void 0 || contract.totalSupply === void 0) return;
6141
+ try {
6142
+ position.balance = convertToAssets({
6143
+ shares,
6144
+ totalAssets: contract.totalAssets,
6145
+ totalSupply: contract.totalSupply,
6146
+ decimalsOffset: contract.decimalsOffset
6147
+ });
6148
+ position.asset = contract.asset;
6149
+ position.blockNumber = blockNumber;
6150
+ } catch (err) {
6151
+ if (err instanceof DenominatorIsZeroError) {
6152
+ logger.error({
6153
+ msg: "Failed to convert shares to assets",
6154
+ chain_id: client.chain.id,
6155
+ block_number: blockNumber,
6156
+ position_contract: position.contract,
6157
+ position_user: position.user,
6158
+ shares,
6159
+ err
6160
+ });
6161
+ return;
6162
+ }
6163
+ throw err;
6164
+ }
6165
+ }
6166
+ });
6167
+ if (contracts.has(position.contract.toLowerCase())) continue;
6168
+ calls.push({
6169
+ address: position.contract,
6170
+ abi: MetaMorpho,
6171
+ functionName: "DECIMALS_OFFSET",
6172
+ args: []
6173
+ }, {
6174
+ address: position.contract,
6175
+ abi: MetaMorpho,
6176
+ functionName: "totalAssets",
6177
+ args: []
6178
+ }, {
6179
+ address: position.contract,
6180
+ abi: MetaMorpho,
6181
+ functionName: "totalSupply",
6182
+ args: []
6183
+ }, {
6184
+ address: position.contract,
6185
+ abi: MetaMorpho,
6186
+ functionName: "asset",
6187
+ args: []
6188
+ });
6189
+ contracts.set(position.contract.toLowerCase(), {
6190
+ decimalsOffset: void 0,
6191
+ totalAssets: void 0,
6192
+ totalSupply: void 0,
6193
+ asset: void 0
6194
+ });
6195
+ }
6196
+ const results = await batchMulticall({
6197
+ client,
6198
+ calls,
6199
+ blockNumber: BigInt(blockNumber),
6200
+ batchSize: maxBatchSize,
6201
+ retryAttempts,
6202
+ retryDelayMs
6203
+ });
6204
+ const convertToAssetsList = [];
6205
+ for (let i = 0; i < results.length; i++) {
6206
+ const call = calls[i];
6207
+ const value = results[i];
6208
+ const contract = contracts.get(call.address.toLowerCase());
6209
+ if (!contract) continue;
6210
+ switch (call.functionName) {
6211
+ case "balanceOf":
6212
+ convertToAssetsList.push(() => call.convertToAssets(value));
6213
+ break;
6214
+ case "DECIMALS_OFFSET":
6215
+ contract.decimalsOffset = value;
6216
+ break;
6217
+ case "totalAssets":
6218
+ contract.totalAssets = value;
6219
+ break;
6220
+ case "totalSupply":
6221
+ contract.totalSupply = value;
6222
+ break;
6223
+ case "asset":
6224
+ contract.asset = value.toLowerCase();
6225
+ break;
6226
+ }
6227
+ }
6228
+ for (const convertToAssets of convertToAssetsList) convertToAssets();
6229
+ return positions;
6230
+ }
6231
+
6232
+ //#endregion
6233
+ //#region src/indexer/collectors/CollectFunctions/collectPositions.ts
6234
+ async function* collectPositions(parameters) {
6235
+ let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, retryAttempts = 5, retryDelayMs = 500, blockWindow } = {} } = parameters;
6236
+ const logger = getLogger();
6237
+ let startBlock = blockNumber;
6238
+ let reorgDetected = false;
6239
+ const TransferEvent = {
6240
+ type: "event",
6241
+ name: "Transfer",
6242
+ inputs: [
6243
+ {
6244
+ name: "from",
6245
+ type: "address",
6246
+ indexed: true
6247
+ },
6248
+ {
6249
+ name: "to",
6250
+ type: "address",
6251
+ indexed: true
6252
+ },
6253
+ {
6254
+ name: "value",
6255
+ type: "uint256",
6256
+ indexed: false
6257
+ }
6258
+ ]
6259
+ };
6260
+ const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
6261
+ const stream = streamLogs({
6262
+ client,
6263
+ event: TransferEvent,
6264
+ blockNumberGte: blockNumber,
6265
+ blockNumberLte: latestBlockNumberChain,
6266
+ order: "asc",
6267
+ options: {
6268
+ maxBatchSize,
6269
+ blockWindow
6270
+ }
6271
+ });
6272
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
6273
+ blockNumber = lastStreamBlockNumber;
6274
+ const parsedLogs = parseEventLogs({
6275
+ abi: [TransferEvent],
6276
+ logs
6277
+ });
6278
+ const transfers = [];
6279
+ for (const log of parsedLogs) {
6280
+ if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
6281
+ logger.debug({
6282
+ collector,
6283
+ chainId: client.chain.id,
6284
+ msg: "Skipping log because it is missing required fields"
6285
+ });
6286
+ continue;
6287
+ }
6288
+ transfers.push(from$8({
6289
+ id: `${client.chain.id}-${log.blockNumber.toString()}-${log.transactionHash}-${log.logIndex.toString()}`,
6290
+ chainId: client.chain.id,
6291
+ contract: log.address,
6292
+ from: log.args.from,
6293
+ to: log.args.to,
6294
+ value: log.args.value,
6295
+ blockNumber: Number(log.blockNumber)
6296
+ }));
6297
+ }
6298
+ const { positions } = await db.positions.get({
6299
+ chainId: client.chain.id,
6300
+ filled: false
6301
+ });
6302
+ const newPositions = [];
6303
+ try {
6304
+ newPositions.push(...(await _snapshot({
6305
+ positions,
6306
+ blockNumber: latestBlockNumberChain,
6307
+ client,
6308
+ maxBatchSize,
6309
+ retryAttempts,
6310
+ retryDelayMs
6311
+ })).map((p) => ({
6312
+ ...p,
6313
+ blockNumber: p.blockNumber + 1
6314
+ })));
6315
+ } catch (err) {
6316
+ logger.error({
6317
+ msg: "Failed to snapshot new empty positions",
6318
+ collector,
6319
+ chain_id: client.chain.id,
6320
+ block_number: latestBlockNumberChain,
6321
+ err
6322
+ });
6323
+ yield startBlock;
6324
+ return;
6325
+ }
6326
+ try {
6327
+ await db.transaction(async (dbTx) => {
6328
+ const insertPositions = async () => {
6329
+ if (newPositions.length === 0) return;
6330
+ try {
6331
+ const count = await dbTx.positions.upsert(newPositions);
6332
+ logger.info({
6333
+ msg: `New positions`,
6334
+ collector,
6335
+ count,
6336
+ chain_id: client.chain.id,
6337
+ block_number: latestBlockNumberChain
6338
+ });
6339
+ } catch (err) {
6340
+ throw new InsertPositionsError(err);
6341
+ }
6342
+ };
6343
+ const insertTransfers = async () => {
6344
+ if (transfers.length === 0) return;
6345
+ try {
6346
+ const created = await dbTx.transfers.create(transfers);
6347
+ logger.info({
6348
+ msg: `New transfers`,
6349
+ collector,
6350
+ count: created,
6351
+ chain_id: client.chain.id,
6352
+ block_range: [startBlock, blockNumber]
6353
+ });
6354
+ } catch (err) {
6355
+ throw new InsertTransfersError(err);
6356
+ }
6357
+ };
6358
+ const saveBlockNumber = async () => {
6359
+ try {
6360
+ await dbTx.blocks.advanceCollector({
6361
+ collectorName: collector,
6362
+ chainId: client.chain.id,
6363
+ blockNumber,
6364
+ epoch
6365
+ });
6366
+ } catch (_) {
6367
+ throw new ReorgError(blockNumber);
6368
+ }
6369
+ };
6370
+ await insertPositions();
6371
+ await insertTransfers();
6372
+ await saveBlockNumber();
6373
+ });
6374
+ } catch (err) {
6375
+ if (err instanceof ReorgError) {
6376
+ logger.info({
6377
+ msg: "Reorg detected, positions and transfers insertion aborted",
6378
+ collector,
6379
+ count: newPositions.length,
6380
+ chain_id: client.chain.id,
6381
+ block_number: blockNumber
6382
+ });
6383
+ reorgDetected = true;
6384
+ }
6385
+ if (err instanceof InsertPositionsError) {
6386
+ logger.error({
6387
+ msg: "Failed to insert positions",
6388
+ collector,
6389
+ count: newPositions.length,
6390
+ chain_id: client.chain.id,
6391
+ block_number: latestBlockNumberChain,
6392
+ err
6393
+ });
6394
+ throw err.cause;
6395
+ }
6396
+ if (err instanceof InsertTransfersError) {
6397
+ logger.error({
6398
+ msg: "Failed to insert transfers",
6399
+ collector,
6400
+ count: transfers.length,
6401
+ chain_id: client.chain.id,
6402
+ block_number: blockNumber,
6403
+ err
6404
+ });
6405
+ throw err.cause;
6406
+ }
6407
+ }
6408
+ if (!reorgDetected) {
6409
+ startBlock = blockNumber;
6410
+ if (newPositions.length === 0 && transfers.length === 0 && lastStreamBlockNumber !== latestBlockNumberChain) continue;
6411
+ yield blockNumber;
6412
+ continue;
6413
+ }
6414
+ await db.transaction(async (dbTx) => {
6415
+ try {
6416
+ const ancestor = await dbTx.blocks.getCollector({
6417
+ collectorName: collector,
6418
+ chainId: client.chain.id
6419
+ });
6420
+ blockNumber = ancestor.blockNumber;
6421
+ const emptied = await dbTx.positions.setEmptyAfter({
6422
+ chainId: client.chain.id,
6423
+ blockNumber: blockNumber + 1
6424
+ });
6425
+ logger.info({
6426
+ msg: "Reorg detected, positions set to empty",
6427
+ collector,
6428
+ count: emptied,
6429
+ chain_id: client.chain.id,
6430
+ block_number_gte: blockNumber + 1
6431
+ });
6432
+ await dbTx.blocks.advanceCollector({
6433
+ collectorName: collector,
6434
+ chainId: client.chain.id,
6435
+ blockNumber,
6436
+ epoch: ancestor.epoch
6437
+ });
6438
+ } catch (err) {
6439
+ const msg = "Failed to revert to ancestor block when handling reorg.";
6440
+ logger.error({
6441
+ collector,
6442
+ chainId: client.chain.id,
6443
+ msg,
6444
+ err
6445
+ });
6446
+ throw new Error(msg);
6447
+ }
6448
+ });
6449
+ return;
6450
+ }
6451
+ }
6452
+ /**
6453
+ * @internal
6454
+ *
6455
+ * Snapshots positions and returns the new positions.
6456
+ * @param parameters - {@link _snapshot.Parameters}
6457
+ * @returns The new positions. {@link _snapshot.ReturnType}
6458
+ */
6459
+ async function _snapshot(parameters) {
6460
+ const { positions, blockNumber, client, maxBatchSize, retryAttempts, retryDelayMs } = parameters;
6461
+ const vaultV1Positions = [];
6462
+ const erc20Positions = [];
6463
+ for (const position of positions) switch (position.type) {
6464
+ case Type.VAULT_V1:
6465
+ vaultV1Positions.push(position);
6466
+ break;
6467
+ case Type.ERC20:
6468
+ erc20Positions.push(position);
6469
+ break;
6470
+ default: throw new Error("Invalid position type");
6471
+ }
6472
+ const promises = [snapshotVaultPositions({
6473
+ client,
6474
+ positions: vaultV1Positions,
6475
+ blockNumber,
6476
+ options: {
6477
+ maxBatchSize,
6478
+ retryAttempts,
6479
+ retryDelayMs
6480
+ }
6481
+ }), snapshotERC20Positions({
6482
+ client,
6483
+ positions: erc20Positions,
6484
+ blockNumber,
6485
+ options: {
6486
+ maxBatchSize,
6487
+ retryAttempts,
6488
+ retryDelayMs
6489
+ }
6490
+ })];
6491
+ return (await Promise.all(promises)).flat();
6492
+ }
6493
+ var InsertPositionsError = class extends BaseError {
6494
+ name = "InsertPositionsError";
6495
+ constructor(err) {
6496
+ super("Failed to insert positions", { cause: err });
6497
+ }
6498
+ };
6499
+ var InsertTransfersError = class extends BaseError {
6500
+ name = "InsertTransfersError";
6501
+ constructor(err) {
6502
+ super("Failed to insert transfers", { cause: err });
6503
+ }
6504
+ };
6505
+
6506
+ //#endregion
6507
+ //#region src/indexer/collectors/CollectFunctions/collectPrices.ts
6508
+ /**
6509
+ * Collects oracle prices from on-chain oracles and persists them.
6510
+ *
6511
+ * - Reads oracle definitions as {@link Oracle.Oracle}.
6512
+ * - Uses chain metadata from {@link Chain.Chain}.
6513
+ *
6514
+ * @param parameters - {@link collectPrices.Parameters} (extends {@link Collector.CollectParameters})
6515
+ * with a client supporting {@link PublicActions.multicall | multicall}.
6516
+ * @yields Latest processed block number after each successful update.
6517
+ */
6518
+ async function* collectPrices(parameters) {
6519
+ const { db, collector, client, options: { maxBatchSize = 5e3, retryAttempts = 5, retryDelayMs = 500 } = {} } = parameters;
6520
+ const logger = getLogger();
6521
+ let blockNumber = parameters.lastBlockNumber;
6522
+ const [oracles, { blockNumber: latestBlockNumberChain, epoch }] = await Promise.all([db.oracles.get({ chainId: client.chain.id }), db.blocks.getChain(client.chain.id)]);
6523
+ const updatedOracles = [];
6524
+ try {
6525
+ const pricesMap = await fetchOraclePrices({
6526
+ client,
6527
+ oracles: oracles.map((oracle) => oracle.address),
6528
+ options: {
6529
+ batchSize: maxBatchSize,
6530
+ blockNumber: latestBlockNumberChain,
6531
+ retryAttempts,
6532
+ retryDelayMs
6533
+ }
6534
+ });
6535
+ for (const oracle of oracles) {
6536
+ const price = pricesMap.get(oracle.address);
6537
+ if (price !== void 0) updatedOracles.push({
6538
+ chainId: client.chain.id,
6539
+ address: oracle.address,
6540
+ price,
6541
+ blockNumber: latestBlockNumberChain
6542
+ });
6543
+ }
6544
+ } catch (err) {
6545
+ logger.error({
6546
+ msg: "Failed to fetch oracle prices",
6547
+ collector,
6548
+ chain_id: client.chain.id,
6549
+ block_number: latestBlockNumberChain,
6550
+ err
6551
+ });
6552
+ yield blockNumber;
6553
+ return;
6554
+ }
6555
+ let reorgDetected = false;
6556
+ try {
6557
+ await db.transaction(async (dbTx) => {
6558
+ if (updatedOracles.length > 0) {
6559
+ await dbTx.oracles.upsert(updatedOracles);
6560
+ logger.info({
6561
+ msg: "Oracle prices updated",
6562
+ collector,
6563
+ count: updatedOracles.length,
6564
+ chain_id: client.chain.id,
6565
+ block_number: latestBlockNumberChain
6566
+ });
6567
+ }
6568
+ try {
6569
+ await dbTx.blocks.advanceCollector({
6570
+ collectorName: collector,
6571
+ chainId: client.chain.id,
6572
+ blockNumber: latestBlockNumberChain,
6573
+ epoch
6574
+ });
6575
+ } catch (_) {
6576
+ throw new ReorgError(latestBlockNumberChain);
6577
+ }
6578
+ blockNumber = latestBlockNumberChain;
6579
+ });
6580
+ } catch (err) {
6581
+ if (err instanceof ReorgError) {
6582
+ logger.info({
6583
+ msg: "Reorg detected, prices update aborted",
6584
+ collector,
6585
+ count: updatedOracles.length,
6586
+ chain_id: client.chain.id,
6587
+ block_number: latestBlockNumberChain
6588
+ });
6589
+ reorgDetected = true;
6590
+ } else throw new Error("Failed to collect oracle prices", { cause: err });
6591
+ }
6592
+ if (!reorgDetected) {
6593
+ yield blockNumber;
6594
+ return;
6595
+ }
6596
+ await db.transaction(async (dbTx) => {
6597
+ try {
6598
+ const ancestor = await dbTx.blocks.getCollector({
6599
+ collectorName: collector,
6600
+ chainId: client.chain.id
6601
+ });
6602
+ blockNumber = ancestor.blockNumber;
6603
+ await dbTx.blocks.advanceCollector({
6604
+ collectorName: collector,
6605
+ chainId: client.chain.id,
6606
+ blockNumber,
6607
+ epoch: ancestor.epoch
6608
+ });
6609
+ } catch (err) {
6610
+ const msg = "Failed to revert to ancestor block when handling reorg.";
6611
+ logger.error({
6612
+ collector,
6613
+ chainId: client.chain.id,
6614
+ msg,
6615
+ err
6616
+ });
6617
+ throw new Error(msg);
6618
+ }
6619
+ });
6620
+ yield blockNumber;
6621
+ }
6622
+
6623
+ //#endregion
6624
+ //#region src/indexer/collectors/CollectorBuilder.ts
6625
+ function createBuilder(parameters) {
6626
+ const { client, db, gatekeeper, options: { maxBlockNumber, blockWindow, interval } = {} } = parameters;
6627
+ const createCollector = (name, collect) => create$16({
6628
+ name,
6629
+ collect,
6630
+ client,
6631
+ db,
6632
+ options: {
6633
+ maxBlockNumber,
6634
+ interval
6635
+ }
6636
+ });
6637
+ return {
6638
+ buildOffersCollector: ({ options: { maxBatchSize = 1e3 } = {} }) => {
6639
+ return createCollector("offers", (p) => collectOffersV2({
6640
+ ...p,
6641
+ gatekeeper,
6642
+ collector: "offers",
6643
+ client,
6644
+ options: {
6645
+ maxBatchSize,
6646
+ blockWindow
6647
+ }
6648
+ }));
6649
+ },
6650
+ buildConsumedEventsCollector: ({ options: { maxBatchSize = 1e3 } = {} } = {}) => {
6651
+ return createCollector("consumed_events", (p) => collectConsumedEvents({
6652
+ ...p,
6653
+ collector: "consumed_events",
6654
+ options: {
6655
+ maxBatchSize,
6656
+ blockWindow
6657
+ }
6658
+ }));
6659
+ },
6660
+ buildPricesCollector: ({ options: { maxBatchSize = 5e3, retryAttempts, retryDelayMs } = {} } = {}) => {
6661
+ return createCollector("prices", (p) => collectPrices({
6662
+ ...p,
6663
+ collector: "prices",
6664
+ options: {
6665
+ maxBatchSize,
6666
+ retryAttempts,
6667
+ retryDelayMs
6668
+ }
6669
+ }));
6670
+ },
6671
+ buildPositionsCollector: ({ options: { maxBatchSize = 1e3, retryAttempts, retryDelayMs } = {} } = {}) => {
6672
+ return createCollector("positions", (p) => collectPositions({
6673
+ ...p,
6674
+ collector: "positions",
6675
+ options: {
6676
+ maxBatchSize,
6677
+ retryAttempts,
6678
+ retryDelayMs,
6679
+ blockWindow
6680
+ }
6681
+ }));
6682
+ }
6683
+ };
6684
+ }
6685
+
6686
+ //#endregion
6687
+ //#region src/indexer/collectors/Collectors.ts
6688
+ const from$2 = (parameters) => {
6689
+ const { client, db, gatekeeper, maxBatchSize, maxBlockNumber, blockWindow, interval, retryAttempts, retryDelayMs } = parameters;
6690
+ const collectorBuilder = createBuilder({
6691
+ client,
6692
+ db,
6693
+ gatekeeper,
6694
+ options: {
6695
+ maxBlockNumber,
6696
+ blockWindow,
6697
+ interval
6698
+ }
6699
+ });
6700
+ return {
6701
+ offersCollector: collectorBuilder.buildOffersCollector({ options: { maxBatchSize } }),
6702
+ consumedEventsCollector: collectorBuilder.buildConsumedEventsCollector({ options: { maxBatchSize } }),
6703
+ pricesCollector: collectorBuilder.buildPricesCollector({ options: {
6704
+ maxBatchSize,
6705
+ retryAttempts,
6706
+ retryDelayMs
6707
+ } }),
6708
+ positionsCollector: collectorBuilder.buildPositionsCollector({ options: {
6709
+ maxBatchSize,
6710
+ retryAttempts,
6711
+ retryDelayMs
6712
+ } })
6713
+ };
6714
+ };
6715
+
6716
+ //#endregion
6717
+ //#region src/indexer/Indexer.ts
6718
+ function from$1(config) {
6719
+ const { client, gatekeeper, db, interval = 1e4, maxBatchSize = 1e3, maxBlockNumber, blockWindow, retryAttempts, retryDelayMs } = config;
6720
+ const { offersCollector, consumedEventsCollector, positionsCollector, pricesCollector } = from$2({
6721
+ client,
6722
+ db,
6723
+ gatekeeper,
6724
+ maxBatchSize,
6725
+ maxBlockNumber,
6726
+ blockWindow,
6727
+ interval,
6728
+ retryAttempts,
6729
+ retryDelayMs
6730
+ });
6731
+ return create$18({
6732
+ client,
6733
+ collectors: [
6734
+ offersCollector,
6735
+ consumedEventsCollector,
6736
+ positionsCollector,
6737
+ pricesCollector
6738
+ ]
6739
+ });
6740
+ }
6741
+ function create$18(params) {
6742
+ const { collectors, client } = params;
6743
+ const indexerId = `${client.chain.id.toString()}.indexer`;
6744
+ const tracer = getTracer(`router.${indexerId}`);
6745
+ const iterators = collectors.map((collector) => collector.collect());
6746
+ const next = async () => {
6747
+ await startActiveSpan(tracer, `${indexerId}.next`, async () => {
6748
+ await Promise.all(iterators.map((iterator) => iterator.next()));
6749
+ });
6750
+ };
6751
+ const _return = async () => {
6752
+ await Promise.all(iterators.map(async (iterator) => iterator.return()));
6753
+ };
6754
+ return {
6755
+ start: () => {
6756
+ const stops = collectors.map((collector) => start(collector));
6757
+ return () => {
6758
+ stops.forEach((stop) => {
6759
+ stop();
6760
+ });
6761
+ };
6762
+ },
6763
+ next,
6764
+ return: _return
6765
+ };
6766
+ }
6767
+
6768
+ //#endregion
6769
+ //#region src/indexer/collectors/Admin.ts
6770
+ function create$17(parameters) {
6771
+ const collector = "admin";
6772
+ const { client, db, options: { maxBatchSize = 25, maxBlockNumber } = {} } = parameters;
6773
+ const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
6774
+ let finalizedBlock = null;
6775
+ let unfinalizedBlocks = [];
6776
+ let initialized = false;
6777
+ const logger = getLogger();
6778
+ let isMaxBlockNumberReached = false;
6779
+ let tick = 0;
6780
+ return { syncBlock: async () => {
6781
+ if (!initialized) {
6782
+ await Promise.all(names.map((collectorName) => db.blocks.init({
6783
+ chainId: client.chain.id,
6784
+ collectorName
6785
+ })));
6786
+ initialized = true;
6787
+ }
6788
+ if (isMaxBlockNumberReached) return true;
6789
+ const head = await client.getBlock({
6790
+ blockTag: "latest",
6791
+ includeTransactions: false
6792
+ });
6793
+ await db.transaction(async (dbTx) => {
6794
+ const { epoch, blockNumber: latestSavedBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
6795
+ if (maxBlockNumberBI !== void 0 && head.number >= maxBlockNumberBI) {
6796
+ logger.info({
6797
+ msg: `Head is greater than max block number`,
6798
+ collector,
6799
+ chainId: client.chain.id,
6800
+ block_number: head.number,
6801
+ max_block_number: maxBlockNumber
6802
+ });
6803
+ await dbTx.blocks.handleReorg({
6804
+ chainId: client.chain.id,
6805
+ blockNumber: maxBlockNumber,
6806
+ epoch: epoch + 1n
6807
+ });
6808
+ isMaxBlockNumberReached = true;
6809
+ return isMaxBlockNumberReached;
6810
+ }
6811
+ finalizedBlock = await fetchFinalizedBlock({
6812
+ tick,
6813
+ client,
6814
+ logger,
6815
+ collector,
6816
+ unfinalizedBlocks,
6817
+ previousFinalizedBlock: finalizedBlock
6818
+ });
6819
+ tick++;
6820
+ let { block: returnedBlock, didReorgHappened, unfinalizedBlocks: newUnfinalizedBlocks } = await reconcile({
6821
+ client,
6822
+ block: head,
6823
+ unfinalizedBlocks,
6824
+ finalizedBlock,
6825
+ logger,
6826
+ collector,
6827
+ maxBatchSize
6828
+ });
6829
+ unfinalizedBlocks = newUnfinalizedBlocks;
6830
+ const blockNumber = Number(returnedBlock.number);
6831
+ didReorgHappened = didReorgHappened || blockNumber < latestSavedBlockNumber;
6832
+ if (didReorgHappened) await dbTx.blocks.handleReorg({
6833
+ chainId: client.chain.id,
6834
+ blockNumber,
6835
+ epoch: epoch + 1n
6836
+ });
6837
+ else await dbTx.blocks.advanceChain({
6838
+ chainId: client.chain.id,
6839
+ blockNumber,
6840
+ epoch
6841
+ });
6842
+ });
6843
+ return isMaxBlockNumberReached;
6844
+ } };
6845
+ }
6846
+ const commonAncestor = (block, unfinalizedBlocks) => {
6847
+ const parent = unfinalizedBlocks.find((b) => b.hash === block.parentHash);
6848
+ if (parent) return parent;
6849
+ return null;
6850
+ };
6851
+ const fetchFinalizedBlock = async (parameters) => {
6852
+ let { tick, client, logger, collector, unfinalizedBlocks, previousFinalizedBlock } = parameters;
6853
+ let finalizedBlock = previousFinalizedBlock;
6854
+ if (tick % 20 === 0 || previousFinalizedBlock === null) {
6855
+ finalizedBlock = await client.getBlock({
6856
+ blockTag: "finalized",
6857
+ includeTransactions: false
6858
+ });
6859
+ if (finalizedBlock === null || finalizedBlock.number === null) {
6860
+ const msg = "Failed to get finalized block";
6861
+ logger.fatal({
6862
+ collector,
6863
+ chainId: client.chain.id,
6864
+ msg
6865
+ });
6866
+ throw new Error(msg);
6867
+ }
6868
+ unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number >= finalizedBlock.number);
6869
+ }
6870
+ if (finalizedBlock.number === null || finalizedBlock.hash === null || finalizedBlock.parentHash === null) {
6871
+ const msg = "Failed to get finalized block";
6872
+ logger.fatal({
6873
+ collector,
6874
+ chainId: client.chain.id,
6875
+ msg
6876
+ });
6877
+ throw new Error(msg);
6878
+ }
6879
+ return {
6880
+ hash: finalizedBlock.hash,
6881
+ number: finalizedBlock.number,
6882
+ parentHash: finalizedBlock.parentHash
6883
+ };
6884
+ };
6885
+ const reconcile = async (parameters) => {
6886
+ let { client, block, unfinalizedBlocks, finalizedBlock, logger, collector, maxBatchSize } = parameters;
6887
+ const chain = client.chain;
6888
+ if (block.hash === null || block.number === null || block.parentHash === null) throw new Error("Failed to get block");
6889
+ const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
6890
+ if (latestBlock === void 0) {
6891
+ const newBlock = {
6892
+ hash: block.hash,
6893
+ number: block.number,
6894
+ parentHash: block.parentHash
6895
+ };
6896
+ unfinalizedBlocks.push(newBlock);
6897
+ return {
6898
+ block: newBlock,
6899
+ didReorgHappened: false,
6900
+ unfinalizedBlocks
6901
+ };
6902
+ }
6903
+ if (latestBlock.hash === block.hash) return {
6904
+ block: latestBlock,
6905
+ didReorgHappened: false,
6906
+ unfinalizedBlocks
6907
+ };
6908
+ if (latestBlock.number >= block.number) {
6909
+ const ancestor = commonAncestor(block, unfinalizedBlocks) || finalizedBlock;
6910
+ logger.info({
6911
+ msg: `Reorg detected, latestBlock.number >= block.number`,
6912
+ collector,
6913
+ chain_id: chain.id,
6914
+ ancestor: ancestor.number,
6915
+ latest_block_number: latestBlock.number,
6916
+ block_number: block.number
6917
+ });
6918
+ unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number <= ancestor.number);
6919
+ return {
6920
+ block: ancestor,
6921
+ didReorgHappened: true,
6922
+ unfinalizedBlocks
6923
+ };
6924
+ }
6925
+ if (latestBlock.number + 1n < block.number) {
6926
+ logger.debug({
6927
+ collector,
6928
+ chain_id: chain.id,
6929
+ block_range: [latestBlock.number, block.number],
6930
+ msg: `Missing blocks`
6931
+ });
6932
+ const missingBlockNumbers = (() => {
6933
+ const missingBlockNumbers = [];
6934
+ let start = latestBlock.number + 1n;
6935
+ const threshold = latestBlock.number + BigInt(maxBatchSize) > block.number ? block.number : latestBlock.number + BigInt(maxBatchSize);
6936
+ while (start < threshold) {
6937
+ missingBlockNumbers.push(start);
6938
+ start = start + 1n;
6939
+ }
6940
+ return missingBlockNumbers;
6941
+ })();
6942
+ const missingBlocks = await Promise.all(missingBlockNumbers.map((blockNumber) => retry(async () => await client.getBlock({
6943
+ blockNumber,
6944
+ includeTransactions: false
6945
+ }))));
6946
+ for (const missingBlock of missingBlocks) {
6947
+ const { block: returnedBlock, didReorgHappened, unfinalizedBlocks: newUnfinalizedBlocks } = await reconcile({
6948
+ client,
6949
+ block: missingBlock,
6950
+ unfinalizedBlocks,
6951
+ finalizedBlock,
6952
+ logger,
6953
+ collector,
6954
+ maxBatchSize
6955
+ });
6956
+ if (returnedBlock.number !== missingBlock.number) return {
6957
+ block: returnedBlock,
6958
+ didReorgHappened,
6959
+ unfinalizedBlocks: newUnfinalizedBlocks
6960
+ };
6961
+ }
6962
+ return reconcile({
6963
+ client,
6964
+ block,
6965
+ unfinalizedBlocks,
6966
+ finalizedBlock,
6967
+ logger,
6968
+ collector,
6969
+ maxBatchSize
6970
+ });
6971
+ }
6972
+ if (block.parentHash !== latestBlock.hash) {
6973
+ const ancestor = commonAncestor(block, unfinalizedBlocks) || finalizedBlock;
6974
+ logger.info({
6975
+ msg: `Reorg detected, block parent hash !== latest block hash`,
6976
+ collector,
6977
+ chain_id: chain.id,
6978
+ ancestor: ancestor.number,
6979
+ latest_block_number: latestBlock.number,
6980
+ block_number: block.number
6981
+ });
6982
+ unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number <= ancestor.number);
6983
+ return {
6984
+ block: ancestor,
6985
+ didReorgHappened: true,
6986
+ unfinalizedBlocks
6987
+ };
6988
+ }
6989
+ const newBlock = {
6990
+ hash: block.hash,
6991
+ number: block.number,
6992
+ parentHash: block.parentHash
6993
+ };
6994
+ unfinalizedBlocks.push(newBlock);
6995
+ return {
6996
+ block: newBlock,
6997
+ didReorgHappened: false,
6998
+ unfinalizedBlocks
6999
+ };
7000
+ };
7001
+
7002
+ //#endregion
7003
+ //#region src/indexer/collectors/Collector.ts
7004
+ const names = [
7005
+ "offers",
7006
+ "consumed_events",
7007
+ "positions",
7008
+ "prices"
7009
+ ];
7010
+ function create$16({ name, collect, client, db, options }) {
7011
+ const admin = create$17({
7012
+ client,
7013
+ db,
7014
+ options
7015
+ });
7016
+ return {
7017
+ name,
7018
+ chain: client.chain,
7019
+ client,
7020
+ db,
7021
+ interval: options.interval ?? 1e4,
7022
+ collect: async function* () {
7023
+ const collector = name;
7024
+ const chain = client.chain;
7025
+ const logger = getLogger();
7026
+ const collectorId = `${client.chain.id.toString()}.collector.${collector}`;
7027
+ const tracer = getTracer(`router.${collectorId}`);
7028
+ logger.info({
7029
+ msg: `Collector started`,
7030
+ collector,
7031
+ chain_id: chain.id
7032
+ });
7033
+ let iterator = null;
7034
+ let lastBlockNumber;
7035
+ while (true) try {
7036
+ if (iterator === null) iterator = await startActiveSpan(tracer, `${collectorId}.init`, async () => {
7037
+ const { collector: collectorBlock } = await db.blocks.init({
7038
+ collectorName: name,
7039
+ chainId: chain.id
7040
+ });
7041
+ lastBlockNumber = collectorBlock.blockNumber;
7042
+ return collect({
7043
+ client,
7044
+ collector: name,
7045
+ epoch: collectorBlock.epoch,
7046
+ lastBlockNumber,
7047
+ db
7048
+ });
7049
+ });
7050
+ if (await startActiveSpan(tracer, `${collectorId}.syncBlock`, async (span) => {
7051
+ const isMaxBlockNumberReached = await admin.syncBlock();
7052
+ span.setAttribute("collector.is_max_block_reached", isMaxBlockNumberReached);
7053
+ return isMaxBlockNumberReached;
7054
+ }) && options.maxBlockNumber !== void 0 && lastBlockNumber !== void 0 && options.maxBlockNumber === lastBlockNumber) return;
7055
+ const { blockNumber, done } = await startActiveSpan(tracer, `${collectorId}.next`, async () => {
7056
+ if (iterator === null) throw new Error("Iterator is not initialized");
7057
+ const { value: blockNumber, done } = await iterator.next();
7058
+ return {
7059
+ blockNumber,
7060
+ done
7061
+ };
7062
+ });
7063
+ if (done) iterator = null;
7064
+ else {
7065
+ lastBlockNumber = blockNumber;
7066
+ yield blockNumber;
7067
+ }
7068
+ } catch (err) {
7069
+ const isError = err instanceof Error;
7070
+ logger.error({
7071
+ msg: "Collector error",
7072
+ collector,
7073
+ chain_id: chain.id,
7074
+ error: isError ? err.message : String(err),
7075
+ stack: isError ? err.stack : void 0
7076
+ });
7077
+ }
7078
+ }
7079
+ };
7080
+ }
7081
+ /** Start a collector with its own polling cadence based on chain head lag.
7082
+ * @param collector - The collector to start.
7083
+ * @returns A function to stop the collector.
7084
+ */
7085
+ function start(collector) {
7086
+ let stopped = false;
7087
+ const it = collector.collect();
7088
+ const logger = getLogger();
7089
+ const collectorId = `${collector.chain.id.toString()}.collector.${collector.name}`;
7090
+ const tracer = getTracer(`router.${collectorId}`);
7091
+ const blocks = collector.db.blocks;
7092
+ let initialized = false;
7093
+ (async () => {
7094
+ while (!stopped) try {
7095
+ await startActiveSpan(tracer, `${collectorId}.poll`, async () => {
7096
+ if (!initialized) {
7097
+ await blocks.init({
7098
+ chainId: collector.chain.id,
7099
+ collectorName: collector.name
7100
+ });
7101
+ initialized = true;
7102
+ }
7103
+ const [block, { blockNumber: collectorBlockNumber }] = await Promise.all([collector.client.getBlock({
7104
+ blockTag: "latest",
7105
+ includeTransactions: false
7106
+ }), blocks.getCollector({
7107
+ collectorName: collector.name,
7108
+ chainId: collector.chain.id
7109
+ })]);
7110
+ const delay = Number(block.number) > collectorBlockNumber + 10 ? 250 : collector.interval;
7111
+ const waitSpan = tracer.startSpan(`${collectorId}.wait`);
7112
+ try {
7113
+ await wait(delay);
7114
+ } finally {
7115
+ waitSpan.end();
7116
+ }
7117
+ await it.next();
7118
+ });
7119
+ } catch (err) {
7120
+ const error = err instanceof Error ? err : new Error(String(err));
7121
+ logger.error({
7122
+ msg: "Collector polling error",
7123
+ collector: collector.name,
7124
+ chain_id: collector.chain.id,
7125
+ err: error
7126
+ });
7127
+ }
7128
+ await it.return();
7129
+ })();
7130
+ return () => {
7131
+ stopped = true;
7132
+ };
7133
+ }
7134
+
7173
7135
  //#endregion
7174
7136
  //#region src/database/domains/Blocks.ts
7175
7137
  /** Postgres implementation. */
@@ -7601,35 +7563,15 @@ async function _getOffers(db, params) {
7601
7563
  AND LOWER(pc.contract) = LOWER(c.position_contract)
7602
7564
  AND LOWER(pc."user") = LOWER(c.position_user)
7603
7565
  ),
7604
- -- Compute contribution per callback in loan terms (with oracle price via LEFT JOIN)
7566
+ -- Compute contribution per callback in loan terms (loan token only collateral positions are not indexed)
7605
7567
  callback_loan_contribution AS (
7606
7568
  SELECT
7607
7569
  cc.*,
7608
7570
  CASE
7609
- -- No lot exists: contribution is 0
7610
7571
  WHEN cc.lot_lower IS NULL THEN 0
7611
- -- Loan token position: use lot_balance directly, apply callback limit
7612
- WHEN LOWER(cc.position_asset) = LOWER(cc.loan_token) THEN
7613
- LEAST(
7614
- cc.lot_balance,
7615
- COALESCE(cc.callback_amount::numeric, cc.lot_balance)
7616
- )
7617
- -- Collateral position: convert to loan using (amount * price / 10^36) * lltv / 10^18
7618
- ELSE
7619
- (
7620
- LEAST(
7621
- cc.lot_balance,
7622
- COALESCE(cc.callback_amount::numeric, cc.lot_balance)
7623
- ) * COALESCE(collat_oracle.price::numeric, 0) / 1e36
7624
- ) * COALESCE(collat_info.lltv::numeric, 0) / 1e18
7572
+ ELSE LEAST(cc.lot_balance, COALESCE(cc.callback_amount::numeric, cc.lot_balance))
7625
7573
  END AS contribution_in_loan
7626
7574
  FROM callback_contributions cc
7627
- LEFT JOIN ${obligationCollateralsV2} collat_info
7628
- ON collat_info.obligation_id = cc.obligation_id
7629
- AND LOWER(collat_info.asset) = LOWER(cc.position_asset)
7630
- LEFT JOIN ${oracles} collat_oracle
7631
- ON collat_oracle.chain_id = collat_info.oracle_chain_id
7632
- AND LOWER(collat_oracle.address) = LOWER(collat_info.oracle_address)
7633
7575
  ),
7634
7576
  -- Aggregate contributions per offer, deduplicating by position using DISTINCT ON
7635
7577
  offer_contributions AS (
@@ -7666,6 +7608,22 @@ async function _getOffers(db, params) {
7666
7608
  GROUP BY hash, obligation_id, assets, price, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
7667
7609
  callback_address, callback_data, block_number, group_chain_id, group_maker,
7668
7610
  consumed, chain_id, loan_token, session
7611
+ UNION ALL
7612
+ -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
7613
+ SELECT
7614
+ p.hash, p.obligation_id, p.assets, p.price,
7615
+ p.obligation_units, p.obligation_shares,
7616
+ p.maturity, p.expiry, p.start, p.group_group,
7617
+ p.buy, p.callback_address, p.callback_data,
7618
+ p.block_number, p.group_chain_id, p.group_maker,
7619
+ p.consumed, p.chain_id, p.loan_token, p.session,
7620
+ 0 AS total_available
7621
+ FROM paged p
7622
+ WHERE p.buy = false
7623
+ AND NOT EXISTS (
7624
+ SELECT 1 FROM ${offersCallbacks} oc2
7625
+ WHERE oc2.offer_hash = p.hash
7626
+ )
7669
7627
  )
7670
7628
  -- Final SELECT with inline takeable computation
7671
7629
  SELECT
@@ -7688,18 +7646,24 @@ async function _getOffers(db, params) {
7688
7646
  oc.block_number,
7689
7647
  oc.session,
7690
7648
  COALESCE(oc.total_available, 0) AS available,
7691
- -- takeable = min(assets - consumed, total_available)
7692
- GREATEST(0, LEAST(
7693
- oc.assets::numeric - oc.consumed::numeric,
7694
- COALESCE(oc.total_available, 0)
7695
- )) AS takeable,
7649
+ -- takeable: sell offers use assets - consumed directly (collateral positions not indexed yet)
7650
+ CASE WHEN oc.buy = false
7651
+ THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
7652
+ ELSE GREATEST(0, LEAST(
7653
+ oc.assets::numeric - oc.consumed::numeric,
7654
+ COALESCE(oc.total_available, 0)
7655
+ ))
7656
+ END AS takeable,
7696
7657
  c.collaterals
7697
7658
  FROM offer_contributions oc
7698
7659
  LEFT JOIN collats c ON c.obligation_id = oc.obligation_id
7699
- WHERE GREATEST(0, LEAST(
7700
- oc.assets::numeric - oc.consumed::numeric,
7701
- COALESCE(oc.total_available, 0)
7702
- )) > 0
7660
+ WHERE CASE WHEN oc.buy = false
7661
+ THEN GREATEST(0, oc.assets::numeric - oc.consumed::numeric)
7662
+ ELSE GREATEST(0, LEAST(
7663
+ oc.assets::numeric - oc.consumed::numeric,
7664
+ COALESCE(oc.total_available, 0)
7665
+ ))
7666
+ END > 0
7703
7667
  ORDER BY
7704
7668
  oc.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
7705
7669
  oc.block_number ASC,
@@ -8266,9 +8230,9 @@ function create$6(db) {
8266
8230
  blockNumber: r.blockNumber
8267
8231
  }));
8268
8232
  },
8269
- upsert: async (oracles$1) => {
8270
- if (oracles$1.length === 0) return;
8271
- const rows = oracles$1.map((o) => ({
8233
+ upsert: async (oracles$2) => {
8234
+ if (oracles$2.length === 0) return;
8235
+ const rows = oracles$2.map((o) => ({
8272
8236
  chainId: o.chainId,
8273
8237
  address: o.address.toLowerCase(),
8274
8238
  price: o.price !== null ? o.price.toString() : null,
@@ -10073,6 +10037,7 @@ async function getOffersQuery(db, parameters) {
10073
10037
  if (cursor !== null && cursor !== void 0) {
10074
10038
  if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
10075
10039
  }
10040
+ const now = Math.floor((Date.now() - 1) / 1e3);
10076
10041
  const collateralsLateral = db.select({ collaterals: sql`COALESCE(
10077
10042
  jsonb_agg(
10078
10043
  jsonb_build_object(
@@ -10086,27 +10051,14 @@ async function getOffersQuery(db, parameters) {
10086
10051
  AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
10087
10052
  const availableLateral = db.select({ available: sql`COALESCE(SUM(
10088
10053
  CASE
10089
- -- If asset is null, position available is 0
10090
10054
  WHEN ${positions.asset} IS NULL THEN 0
10091
-
10092
- -- Position asset matches loan token: no conversion needed
10093
- WHEN ${positions.asset} = ${obligations.loanToken} THEN
10055
+ ELSE
10094
10056
  CASE
10095
10057
  WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
10096
10058
  ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
10097
10059
  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
10060
  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");
10061
+ ), 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))).where(eq(offersCallbacks.offerHash, offers.hash)).as("available_lateral");
10110
10062
  const rows = (await db.select({
10111
10063
  hash: offers.hash,
10112
10064
  maker: offers.groupMaker,
@@ -10128,17 +10080,24 @@ async function getOffersQuery(db, parameters) {
10128
10080
  collaterals: collateralsLateral.collaterals,
10129
10081
  blockNumber: offers.blockNumber,
10130
10082
  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
- )
10083
+ takeable: sql`FLOOR(GREATEST(0,
10084
+ CASE WHEN ${offers.buy} = false
10085
+ THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
10086
+ ELSE LEAST(
10087
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
10088
+ COALESCE(${availableLateral.available}::numeric, 0)
10089
+ )
10090
+ END
10137
10091
  ))`.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) => {
10092
+ }).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, gte(offers.expiry, now), gte(offers.maturity, now), maker === void 0 ? sql`GREATEST(0,
10093
+ CASE WHEN ${offers.buy} = false
10094
+ THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
10095
+ ELSE LEAST(
10096
+ ${offers.assets}::numeric - ${groups.consumed}::numeric,
10097
+ COALESCE(${availableLateral.available}::numeric, 0)
10098
+ )
10099
+ END
10100
+ ) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
10142
10101
  return {
10143
10102
  hash: row.hash,
10144
10103
  maker: row.maker,
@@ -10248,35 +10207,6 @@ async function getUserPositions(queryParameters, db) {
10248
10207
  }
10249
10208
  }
10250
10209
 
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
-
10280
10210
  //#endregion
10281
10211
  //#region src/api/Api.ts
10282
10212
  function from(config) {
@@ -10346,24 +10276,9 @@ function serve$1(parameters) {
10346
10276
  const { statusCode, body } = await gatekeeper.validate(reqBody);
10347
10277
  return c.json(body, statusCode);
10348
10278
  } catch (err) {
10349
- const failure$1 = failure(err);
10350
- return c.json(failure$1.body, failure$1.statusCode);
10351
- }
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"));
10279
+ const failure$2 = failure(err);
10363
10280
  return c.json(failure$2.body, failure$2.statusCode);
10364
10281
  }
10365
- const { statusCode, body: responseBody } = await resolveCallbackTypes$1(body, chainRegistry.list());
10366
- return c.json(responseBody, statusCode);
10367
10282
  });
10368
10283
  app.get("/v1/users/:userAddress/positions", async (c) => {
10369
10284
  const query = c.req.query();
@@ -10395,8 +10310,8 @@ function serve$1(parameters) {
10395
10310
  const { statusCode, body } = await gatekeeper.getConfigRules(c.req.query());
10396
10311
  return c.json(body, statusCode);
10397
10312
  } catch (err) {
10398
- const failure$4 = failure(err);
10399
- return c.json(failure$4.body, failure$4.statusCode);
10313
+ const failure$1 = failure(err);
10314
+ return c.json(failure$1.body, failure$1.statusCode);
10400
10315
  }
10401
10316
  });
10402
10317
  app.get("/docs/openapi", async (c) => c.text(JSON.stringify(await getSwaggerJson()), 200, { "Content-Type": "application/json; charset=utf-8" }));
@@ -10487,23 +10402,11 @@ function createHttpClient(config) {
10487
10402
  issues: []
10488
10403
  };
10489
10404
  };
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
10405
  return {
10502
10406
  baseUrl,
10503
10407
  validate,
10504
10408
  getConfigRules,
10505
- isAllowed,
10506
- getCallbackTypes
10409
+ isAllowed
10507
10410
  };
10508
10411
  }
10509
10412
  function mergeHeaders(base, extra) {
@@ -10637,15 +10540,33 @@ async function seedMockOffers(parameters) {
10637
10540
  const { db, gatekeeper, chainRegistry, count } = parameters;
10638
10541
  if (count <= 0) return [];
10639
10542
  const configuredChains = chainRegistry.list();
10640
- const tokens = {
10641
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": 6,
10642
- "0x6B175474E89094C44Da98b954EedeAC495271d0F": 18,
10643
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": 18,
10644
- "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": 8,
10543
+ const assetsDecimals = {
10544
+ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": 6,
10545
+ "0x6b175474e89094c44da98b954eedeac495271d0f": 18,
10546
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": 18,
10547
+ "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": 8,
10548
+ "0x1abaea1f7c830bd89acc67ec4af516284b1bc33c": 6,
10549
+ "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0": 18,
10550
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
10551
+ "0x50c5725949a6f0c72e6c4a641f24049a917db0cb": 18,
10645
10552
  "0x4200000000000000000000000000000000000006": 18,
10646
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": 6,
10647
- "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": 18
10553
+ "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": 8,
10554
+ "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": 18,
10555
+ "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42": 6,
10556
+ "0xce79ddb3152d52ff8fe65a4c7e058b035fcb560a": 18
10648
10557
  };
10558
+ const assetsByChainId = {};
10559
+ const oraclesByChainId = {};
10560
+ for (const chain of configuredChains) {
10561
+ const assets$1 = assets[chain.id.toString()]?.map((asset) => asset.toLowerCase()) ?? [];
10562
+ if (assets$1.length === 0) throw new Error(`No gatekeeper assets configured for chain ${chain.id}`);
10563
+ const missingDecimals = assets$1.filter((asset) => assetsDecimals[asset] === void 0);
10564
+ if (missingDecimals.length > 0) throw new Error(`Missing decimals for assets on chain ${chain.id}: ${missingDecimals.join(", ")}`);
10565
+ assetsByChainId[chain.id] = assets$1;
10566
+ const oracles = oracles$1[chain.id.toString()]?.map((oracle) => oracle.toLowerCase()) ?? [];
10567
+ if (oracles.length === 0) throw new Error(`No gatekeeper oracles configured for chain ${chain.id}`);
10568
+ oraclesByChainId[chain.id] = oracles;
10569
+ }
10649
10570
  const now = Math.floor(Date.now() / 1e3);
10650
10571
  const offerBlockNumber = 1;
10651
10572
  const positionBlockNumber = offerBlockNumber + 1;
@@ -10656,8 +10577,9 @@ async function seedMockOffers(parameters) {
10656
10577
  const offers = createMockOffers({
10657
10578
  count,
10658
10579
  chains: configuredChains,
10659
- loanTokens: Object.keys(tokens).map((key) => key),
10660
- assetsDecimals: tokens,
10580
+ assetsByChainId,
10581
+ oraclesByChainId,
10582
+ assetsDecimals,
10661
10583
  start,
10662
10584
  expiry,
10663
10585
  maturity,
@@ -10683,43 +10605,54 @@ async function getOffersFromFile(filePath) {
10683
10605
  return Array.isArray(data) ? data.map(from$12) : [from$12(data)];
10684
10606
  }
10685
10607
  function createMockOffers(parameters) {
10686
- const { count, chains, loanTokens, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
10608
+ const { count, chains, assetsByChainId, oraclesByChainId, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
10609
+ if (chains.length === 0) throw new Error("No chains configured for mock offers");
10687
10610
  return Array.from({ length: count }, () => {
10688
10611
  const buy = Math.random() < .5;
10612
+ const chain = chains[Math.floor(Math.random() * chains.length)];
10613
+ const allowedAssets = assetsByChainId[chain.id] ?? [];
10614
+ if (allowedAssets.length === 0) throw new Error(`No allowed assets configured for chain ${chain.id}`);
10615
+ const loanToken = allowedAssets[Math.floor(Math.random() * allowedAssets.length)];
10689
10616
  const offer = random({
10690
- chains,
10691
- loanTokens,
10617
+ chains: [chain],
10618
+ loanTokens: [loanToken],
10692
10619
  assetsDecimals,
10693
10620
  start,
10694
10621
  expiry,
10695
10622
  maturity,
10696
10623
  buy
10697
10624
  });
10625
+ const allowedOracles = oraclesByChainId[chain.id] ?? [];
10626
+ if (allowedOracles.length === 0) throw new Error(`No allowed oracles configured for chain ${chain.id}`);
10627
+ const collateralAssets = allowedAssets.filter((asset) => asset !== loanToken);
10628
+ if (collateralAssets.length === 0) throw new Error(`No collateral assets available for chain ${chain.id}`);
10629
+ const collateralCount = Math.min(collateralAssets.length, 1 + Math.floor(Math.random() * 3));
10630
+ const collateralPool = [...collateralAssets];
10631
+ const collateralSelections = [];
10632
+ while (collateralSelections.length < collateralCount && collateralPool.length > 0) {
10633
+ const index = Math.floor(Math.random() * collateralPool.length);
10634
+ const [value] = collateralPool.splice(index, 1);
10635
+ if (value !== void 0) collateralSelections.push(value);
10636
+ }
10637
+ collateralSelections.sort();
10638
+ const lltv = offer.collaterals[0]?.lltv ?? .86;
10639
+ const collaterals = collateralSelections.map((asset) => from$14({
10640
+ asset,
10641
+ oracle: allowedOracles[Math.floor(Math.random() * allowedOracles.length)],
10642
+ lltv
10643
+ }));
10698
10644
  if (!chainRegistry.getById(offer.chainId)) throw new Error(`Missing chain config for id ${offer.chainId}`);
10699
- const callbackType = buy ? Type$1.BuyVaultV1Callback : Type$1.SellERC20Callback;
10700
- const callbackAddress = buy ? BUY_CALLBACK_ADDRESS : SELL_CALLBACK_ADDRESS;
10701
- const callbackData = buildMockCallbackData(callbackType, offer);
10702
10645
  return from$12({
10703
10646
  ...offer,
10647
+ loanToken,
10648
+ collaterals,
10704
10649
  callback: {
10705
- address: callbackAddress,
10706
- data: callbackData
10650
+ address: zeroAddress,
10651
+ data: "0x"
10707
10652
  }
10708
10653
  });
10709
10654
  });
10710
10655
  }
10711
- function buildMockCallbackData(callbackType, offer) {
10712
- const assets = offer.collaterals.map((collateral) => collateral.asset);
10713
- const amounts = offer.collaterals.map(() => offer.assets);
10714
- if (callbackType === Type$1.BuyVaultV1Callback) return encodeBuyVaultV1Callback({
10715
- vaults: assets,
10716
- amounts
10717
- });
10718
- return encodeSellERC20Callback({
10719
- collaterals: assets,
10720
- amounts
10721
- });
10722
- }
10723
10656
  function seedMockOracles(offers, price, blockNumber) {
10724
10657
  const oracleMap = /* @__PURE__ */ new Map();
10725
10658
  for (const offer of offers) for (const collateral of offer.collaterals) {
@@ -10734,8 +10667,6 @@ function seedMockOracles(offers, price, blockNumber) {
10734
10667
  }
10735
10668
  return Array.from(oracleMap.values());
10736
10669
  }
10737
- const BUY_CALLBACK_ADDRESS = "0x3333333333333333333333333333333333333333";
10738
- const SELL_CALLBACK_ADDRESS = "0x1111111111111111111111111111111111111111";
10739
10670
  async function seedOffers(parameters) {
10740
10671
  const { db, offers, blockNumber } = parameters;
10741
10672
  if (offers.length === 0) return;
@@ -10752,14 +10683,10 @@ async function seedOffers(parameters) {
10752
10683
  }]);
10753
10684
  }
10754
10685
  async function seedOfferAssociations(parameters) {
10755
- const { db, gatekeeper, offers, blockNumber } = parameters;
10686
+ const { db, offers, blockNumber } = parameters;
10756
10687
  if (offers.length === 0) return;
10757
10688
  const { callbacks, positions, lots } = buildOfferAssociationsFromOffers({
10758
10689
  offers,
10759
- callbackTypes: await resolveCallbackTypes({
10760
- gatekeeper,
10761
- offers
10762
- }),
10763
10690
  blockNumber
10764
10691
  });
10765
10692
  if (positions.length > 0) await db.positions.upsert(positions);
@@ -10802,83 +10729,40 @@ function buildOfferDependencies(parameters) {
10802
10729
  groups: Array.from(groupsByKey.values())
10803
10730
  };
10804
10731
  }
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
10732
  function buildOfferAssociationsFromOffers(parameters) {
10835
- const { offers, callbackTypes, blockNumber } = parameters;
10733
+ const { offers, blockNumber } = parameters;
10836
10734
  const callbacks = [];
10837
10735
  const positions = [];
10838
10736
  const lots = [];
10839
10737
  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;
10738
+ if (!offer.buy) continue;
10739
+ const loanToken = offer.loanToken.toLowerCase();
10740
+ const offerHash = hash(offer);
10741
+ positions.push(from$10({
10742
+ chainId: offer.chainId,
10743
+ contract: loanToken,
10744
+ user: offer.maker,
10745
+ type: Type.ERC20,
10746
+ asset: loanToken,
10747
+ balance: offer.assets,
10748
+ blockNumber
10749
+ }));
10852
10750
  callbacks.push({
10853
- offerHash: hash(offer),
10854
- callbacks: decoded.map((callback) => ({
10751
+ offerHash,
10752
+ callbacks: [{
10855
10753
  chainId: offer.chainId,
10856
- contract: callback.contract,
10754
+ contract: loanToken,
10857
10755
  user: offer.maker,
10858
- amount: callback.amount
10859
- }))
10756
+ amount: offer.assets
10757
+ }]
10758
+ });
10759
+ lots.push({
10760
+ positionChainId: offer.chainId,
10761
+ positionContract: loanToken,
10762
+ positionUser: offer.maker,
10763
+ group: offer.group,
10764
+ size: offer.assets
10860
10765
  });
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
10766
  }
10883
10767
  return {
10884
10768
  callbacks,