@morpho-dev/router 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/cli.js +441 -141
  2. package/dist/drizzle/migrations/0000_setup_single_migration_folder.sql +64 -64
  3. package/dist/drizzle/migrations/0001_add-trigger-for-consumed-events.sql +5 -5
  4. package/dist/drizzle/migrations/0002_insert-status-code.sql +1 -1
  5. package/dist/drizzle/migrations/0003_update-triggers-for-consumed-events.sql +1 -1
  6. package/dist/drizzle/migrations/0004_drop-status-offers-foreign-key-constraint.sql +1 -1
  7. package/dist/drizzle/migrations/0005_add-index-to-boost-group-query-and-offer-hash.sql +1 -1
  8. package/dist/drizzle/migrations/0006_add-callbacks-and-positions-relations.sql +11 -11
  9. package/dist/drizzle/migrations/0008_validation.sql +10 -10
  10. package/dist/drizzle/migrations/0009_add-transfers-table.sql +4 -4
  11. package/dist/drizzle/migrations/0010_add-price.sql +1 -1
  12. package/dist/drizzle/migrations/0011_nullable-callback-amount.sql +1 -1
  13. package/dist/drizzle/migrations/0012_add-position-asset.sql +1 -1
  14. package/dist/drizzle/migrations/0013_remove-depecrated-domains.sql +13 -13
  15. package/dist/drizzle/migrations/0014_rename-offers-v2-into-offers.sql +19 -19
  16. package/dist/drizzle/migrations/0015_add-lots-table.sql +3 -3
  17. package/dist/drizzle/migrations/0016_merkle-metadata.sql +7 -7
  18. package/dist/drizzle/migrations/0017_dusty_the_hunter.sql +1 -1
  19. package/dist/drizzle/migrations/0018_add_chain_collector_constraints.sql +3 -3
  20. package/dist/drizzle/migrations/0019_add-obligation-units-shares.sql +2 -2
  21. package/dist/drizzle/migrations/0020_add-session.sql +1 -1
  22. package/dist/drizzle/migrations/0021_drop_chain_collector_epoch_indexes.sql +2 -2
  23. package/dist/drizzle/migrations/0021_migrate-rate-to-price.sql +6 -6
  24. package/dist/drizzle/migrations/0022_consolidate-price.sql +5 -5
  25. package/dist/drizzle/migrations/0023_remove-block-number-for-collaterals.sql +1 -1
  26. package/dist/drizzle/migrations/0024_add-obligation-id-to-lots.sql +8 -0
  27. package/dist/drizzle/migrations/0025_rename-price-to-tick.sql +202 -0
  28. package/dist/drizzle/migrations/meta/0000_snapshot.json +48 -48
  29. package/dist/drizzle/migrations/meta/0001_snapshot.json +48 -48
  30. package/dist/drizzle/migrations/meta/0002_snapshot.json +48 -48
  31. package/dist/drizzle/migrations/meta/0003_snapshot.json +48 -48
  32. package/dist/drizzle/migrations/meta/0004_snapshot.json +47 -47
  33. package/dist/drizzle/migrations/meta/0005_snapshot.json +47 -47
  34. package/dist/drizzle/migrations/meta/0006_snapshot.json +61 -61
  35. package/dist/drizzle/migrations/meta/0008_snapshot.json +62 -62
  36. package/dist/drizzle/migrations/meta/0009_snapshot.json +66 -66
  37. package/dist/drizzle/migrations/meta/0010_snapshot.json +66 -66
  38. package/dist/drizzle/migrations/meta/0013_snapshot.json +48 -48
  39. package/dist/drizzle/migrations/meta/0014_snapshot.json +48 -48
  40. package/dist/drizzle/migrations/meta/0015_snapshot.json +52 -52
  41. package/dist/drizzle/migrations/meta/0016_snapshot.json +61 -61
  42. package/dist/drizzle/migrations/meta/0017_snapshot.json +61 -61
  43. package/dist/drizzle/migrations/meta/0018_snapshot.json +62 -62
  44. package/dist/drizzle/migrations/meta/0019_snapshot.json +62 -62
  45. package/dist/drizzle/migrations/meta/0023_snapshot.json +62 -62
  46. package/dist/drizzle/migrations/meta/0024_snapshot.json +1448 -0
  47. package/dist/drizzle/migrations/meta/0025_snapshot.json +1448 -0
  48. package/dist/drizzle/migrations/meta/_journal.json +14 -0
  49. package/dist/evm/bytecode/erc20.txt +1 -1
  50. package/dist/evm/bytecode/morpho.txt +1 -1
  51. package/dist/evm/bytecode/multicall3.txt +1 -1
  52. package/dist/evm/bytecode/oracle.txt +1 -1
  53. package/dist/evm/bytecode/vault.txt +1 -1
  54. package/dist/index.browser.d.mts +1157 -81
  55. package/dist/index.browser.d.mts.map +1 -1
  56. package/dist/index.browser.d.ts +1157 -81
  57. package/dist/index.browser.d.ts.map +1 -1
  58. package/dist/index.browser.js +371 -151
  59. package/dist/index.browser.js.map +1 -1
  60. package/dist/index.browser.mjs +366 -152
  61. package/dist/index.browser.mjs.map +1 -1
  62. package/dist/index.node.d.mts +1196 -70
  63. package/dist/index.node.d.mts.map +1 -1
  64. package/dist/index.node.d.ts +1196 -70
  65. package/dist/index.node.d.ts.map +1 -1
  66. package/dist/index.node.js +491 -145
  67. package/dist/index.node.js.map +1 -1
  68. package/dist/index.node.mjs +486 -146
  69. package/dist/index.node.mjs.map +1 -1
  70. package/package.json +1 -1
@@ -949,6 +949,61 @@ const MetaMorpho = parseAbi([
949
949
  //#region src/core/Abi/MetaMorphoFactory.ts
950
950
  const MetaMorphoFactory = parseAbi(["event CreateMetaMorpho(address indexed metaMorpho,address indexed caller,address initialOwner,uint256 initialTimelock,address indexed asset,string name,string symbol,bytes32 salt)", "function isMetaMorpho(address) view returns (bool)"]);
951
951
 
952
+ //#endregion
953
+ //#region src/core/Abi/MorphoV2.ts
954
+ const MorphoV2 = parseAbi([
955
+ "constructor()",
956
+ "function collateralOf(bytes32 id, address user, address collateralToken) view returns (uint256)",
957
+ "function consume(bytes32 group, uint256 amount)",
958
+ "function consumed(address user, bytes32 group) view returns (uint256)",
959
+ "function debtOf(bytes32 id, address user) view returns (uint256)",
960
+ "function defaultFees(address loanToken, uint256 index) view returns (uint16)",
961
+ "function feeSetter() view returns (address)",
962
+ "function fees(bytes32 id) view returns (uint16[6])",
963
+ "function flashLoan(address token, uint256 assets, address callback, bytes data)",
964
+ "function isHealthy((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, bytes32 id, address borrower) view returns (bool)",
965
+ "function liquidate((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, (uint256 collateralIndex, uint256 repaid, uint256 seized)[] seizures, address borrower, bytes data) returns ((uint256 collateralIndex, uint256 repaid, uint256 seized)[])",
966
+ "function multicall(bytes[] calls)",
967
+ "function obligationCreated(bytes32 id) view returns (bool)",
968
+ "function obligationState(bytes32 id) view returns (uint128 totalUnits, uint128 totalShares, uint256 withdrawable, bool created)",
969
+ "function owner() view returns (address)",
970
+ "function repay((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, uint256 obligationUnits, address onBehalf)",
971
+ "function session(address user) view returns (bytes32)",
972
+ "function setDefaultTradingFee(address loanToken, uint256 index, uint256 newTradingFee)",
973
+ "function setFeeSetter(address newFeeSetter)",
974
+ "function setObligationTradingFee(bytes32 id, uint256 index, uint256 newTradingFee)",
975
+ "function setOwner(address newOwner)",
976
+ "function setTradingFeeRecipient(address recipient)",
977
+ "function sharesOf(bytes32 id, address user) view returns (uint256)",
978
+ "function shuffleSession()",
979
+ "function supplyCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, address collateral, uint256 assets, address onBehalf)",
980
+ "function take(uint256 buyerAssets, uint256 sellerAssets, uint256 obligationUnits, uint256 obligationShares, address taker, ((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, bool buy, address maker, uint256 assets, uint256 obligationUnits, uint256 obligationShares, uint256 start, uint256 expiry, uint256 tick, bytes32 group, bytes32 session, address callback, bytes callbackData) offer, (uint8 v, bytes32 r, bytes32 s) sig, bytes32 root, bytes32[] proof, address takerCallback, bytes takerCallbackData) returns (uint256, uint256, uint256, uint256)",
981
+ "function totalShares(bytes32 id) view returns (uint256)",
982
+ "function totalUnits(bytes32 id) view returns (uint256)",
983
+ "function touchObligation((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation) returns (bytes32)",
984
+ "function tradingFee(bytes32 id, uint256 timeToMaturity) view returns (uint256)",
985
+ "function tradingFeeRecipient() view returns (address)",
986
+ "function withdraw((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, uint256 obligationUnits, uint256 shares, address onBehalf) returns (uint256, uint256)",
987
+ "function withdrawCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, address collateral, uint256 assets, address onBehalf)",
988
+ "function withdrawable(bytes32 id) view returns (uint256)",
989
+ "event Constructor(address indexed owner)",
990
+ "event Consume(address indexed user, bytes32 indexed group, uint256 amount)",
991
+ "event FlashLoan(address indexed caller, address indexed token, uint256 assets)",
992
+ "event Liquidate(address indexed caller, bytes32 indexed id, (uint256 collateralIndex, uint256 repaid, uint256 seized)[] seizures, address indexed borrower, uint256 totalRepaid, uint256 badDebt)",
993
+ "event ObligationCreated(bytes32 indexed id, (address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation)",
994
+ "event Repay(address indexed caller, bytes32 indexed id, uint256 obligationUnits, address indexed onBehalf)",
995
+ "event SetDefaultTradingFee(address indexed loanToken, uint256 indexed index, uint256 newTradingFee)",
996
+ "event SetFeeSetter(address indexed feeSetter)",
997
+ "event SetObligationTradingFee(bytes32 indexed id, uint256 indexed index, uint256 newTradingFee)",
998
+ "event SetOwner(address indexed owner)",
999
+ "event SetTradingFeeRecipient(address indexed recipient)",
1000
+ "event ShuffleSession(address indexed user, bytes32 session)",
1001
+ "event SupplyCollateral(address caller, bytes32 indexed id, address indexed collateral, uint256 assets, address indexed onBehalf)",
1002
+ "event Take(address caller, bytes32 indexed id, address indexed maker, address indexed taker, bool offerIsBuy, uint256 buyerAssets, uint256 sellerAssets, uint256 obligationUnits, uint256 obligationShares, bool buyerIsLender, bool sellerIsBorrower, bytes32 group, uint256 consumed)",
1003
+ "event Withdraw(address indexed caller, bytes32 indexed id, uint256 obligationUnits, uint256 shares, address indexed onBehalf)",
1004
+ "event WithdrawCollateral(address caller, bytes32 indexed id, address indexed collateral, uint256 assets, address indexed onBehalf)"
1005
+ ]);
1006
+
952
1007
  //#endregion
953
1008
  //#region src/core/Abi/index.ts
954
1009
  var Abi_exports = /* @__PURE__ */ __exportAll({
@@ -956,6 +1011,7 @@ var Abi_exports = /* @__PURE__ */ __exportAll({
956
1011
  MetaMorpho: () => MetaMorpho,
957
1012
  MetaMorphoFactory: () => MetaMorphoFactory,
958
1013
  Morpho: () => Morpho,
1014
+ MorphoV2: () => MorphoV2,
959
1015
  Oracle: () => Oracle
960
1016
  });
961
1017
  const Oracle = [{
@@ -1217,8 +1273,8 @@ const chains$2 = {
1217
1273
  name: "ethereum-virtual-testnet",
1218
1274
  custom: {
1219
1275
  morpho: {
1220
- address: "0x11a002d45db720ed47a80d2f3489cba5b833eaf5",
1221
- blockCreated: 0
1276
+ address: "0x634b095371e4e45feed94c1a45c37798e173ea50",
1277
+ blockCreated: 23226700
1222
1278
  },
1223
1279
  morphoBlue: {
1224
1280
  address: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
@@ -2024,7 +2080,7 @@ const OfferSchema = () => {
2024
2080
  assets: z$1.bigint({ coerce: true }).min(0n).max(maxUint256),
2025
2081
  obligationUnits: z$1.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2026
2082
  obligationShares: z$1.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2027
- price: z$1.bigint({ coerce: true }).min(0n).max(maxUint256),
2083
+ tick: z$1.coerce.number().int().min(0).max(990),
2028
2084
  maturity: MaturitySchema,
2029
2085
  expiry: z$1.number().int().max(Number.MAX_SAFE_INTEGER),
2030
2086
  start: z$1.number().int().max(Number.MAX_SAFE_INTEGER),
@@ -2096,7 +2152,7 @@ const serialize = (offer) => ({
2096
2152
  assets: offer.assets.toString(),
2097
2153
  obligationUnits: offer.obligationUnits.toString(),
2098
2154
  obligationShares: offer.obligationShares.toString(),
2099
- price: offer.price.toString(),
2155
+ tick: offer.tick,
2100
2156
  maturity: Number(offer.maturity),
2101
2157
  expiry: Number(offer.expiry),
2102
2158
  start: Number(offer.start),
@@ -2141,14 +2197,13 @@ function random$1(config) {
2141
2197
  [.98, 2]
2142
2198
  ]));
2143
2199
  const buy = config?.buy !== void 0 ? config.buy : bool();
2144
- const ONE = 1000000000000000000n;
2145
- const qMin = buy ? 16 : 4;
2146
- const len = (buy ? 32 : 16) - qMin + 1;
2147
- const pricePairs = Array.from({ length: len }, (_, idx) => {
2148
- const q = qMin + idx;
2149
- return [BigInt(q) * (ONE / 4n), buy ? 1 + idx : 1 + (len - 1 - idx)];
2200
+ const tickMin = buy ? 0 : 495;
2201
+ const len = (buy ? 495 : 990) - tickMin + 1;
2202
+ const tickPairs = Array.from({ length: len }, (_, idx) => {
2203
+ const weight = buy ? 1 + idx : 1 + (len - 1 - idx);
2204
+ return [tickMin + idx, weight];
2150
2205
  });
2151
- const price = config?.price ?? weightedChoice(pricePairs);
2206
+ const tick = config?.tick ?? weightedChoice(tickPairs);
2152
2207
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
2153
2208
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2154
2209
  const amountBase = BigInt(100 + int(999901));
@@ -2162,7 +2217,7 @@ function random$1(config) {
2162
2217
  assets: assetsScaled,
2163
2218
  obligationUnits: config?.obligationUnits ?? 0n,
2164
2219
  obligationShares: config?.obligationShares ?? 0n,
2165
- price,
2220
+ tick,
2166
2221
  maturity,
2167
2222
  expiry: config?.expiry ?? maturity - 1,
2168
2223
  start: config?.start ?? maturity - 10,
@@ -2227,7 +2282,7 @@ const types = {
2227
2282
  type: "uint256"
2228
2283
  },
2229
2284
  {
2230
- name: "price",
2285
+ name: "tick",
2231
2286
  type: "uint256"
2232
2287
  },
2233
2288
  {
@@ -2295,7 +2350,7 @@ function hash(offer) {
2295
2350
  assets: offer.assets,
2296
2351
  obligationUnits: offer.obligationUnits,
2297
2352
  obligationShares: offer.obligationShares,
2298
- price: offer.price,
2353
+ tick: BigInt(offer.tick),
2299
2354
  maturity: BigInt(offer.maturity),
2300
2355
  expiry: BigInt(offer.expiry),
2301
2356
  group: offer.group,
@@ -2346,7 +2401,7 @@ const OfferAbi = [
2346
2401
  type: "uint256"
2347
2402
  },
2348
2403
  {
2349
- name: "price",
2404
+ name: "tick",
2350
2405
  type: "uint256"
2351
2406
  },
2352
2407
  {
@@ -2417,7 +2472,7 @@ function encode$1(offer) {
2417
2472
  offer.assets,
2418
2473
  offer.obligationUnits,
2419
2474
  offer.obligationShares,
2420
- offer.price,
2475
+ BigInt(offer.tick),
2421
2476
  BigInt(offer.maturity),
2422
2477
  BigInt(offer.expiry),
2423
2478
  offer.group,
@@ -2442,7 +2497,7 @@ function decode$1(data) {
2442
2497
  assets: decoded[1],
2443
2498
  obligationUnits: decoded[2],
2444
2499
  obligationShares: decoded[3],
2445
- price: decoded[4],
2500
+ tick: Number(decoded[4]),
2446
2501
  maturity: from$16(Number(decoded[5])),
2447
2502
  expiry: Number(decoded[6]),
2448
2503
  group: decoded[7],
@@ -2792,6 +2847,85 @@ var InvalidQuoteError = class extends BaseError {
2792
2847
  }
2793
2848
  };
2794
2849
 
2850
+ //#endregion
2851
+ //#region src/core/Tick.ts
2852
+ var Tick_exports = /* @__PURE__ */ __exportAll({
2853
+ InvalidPriceError: () => InvalidPriceError,
2854
+ InvalidTickError: () => InvalidTickError,
2855
+ MAX_PRICE: () => MAX_PRICE,
2856
+ TICK_RANGE: () => TICK_RANGE,
2857
+ priceToTick: () => priceToTick,
2858
+ tickToPrice: () => tickToPrice
2859
+ });
2860
+ /** ln(1 + 0.025), scaled by 1e18. Matches TickLib onchain constant. */
2861
+ const LN_ONE_PLUS_DELTA = 24692612590371501n;
2862
+ /** ln(2), scaled by 1e18. Matches TickLib onchain constant. */
2863
+ const LN2 = 693147180559945309n;
2864
+ const WAD$1 = 10n ** 18n;
2865
+ const WAD_SQUARED = 10n ** 36n;
2866
+ const PRICE_STEP = 10n ** 13n;
2867
+ const HALF_TICK_RANGE = 495n;
2868
+ /** Tick domain supported by Morpho V2. */
2869
+ const TICK_RANGE = 990;
2870
+ /** Max allowed price (1e18 in wad). */
2871
+ const MAX_PRICE = WAD$1;
2872
+ /**
2873
+ * Converts a tick to a wad price using the same approximation and rounding as TickLib.
2874
+ * @param tick - Tick value in the inclusive range [0, 990].
2875
+ * @returns The price in wad units.
2876
+ * @throws {@link InvalidTickError} If tick is not an integer in range [0, 990].
2877
+ */
2878
+ function tickToPrice(tick) {
2879
+ assertTick(tick);
2880
+ return divHalfDownUnchecked(divHalfDownUnchecked(WAD_SQUARED, WAD$1 + wExp(LN_ONE_PLUS_DELTA * (HALF_TICK_RANGE - BigInt(tick)))), PRICE_STEP) * PRICE_STEP;
2881
+ }
2882
+ /**
2883
+ * Returns the lowest tick with a higher-or-equal price.
2884
+ * @param price - Price in wad units.
2885
+ * @returns The first tick whose {@link tickToPrice} is greater than or equal to `price`.
2886
+ * @throws {@link InvalidPriceError} If price is outside [0, 1e18].
2887
+ */
2888
+ function priceToTick(price) {
2889
+ assertPrice(price);
2890
+ let low = 0;
2891
+ let high = TICK_RANGE;
2892
+ while (low !== high) {
2893
+ const mid = Math.floor((low + high) / 2);
2894
+ if (tickToPrice(mid) < price) low = mid + 1;
2895
+ else high = mid;
2896
+ }
2897
+ return low;
2898
+ }
2899
+ function divHalfDownUnchecked(x, d) {
2900
+ return (x + (d - 1n) / 2n) / d;
2901
+ }
2902
+ function wExp(x) {
2903
+ if (x < 0n) return WAD_SQUARED / wExp(-x);
2904
+ const q = (x + LN2 / 2n) / LN2;
2905
+ const r = x - q * LN2;
2906
+ const secondTerm = r * r / (2n * WAD$1);
2907
+ const thirdTerm = secondTerm * r / (3n * WAD$1);
2908
+ return WAD$1 + r + secondTerm + thirdTerm << q;
2909
+ }
2910
+ function assertTick(tick) {
2911
+ if (!Number.isInteger(tick) || tick < 0 || tick > TICK_RANGE) throw new InvalidTickError(tick);
2912
+ }
2913
+ function assertPrice(price) {
2914
+ if (price < 0n || price > MAX_PRICE) throw new InvalidPriceError(price);
2915
+ }
2916
+ var InvalidTickError = class extends BaseError {
2917
+ name = "Tick.InvalidTickError";
2918
+ constructor(tick) {
2919
+ super(`Invalid tick: ${tick}. Tick must be an integer between 0 and ${TICK_RANGE}.`);
2920
+ }
2921
+ };
2922
+ var InvalidPriceError = class extends BaseError {
2923
+ name = "Tick.InvalidPriceError";
2924
+ constructor(price) {
2925
+ super(`Invalid price: ${price}. Price must be between 0 and ${MAX_PRICE}.`);
2926
+ }
2927
+ };
2928
+
2795
2929
  //#endregion
2796
2930
  //#region src/core/TradingFee.ts
2797
2931
  var TradingFee_exports = /* @__PURE__ */ __exportAll({
@@ -3305,7 +3439,7 @@ const BrandTypeId = Symbol.for("mempool/Brand");
3305
3439
 
3306
3440
  //#endregion
3307
3441
  //#region src/database/drizzle/VERSION.ts
3308
- const VERSION = "router_v1.6";
3442
+ const VERSION = "router_v1.8";
3309
3443
 
3310
3444
  //#endregion
3311
3445
  //#region src/database/drizzle/schema.ts
@@ -3457,10 +3591,7 @@ const offers = s.table(EnumTableName.OFFERS, {
3457
3591
  precision: 78,
3458
3592
  scale: 0
3459
3593
  }).notNull().default("0"),
3460
- price: numeric("price", {
3461
- precision: 78,
3462
- scale: 0
3463
- }).notNull(),
3594
+ tick: integer("tick").notNull(),
3464
3595
  maturity: integer("maturity").notNull(),
3465
3596
  expiry: integer("expiry").notNull(),
3466
3597
  start: integer("start").notNull(),
@@ -3525,6 +3656,7 @@ const lots = s.table(EnumTableName.LOTS, {
3525
3656
  user: varchar("user", { length: 42 }).notNull(),
3526
3657
  contract: varchar("contract", { length: 42 }).notNull(),
3527
3658
  group: varchar("group", { length: 66 }).notNull(),
3659
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
3528
3660
  lower: numeric("lower", {
3529
3661
  precision: 78,
3530
3662
  scale: 0
@@ -3539,7 +3671,8 @@ const lots = s.table(EnumTableName.LOTS, {
3539
3671
  table.chainId,
3540
3672
  table.user,
3541
3673
  table.contract,
3542
- table.group
3674
+ table.group,
3675
+ table.obligationId
3543
3676
  ],
3544
3677
  name: "lots_pk"
3545
3678
  }),
@@ -3575,6 +3708,7 @@ const offsets = s.table(EnumTableName.OFFSETS, {
3575
3708
  user: varchar("user", { length: 42 }).notNull(),
3576
3709
  contract: varchar("contract", { length: 42 }).notNull(),
3577
3710
  group: varchar("group", { length: 66 }).notNull(),
3711
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
3578
3712
  value: numeric("value", {
3579
3713
  precision: 78,
3580
3714
  scale: 0
@@ -3584,7 +3718,8 @@ const offsets = s.table(EnumTableName.OFFSETS, {
3584
3718
  table.chainId,
3585
3719
  table.user,
3586
3720
  table.contract,
3587
- table.group
3721
+ table.group,
3722
+ table.obligationId
3588
3723
  ],
3589
3724
  name: "offsets_pk"
3590
3725
  }), foreignKey({
@@ -4175,6 +4310,7 @@ function decodeCallbacks(parameters) {
4175
4310
  positionContract: loanToken,
4176
4311
  positionUser: offer.maker,
4177
4312
  group: offer.group,
4313
+ obligationId: obligationId(offer),
4178
4314
  size: offer.assets
4179
4315
  });
4180
4316
  callbacks.push({
@@ -5113,8 +5249,10 @@ async function getRemoteBlockNumbers(healthClients) {
5113
5249
  //#region src/api/Schema/BookResponse.ts
5114
5250
  var BookResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$5 });
5115
5251
  function from$5(level) {
5252
+ const price = tickToPrice(level.tick);
5116
5253
  return {
5117
- price: level.price.toString(),
5254
+ tick: level.tick,
5255
+ price: price.toString(),
5118
5256
  assets: level.assets.toString(),
5119
5257
  count: level.count
5120
5258
  };
@@ -5182,6 +5320,16 @@ function from$4(obligation, quote) {
5182
5320
  //#endregion
5183
5321
  //#region src/api/Schema/OfferResponse.ts
5184
5322
  var OfferResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$3 });
5323
+ function normalizeChainId(chainId) {
5324
+ const parsedChainId = Number(chainId);
5325
+ if (!Number.isInteger(parsedChainId) || parsedChainId <= 0) throw new Error(`Invalid chain id: ${String(chainId)}`);
5326
+ return parsedChainId;
5327
+ }
5328
+ function normalizeBlockNumber(blockNumber) {
5329
+ const parsedBlockNumber = Number(blockNumber);
5330
+ if (!Number.isInteger(parsedBlockNumber) || parsedBlockNumber < 0) throw new Error(`Invalid block number: ${String(blockNumber)}`);
5331
+ return parsedBlockNumber;
5332
+ }
5185
5333
  /**
5186
5334
  * Creates an `OfferResponse` matching the Solidity Offer struct layout.
5187
5335
  * @constructor
@@ -5189,6 +5337,8 @@ var OfferResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$3 });
5189
5337
  * @returns The created `OfferResponse`. {@link OfferResponse}
5190
5338
  */
5191
5339
  function from$3(input) {
5340
+ const chainId = normalizeChainId(input.chainId);
5341
+ const blockNumber = normalizeBlockNumber(input.blockNumber);
5192
5342
  const base = {
5193
5343
  offer: {
5194
5344
  obligation: {
@@ -5207,7 +5357,7 @@ function from$3(input) {
5207
5357
  obligation_shares: input.obligationShares.toString(),
5208
5358
  start: input.start,
5209
5359
  expiry: input.expiry,
5210
- price: input.price.toString(),
5360
+ tick: input.tick,
5211
5361
  group: input.group,
5212
5362
  session: input.session,
5213
5363
  callback: input.callback.address,
@@ -5215,15 +5365,15 @@ function from$3(input) {
5215
5365
  },
5216
5366
  offer_hash: input.hash,
5217
5367
  obligation_id: id({
5218
- chainId: input.chainId,
5368
+ chainId,
5219
5369
  loanToken: input.loanToken,
5220
5370
  collaterals: [...input.collaterals],
5221
5371
  maturity: input.maturity
5222
5372
  }),
5223
- chain_id: input.chainId,
5373
+ chain_id: chainId,
5224
5374
  consumed: input.consumed.toString(),
5225
5375
  takeable: input.takeable.toString(),
5226
- block_number: input.blockNumber
5376
+ block_number: blockNumber
5227
5377
  };
5228
5378
  if (!input.proof || !input.root || !input.signature) return {
5229
5379
  ...base,
@@ -5369,7 +5519,7 @@ const offerExample = {
5369
5519
  obligation_shares: "0",
5370
5520
  start: 1761922790,
5371
5521
  expiry: 1761922799,
5372
- price: "2750000000000000000",
5522
+ tick: 495,
5373
5523
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
5374
5524
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
5375
5525
  callback: "0x0000000000000000000000000000000000000000",
@@ -5410,7 +5560,7 @@ const validateOfferExample = {
5410
5560
  assets: "369216000000000000000000",
5411
5561
  obligation_units: "0",
5412
5562
  obligation_shares: "0",
5413
- price: "2750000000000000000",
5563
+ tick: 495,
5414
5564
  maturity: 1761922799,
5415
5565
  expiry: 1761922799,
5416
5566
  start: 1761922790,
@@ -5554,9 +5704,11 @@ __decorate([ApiProperty({
5554
5704
  example: offerExample.offer.expiry
5555
5705
  })], OfferDataResponse.prototype, "expiry", void 0);
5556
5706
  __decorate([ApiProperty({
5557
- type: "string",
5558
- example: offerExample.offer.price
5559
- })], OfferDataResponse.prototype, "price", void 0);
5707
+ type: "number",
5708
+ example: offerExample.offer.tick,
5709
+ minimum: 0,
5710
+ maximum: 990
5711
+ })], OfferDataResponse.prototype, "tick", void 0);
5560
5712
  __decorate([ApiProperty({
5561
5713
  type: "string",
5562
5714
  example: offerExample.offer.group
@@ -5797,9 +5949,11 @@ __decorate([ApiProperty({
5797
5949
  required: false
5798
5950
  })], ValidateOfferRequest.prototype, "obligation_shares", void 0);
5799
5951
  __decorate([ApiProperty({
5800
- type: "string",
5801
- example: validateOfferExample.price
5802
- })], ValidateOfferRequest.prototype, "price", void 0);
5952
+ type: "number",
5953
+ example: validateOfferExample.tick,
5954
+ minimum: 0,
5955
+ maximum: 990
5956
+ })], ValidateOfferRequest.prototype, "tick", void 0);
5803
5957
  __decorate([ApiProperty({
5804
5958
  type: "number",
5805
5959
  example: validateOfferExample.maturity
@@ -5899,9 +6053,16 @@ __decorate([ApiProperty({
5899
6053
  description: "List of validation issues. Returned when any offer fails validation."
5900
6054
  })], ValidationFailureResponse.prototype, "data", void 0);
5901
6055
  var BookLevelResponse = class {};
6056
+ __decorate([ApiProperty({
6057
+ type: "number",
6058
+ example: 495,
6059
+ minimum: 0,
6060
+ maximum: 990
6061
+ })], BookLevelResponse.prototype, "tick", void 0);
5902
6062
  __decorate([ApiProperty({
5903
6063
  type: "string",
5904
- example: "2750000000000000000"
6064
+ example: "500000000000000000",
6065
+ description: "Price derived from tick, scaled by 1e18."
5905
6066
  })], BookLevelResponse.prototype, "price", void 0);
5906
6067
  __decorate([ApiProperty({
5907
6068
  type: "string",
@@ -5915,6 +6076,7 @@ const positionExample = {
5915
6076
  chain_id: 1,
5916
6077
  contract: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
5917
6078
  user: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6079
+ obligation_id: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
5918
6080
  reserved: "200000000000000000000",
5919
6081
  block_number: 21345678
5920
6082
  };
@@ -5931,6 +6093,12 @@ __decorate([ApiProperty({
5931
6093
  type: "string",
5932
6094
  example: positionExample.user
5933
6095
  })], PositionListItemResponse.prototype, "user", void 0);
6096
+ __decorate([ApiProperty({
6097
+ type: "string",
6098
+ nullable: true,
6099
+ example: positionExample.obligation_id,
6100
+ description: "Obligation id this reserved amount belongs to, or null if no lots exist."
6101
+ })], PositionListItemResponse.prototype, "obligation_id", void 0);
5934
6102
  __decorate([ApiProperty({
5935
6103
  type: "string",
5936
6104
  example: positionExample.reserved
@@ -5958,7 +6126,7 @@ __decorate([ApiProperty({
5958
6126
  })], BookListResponse.prototype, "cursor", void 0);
5959
6127
  __decorate([ApiProperty({
5960
6128
  type: () => [BookLevelResponse],
5961
- description: "Aggregated book levels grouped by computed price."
6129
+ description: "Aggregated book levels grouped by offer tick."
5962
6130
  })], BookListResponse.prototype, "data", void 0);
5963
6131
  let BooksController = class BooksController {
5964
6132
  async getBook() {}
@@ -5968,7 +6136,7 @@ __decorate([
5968
6136
  methods: ["get"],
5969
6137
  path: "/v1/books/{obligationId}/{side}",
5970
6138
  summary: "Get aggregated book",
5971
- description: "Returns aggregated book data for a given obligation and side. Offers are grouped by computed price with summed takeable amounts. Book levels are sorted by price (ascending for buy side, descending for sell side)."
6139
+ description: "Returns aggregated book data for a given obligation and side. Offers are grouped by tick with summed takeable amounts, and each level includes the corresponding wad-scaled price. Book levels are sorted by tick (ascending for sell side, descending for buy side)."
5972
6140
  }),
5973
6141
  ApiParam({
5974
6142
  name: "obligationId",
@@ -5993,7 +6161,7 @@ __decorate([
5993
6161
  name: "limit",
5994
6162
  type: "number",
5995
6163
  example: 10,
5996
- description: "Maximum number of price levels to return."
6164
+ description: "Maximum number of tick levels to return."
5997
6165
  }),
5998
6166
  ApiResponse({
5999
6167
  status: 200,
@@ -6187,6 +6355,11 @@ const configRulesLoanTokenExample = {
6187
6355
  chain_id: 1,
6188
6356
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
6189
6357
  };
6358
+ const configRulesCollateralTokenExample = {
6359
+ type: "collateral_token",
6360
+ chain_id: 1,
6361
+ address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
6362
+ };
6190
6363
  const configRulesOracleExample = {
6191
6364
  type: "oracle",
6192
6365
  chain_id: 1,
@@ -6196,6 +6369,7 @@ const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
6196
6369
  const configRulesPayloadExample = [
6197
6370
  configRulesMaturityExample,
6198
6371
  configRulesLoanTokenExample,
6372
+ configRulesCollateralTokenExample,
6199
6373
  configRulesOracleExample
6200
6374
  ];
6201
6375
  const configContractNames = [
@@ -6322,7 +6496,7 @@ __decorate([
6322
6496
  methods: ["get"],
6323
6497
  path: "/v1/config/rules",
6324
6498
  summary: "Get config rules",
6325
- description: "Returns configured rules (maturities, loan tokens, oracles) for supported chains."
6499
+ description: "Returns configured rules (maturities, loan tokens, collateral tokens, oracles) for supported chains."
6326
6500
  }),
6327
6501
  ApiQuery({
6328
6502
  name: "cursor",
@@ -6342,7 +6516,7 @@ __decorate([
6342
6516
  name: "types",
6343
6517
  type: ["string"],
6344
6518
  required: false,
6345
- example: "maturity,loan_token,oracle",
6519
+ example: "maturity,loan_token,collateral_token,oracle",
6346
6520
  description: "Filter by rule types (comma-separated).",
6347
6521
  style: "form",
6348
6522
  explode: false
@@ -6460,7 +6634,7 @@ __decorate([
6460
6634
  methods: ["get"],
6461
6635
  path: "/v1/users/{userAddress}/positions",
6462
6636
  summary: "Get user positions",
6463
- description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
6637
+ description: "Returns positions for a user with reserved balance per obligation. Each (position, obligation) pair is returned as a separate row. Positions with no lots return a single row with obligation_id = null and reserved = 0."
6464
6638
  }),
6465
6639
  ApiParam({
6466
6640
  name: "userAddress",
@@ -6548,6 +6722,7 @@ function from$2(position) {
6548
6722
  chain_id: position.chainId,
6549
6723
  contract: position.contract,
6550
6724
  user: position.user,
6725
+ obligation_id: position.obligationId,
6551
6726
  reserved: position.reserved.toString(),
6552
6727
  block_number: position.blockNumber
6553
6728
  };
@@ -6601,10 +6776,11 @@ const ConfigRuleTypes = z$1.enum([
6601
6776
  "maturity",
6602
6777
  "callback",
6603
6778
  "loan_token",
6779
+ "collateral_token",
6604
6780
  "oracle"
6605
6781
  ]);
6606
6782
  const GetConfigRulesQueryParams = z$1.object({
6607
- cursor: z$1.string().regex(/^(maturity|callback|loan_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6783
+ cursor: z$1.string().regex(/^(maturity|callback|loan_token|collateral_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6608
6784
  description: "Pagination cursor in type:chain_id:<value> format",
6609
6785
  example: "maturity:1:1730415600:end_of_next_month"
6610
6786
  }),
@@ -6614,7 +6790,7 @@ const GetConfigRulesQueryParams = z$1.object({
6614
6790
  }),
6615
6791
  types: csvArray(ConfigRuleTypes).meta({
6616
6792
  description: "Filter by rule types (comma-separated).",
6617
- example: "maturity,loan_token,oracle"
6793
+ example: "maturity,loan_token,collateral_token,oracle"
6618
6794
  }),
6619
6795
  chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6620
6796
  description: "Filter by chain IDs (comma-separated).",
@@ -6714,12 +6890,11 @@ const GetObligationParams = z$1.object({ obligation_id: z$1.string({ error: "Obl
6714
6890
  description: "Obligation id",
6715
6891
  example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6716
6892
  }) });
6717
- /** Validate a book cursor format: {side, lastPrice, offersCursor} */
6893
+ /** Validate a book cursor format: {side, lastTick, offersCursor} */
6718
6894
  function isValidBookCursor(cursorString) {
6719
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
6720
6895
  try {
6721
6896
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
6722
- return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6897
+ return (v?.side === "buy" || v?.side === "sell") && typeof v?.lastTick === "number" && Number.isInteger(v.lastTick) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6723
6898
  } catch {
6724
6899
  return false;
6725
6900
  }
@@ -6793,12 +6968,34 @@ async function getBook(params, db) {
6793
6968
  if (!result.success) return failure(result.error);
6794
6969
  const query = result.data;
6795
6970
  try {
6971
+ logger.debug({
6972
+ service: "api_controller",
6973
+ endpoint: "get_book",
6974
+ msg: "Loading book levels",
6975
+ obligation_id: query.obligation_id,
6976
+ side: query.side,
6977
+ limit: query.limit ?? null,
6978
+ has_cursor: query.cursor != null
6979
+ });
6796
6980
  const { levels, nextCursor } = await db.book.get({
6797
6981
  side: query.side,
6798
6982
  obligationId: query.obligation_id,
6799
6983
  cursor: query.cursor,
6800
6984
  limit: query.limit
6801
6985
  });
6986
+ const firstLevel = levels[0];
6987
+ logger.debug({
6988
+ service: "api_controller",
6989
+ endpoint: "get_book",
6990
+ msg: "Loaded book levels",
6991
+ obligation_id: query.obligation_id,
6992
+ side: query.side,
6993
+ levels_count: levels.length,
6994
+ has_next_cursor: nextCursor != null,
6995
+ first_level_tick: firstLevel?.tick ?? null,
6996
+ first_level_assets: firstLevel?.assets.toString() ?? null,
6997
+ first_level_count: firstLevel?.count ?? null
6998
+ });
6802
6999
  return success({
6803
7000
  data: levels.map(from$5),
6804
7001
  cursor: nextCursor
@@ -6947,6 +7144,33 @@ const assets = {
6947
7144
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
6948
7145
  ]
6949
7146
  };
7147
+ const collateralAssets = {
7148
+ [ChainId.ETHEREUM.toString()]: [
7149
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
7150
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
7151
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
7152
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7153
+ ],
7154
+ [ChainId.BASE.toString()]: [
7155
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
7156
+ "0x4200000000000000000000000000000000000006",
7157
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
7158
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
7159
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
7160
+ ],
7161
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
7162
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
7163
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
7164
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
7165
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7166
+ ],
7167
+ [ChainId.ANVIL.toString()]: [
7168
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
7169
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
7170
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
7171
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7172
+ ]
7173
+ };
6950
7174
  const oracles = {
6951
7175
  [ChainId.ETHEREUM.toString()]: [
6952
7176
  "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
@@ -7006,7 +7230,7 @@ const configs = {
7006
7230
  //#endregion
7007
7231
  //#region src/gatekeeper/ConfigRules.ts
7008
7232
  /**
7009
- * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
7233
+ * Build the configured rules (maturities + callback addresses + loan tokens + collateral tokens + oracles) for the provided chains.
7010
7234
  * @param chains - Chains to include in the configured rules.
7011
7235
  * @returns Sorted list of config rules.
7012
7236
  */
@@ -7026,6 +7250,12 @@ function buildConfigRules(chains) {
7026
7250
  chain_id: chain.id,
7027
7251
  address: normalizeAddress(address)
7028
7252
  });
7253
+ const collateralTokens = collateralAssets[chain.id.toString()] ?? [];
7254
+ for (const address of collateralTokens) rules.push({
7255
+ type: "collateral_token",
7256
+ chain_id: chain.id,
7257
+ address: normalizeAddress(address)
7258
+ });
7029
7259
  const oracles$2 = oracles[chain.id.toString()] ?? [];
7030
7260
  for (const address of oracles$2) rules.push({
7031
7261
  type: "oracle",
@@ -7053,6 +7283,10 @@ function buildConfigRulesChecksum(rules) {
7053
7283
  hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
7054
7284
  continue;
7055
7285
  }
7286
+ if (rule.type === "collateral_token") {
7287
+ hash.update(`collateral_token:${rule.chain_id}:${rule.address}\n`);
7288
+ continue;
7289
+ }
7056
7290
  if (rule.type === "oracle") {
7057
7291
  hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
7058
7292
  continue;
@@ -7073,6 +7307,7 @@ function compareConfigRules(left, right) {
7073
7307
  return left.address.localeCompare(right.address);
7074
7308
  }
7075
7309
  if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
7310
+ if (left.type === "collateral_token" && right.type === "collateral_token") return left.address.localeCompare(right.address);
7076
7311
  if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
7077
7312
  return 0;
7078
7313
  }
@@ -7119,6 +7354,7 @@ function formatCursor(rule) {
7119
7354
  if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
7120
7355
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
7121
7356
  if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
7357
+ if (rule.type === "collateral_token") return `collateral_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
7122
7358
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
7123
7359
  }
7124
7360
  function parseCursor(cursor) {
@@ -7151,7 +7387,7 @@ function parseCursor(cursor) {
7151
7387
  address: parseAddress(addressValue, "Cursor address")
7152
7388
  };
7153
7389
  }
7154
- if (type === "loan_token" || type === "oracle") {
7390
+ if (type === "loan_token" || type === "collateral_token" || type === "oracle") {
7155
7391
  const addressValue = rest.join(":");
7156
7392
  if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
7157
7393
  return {
@@ -7178,7 +7414,7 @@ function parseAddress(address, label) {
7178
7414
  return address.toLowerCase();
7179
7415
  }
7180
7416
  function isConfigRuleType(value) {
7181
- return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
7417
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "collateral_token" || value === "oracle";
7182
7418
  }
7183
7419
  function isMaturityType(value) {
7184
7420
  return Object.values(MaturityType).includes(value);
@@ -7525,7 +7761,7 @@ function create$15(config) {
7525
7761
  assets: offers.assets,
7526
7762
  obligationUnits: offers.obligationUnits,
7527
7763
  obligationShares: offers.obligationShares,
7528
- price: offers.price,
7764
+ tick: offers.tick,
7529
7765
  maturity: offers.maturity,
7530
7766
  expiry: offers.expiry,
7531
7767
  start: offers.start,
@@ -7545,7 +7781,7 @@ function create$15(config) {
7545
7781
  assets: BigInt(row.assets),
7546
7782
  obligationUnits: BigInt(row.obligationUnits),
7547
7783
  obligationShares: BigInt(row.obligationShares),
7548
- price: BigInt(row.price),
7784
+ tick: row.tick,
7549
7785
  maturity: from$16(row.maturity),
7550
7786
  expiry: row.expiry,
7551
7787
  start: row.start,
@@ -7627,8 +7863,8 @@ function create$15(config) {
7627
7863
  const now$2 = now();
7628
7864
  const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
7629
7865
  obligationId: offers.obligationId,
7630
- price: offers.price
7631
- }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, eq(offers.hash, validations.offerHash)).leftJoin(status, eq(validations.statusId, status.id)).where(and(inArray(offers.obligationId, obligationIds), eq(offers.buy, side === "buy"), gte(offers.expiry, now$2), gte(offers.maturity, now$2), lte(offers.start, now$2), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(offers.obligationId, side === "buy" ? sql`${offers.price}::numeric ASC` : sql`${offers.price}::numeric DESC`);
7866
+ price: offers.tick
7867
+ }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, eq(offers.hash, validations.offerHash)).leftJoin(status, eq(validations.statusId, status.id)).where(and(inArray(offers.obligationId, obligationIds), eq(offers.buy, side === "buy"), gte(offers.expiry, now$2), gte(offers.maturity, now$2), lte(offers.start, now$2), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(offers.obligationId, side === "buy" ? sql`${offers.tick} ASC` : sql`${offers.tick} DESC`);
7632
7868
  const [bestBuys, bestSells] = await Promise.all([query({ side: "buy" }), query({ side: "sell" })]);
7633
7869
  const quotes = /* @__PURE__ */ new Map();
7634
7870
  for (const row of bestSells) quotes.set(row.obligationId, {
@@ -7686,16 +7922,57 @@ async function getOffersQuery(db, parameters) {
7686
7922
  '[]'::jsonb
7687
7923
  )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles$1, sql`${obligationCollateralsV2.oracleChainId} = ${oracles$1.chainId}
7688
7924
  AND ${obligationCollateralsV2.oracleAddress} = ${oracles$1.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
7689
- const availableLateral = db.select({ available: sql`COALESCE(SUM(
7690
- CASE
7691
- WHEN ${positions.asset} IS NULL THEN 0
7692
- ELSE
7693
- CASE
7694
- WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
7695
- ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
7696
- END
7697
- END
7698
- ), 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");
7925
+ const lotBalanceExpr = sql`GREATEST(0, LEAST(
7926
+ COALESCE(${positions.balance}, 0)::numeric
7927
+ + COALESCE((
7928
+ SELECT SUM(${offsets.value}::numeric)
7929
+ FROM ${offsets}
7930
+ WHERE ${offsets.chainId} = ${callbacks.positionChainId}
7931
+ AND LOWER(${offsets.contract}) = LOWER(${callbacks.positionContract})
7932
+ AND LOWER(${offsets.user}) = LOWER(${callbacks.positionUser})
7933
+ ), 0)
7934
+ - COALESCE(${lots.lower}::numeric, 0),
7935
+ (COALESCE(${lots.upper}::numeric, 0) - COALESCE(${lots.lower}::numeric, 0))
7936
+ - CASE
7937
+ WHEN ${offers.assets}::numeric > 0
7938
+ THEN COALESCE(${groups.consumed}::numeric, 0)
7939
+ * (COALESCE(${lots.upper}::numeric, 0) - COALESCE(${lots.lower}::numeric, 0))
7940
+ / ${offers.assets}::numeric
7941
+ ELSE 0
7942
+ END
7943
+ ))`;
7944
+ const contributionExpr = sql`CASE
7945
+ WHEN ${positions.asset} IS NULL OR ${lots.lower} IS NULL THEN 0
7946
+ ELSE LEAST(COALESCE(${callbacks.amount}::numeric, ${lotBalanceExpr}), ${lotBalanceExpr})
7947
+ END`;
7948
+ const availableExpr = sql`COALESCE((
7949
+ SELECT SUM(deduped.contribution)
7950
+ FROM (
7951
+ SELECT DISTINCT ON (
7952
+ ${callbacks.positionChainId},
7953
+ LOWER(${callbacks.positionContract}),
7954
+ LOWER(${callbacks.positionUser})
7955
+ )
7956
+ ${contributionExpr} AS contribution
7957
+ FROM ${offersCallbacks}
7958
+ INNER JOIN ${callbacks} ON ${offersCallbacks.callbackId} = ${callbacks.id}
7959
+ LEFT JOIN ${positions}
7960
+ ON ${positions.chainId} = ${callbacks.positionChainId}
7961
+ AND LOWER(${positions.contract}) = LOWER(${callbacks.positionContract})
7962
+ AND LOWER(${positions.user}) = LOWER(${callbacks.positionUser})
7963
+ LEFT JOIN ${lots}
7964
+ ON ${lots.chainId} = ${callbacks.positionChainId}
7965
+ AND LOWER(${lots.contract}) = LOWER(${callbacks.positionContract})
7966
+ AND LOWER(${lots.user}) = LOWER(${callbacks.positionUser})
7967
+ AND LOWER(${lots.group}) = LOWER(${offers.group})
7968
+ WHERE ${offersCallbacks.offerHash} = ${offers.hash}
7969
+ ORDER BY
7970
+ ${callbacks.positionChainId},
7971
+ LOWER(${callbacks.positionContract}),
7972
+ LOWER(${callbacks.positionUser}),
7973
+ ${contributionExpr} DESC
7974
+ ) deduped
7975
+ ), 0)`;
7699
7976
  const rows = (await db.select({
7700
7977
  hash: offers.hash,
7701
7978
  maker: offers.groupMaker,
@@ -7703,7 +7980,7 @@ async function getOffersQuery(db, parameters) {
7703
7980
  obligationUnits: offers.obligationUnits,
7704
7981
  obligationShares: offers.obligationShares,
7705
7982
  consumed: groups.consumed,
7706
- price: offers.price,
7983
+ tick: offers.tick,
7707
7984
  maturity: offers.maturity,
7708
7985
  expiry: offers.expiry,
7709
7986
  start: offers.start,
@@ -7716,22 +7993,22 @@ async function getOffersQuery(db, parameters) {
7716
7993
  callbackData: offers.callbackData,
7717
7994
  collaterals: collateralsLateral.collaterals,
7718
7995
  blockNumber: offers.blockNumber,
7719
- available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
7996
+ available: sql`${availableExpr}::numeric`.as("available"),
7720
7997
  takeable: sql`FLOOR(GREATEST(0,
7721
7998
  CASE WHEN ${offers.buy} = false
7722
7999
  THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
7723
8000
  ELSE LEAST(
7724
8001
  ${offers.assets}::numeric - ${groups.consumed}::numeric,
7725
- COALESCE(${availableLateral.available}::numeric, 0)
8002
+ ${availableExpr}::numeric
7726
8003
  )
7727
8004
  END
7728
8005
  ))`.as("takeable")
7729
- }).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,
8006
+ }).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`).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,
7730
8007
  CASE WHEN ${offers.buy} = false
7731
8008
  THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
7732
8009
  ELSE LEAST(
7733
8010
  ${offers.assets}::numeric - ${groups.consumed}::numeric,
7734
- COALESCE(${availableLateral.available}::numeric, 0)
8011
+ ${availableExpr}::numeric
7735
8012
  )
7736
8013
  END
7737
8014
  ) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
@@ -7741,7 +8018,7 @@ async function getOffersQuery(db, parameters) {
7741
8018
  assets: BigInt(row.assets),
7742
8019
  obligationUnits: BigInt(row.obligationUnits),
7743
8020
  obligationShares: BigInt(row.obligationShares),
7744
- price: BigInt(row.price),
8021
+ tick: row.tick,
7745
8022
  maturity: from$16(row.maturity),
7746
8023
  expiry: row.expiry,
7747
8024
  start: row.start,
@@ -8140,7 +8417,7 @@ async function getOffers(apiClient, parameters) {
8140
8417
  assets: offerData.assets,
8141
8418
  obligation_units: offerData.obligation_units,
8142
8419
  obligation_shares: offerData.obligation_shares,
8143
- price: offerData.price,
8420
+ tick: offerData.tick,
8144
8421
  maturity: from$16(offerData.obligation.maturity),
8145
8422
  expiry: offerData.expiry,
8146
8423
  start: offerData.start,
@@ -8499,6 +8776,7 @@ function create$12(config) {
8499
8776
  return {
8500
8777
  get: async (parameters) => {
8501
8778
  const { side, obligationId, cursor: cursorString, limit = DEFAULT_LIMIT$2 } = parameters;
8779
+ const tickSortDirection = side === "sell" ? "asc" : "desc";
8502
8780
  const inputCursor = LevelCursor.decode(cursorString, logger);
8503
8781
  if (cursorString != null && inputCursor === null) return {
8504
8782
  levels: [],
@@ -8513,23 +8791,23 @@ function create$12(config) {
8513
8791
  cursor: inputCursor?.offersCursor ?? void 0,
8514
8792
  limit: fetchLimit
8515
8793
  });
8516
- const priceMap = /* @__PURE__ */ new Map();
8794
+ const tickMap = /* @__PURE__ */ new Map();
8517
8795
  for (const row of rows) {
8518
- const priceKey = row.price.toString();
8519
- const existing = priceMap.get(priceKey);
8796
+ const existing = tickMap.get(row.tick);
8520
8797
  if (existing) {
8521
8798
  existing.assets += row.takeable;
8522
8799
  existing.count += 1;
8523
- } else priceMap.set(priceKey, {
8800
+ } else tickMap.set(row.tick, {
8524
8801
  assets: row.takeable,
8525
8802
  count: 1
8526
8803
  });
8527
8804
  }
8528
- const levels = Array.from(priceMap.entries()).map(([price, data]) => ({
8529
- price: BigInt(price),
8530
- assets: data.assets,
8531
- count: data.count
8805
+ const levels = Array.from(tickMap.entries()).map(([tick, level]) => ({
8806
+ tick,
8807
+ assets: level.assets,
8808
+ count: level.count
8532
8809
  }));
8810
+ levels.sort((a, b) => tickSortDirection === "asc" ? a.tick - b.tick : b.tick - a.tick);
8533
8811
  const paginatedLevels = levels.slice(0, limit);
8534
8812
  const hasMore = levels.length > limit || offersNextCursor !== null;
8535
8813
  const lastLevel = paginatedLevels[paginatedLevels.length - 1];
@@ -8575,14 +8853,14 @@ async function _getOffers(db, params) {
8575
8853
  AND (s.code IS NULL OR s.code = ${Status.VALID})
8576
8854
  ORDER BY
8577
8855
  o.group_chain_id, o.group_maker, o."group_group",
8578
- o.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, o.block_number ASC, o.assets DESC, o.hash ASC
8856
+ o.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, o.block_number ASC, o.assets DESC, o.hash ASC
8579
8857
  ),
8580
8858
  enriched AS (
8581
8859
  SELECT
8582
8860
  w.*,
8583
8861
  g.consumed, g.chain_id, obl.loan_token,
8584
8862
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
8585
- THEN w.price::numeric ELSE -w.price::numeric END AS price_norm,
8863
+ THEN w.tick ELSE -w.tick END AS tick_norm,
8586
8864
  w.block_number AS block_norm,
8587
8865
  -w.assets AS assets_norm,
8588
8866
  w.hash AS hash_norm
@@ -8599,33 +8877,35 @@ async function _getOffers(db, params) {
8599
8877
  FROM enriched e
8600
8878
  ${cursor != null ? sql`
8601
8879
  WHERE
8602
- (e.price_norm, e.block_norm, e.assets_norm, e.hash_norm)
8880
+ (e.tick_norm, e.block_norm, e.assets_norm, e.hash_norm)
8603
8881
  > (
8604
8882
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
8605
- THEN ${cursor.price}::numeric ELSE -${cursor.price}::numeric END,
8883
+ THEN ${cursor.tick}::integer ELSE -${cursor.tick}::integer END,
8606
8884
  ${cursor.blockNumber},
8607
8885
  -${cursor.assets}::numeric,
8608
8886
  ${cursor.hash}
8609
8887
  )` : sql``}
8610
- ORDER BY e.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
8888
+ ORDER BY e.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
8611
8889
  LIMIT ${limit}
8612
8890
  ),
8613
- -- Compute sum of offsets per position
8891
+ -- Compute sum of offsets per position and obligation
8614
8892
  position_offsets AS (
8615
8893
  SELECT
8616
8894
  chain_id,
8617
8895
  "user",
8618
8896
  contract,
8897
+ obligation_id,
8619
8898
  SUM(value::numeric) AS total_offset
8620
8899
  FROM ${offsets}
8621
- GROUP BY chain_id, "user", contract
8900
+ GROUP BY chain_id, "user", contract, obligation_id
8622
8901
  ),
8623
- -- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
8902
+ -- Compute position_consumed: sum of consumed from all groups with lots on each position+obligation (converted to lot terms)
8624
8903
  position_consumed AS (
8625
8904
  SELECT
8626
8905
  l.chain_id,
8627
8906
  l.contract,
8628
8907
  l."user",
8908
+ l.obligation_id,
8629
8909
  SUM(
8630
8910
  CASE
8631
8911
  WHEN wo.assets::numeric > 0
@@ -8642,7 +8922,7 @@ async function _getOffers(db, params) {
8642
8922
  ON wo.group_chain_id = g.chain_id
8643
8923
  AND LOWER(wo.group_maker) = LOWER(g.maker)
8644
8924
  AND wo.group_group = g."group"
8645
- GROUP BY l.chain_id, l.contract, l."user"
8925
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
8646
8926
  ),
8647
8927
  -- Compute callback contributions with lot balance
8648
8928
  callback_contributions AS (
@@ -8650,7 +8930,7 @@ async function _getOffers(db, params) {
8650
8930
  p.hash,
8651
8931
  p.obligation_id,
8652
8932
  p.assets,
8653
- p.price,
8933
+ p.tick,
8654
8934
  p.obligation_units,
8655
8935
  p.obligation_shares,
8656
8936
  p.maturity,
@@ -8695,6 +8975,7 @@ async function _getOffers(db, params) {
8695
8975
  AND LOWER(l.contract) = LOWER(c.position_contract)
8696
8976
  AND LOWER(l."user") = LOWER(c.position_user)
8697
8977
  AND l."group" = p.group_group
8978
+ AND l.obligation_id = p.obligation_id
8698
8979
  LEFT JOIN ${positions} pos
8699
8980
  ON pos.chain_id = c.position_chain_id
8700
8981
  AND LOWER(pos.contract) = LOWER(c.position_contract)
@@ -8703,10 +8984,12 @@ async function _getOffers(db, params) {
8703
8984
  ON pos_offsets.chain_id = c.position_chain_id
8704
8985
  AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
8705
8986
  AND LOWER(pos_offsets."user") = LOWER(c.position_user)
8987
+ AND pos_offsets.obligation_id = p.obligation_id
8706
8988
  LEFT JOIN position_consumed pc
8707
8989
  ON pc.chain_id = c.position_chain_id
8708
8990
  AND LOWER(pc.contract) = LOWER(c.position_contract)
8709
8991
  AND LOWER(pc."user") = LOWER(c.position_user)
8992
+ AND pc.obligation_id = p.obligation_id
8710
8993
  ),
8711
8994
  -- Compute contribution per callback in loan terms (loan token only — collateral positions are not indexed)
8712
8995
  callback_loan_contribution AS (
@@ -8724,7 +9007,7 @@ async function _getOffers(db, params) {
8724
9007
  hash,
8725
9008
  obligation_id,
8726
9009
  assets,
8727
- price,
9010
+ tick,
8728
9011
  obligation_units,
8729
9012
  obligation_shares,
8730
9013
  maturity,
@@ -8750,13 +9033,13 @@ async function _getOffers(db, params) {
8750
9033
  WHERE clc.callback_id IS NOT NULL
8751
9034
  ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
8752
9035
  ) deduped
8753
- GROUP BY hash, obligation_id, assets, price, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
9036
+ GROUP BY hash, obligation_id, assets, tick, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
8754
9037
  callback_address, callback_data, block_number, group_chain_id, group_maker,
8755
9038
  consumed, chain_id, loan_token, session
8756
9039
  UNION ALL
8757
9040
  -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
8758
9041
  SELECT
8759
- p.hash, p.obligation_id, p.assets, p.price,
9042
+ p.hash, p.obligation_id, p.assets, p.tick,
8760
9043
  p.obligation_units, p.obligation_shares,
8761
9044
  p.maturity, p.expiry, p.start, p.group_group,
8762
9045
  p.buy, p.callback_address, p.callback_data,
@@ -8778,7 +9061,7 @@ async function _getOffers(db, params) {
8778
9061
  oc.obligation_units,
8779
9062
  oc.obligation_shares,
8780
9063
  oc.consumed,
8781
- oc.price,
9064
+ oc.tick,
8782
9065
  oc.maturity,
8783
9066
  oc.expiry,
8784
9067
  oc.start,
@@ -8810,7 +9093,7 @@ async function _getOffers(db, params) {
8810
9093
  ))
8811
9094
  END > 0
8812
9095
  ORDER BY
8813
- oc.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
9096
+ oc.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
8814
9097
  oc.block_number ASC,
8815
9098
  oc.assets DESC,
8816
9099
  oc.hash ASC;
@@ -8823,7 +9106,7 @@ async function _getOffers(db, params) {
8823
9106
  assets: BigInt(row.assets),
8824
9107
  obligationUnits: BigInt(row.obligation_units ?? 0),
8825
9108
  obligationShares: BigInt(row.obligation_shares ?? 0),
8826
- price: BigInt(row.price),
9109
+ tick: row.tick,
8827
9110
  maturity: row.maturity,
8828
9111
  expiry: row.expiry,
8829
9112
  start: row.start,
@@ -8855,7 +9138,7 @@ let Cursor;
8855
9138
  function encode(row, totalReturned, now, side) {
8856
9139
  return Buffer.from(JSON.stringify({
8857
9140
  side,
8858
- price: row.price.toString(),
9141
+ tick: row.tick,
8859
9142
  blockNumber: row.blockNumber,
8860
9143
  assets: row.assets.toString(),
8861
9144
  hash: row.hash,
@@ -8866,10 +9149,9 @@ let Cursor;
8866
9149
  _Cursor.encode = encode;
8867
9150
  function decode(cursorString, logger) {
8868
9151
  if (cursorString == null) return null;
8869
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
8870
9152
  try {
8871
9153
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
8872
- if ((v?.side === "buy" || v?.side === "sell") && isNumericString(v?.price) && typeof v?.blockNumber === "number" && Number.isInteger(v.blockNumber) && isNumericString(v?.assets) && isHex(v?.hash) && typeof v?.totalReturned === "number" && Number.isInteger(v.totalReturned) && typeof v?.now === "number" && Number.isInteger(v.now)) return v;
9154
+ if ((v?.side === "buy" || v?.side === "sell") && typeof v?.tick === "number" && Number.isInteger(v.tick) && typeof v?.blockNumber === "number" && Number.isInteger(v.blockNumber) && typeof v?.assets === "string" && /^-?\d+$/.test(v.assets) && isHex(v?.hash) && typeof v?.totalReturned === "number" && Number.isInteger(v.totalReturned) && typeof v?.now === "number" && Number.isInteger(v.now)) return v;
8873
9155
  throw new Error("Invalid cursor");
8874
9156
  } catch {
8875
9157
  logger.error({
@@ -8887,7 +9169,7 @@ let LevelCursor;
8887
9169
  function encode(lastLevel, offersCursor, side, now) {
8888
9170
  return Buffer.from(JSON.stringify({
8889
9171
  side,
8890
- lastPrice: lastLevel.price.toString(),
9172
+ lastTick: lastLevel.tick,
8891
9173
  now,
8892
9174
  offersCursor
8893
9175
  })).toString("base64url");
@@ -8895,10 +9177,9 @@ let LevelCursor;
8895
9177
  _LevelCursor.encode = encode;
8896
9178
  function decode(cursorString, logger) {
8897
9179
  if (cursorString == null) return null;
8898
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
8899
9180
  try {
8900
9181
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
8901
- if ((v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && typeof v?.now === "number" && Number.isInteger(v.now) && (v?.offersCursor === null || typeof v?.offersCursor === "string")) return v;
9182
+ if ((v?.side === "buy" || v?.side === "sell") && typeof v?.lastTick === "number" && Number.isInteger(v.lastTick) && typeof v?.now === "number" && Number.isInteger(v.now) && (v?.offersCursor === null || typeof v?.offersCursor === "string")) return v;
8902
9183
  throw new Error("Invalid book cursor");
8903
9184
  } catch {
8904
9185
  logger.error({
@@ -9041,31 +9322,33 @@ function create$9(db) {
9041
9322
  function create$8(db) {
9042
9323
  return {
9043
9324
  get: async (parameters) => {
9044
- const { chainId, user, contract, group } = parameters ?? {};
9325
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
9045
9326
  const conditions = [];
9046
9327
  if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
9047
9328
  if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
9048
9329
  if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
9049
9330
  if (group !== void 0) conditions.push(eq(lots.group, group));
9331
+ if (obligationId !== void 0) conditions.push(eq(lots.obligationId, obligationId));
9050
9332
  return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
9051
9333
  chainId: row.chainId,
9052
9334
  user: row.user,
9053
9335
  contract: row.contract,
9054
9336
  group: row.group,
9337
+ obligationId: row.obligationId,
9055
9338
  lower: BigInt(row.lower),
9056
9339
  upper: BigInt(row.upper)
9057
9340
  }));
9058
9341
  },
9059
9342
  create: async (parameters) => {
9060
9343
  if (parameters.length === 0) return;
9061
- const lotsByPositionGroup = /* @__PURE__ */ new Map();
9344
+ const lotsByKey = /* @__PURE__ */ new Map();
9062
9345
  for (const offer of parameters) {
9063
- const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
9064
- const existing = lotsByPositionGroup.get(key);
9065
- if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
9346
+ const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}-${offer.obligationId}`.toLowerCase();
9347
+ const existing = lotsByKey.get(key);
9348
+ if (!existing || offer.size > existing.size) lotsByKey.set(key, offer);
9066
9349
  }
9067
- for (const offer of lotsByPositionGroup.values()) if ((await db.select().from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.group, offer.group.toLowerCase()))).limit(1)).length === 0) {
9068
- const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase())));
9350
+ for (const offer of lotsByKey.values()) if ((await db.select().from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.group, offer.group.toLowerCase()), eq(lots.obligationId, offer.obligationId.toLowerCase()))).limit(1)).length === 0) {
9351
+ const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.obligationId, offer.obligationId.toLowerCase())));
9069
9352
  const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
9070
9353
  const newUpper = newLower + offer.size;
9071
9354
  await db.insert(lots).values({
@@ -9073,6 +9356,7 @@ function create$8(db) {
9073
9356
  user: offer.positionUser.toLowerCase(),
9074
9357
  contract: offer.positionContract.toLowerCase(),
9075
9358
  group: offer.group.toLowerCase(),
9359
+ obligationId: offer.obligationId.toLowerCase(),
9076
9360
  lower: newLower.toString(),
9077
9361
  upper: newUpper.toString()
9078
9362
  });
@@ -9127,17 +9411,19 @@ function create$7(db) {
9127
9411
  //#region src/database/domains/Offsets.ts
9128
9412
  function create$6(db) {
9129
9413
  return { get: async (parameters) => {
9130
- const { chainId, user, contract, group } = parameters ?? {};
9414
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
9131
9415
  const conditions = [];
9132
9416
  if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
9133
9417
  if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
9134
9418
  if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
9135
9419
  if (group !== void 0) conditions.push(eq(offsets.group, group));
9420
+ if (obligationId !== void 0) conditions.push(eq(offsets.obligationId, obligationId));
9136
9421
  return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
9137
9422
  chainId: row.chainId,
9138
9423
  user: row.user,
9139
9424
  contract: row.contract,
9140
9425
  group: row.group,
9426
+ obligationId: row.obligationId,
9141
9427
  value: BigInt(row.value)
9142
9428
  }));
9143
9429
  } };
@@ -9287,7 +9573,8 @@ const create$4 = (db) => {
9287
9573
  if (!parsed.chainId || !parsed.contract) throw new Error("Invalid cursor format");
9288
9574
  cursor = {
9289
9575
  chainId: parsed.chainId,
9290
- contract: parsed.contract
9576
+ contract: parsed.contract,
9577
+ obligationId: parsed.obligationId ?? null
9291
9578
  };
9292
9579
  }
9293
9580
  const raw = await db.execute(sql`
@@ -9296,16 +9583,18 @@ const create$4 = (db) => {
9296
9583
  chain_id,
9297
9584
  "user",
9298
9585
  contract,
9586
+ obligation_id,
9299
9587
  SUM(value::numeric) AS total_offset
9300
9588
  FROM ${offsets}
9301
9589
  WHERE LOWER("user") = LOWER(${user})
9302
- GROUP BY chain_id, "user", contract
9590
+ GROUP BY chain_id, "user", contract, obligation_id
9303
9591
  ),
9304
9592
  position_consumed AS (
9305
9593
  SELECT
9306
9594
  l.chain_id,
9307
9595
  l.contract,
9308
9596
  l."user",
9597
+ l.obligation_id,
9309
9598
  SUM(
9310
9599
  CASE
9311
9600
  WHEN offer_agg.assets > 0
@@ -9331,50 +9620,64 @@ const create$4 = (db) => {
9331
9620
  AND LOWER(offer_agg.group_maker) = LOWER(g.maker)
9332
9621
  AND offer_agg."group_group" = g."group"
9333
9622
  WHERE LOWER(l."user") = LOWER(${user})
9334
- GROUP BY l.chain_id, l.contract, l."user"
9623
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
9335
9624
  ),
9336
9625
  position_max_lot AS (
9337
9626
  SELECT
9338
9627
  chain_id,
9339
9628
  contract,
9340
9629
  "user",
9630
+ obligation_id,
9341
9631
  MAX(upper::numeric) AS max_upper
9342
9632
  FROM ${lots}
9343
9633
  WHERE LOWER("user") = LOWER(${user})
9344
- GROUP BY chain_id, contract, "user"
9634
+ GROUP BY chain_id, contract, "user", obligation_id
9635
+ ),
9636
+ per_obligation AS (
9637
+ SELECT
9638
+ pml.chain_id,
9639
+ pml.contract,
9640
+ pml."user",
9641
+ pml.obligation_id,
9642
+ GREATEST(0,
9643
+ COALESCE(pml.max_upper, 0)
9644
+ - COALESCE(po.total_offset, 0)
9645
+ - COALESCE(pc.consumed, 0)
9646
+ )::text AS reserved_balance
9647
+ FROM position_max_lot pml
9648
+ LEFT JOIN position_offsets po
9649
+ ON po.chain_id = pml.chain_id
9650
+ AND LOWER(po.contract) = LOWER(pml.contract)
9651
+ AND LOWER(po."user") = LOWER(pml."user")
9652
+ AND po.obligation_id = pml.obligation_id
9653
+ LEFT JOIN position_consumed pc
9654
+ ON pc.chain_id = pml.chain_id
9655
+ AND LOWER(pc.contract) = LOWER(pml.contract)
9656
+ AND LOWER(pc."user") = LOWER(pml."user")
9657
+ AND pc.obligation_id = pml.obligation_id
9345
9658
  )
9346
9659
  SELECT
9347
9660
  p.chain_id,
9348
9661
  p.contract,
9349
9662
  p."user",
9350
9663
  p.block_number,
9351
- GREATEST(0,
9352
- COALESCE(pml.max_upper, 0)
9353
- - COALESCE(po.total_offset, 0)
9354
- - COALESCE(pc.consumed, 0)
9355
- )::text AS reserved_balance
9664
+ po.obligation_id,
9665
+ COALESCE(po.reserved_balance, '0') AS reserved_balance
9356
9666
  FROM ${positions} p
9357
- LEFT JOIN position_offsets po
9667
+ LEFT JOIN per_obligation po
9358
9668
  ON po.chain_id = p.chain_id
9359
9669
  AND LOWER(po.contract) = LOWER(p.contract)
9360
9670
  AND LOWER(po."user") = LOWER(p."user")
9361
- LEFT JOIN position_consumed pc
9362
- ON pc.chain_id = p.chain_id
9363
- AND LOWER(pc.contract) = LOWER(p.contract)
9364
- AND LOWER(pc."user") = LOWER(p."user")
9365
- LEFT JOIN position_max_lot pml
9366
- ON pml.chain_id = p.chain_id
9367
- AND LOWER(pml.contract) = LOWER(p.contract)
9368
- AND LOWER(pml."user") = LOWER(p."user")
9369
9671
  WHERE LOWER(p."user") = LOWER(${user})
9370
9672
  AND p."user" != ${zeroAddress}
9371
- ${cursor !== null ? sql`AND (p.chain_id, p.contract) > (${cursor.chainId}, ${cursor.contract})` : sql``}
9372
- ORDER BY p.chain_id ASC, p.contract ASC
9673
+ ${cursor !== null ? sql`AND (p.chain_id, p.contract, COALESCE(po.obligation_id, '')) > (${cursor.chainId}, ${cursor.contract}, ${cursor.obligationId ?? ""})` : sql``}
9674
+ ORDER BY p.chain_id ASC, p.contract ASC, po.obligation_id ASC NULLS FIRST
9373
9675
  LIMIT ${limit}
9374
9676
  `);
9375
9677
  const nextCursor = raw.rows.length === limit ? Buffer.from(JSON.stringify({
9376
9678
  chainId: raw.rows[raw.rows.length - 1].chain_id.toString(),
9377
- contract: raw.rows[raw.rows.length - 1].contract
9679
+ contract: raw.rows[raw.rows.length - 1].contract,
9680
+ obligationId: raw.rows[raw.rows.length - 1].obligation_id
9378
9681
  })).toString("base64url") : null;
9379
9682
  return {
9380
9683
  positions: raw.rows.map((row) => ({
@@ -9382,6 +9685,7 @@ const create$4 = (db) => {
9382
9685
  contract: row.contract,
9383
9686
  user: row.user,
9384
9687
  blockNumber: row.block_number,
9688
+ obligationId: row.obligation_id,
9385
9689
  reserved: BigInt(row.reserved_balance.split(".")[0] ?? "0")
9386
9690
  })),
9387
9691
  nextCursor
@@ -9660,7 +9964,10 @@ function create$1(db) {
9660
9964
 
9661
9965
  //#endregion
9662
9966
  //#region src/database/Database.ts
9663
- var Database_exports = /* @__PURE__ */ __exportAll({ connect: () => connect$1 });
9967
+ var Database_exports = /* @__PURE__ */ __exportAll({
9968
+ connect: () => connect$1,
9969
+ getSchemaNamesForMigration: () => getSchemaNamesForMigration
9970
+ });
9664
9971
  function createDomains(core, chainRegistry) {
9665
9972
  return {
9666
9973
  book: create$12({ db: core }),
@@ -9757,6 +10064,7 @@ function augmentWithDomains(base, chainRegistry) {
9757
10064
  return wrapped;
9758
10065
  }
9759
10066
  const InMemoryDbMap = /* @__PURE__ */ new Map();
10067
+ const LEGACY_SCHEMA_START_MINOR = 7;
9760
10068
  /**
9761
10069
  * Connect to the database.
9762
10070
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -9811,9 +10119,26 @@ function applyMigrations(kind, driver) {
9811
10119
  async function preMigrate(driver) {
9812
10120
  const tracer = getTracer("db.preMigrate");
9813
10121
  await startActiveSpan(tracer, "db.preMigrate", async () => {
9814
- await driver.execute(`create schema if not exists "${VERSION}"`);
10122
+ const schemaNames = getSchemaNamesForMigration(VERSION);
10123
+ for (const schemaName of schemaNames) await driver.execute(`create schema if not exists "${schemaName}"`);
9815
10124
  });
9816
10125
  }
10126
+ /**
10127
+ * Build the list of router schemas that should exist before running migrations.
10128
+ * @param version - Current schema version (e.g. `router_v1.8`).
10129
+ * @returns Ordered schema names from `router_v1.7` to current, or just current if parsing fails.
10130
+ */
10131
+ function getSchemaNamesForMigration(version) {
10132
+ const parsed = /^router_v(?<major>\d+)\.(?<minor>\d+)$/.exec(version);
10133
+ if (!parsed?.groups?.major || !parsed.groups.minor) return [version];
10134
+ const major = Number.parseInt(parsed.groups.major, 10);
10135
+ const currentMinor = Number.parseInt(parsed.groups.minor, 10);
10136
+ if (!Number.isInteger(major) || !Number.isInteger(currentMinor)) return [version];
10137
+ if (currentMinor < LEGACY_SCHEMA_START_MINOR) return [version];
10138
+ const schemaNames = [];
10139
+ for (let minor = LEGACY_SCHEMA_START_MINOR; minor <= currentMinor; minor += 1) schemaNames.push(`router_v${major}.${minor}`);
10140
+ return schemaNames;
10141
+ }
9817
10142
  async function postMigrate(driver) {
9818
10143
  const tracer = getTracer("db.postMigrate");
9819
10144
  await startActiveSpan(tracer, "db.postMigrate", async () => {
@@ -10083,15 +10408,16 @@ async function postMigrate(driver) {
10083
10408
  RETURNS trigger
10084
10409
  LANGUAGE plpgsql AS $$
10085
10410
  BEGIN
10086
- INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
10411
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", obligation_id, value)
10087
10412
  VALUES (
10088
10413
  OLD.chain_id,
10089
10414
  OLD."user",
10090
10415
  OLD.contract,
10091
10416
  OLD."group",
10417
+ OLD.obligation_id,
10092
10418
  OLD.upper::numeric - OLD.lower::numeric
10093
10419
  )
10094
- ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
10420
+ ON CONFLICT (chain_id, "user", contract, "group", obligation_id) DO NOTHING;
10095
10421
  RETURN OLD;
10096
10422
  END;
10097
10423
  $$;
@@ -10345,10 +10671,11 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
10345
10671
  amountMutualExclusivity: () => amountMutualExclusivity,
10346
10672
  callback: () => callback,
10347
10673
  chains: () => chains,
10674
+ collateralToken: () => collateralToken,
10675
+ loanToken: () => loanToken,
10348
10676
  maturity: () => maturity,
10349
10677
  oracle: () => oracle,
10350
10678
  sameMaker: () => sameMaker,
10351
- token: () => token,
10352
10679
  validity: () => validity
10353
10680
  });
10354
10681
  /**
@@ -10376,15 +10703,25 @@ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy
10376
10703
  if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
10377
10704
  });
10378
10705
  /**
10379
- * A validation rule that checks if the offer's tokens are allowed for its chain.
10380
- * @param assetsByChainId - Allowed assets indexed by chain id.
10706
+ * A validation rule that checks if the offer's loan token is allowed for its chain.
10707
+ * @param assetsByChainId - Allowed loan tokens indexed by chain id.
10708
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
10709
+ */
10710
+ const loanToken = ({ assetsByChainId }) => single("loan_token", "Validates that offer loan token is in the allowed token list for the offer chain", (offer) => {
10711
+ const allowedLoanTokens = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
10712
+ if (!allowedLoanTokens || allowedLoanTokens.length === 0) return { message: `No allowed loan tokens for chain ${offer.chainId}` };
10713
+ if (!allowedLoanTokens.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
10714
+ });
10715
+ /**
10716
+ * A validation rule that checks if the offer's collateral tokens are allowed for its chain.
10717
+ * @param collateralAssetsByChainId - Allowed collateral tokens indexed by chain id.
10381
10718
  * @returns The issue that was found. If the offer is valid, this will be undefined.
10382
10719
  */
10383
- const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
10384
- const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
10385
- if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
10386
- if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
10387
- if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
10720
+ const collateralToken = ({ collateralAssetsByChainId }) => single("collateral_token", "Validates that offer collateral tokens are in the allowed token list for the offer chain", (offer) => {
10721
+ const allowedCollateralTokens = collateralAssetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase()) ?? [];
10722
+ if (allowedCollateralTokens.length === 0) return { message: `No allowed collateral tokens for chain ${offer.chainId}` };
10723
+ if (offer.collaterals.length === 0) return { message: "At least one collateral token is required" };
10724
+ if (offer.collaterals.some((collateral) => !allowedCollateralTokens.includes(collateral.asset.toLowerCase()))) return { message: "Collateral token is not allowed" };
10388
10725
  });
10389
10726
  /**
10390
10727
  * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
@@ -10427,9 +10764,11 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
10427
10764
  //#region src/gatekeeper/morphoRules.ts
10428
10765
  const morphoRules = (chains$3) => {
10429
10766
  const assetsByChainId = {};
10767
+ const collateralAssetsByChainId = {};
10430
10768
  const oraclesByChainId = {};
10431
10769
  for (const chain of chains$3) {
10432
10770
  assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
10771
+ collateralAssetsByChainId[chain.id] = collateralAssets[chain.id.toString()] ?? [];
10433
10772
  oraclesByChainId[chain.id] = oracles[chain.id.toString()] ?? [];
10434
10773
  }
10435
10774
  return [
@@ -10441,7 +10780,8 @@ const morphoRules = (chains$3) => {
10441
10780
  callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
10442
10781
  allowedAddresses: []
10443
10782
  }),
10444
- token({ assetsByChainId }),
10783
+ loanToken({ assetsByChainId }),
10784
+ collateralToken({ collateralAssetsByChainId }),
10445
10785
  oracle({ oraclesByChainId })
10446
10786
  ];
10447
10787
  };
@@ -10624,5 +10964,5 @@ var mempool_exports = /* @__PURE__ */ __exportAll({
10624
10964
  });
10625
10965
 
10626
10966
  //#endregion
10627
- export { Abi_exports as Abi, BookResponse_exports as BookResponse, BooksController, BrandTypeId, Callback_exports as Callback, Chain_exports as Chain, ChainHealth, ChainRegistry_exports as ChainRegistry, ChainsHealthResponse, Collateral_exports as Collateral, CollectorHealth, CollectorsHealthResponse, ConfigContractsController, ConfigRulesController, Database_exports as Database, ERC4626_exports as ERC4626, Errors_exports as Errors, Format_exports as Format, Gatekeeper_exports as Gatekeeper, Client_exports as GatekeeperClient, Health_exports as Health, HealthController, Indexer_exports as Indexer, LLTV_exports as LLTV, Liquidity_exports as Liquidity, Logger_exports as Logger, Maturity_exports as Maturity, mempool_exports as Mempool, Obligation_exports as Obligation, ObligationResponse_exports as ObligationResponse, ObligationsController, Offer_exports as Offer, OfferResponse_exports as OfferResponse, OffersController, drizzle_exports as OffersSchema, OpenApi, Oracle_exports as Oracle, Position_exports as Position, PositionResponse_exports as PositionResponse, Quote_exports as Quote, RouterApi_exports as RouterApi, Client_exports$1 as RouterClient, RouterStatusResponse, Rules_exports as Rules, time_exports as Time, TradingFee_exports as TradingFee, Transfer_exports as Transfer, Tree_exports as Tree, UsersController, utils_exports as Utils, ValidateController, Gate_exports as Validation, morphoRules, parse, safeParse };
10967
+ export { Abi_exports as Abi, BookResponse_exports as BookResponse, BooksController, BrandTypeId, Callback_exports as Callback, Chain_exports as Chain, ChainHealth, ChainRegistry_exports as ChainRegistry, ChainsHealthResponse, Collateral_exports as Collateral, CollectorHealth, CollectorsHealthResponse, ConfigContractsController, ConfigRulesController, Database_exports as Database, ERC4626_exports as ERC4626, Errors_exports as Errors, Format_exports as Format, Gatekeeper_exports as Gatekeeper, Client_exports as GatekeeperClient, Health_exports as Health, HealthController, Indexer_exports as Indexer, LLTV_exports as LLTV, Liquidity_exports as Liquidity, Logger_exports as Logger, Maturity_exports as Maturity, mempool_exports as Mempool, Obligation_exports as Obligation, ObligationResponse_exports as ObligationResponse, ObligationsController, Offer_exports as Offer, OfferResponse_exports as OfferResponse, OffersController, drizzle_exports as OffersSchema, OpenApi, Oracle_exports as Oracle, Position_exports as Position, PositionResponse_exports as PositionResponse, Quote_exports as Quote, RouterApi_exports as RouterApi, Client_exports$1 as RouterClient, RouterStatusResponse, Rules_exports as Rules, Tick_exports as Tick, time_exports as Time, TradingFee_exports as TradingFee, Transfer_exports as Transfer, Tree_exports as Tree, UsersController, utils_exports as Utils, ValidateController, Gate_exports as Validation, morphoRules, parse, safeParse };
10628
10968
  //# sourceMappingURL=index.node.mjs.map