@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
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.7.1";
155
+ var version = "0.8.0";
156
156
  var description = "Router package for Morpho protocol";
157
157
 
158
158
  //#endregion
@@ -331,8 +331,8 @@ const chains$2 = {
331
331
  name: "ethereum-virtual-testnet",
332
332
  custom: {
333
333
  morpho: {
334
- address: "0x11a002d45db720ed47a80d2f3489cba5b833eaf5",
335
- blockCreated: 0
334
+ address: "0x634b095371e4e45feed94c1a45c37798e173ea50",
335
+ blockCreated: 23226700
336
336
  },
337
337
  morphoBlue: {
338
338
  address: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
@@ -561,7 +561,7 @@ async function deployContract(client, account, bytecode, name) {
561
561
  */
562
562
  async function startAnvil(parameters) {
563
563
  const args = buildAnvilArgs(parameters);
564
- const logFd = fs.openSync(parameters.logPath, "a");
564
+ const logFd = fs.openSync(parameters.logPath, "w");
565
565
  const subprocess = spawn("anvil", args, {
566
566
  detached: true,
567
567
  stdio: [
@@ -1640,6 +1640,33 @@ const assets = {
1640
1640
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
1641
1641
  ]
1642
1642
  };
1643
+ const collateralAssets = {
1644
+ [ChainId.ETHEREUM.toString()]: [
1645
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1646
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
1647
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1648
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
1649
+ ],
1650
+ [ChainId.BASE.toString()]: [
1651
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
1652
+ "0x4200000000000000000000000000000000000006",
1653
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
1654
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
1655
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
1656
+ ],
1657
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
1658
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1659
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
1660
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1661
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
1662
+ ],
1663
+ [ChainId.ANVIL.toString()]: [
1664
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
1665
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
1666
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
1667
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
1668
+ ]
1669
+ };
1643
1670
  const oracles$1 = {
1644
1671
  [ChainId.ETHEREUM.toString()]: [
1645
1672
  "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
@@ -1714,6 +1741,61 @@ const MetaMorpho = parseAbi([
1714
1741
  //#region src/core/Abi/MetaMorphoFactory.ts
1715
1742
  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)"]);
1716
1743
 
1744
+ //#endregion
1745
+ //#region src/core/Abi/MorphoV2.ts
1746
+ const MorphoV2 = parseAbi([
1747
+ "constructor()",
1748
+ "function collateralOf(bytes32 id, address user, address collateralToken) view returns (uint256)",
1749
+ "function consume(bytes32 group, uint256 amount)",
1750
+ "function consumed(address user, bytes32 group) view returns (uint256)",
1751
+ "function debtOf(bytes32 id, address user) view returns (uint256)",
1752
+ "function defaultFees(address loanToken, uint256 index) view returns (uint16)",
1753
+ "function feeSetter() view returns (address)",
1754
+ "function fees(bytes32 id) view returns (uint16[6])",
1755
+ "function flashLoan(address token, uint256 assets, address callback, bytes data)",
1756
+ "function isHealthy((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, bytes32 id, address borrower) view returns (bool)",
1757
+ "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)[])",
1758
+ "function multicall(bytes[] calls)",
1759
+ "function obligationCreated(bytes32 id) view returns (bool)",
1760
+ "function obligationState(bytes32 id) view returns (uint128 totalUnits, uint128 totalShares, uint256 withdrawable, bool created)",
1761
+ "function owner() view returns (address)",
1762
+ "function repay((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, uint256 obligationUnits, address onBehalf)",
1763
+ "function session(address user) view returns (bytes32)",
1764
+ "function setDefaultTradingFee(address loanToken, uint256 index, uint256 newTradingFee)",
1765
+ "function setFeeSetter(address newFeeSetter)",
1766
+ "function setObligationTradingFee(bytes32 id, uint256 index, uint256 newTradingFee)",
1767
+ "function setOwner(address newOwner)",
1768
+ "function setTradingFeeRecipient(address recipient)",
1769
+ "function sharesOf(bytes32 id, address user) view returns (uint256)",
1770
+ "function shuffleSession()",
1771
+ "function supplyCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, address collateral, uint256 assets, address onBehalf)",
1772
+ "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)",
1773
+ "function totalShares(bytes32 id) view returns (uint256)",
1774
+ "function totalUnits(bytes32 id) view returns (uint256)",
1775
+ "function touchObligation((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation) returns (bytes32)",
1776
+ "function tradingFee(bytes32 id, uint256 timeToMaturity) view returns (uint256)",
1777
+ "function tradingFeeRecipient() view returns (address)",
1778
+ "function withdraw((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, uint256 obligationUnits, uint256 shares, address onBehalf) returns (uint256, uint256)",
1779
+ "function withdrawCollateral((address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation, address collateral, uint256 assets, address onBehalf)",
1780
+ "function withdrawable(bytes32 id) view returns (uint256)",
1781
+ "event Constructor(address indexed owner)",
1782
+ "event Consume(address indexed user, bytes32 indexed group, uint256 amount)",
1783
+ "event FlashLoan(address indexed caller, address indexed token, uint256 assets)",
1784
+ "event Liquidate(address indexed caller, bytes32 indexed id, (uint256 collateralIndex, uint256 repaid, uint256 seized)[] seizures, address indexed borrower, uint256 totalRepaid, uint256 badDebt)",
1785
+ "event ObligationCreated(bytes32 indexed id, (address loanToken, (address token, uint256 lltv, address oracle)[] collaterals, uint256 maturity) obligation)",
1786
+ "event Repay(address indexed caller, bytes32 indexed id, uint256 obligationUnits, address indexed onBehalf)",
1787
+ "event SetDefaultTradingFee(address indexed loanToken, uint256 indexed index, uint256 newTradingFee)",
1788
+ "event SetFeeSetter(address indexed feeSetter)",
1789
+ "event SetObligationTradingFee(bytes32 indexed id, uint256 indexed index, uint256 newTradingFee)",
1790
+ "event SetOwner(address indexed owner)",
1791
+ "event SetTradingFeeRecipient(address indexed recipient)",
1792
+ "event ShuffleSession(address indexed user, bytes32 session)",
1793
+ "event SupplyCollateral(address caller, bytes32 indexed id, address indexed collateral, uint256 assets, address indexed onBehalf)",
1794
+ "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)",
1795
+ "event Withdraw(address indexed caller, bytes32 indexed id, uint256 obligationUnits, uint256 shares, address indexed onBehalf)",
1796
+ "event WithdrawCollateral(address caller, bytes32 indexed id, address indexed collateral, uint256 assets, address indexed onBehalf)"
1797
+ ]);
1798
+
1717
1799
  //#endregion
1718
1800
  //#region src/core/Abi/index.ts
1719
1801
  const Oracle = [{
@@ -2071,7 +2153,7 @@ const OfferSchema = () => {
2071
2153
  assets: z$2.bigint({ coerce: true }).min(0n).max(maxUint256),
2072
2154
  obligationUnits: z$2.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2073
2155
  obligationShares: z$2.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2074
- price: z$2.bigint({ coerce: true }).min(0n).max(maxUint256),
2156
+ tick: z$2.coerce.number().int().min(0).max(990),
2075
2157
  maturity: MaturitySchema,
2076
2158
  expiry: z$2.number().int().max(Number.MAX_SAFE_INTEGER),
2077
2159
  start: z$2.number().int().max(Number.MAX_SAFE_INTEGER),
@@ -2143,7 +2225,7 @@ const serialize = (offer) => ({
2143
2225
  assets: offer.assets.toString(),
2144
2226
  obligationUnits: offer.obligationUnits.toString(),
2145
2227
  obligationShares: offer.obligationShares.toString(),
2146
- price: offer.price.toString(),
2228
+ tick: offer.tick,
2147
2229
  maturity: Number(offer.maturity),
2148
2230
  expiry: Number(offer.expiry),
2149
2231
  start: Number(offer.start),
@@ -2188,14 +2270,13 @@ function random(config) {
2188
2270
  [.98, 2]
2189
2271
  ]));
2190
2272
  const buy = config?.buy !== void 0 ? config.buy : bool();
2191
- const ONE = 1000000000000000000n;
2192
- const qMin = buy ? 16 : 4;
2193
- const len = (buy ? 32 : 16) - qMin + 1;
2194
- const pricePairs = Array.from({ length: len }, (_, idx) => {
2195
- const q = qMin + idx;
2196
- return [BigInt(q) * (ONE / 4n), buy ? 1 + idx : 1 + (len - 1 - idx)];
2273
+ const tickMin = buy ? 0 : 495;
2274
+ const len = (buy ? 495 : 990) - tickMin + 1;
2275
+ const tickPairs = Array.from({ length: len }, (_, idx) => {
2276
+ const weight = buy ? 1 + idx : 1 + (len - 1 - idx);
2277
+ return [tickMin + idx, weight];
2197
2278
  });
2198
- const price = config?.price ?? weightedChoice(pricePairs);
2279
+ const tick = config?.tick ?? weightedChoice(tickPairs);
2199
2280
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
2200
2281
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2201
2282
  const amountBase = BigInt(100 + int(999901));
@@ -2209,7 +2290,7 @@ function random(config) {
2209
2290
  assets: assetsScaled,
2210
2291
  obligationUnits: config?.obligationUnits ?? 0n,
2211
2292
  obligationShares: config?.obligationShares ?? 0n,
2212
- price,
2293
+ tick,
2213
2294
  maturity,
2214
2295
  expiry: config?.expiry ?? maturity - 1,
2215
2296
  start: config?.start ?? maturity - 10,
@@ -2274,7 +2355,7 @@ const types = {
2274
2355
  type: "uint256"
2275
2356
  },
2276
2357
  {
2277
- name: "price",
2358
+ name: "tick",
2278
2359
  type: "uint256"
2279
2360
  },
2280
2361
  {
@@ -2342,7 +2423,7 @@ function hash(offer) {
2342
2423
  assets: offer.assets,
2343
2424
  obligationUnits: offer.obligationUnits,
2344
2425
  obligationShares: offer.obligationShares,
2345
- price: offer.price,
2426
+ tick: BigInt(offer.tick),
2346
2427
  maturity: BigInt(offer.maturity),
2347
2428
  expiry: BigInt(offer.expiry),
2348
2429
  group: offer.group,
@@ -2607,6 +2688,49 @@ var InvalidQuoteError = class extends BaseError {
2607
2688
  }
2608
2689
  };
2609
2690
 
2691
+ //#endregion
2692
+ //#region src/core/Tick.ts
2693
+ /** ln(1 + 0.025), scaled by 1e18. Matches TickLib onchain constant. */
2694
+ const LN_ONE_PLUS_DELTA = 24692612590371501n;
2695
+ /** ln(2), scaled by 1e18. Matches TickLib onchain constant. */
2696
+ const LN2 = 693147180559945309n;
2697
+ const WAD$1 = 10n ** 18n;
2698
+ const WAD_SQUARED = 10n ** 36n;
2699
+ const PRICE_STEP = 10n ** 13n;
2700
+ const HALF_TICK_RANGE = 495n;
2701
+ /** Tick domain supported by Morpho V2. */
2702
+ const TICK_RANGE = 990;
2703
+ /**
2704
+ * Converts a tick to a wad price using the same approximation and rounding as TickLib.
2705
+ * @param tick - Tick value in the inclusive range [0, 990].
2706
+ * @returns The price in wad units.
2707
+ * @throws {@link InvalidTickError} If tick is not an integer in range [0, 990].
2708
+ */
2709
+ function tickToPrice(tick) {
2710
+ assertTick(tick);
2711
+ return divHalfDownUnchecked(divHalfDownUnchecked(WAD_SQUARED, WAD$1 + wExp(LN_ONE_PLUS_DELTA * (HALF_TICK_RANGE - BigInt(tick)))), PRICE_STEP) * PRICE_STEP;
2712
+ }
2713
+ function divHalfDownUnchecked(x, d) {
2714
+ return (x + (d - 1n) / 2n) / d;
2715
+ }
2716
+ function wExp(x) {
2717
+ if (x < 0n) return WAD_SQUARED / wExp(-x);
2718
+ const q = (x + LN2 / 2n) / LN2;
2719
+ const r = x - q * LN2;
2720
+ const secondTerm = r * r / (2n * WAD$1);
2721
+ const thirdTerm = secondTerm * r / (3n * WAD$1);
2722
+ return WAD$1 + r + secondTerm + thirdTerm << q;
2723
+ }
2724
+ function assertTick(tick) {
2725
+ if (!Number.isInteger(tick) || tick < 0 || tick > TICK_RANGE) throw new InvalidTickError(tick);
2726
+ }
2727
+ var InvalidTickError = class extends BaseError {
2728
+ name = "Tick.InvalidTickError";
2729
+ constructor(tick) {
2730
+ super(`Invalid tick: ${tick}. Tick must be an integer between 0 and ${TICK_RANGE}.`);
2731
+ }
2732
+ };
2733
+
2610
2734
  //#endregion
2611
2735
  //#region src/core/TradingFee.ts
2612
2736
  /** WAD constant (1e18) for fee scaling. */
@@ -2895,15 +3019,25 @@ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy
2895
3019
  if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
2896
3020
  });
2897
3021
  /**
2898
- * A validation rule that checks if the offer's tokens are allowed for its chain.
2899
- * @param assetsByChainId - Allowed assets indexed by chain id.
3022
+ * A validation rule that checks if the offer's loan token is allowed for its chain.
3023
+ * @param assetsByChainId - Allowed loan tokens indexed by chain id.
2900
3024
  * @returns The issue that was found. If the offer is valid, this will be undefined.
2901
3025
  */
2902
- const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
2903
- const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
2904
- if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
2905
- if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
2906
- if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
3026
+ const loanToken = ({ assetsByChainId }) => single("loan_token", "Validates that offer loan token is in the allowed token list for the offer chain", (offer) => {
3027
+ const allowedLoanTokens = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
3028
+ if (!allowedLoanTokens || allowedLoanTokens.length === 0) return { message: `No allowed loan tokens for chain ${offer.chainId}` };
3029
+ if (!allowedLoanTokens.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
3030
+ });
3031
+ /**
3032
+ * A validation rule that checks if the offer's collateral tokens are allowed for its chain.
3033
+ * @param collateralAssetsByChainId - Allowed collateral tokens indexed by chain id.
3034
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
3035
+ */
3036
+ const collateralToken = ({ collateralAssetsByChainId }) => single("collateral_token", "Validates that offer collateral tokens are in the allowed token list for the offer chain", (offer) => {
3037
+ const allowedCollateralTokens = collateralAssetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase()) ?? [];
3038
+ if (allowedCollateralTokens.length === 0) return { message: `No allowed collateral tokens for chain ${offer.chainId}` };
3039
+ if (offer.collaterals.length === 0) return { message: "At least one collateral token is required" };
3040
+ if (offer.collaterals.some((collateral) => !allowedCollateralTokens.includes(collateral.asset.toLowerCase()))) return { message: "Collateral token is not allowed" };
2907
3041
  });
2908
3042
  /**
2909
3043
  * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
@@ -2946,9 +3080,11 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
2946
3080
  //#region src/gatekeeper/morphoRules.ts
2947
3081
  const morphoRules = (chains) => {
2948
3082
  const assetsByChainId = {};
3083
+ const collateralAssetsByChainId = {};
2949
3084
  const oraclesByChainId = {};
2950
3085
  for (const chain of chains) {
2951
3086
  assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
3087
+ collateralAssetsByChainId[chain.id] = collateralAssets[chain.id.toString()] ?? [];
2952
3088
  oraclesByChainId[chain.id] = oracles$1[chain.id.toString()] ?? [];
2953
3089
  }
2954
3090
  return [
@@ -2960,7 +3096,8 @@ const morphoRules = (chains) => {
2960
3096
  callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
2961
3097
  allowedAddresses: []
2962
3098
  }),
2963
- token({ assetsByChainId }),
3099
+ loanToken({ assetsByChainId }),
3100
+ collateralToken({ collateralAssetsByChainId }),
2964
3101
  oracle({ oraclesByChainId })
2965
3102
  ];
2966
3103
  };
@@ -2968,7 +3105,7 @@ const morphoRules = (chains) => {
2968
3105
  //#endregion
2969
3106
  //#region src/gatekeeper/ConfigRules.ts
2970
3107
  /**
2971
- * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
3108
+ * Build the configured rules (maturities + callback addresses + loan tokens + collateral tokens + oracles) for the provided chains.
2972
3109
  * @param chains - Chains to include in the configured rules.
2973
3110
  * @returns Sorted list of config rules.
2974
3111
  */
@@ -2988,6 +3125,12 @@ function buildConfigRules(chains) {
2988
3125
  chain_id: chain.id,
2989
3126
  address: normalizeAddress(address)
2990
3127
  });
3128
+ const collateralTokens = collateralAssets[chain.id.toString()] ?? [];
3129
+ for (const address of collateralTokens) rules.push({
3130
+ type: "collateral_token",
3131
+ chain_id: chain.id,
3132
+ address: normalizeAddress(address)
3133
+ });
2991
3134
  const oracles = oracles$1[chain.id.toString()] ?? [];
2992
3135
  for (const address of oracles) rules.push({
2993
3136
  type: "oracle",
@@ -3015,6 +3158,10 @@ function buildConfigRulesChecksum(rules) {
3015
3158
  hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
3016
3159
  continue;
3017
3160
  }
3161
+ if (rule.type === "collateral_token") {
3162
+ hash.update(`collateral_token:${rule.chain_id}:${rule.address}\n`);
3163
+ continue;
3164
+ }
3018
3165
  if (rule.type === "oracle") {
3019
3166
  hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
3020
3167
  continue;
@@ -3035,6 +3182,7 @@ function compareConfigRules(left, right) {
3035
3182
  return left.address.localeCompare(right.address);
3036
3183
  }
3037
3184
  if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
3185
+ if (left.type === "collateral_token" && right.type === "collateral_token") return left.address.localeCompare(right.address);
3038
3186
  if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
3039
3187
  return 0;
3040
3188
  }
@@ -3042,8 +3190,10 @@ function compareConfigRules(left, right) {
3042
3190
  //#endregion
3043
3191
  //#region src/api/Schema/BookResponse.ts
3044
3192
  function from$6(level) {
3193
+ const price = tickToPrice(level.tick);
3045
3194
  return {
3046
- price: level.price.toString(),
3195
+ tick: level.tick,
3196
+ price: price.toString(),
3047
3197
  assets: level.assets.toString(),
3048
3198
  count: level.count
3049
3199
  };
@@ -3109,6 +3259,16 @@ function from$5(obligation, quote) {
3109
3259
 
3110
3260
  //#endregion
3111
3261
  //#region src/api/Schema/OfferResponse.ts
3262
+ function normalizeChainId(chainId) {
3263
+ const parsedChainId = Number(chainId);
3264
+ if (!Number.isInteger(parsedChainId) || parsedChainId <= 0) throw new Error(`Invalid chain id: ${String(chainId)}`);
3265
+ return parsedChainId;
3266
+ }
3267
+ function normalizeBlockNumber(blockNumber) {
3268
+ const parsedBlockNumber = Number(blockNumber);
3269
+ if (!Number.isInteger(parsedBlockNumber) || parsedBlockNumber < 0) throw new Error(`Invalid block number: ${String(blockNumber)}`);
3270
+ return parsedBlockNumber;
3271
+ }
3112
3272
  /**
3113
3273
  * Creates an `OfferResponse` matching the Solidity Offer struct layout.
3114
3274
  * @constructor
@@ -3116,6 +3276,8 @@ function from$5(obligation, quote) {
3116
3276
  * @returns The created `OfferResponse`. {@link OfferResponse}
3117
3277
  */
3118
3278
  function from$4(input) {
3279
+ const chainId = normalizeChainId(input.chainId);
3280
+ const blockNumber = normalizeBlockNumber(input.blockNumber);
3119
3281
  const base = {
3120
3282
  offer: {
3121
3283
  obligation: {
@@ -3134,7 +3296,7 @@ function from$4(input) {
3134
3296
  obligation_shares: input.obligationShares.toString(),
3135
3297
  start: input.start,
3136
3298
  expiry: input.expiry,
3137
- price: input.price.toString(),
3299
+ tick: input.tick,
3138
3300
  group: input.group,
3139
3301
  session: input.session,
3140
3302
  callback: input.callback.address,
@@ -3142,15 +3304,15 @@ function from$4(input) {
3142
3304
  },
3143
3305
  offer_hash: input.hash,
3144
3306
  obligation_id: id({
3145
- chainId: input.chainId,
3307
+ chainId,
3146
3308
  loanToken: input.loanToken,
3147
3309
  collaterals: [...input.collaterals],
3148
3310
  maturity: input.maturity
3149
3311
  }),
3150
- chain_id: input.chainId,
3312
+ chain_id: chainId,
3151
3313
  consumed: input.consumed.toString(),
3152
3314
  takeable: input.takeable.toString(),
3153
- block_number: input.blockNumber
3315
+ block_number: blockNumber
3154
3316
  };
3155
3317
  if (!input.proof || !input.root || !input.signature) return {
3156
3318
  ...base,
@@ -3296,7 +3458,7 @@ const offerExample = {
3296
3458
  obligation_shares: "0",
3297
3459
  start: 1761922790,
3298
3460
  expiry: 1761922799,
3299
- price: "2750000000000000000",
3461
+ tick: 495,
3300
3462
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
3301
3463
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
3302
3464
  callback: "0x0000000000000000000000000000000000000000",
@@ -3337,7 +3499,7 @@ const validateOfferExample = {
3337
3499
  assets: "369216000000000000000000",
3338
3500
  obligation_units: "0",
3339
3501
  obligation_shares: "0",
3340
- price: "2750000000000000000",
3502
+ tick: 495,
3341
3503
  maturity: 1761922799,
3342
3504
  expiry: 1761922799,
3343
3505
  start: 1761922790,
@@ -3481,9 +3643,11 @@ __decorate([ApiProperty({
3481
3643
  example: offerExample.offer.expiry
3482
3644
  })], OfferDataResponse.prototype, "expiry", void 0);
3483
3645
  __decorate([ApiProperty({
3484
- type: "string",
3485
- example: offerExample.offer.price
3486
- })], OfferDataResponse.prototype, "price", void 0);
3646
+ type: "number",
3647
+ example: offerExample.offer.tick,
3648
+ minimum: 0,
3649
+ maximum: 990
3650
+ })], OfferDataResponse.prototype, "tick", void 0);
3487
3651
  __decorate([ApiProperty({
3488
3652
  type: "string",
3489
3653
  example: offerExample.offer.group
@@ -3724,9 +3888,11 @@ __decorate([ApiProperty({
3724
3888
  required: false
3725
3889
  })], ValidateOfferRequest.prototype, "obligation_shares", void 0);
3726
3890
  __decorate([ApiProperty({
3727
- type: "string",
3728
- example: validateOfferExample.price
3729
- })], ValidateOfferRequest.prototype, "price", void 0);
3891
+ type: "number",
3892
+ example: validateOfferExample.tick,
3893
+ minimum: 0,
3894
+ maximum: 990
3895
+ })], ValidateOfferRequest.prototype, "tick", void 0);
3730
3896
  __decorate([ApiProperty({
3731
3897
  type: "number",
3732
3898
  example: validateOfferExample.maturity
@@ -3826,9 +3992,16 @@ __decorate([ApiProperty({
3826
3992
  description: "List of validation issues. Returned when any offer fails validation."
3827
3993
  })], ValidationFailureResponse.prototype, "data", void 0);
3828
3994
  var BookLevelResponse = class {};
3995
+ __decorate([ApiProperty({
3996
+ type: "number",
3997
+ example: 495,
3998
+ minimum: 0,
3999
+ maximum: 990
4000
+ })], BookLevelResponse.prototype, "tick", void 0);
3829
4001
  __decorate([ApiProperty({
3830
4002
  type: "string",
3831
- example: "2750000000000000000"
4003
+ example: "500000000000000000",
4004
+ description: "Price derived from tick, scaled by 1e18."
3832
4005
  })], BookLevelResponse.prototype, "price", void 0);
3833
4006
  __decorate([ApiProperty({
3834
4007
  type: "string",
@@ -3842,6 +4015,7 @@ const positionExample = {
3842
4015
  chain_id: 1,
3843
4016
  contract: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
3844
4017
  user: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
4018
+ obligation_id: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
3845
4019
  reserved: "200000000000000000000",
3846
4020
  block_number: 21345678
3847
4021
  };
@@ -3858,6 +4032,12 @@ __decorate([ApiProperty({
3858
4032
  type: "string",
3859
4033
  example: positionExample.user
3860
4034
  })], PositionListItemResponse.prototype, "user", void 0);
4035
+ __decorate([ApiProperty({
4036
+ type: "string",
4037
+ nullable: true,
4038
+ example: positionExample.obligation_id,
4039
+ description: "Obligation id this reserved amount belongs to, or null if no lots exist."
4040
+ })], PositionListItemResponse.prototype, "obligation_id", void 0);
3861
4041
  __decorate([ApiProperty({
3862
4042
  type: "string",
3863
4043
  example: positionExample.reserved
@@ -3885,7 +4065,7 @@ __decorate([ApiProperty({
3885
4065
  })], BookListResponse.prototype, "cursor", void 0);
3886
4066
  __decorate([ApiProperty({
3887
4067
  type: () => [BookLevelResponse],
3888
- description: "Aggregated book levels grouped by computed price."
4068
+ description: "Aggregated book levels grouped by offer tick."
3889
4069
  })], BookListResponse.prototype, "data", void 0);
3890
4070
  let BooksController = class BooksController {
3891
4071
  async getBook() {}
@@ -3895,7 +4075,7 @@ __decorate([
3895
4075
  methods: ["get"],
3896
4076
  path: "/v1/books/{obligationId}/{side}",
3897
4077
  summary: "Get aggregated book",
3898
- 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)."
4078
+ 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)."
3899
4079
  }),
3900
4080
  ApiParam({
3901
4081
  name: "obligationId",
@@ -3920,7 +4100,7 @@ __decorate([
3920
4100
  name: "limit",
3921
4101
  type: "number",
3922
4102
  example: 10,
3923
- description: "Maximum number of price levels to return."
4103
+ description: "Maximum number of tick levels to return."
3924
4104
  }),
3925
4105
  ApiResponse({
3926
4106
  status: 200,
@@ -4114,6 +4294,11 @@ const configRulesLoanTokenExample = {
4114
4294
  chain_id: 1,
4115
4295
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
4116
4296
  };
4297
+ const configRulesCollateralTokenExample = {
4298
+ type: "collateral_token",
4299
+ chain_id: 1,
4300
+ address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
4301
+ };
4117
4302
  const configRulesOracleExample = {
4118
4303
  type: "oracle",
4119
4304
  chain_id: 1,
@@ -4123,6 +4308,7 @@ const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
4123
4308
  const configRulesPayloadExample = [
4124
4309
  configRulesMaturityExample,
4125
4310
  configRulesLoanTokenExample,
4311
+ configRulesCollateralTokenExample,
4126
4312
  configRulesOracleExample
4127
4313
  ];
4128
4314
  const configContractNames = [
@@ -4249,7 +4435,7 @@ __decorate([
4249
4435
  methods: ["get"],
4250
4436
  path: "/v1/config/rules",
4251
4437
  summary: "Get config rules",
4252
- description: "Returns configured rules (maturities, loan tokens, oracles) for supported chains."
4438
+ description: "Returns configured rules (maturities, loan tokens, collateral tokens, oracles) for supported chains."
4253
4439
  }),
4254
4440
  ApiQuery({
4255
4441
  name: "cursor",
@@ -4269,7 +4455,7 @@ __decorate([
4269
4455
  name: "types",
4270
4456
  type: ["string"],
4271
4457
  required: false,
4272
- example: "maturity,loan_token,oracle",
4458
+ example: "maturity,loan_token,collateral_token,oracle",
4273
4459
  description: "Filter by rule types (comma-separated).",
4274
4460
  style: "form",
4275
4461
  explode: false
@@ -4387,7 +4573,7 @@ __decorate([
4387
4573
  methods: ["get"],
4388
4574
  path: "/v1/users/{userAddress}/positions",
4389
4575
  summary: "Get user positions",
4390
- description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
4576
+ 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."
4391
4577
  }),
4392
4578
  ApiParam({
4393
4579
  name: "userAddress",
@@ -4474,6 +4660,7 @@ function from$3(position) {
4474
4660
  chain_id: position.chainId,
4475
4661
  contract: position.contract,
4476
4662
  user: position.user,
4663
+ obligation_id: position.obligationId,
4477
4664
  reserved: position.reserved.toString(),
4478
4665
  block_number: position.blockNumber
4479
4666
  };
@@ -4527,10 +4714,11 @@ const ConfigRuleTypes = z$2.enum([
4527
4714
  "maturity",
4528
4715
  "callback",
4529
4716
  "loan_token",
4717
+ "collateral_token",
4530
4718
  "oracle"
4531
4719
  ]);
4532
4720
  const GetConfigRulesQueryParams = z$2.object({
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({
4721
+ cursor: z$2.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({
4534
4722
  description: "Pagination cursor in type:chain_id:<value> format",
4535
4723
  example: "maturity:1:1730415600:end_of_next_month"
4536
4724
  }),
@@ -4540,7 +4728,7 @@ const GetConfigRulesQueryParams = z$2.object({
4540
4728
  }),
4541
4729
  types: csvArray(ConfigRuleTypes).meta({
4542
4730
  description: "Filter by rule types (comma-separated).",
4543
- example: "maturity,loan_token,oracle"
4731
+ example: "maturity,loan_token,collateral_token,oracle"
4544
4732
  }),
4545
4733
  chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4546
4734
  description: "Filter by chain IDs (comma-separated).",
@@ -4640,12 +4828,11 @@ const GetObligationParams = z$2.object({ obligation_id: z$2.string({ error: "Obl
4640
4828
  description: "Obligation id",
4641
4829
  example: "0x1234567890123456789012345678901234567890123456789012345678901234"
4642
4830
  }) });
4643
- /** Validate a book cursor format: {side, lastPrice, offersCursor} */
4831
+ /** Validate a book cursor format: {side, lastTick, offersCursor} */
4644
4832
  function isValidBookCursor(cursorString) {
4645
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
4646
4833
  try {
4647
4834
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
4648
- return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
4835
+ return (v?.side === "buy" || v?.side === "sell") && typeof v?.lastTick === "number" && Number.isInteger(v.lastTick) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
4649
4836
  } catch {
4650
4837
  return false;
4651
4838
  }
@@ -4750,6 +4937,7 @@ function formatCursor$1(rule) {
4750
4937
  if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
4751
4938
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
4752
4939
  if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
4940
+ if (rule.type === "collateral_token") return `collateral_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
4753
4941
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
4754
4942
  }
4755
4943
  function parseCursor$1(cursor) {
@@ -4782,7 +4970,7 @@ function parseCursor$1(cursor) {
4782
4970
  address: parseAddress(addressValue, "Cursor address")
4783
4971
  };
4784
4972
  }
4785
- if (type === "loan_token" || type === "oracle") {
4973
+ if (type === "loan_token" || type === "collateral_token" || type === "oracle") {
4786
4974
  const addressValue = rest.join(":");
4787
4975
  if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
4788
4976
  return {
@@ -4809,7 +4997,7 @@ function parseAddress(address, label) {
4809
4997
  return address.toLowerCase();
4810
4998
  }
4811
4999
  function isConfigRuleType(value) {
4812
- return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
5000
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "collateral_token" || value === "oracle";
4813
5001
  }
4814
5002
  function isMaturityType(value) {
4815
5003
  return Object.values(MaturityType).includes(value);
@@ -5082,7 +5270,7 @@ function now() {
5082
5270
 
5083
5271
  //#endregion
5084
5272
  //#region src/database/drizzle/VERSION.ts
5085
- const VERSION = "router_v1.6";
5273
+ const VERSION = "router_v1.8";
5086
5274
 
5087
5275
  //#endregion
5088
5276
  //#region src/database/drizzle/schema.ts
@@ -5234,10 +5422,7 @@ const offers = s.table(EnumTableName.OFFERS, {
5234
5422
  precision: 78,
5235
5423
  scale: 0
5236
5424
  }).notNull().default("0"),
5237
- price: numeric("price", {
5238
- precision: 78,
5239
- scale: 0
5240
- }).notNull(),
5425
+ tick: integer("tick").notNull(),
5241
5426
  maturity: integer("maturity").notNull(),
5242
5427
  expiry: integer("expiry").notNull(),
5243
5428
  start: integer("start").notNull(),
@@ -5302,6 +5487,7 @@ const lots = s.table(EnumTableName.LOTS, {
5302
5487
  user: varchar("user", { length: 42 }).notNull(),
5303
5488
  contract: varchar("contract", { length: 42 }).notNull(),
5304
5489
  group: varchar("group", { length: 66 }).notNull(),
5490
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5305
5491
  lower: numeric("lower", {
5306
5492
  precision: 78,
5307
5493
  scale: 0
@@ -5316,7 +5502,8 @@ const lots = s.table(EnumTableName.LOTS, {
5316
5502
  table.chainId,
5317
5503
  table.user,
5318
5504
  table.contract,
5319
- table.group
5505
+ table.group,
5506
+ table.obligationId
5320
5507
  ],
5321
5508
  name: "lots_pk"
5322
5509
  }),
@@ -5352,6 +5539,7 @@ const offsets = s.table(EnumTableName.OFFSETS, {
5352
5539
  user: varchar("user", { length: 42 }).notNull(),
5353
5540
  contract: varchar("contract", { length: 42 }).notNull(),
5354
5541
  group: varchar("group", { length: 66 }).notNull(),
5542
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5355
5543
  value: numeric("value", {
5356
5544
  precision: 78,
5357
5545
  scale: 0
@@ -5361,7 +5549,8 @@ const offsets = s.table(EnumTableName.OFFSETS, {
5361
5549
  table.chainId,
5362
5550
  table.user,
5363
5551
  table.contract,
5364
- table.group
5552
+ table.group,
5553
+ table.obligationId
5365
5554
  ],
5366
5555
  name: "offsets_pk"
5367
5556
  }), foreignKey({
@@ -5952,6 +6141,7 @@ function decodeCallbacks(parameters) {
5952
6141
  positionContract: loanToken,
5953
6142
  positionUser: offer.maker,
5954
6143
  group: offer.group,
6144
+ obligationId: obligationId(offer),
5955
6145
  size: offer.assets
5956
6146
  });
5957
6147
  callbacks.push({
@@ -7354,6 +7544,7 @@ function create$14(config) {
7354
7544
  return {
7355
7545
  get: async (parameters) => {
7356
7546
  const { side, obligationId, cursor: cursorString, limit = DEFAULT_LIMIT$3 } = parameters;
7547
+ const tickSortDirection = side === "sell" ? "asc" : "desc";
7357
7548
  const inputCursor = LevelCursor.decode(cursorString, logger);
7358
7549
  if (cursorString != null && inputCursor === null) return {
7359
7550
  levels: [],
@@ -7368,23 +7559,23 @@ function create$14(config) {
7368
7559
  cursor: inputCursor?.offersCursor ?? void 0,
7369
7560
  limit: fetchLimit
7370
7561
  });
7371
- const priceMap = /* @__PURE__ */ new Map();
7562
+ const tickMap = /* @__PURE__ */ new Map();
7372
7563
  for (const row of rows) {
7373
- const priceKey = row.price.toString();
7374
- const existing = priceMap.get(priceKey);
7564
+ const existing = tickMap.get(row.tick);
7375
7565
  if (existing) {
7376
7566
  existing.assets += row.takeable;
7377
7567
  existing.count += 1;
7378
- } else priceMap.set(priceKey, {
7568
+ } else tickMap.set(row.tick, {
7379
7569
  assets: row.takeable,
7380
7570
  count: 1
7381
7571
  });
7382
7572
  }
7383
- const levels = Array.from(priceMap.entries()).map(([price, data]) => ({
7384
- price: BigInt(price),
7385
- assets: data.assets,
7386
- count: data.count
7573
+ const levels = Array.from(tickMap.entries()).map(([tick, level]) => ({
7574
+ tick,
7575
+ assets: level.assets,
7576
+ count: level.count
7387
7577
  }));
7578
+ levels.sort((a, b) => tickSortDirection === "asc" ? a.tick - b.tick : b.tick - a.tick);
7388
7579
  const paginatedLevels = levels.slice(0, limit);
7389
7580
  const hasMore = levels.length > limit || offersNextCursor !== null;
7390
7581
  const lastLevel = paginatedLevels[paginatedLevels.length - 1];
@@ -7430,14 +7621,14 @@ async function _getOffers(db, params) {
7430
7621
  AND (s.code IS NULL OR s.code = ${Status.VALID})
7431
7622
  ORDER BY
7432
7623
  o.group_chain_id, o.group_maker, o."group_group",
7433
- o.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, o.block_number ASC, o.assets DESC, o.hash ASC
7624
+ o.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, o.block_number ASC, o.assets DESC, o.hash ASC
7434
7625
  ),
7435
7626
  enriched AS (
7436
7627
  SELECT
7437
7628
  w.*,
7438
7629
  g.consumed, g.chain_id, obl.loan_token,
7439
7630
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
7440
- THEN w.price::numeric ELSE -w.price::numeric END AS price_norm,
7631
+ THEN w.tick ELSE -w.tick END AS tick_norm,
7441
7632
  w.block_number AS block_norm,
7442
7633
  -w.assets AS assets_norm,
7443
7634
  w.hash AS hash_norm
@@ -7454,33 +7645,35 @@ async function _getOffers(db, params) {
7454
7645
  FROM enriched e
7455
7646
  ${cursor != null ? sql`
7456
7647
  WHERE
7457
- (e.price_norm, e.block_norm, e.assets_norm, e.hash_norm)
7648
+ (e.tick_norm, e.block_norm, e.assets_norm, e.hash_norm)
7458
7649
  > (
7459
7650
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
7460
- THEN ${cursor.price}::numeric ELSE -${cursor.price}::numeric END,
7651
+ THEN ${cursor.tick}::integer ELSE -${cursor.tick}::integer END,
7461
7652
  ${cursor.blockNumber},
7462
7653
  -${cursor.assets}::numeric,
7463
7654
  ${cursor.hash}
7464
7655
  )` : sql``}
7465
- ORDER BY e.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
7656
+ ORDER BY e.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
7466
7657
  LIMIT ${limit}
7467
7658
  ),
7468
- -- Compute sum of offsets per position
7659
+ -- Compute sum of offsets per position and obligation
7469
7660
  position_offsets AS (
7470
7661
  SELECT
7471
7662
  chain_id,
7472
7663
  "user",
7473
7664
  contract,
7665
+ obligation_id,
7474
7666
  SUM(value::numeric) AS total_offset
7475
7667
  FROM ${offsets}
7476
- GROUP BY chain_id, "user", contract
7668
+ GROUP BY chain_id, "user", contract, obligation_id
7477
7669
  ),
7478
- -- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
7670
+ -- Compute position_consumed: sum of consumed from all groups with lots on each position+obligation (converted to lot terms)
7479
7671
  position_consumed AS (
7480
7672
  SELECT
7481
7673
  l.chain_id,
7482
7674
  l.contract,
7483
7675
  l."user",
7676
+ l.obligation_id,
7484
7677
  SUM(
7485
7678
  CASE
7486
7679
  WHEN wo.assets::numeric > 0
@@ -7497,7 +7690,7 @@ async function _getOffers(db, params) {
7497
7690
  ON wo.group_chain_id = g.chain_id
7498
7691
  AND LOWER(wo.group_maker) = LOWER(g.maker)
7499
7692
  AND wo.group_group = g."group"
7500
- GROUP BY l.chain_id, l.contract, l."user"
7693
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
7501
7694
  ),
7502
7695
  -- Compute callback contributions with lot balance
7503
7696
  callback_contributions AS (
@@ -7505,7 +7698,7 @@ async function _getOffers(db, params) {
7505
7698
  p.hash,
7506
7699
  p.obligation_id,
7507
7700
  p.assets,
7508
- p.price,
7701
+ p.tick,
7509
7702
  p.obligation_units,
7510
7703
  p.obligation_shares,
7511
7704
  p.maturity,
@@ -7550,6 +7743,7 @@ async function _getOffers(db, params) {
7550
7743
  AND LOWER(l.contract) = LOWER(c.position_contract)
7551
7744
  AND LOWER(l."user") = LOWER(c.position_user)
7552
7745
  AND l."group" = p.group_group
7746
+ AND l.obligation_id = p.obligation_id
7553
7747
  LEFT JOIN ${positions} pos
7554
7748
  ON pos.chain_id = c.position_chain_id
7555
7749
  AND LOWER(pos.contract) = LOWER(c.position_contract)
@@ -7558,10 +7752,12 @@ async function _getOffers(db, params) {
7558
7752
  ON pos_offsets.chain_id = c.position_chain_id
7559
7753
  AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
7560
7754
  AND LOWER(pos_offsets."user") = LOWER(c.position_user)
7755
+ AND pos_offsets.obligation_id = p.obligation_id
7561
7756
  LEFT JOIN position_consumed pc
7562
7757
  ON pc.chain_id = c.position_chain_id
7563
7758
  AND LOWER(pc.contract) = LOWER(c.position_contract)
7564
7759
  AND LOWER(pc."user") = LOWER(c.position_user)
7760
+ AND pc.obligation_id = p.obligation_id
7565
7761
  ),
7566
7762
  -- Compute contribution per callback in loan terms (loan token only — collateral positions are not indexed)
7567
7763
  callback_loan_contribution AS (
@@ -7579,7 +7775,7 @@ async function _getOffers(db, params) {
7579
7775
  hash,
7580
7776
  obligation_id,
7581
7777
  assets,
7582
- price,
7778
+ tick,
7583
7779
  obligation_units,
7584
7780
  obligation_shares,
7585
7781
  maturity,
@@ -7605,13 +7801,13 @@ async function _getOffers(db, params) {
7605
7801
  WHERE clc.callback_id IS NOT NULL
7606
7802
  ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
7607
7803
  ) deduped
7608
- GROUP BY hash, obligation_id, assets, price, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
7804
+ GROUP BY hash, obligation_id, assets, tick, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
7609
7805
  callback_address, callback_data, block_number, group_chain_id, group_maker,
7610
7806
  consumed, chain_id, loan_token, session
7611
7807
  UNION ALL
7612
7808
  -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
7613
7809
  SELECT
7614
- p.hash, p.obligation_id, p.assets, p.price,
7810
+ p.hash, p.obligation_id, p.assets, p.tick,
7615
7811
  p.obligation_units, p.obligation_shares,
7616
7812
  p.maturity, p.expiry, p.start, p.group_group,
7617
7813
  p.buy, p.callback_address, p.callback_data,
@@ -7633,7 +7829,7 @@ async function _getOffers(db, params) {
7633
7829
  oc.obligation_units,
7634
7830
  oc.obligation_shares,
7635
7831
  oc.consumed,
7636
- oc.price,
7832
+ oc.tick,
7637
7833
  oc.maturity,
7638
7834
  oc.expiry,
7639
7835
  oc.start,
@@ -7665,7 +7861,7 @@ async function _getOffers(db, params) {
7665
7861
  ))
7666
7862
  END > 0
7667
7863
  ORDER BY
7668
- oc.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
7864
+ oc.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
7669
7865
  oc.block_number ASC,
7670
7866
  oc.assets DESC,
7671
7867
  oc.hash ASC;
@@ -7678,7 +7874,7 @@ async function _getOffers(db, params) {
7678
7874
  assets: BigInt(row.assets),
7679
7875
  obligationUnits: BigInt(row.obligation_units ?? 0),
7680
7876
  obligationShares: BigInt(row.obligation_shares ?? 0),
7681
- price: BigInt(row.price),
7877
+ tick: row.tick,
7682
7878
  maturity: row.maturity,
7683
7879
  expiry: row.expiry,
7684
7880
  start: row.start,
@@ -7710,7 +7906,7 @@ let Cursor;
7710
7906
  function encode(row, totalReturned, now, side) {
7711
7907
  return Buffer.from(JSON.stringify({
7712
7908
  side,
7713
- price: row.price.toString(),
7909
+ tick: row.tick,
7714
7910
  blockNumber: row.blockNumber,
7715
7911
  assets: row.assets.toString(),
7716
7912
  hash: row.hash,
@@ -7721,10 +7917,9 @@ let Cursor;
7721
7917
  _Cursor.encode = encode;
7722
7918
  function decode(cursorString, logger) {
7723
7919
  if (cursorString == null) return null;
7724
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
7725
7920
  try {
7726
7921
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
7727
- 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;
7922
+ 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;
7728
7923
  throw new Error("Invalid cursor");
7729
7924
  } catch {
7730
7925
  logger.error({
@@ -7742,7 +7937,7 @@ let LevelCursor;
7742
7937
  function encode(lastLevel, offersCursor, side, now) {
7743
7938
  return Buffer.from(JSON.stringify({
7744
7939
  side,
7745
- lastPrice: lastLevel.price.toString(),
7940
+ lastTick: lastLevel.tick,
7746
7941
  now,
7747
7942
  offersCursor
7748
7943
  })).toString("base64url");
@@ -7750,10 +7945,9 @@ let LevelCursor;
7750
7945
  _LevelCursor.encode = encode;
7751
7946
  function decode(cursorString, logger) {
7752
7947
  if (cursorString == null) return null;
7753
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
7754
7948
  try {
7755
7949
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
7756
- 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;
7950
+ 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;
7757
7951
  throw new Error("Invalid book cursor");
7758
7952
  } catch {
7759
7953
  logger.error({
@@ -7911,31 +8105,33 @@ function create$11(db) {
7911
8105
  function create$10(db) {
7912
8106
  return {
7913
8107
  get: async (parameters) => {
7914
- const { chainId, user, contract, group } = parameters ?? {};
8108
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
7915
8109
  const conditions = [];
7916
8110
  if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
7917
8111
  if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
7918
8112
  if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
7919
8113
  if (group !== void 0) conditions.push(eq(lots.group, group));
8114
+ if (obligationId !== void 0) conditions.push(eq(lots.obligationId, obligationId));
7920
8115
  return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
7921
8116
  chainId: row.chainId,
7922
8117
  user: row.user,
7923
8118
  contract: row.contract,
7924
8119
  group: row.group,
8120
+ obligationId: row.obligationId,
7925
8121
  lower: BigInt(row.lower),
7926
8122
  upper: BigInt(row.upper)
7927
8123
  }));
7928
8124
  },
7929
8125
  create: async (parameters) => {
7930
8126
  if (parameters.length === 0) return;
7931
- const lotsByPositionGroup = /* @__PURE__ */ new Map();
8127
+ const lotsByKey = /* @__PURE__ */ new Map();
7932
8128
  for (const offer of parameters) {
7933
- const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
7934
- const existing = lotsByPositionGroup.get(key);
7935
- if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
8129
+ const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}-${offer.obligationId}`.toLowerCase();
8130
+ const existing = lotsByKey.get(key);
8131
+ if (!existing || offer.size > existing.size) lotsByKey.set(key, offer);
7936
8132
  }
7937
- 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) {
7938
- 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())));
8133
+ 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) {
8134
+ 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())));
7939
8135
  const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
7940
8136
  const newUpper = newLower + offer.size;
7941
8137
  await db.insert(lots).values({
@@ -7943,6 +8139,7 @@ function create$10(db) {
7943
8139
  user: offer.positionUser.toLowerCase(),
7944
8140
  contract: offer.positionContract.toLowerCase(),
7945
8141
  group: offer.group.toLowerCase(),
8142
+ obligationId: offer.obligationId.toLowerCase(),
7946
8143
  lower: newLower.toString(),
7947
8144
  upper: newUpper.toString()
7948
8145
  });
@@ -8059,7 +8256,7 @@ function create$8(config) {
8059
8256
  assets: offers.assets,
8060
8257
  obligationUnits: offers.obligationUnits,
8061
8258
  obligationShares: offers.obligationShares,
8062
- price: offers.price,
8259
+ tick: offers.tick,
8063
8260
  maturity: offers.maturity,
8064
8261
  expiry: offers.expiry,
8065
8262
  start: offers.start,
@@ -8079,7 +8276,7 @@ function create$8(config) {
8079
8276
  assets: BigInt(row.assets),
8080
8277
  obligationUnits: BigInt(row.obligationUnits),
8081
8278
  obligationShares: BigInt(row.obligationShares),
8082
- price: BigInt(row.price),
8279
+ tick: row.tick,
8083
8280
  maturity: from$16(row.maturity),
8084
8281
  expiry: row.expiry,
8085
8282
  start: row.start,
@@ -8161,8 +8358,8 @@ function create$8(config) {
8161
8358
  const now$2 = now();
8162
8359
  const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
8163
8360
  obligationId: offers.obligationId,
8164
- price: offers.price
8165
- }).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`);
8361
+ price: offers.tick
8362
+ }).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`);
8166
8363
  const [bestBuys, bestSells] = await Promise.all([query({ side: "buy" }), query({ side: "sell" })]);
8167
8364
  const quotes = /* @__PURE__ */ new Map();
8168
8365
  for (const row of bestSells) quotes.set(row.obligationId, {
@@ -8197,17 +8394,19 @@ function create$8(config) {
8197
8394
  //#region src/database/domains/Offsets.ts
8198
8395
  function create$7(db) {
8199
8396
  return { get: async (parameters) => {
8200
- const { chainId, user, contract, group } = parameters ?? {};
8397
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
8201
8398
  const conditions = [];
8202
8399
  if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
8203
8400
  if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
8204
8401
  if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
8205
8402
  if (group !== void 0) conditions.push(eq(offsets.group, group));
8403
+ if (obligationId !== void 0) conditions.push(eq(offsets.obligationId, obligationId));
8206
8404
  return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
8207
8405
  chainId: row.chainId,
8208
8406
  user: row.user,
8209
8407
  contract: row.contract,
8210
8408
  group: row.group,
8409
+ obligationId: row.obligationId,
8211
8410
  value: BigInt(row.value)
8212
8411
  }));
8213
8412
  } };
@@ -8357,7 +8556,8 @@ const create$5 = (db) => {
8357
8556
  if (!parsed.chainId || !parsed.contract) throw new Error("Invalid cursor format");
8358
8557
  cursor = {
8359
8558
  chainId: parsed.chainId,
8360
- contract: parsed.contract
8559
+ contract: parsed.contract,
8560
+ obligationId: parsed.obligationId ?? null
8361
8561
  };
8362
8562
  }
8363
8563
  const raw = await db.execute(sql`
@@ -8366,16 +8566,18 @@ const create$5 = (db) => {
8366
8566
  chain_id,
8367
8567
  "user",
8368
8568
  contract,
8569
+ obligation_id,
8369
8570
  SUM(value::numeric) AS total_offset
8370
8571
  FROM ${offsets}
8371
8572
  WHERE LOWER("user") = LOWER(${user})
8372
- GROUP BY chain_id, "user", contract
8573
+ GROUP BY chain_id, "user", contract, obligation_id
8373
8574
  ),
8374
8575
  position_consumed AS (
8375
8576
  SELECT
8376
8577
  l.chain_id,
8377
8578
  l.contract,
8378
8579
  l."user",
8580
+ l.obligation_id,
8379
8581
  SUM(
8380
8582
  CASE
8381
8583
  WHEN offer_agg.assets > 0
@@ -8401,50 +8603,64 @@ const create$5 = (db) => {
8401
8603
  AND LOWER(offer_agg.group_maker) = LOWER(g.maker)
8402
8604
  AND offer_agg."group_group" = g."group"
8403
8605
  WHERE LOWER(l."user") = LOWER(${user})
8404
- GROUP BY l.chain_id, l.contract, l."user"
8606
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
8405
8607
  ),
8406
8608
  position_max_lot AS (
8407
8609
  SELECT
8408
8610
  chain_id,
8409
8611
  contract,
8410
8612
  "user",
8613
+ obligation_id,
8411
8614
  MAX(upper::numeric) AS max_upper
8412
8615
  FROM ${lots}
8413
8616
  WHERE LOWER("user") = LOWER(${user})
8414
- GROUP BY chain_id, contract, "user"
8617
+ GROUP BY chain_id, contract, "user", obligation_id
8618
+ ),
8619
+ per_obligation AS (
8620
+ SELECT
8621
+ pml.chain_id,
8622
+ pml.contract,
8623
+ pml."user",
8624
+ pml.obligation_id,
8625
+ GREATEST(0,
8626
+ COALESCE(pml.max_upper, 0)
8627
+ - COALESCE(po.total_offset, 0)
8628
+ - COALESCE(pc.consumed, 0)
8629
+ )::text AS reserved_balance
8630
+ FROM position_max_lot pml
8631
+ LEFT JOIN position_offsets po
8632
+ ON po.chain_id = pml.chain_id
8633
+ AND LOWER(po.contract) = LOWER(pml.contract)
8634
+ AND LOWER(po."user") = LOWER(pml."user")
8635
+ AND po.obligation_id = pml.obligation_id
8636
+ LEFT JOIN position_consumed pc
8637
+ ON pc.chain_id = pml.chain_id
8638
+ AND LOWER(pc.contract) = LOWER(pml.contract)
8639
+ AND LOWER(pc."user") = LOWER(pml."user")
8640
+ AND pc.obligation_id = pml.obligation_id
8415
8641
  )
8416
8642
  SELECT
8417
8643
  p.chain_id,
8418
8644
  p.contract,
8419
8645
  p."user",
8420
8646
  p.block_number,
8421
- GREATEST(0,
8422
- COALESCE(pml.max_upper, 0)
8423
- - COALESCE(po.total_offset, 0)
8424
- - COALESCE(pc.consumed, 0)
8425
- )::text AS reserved_balance
8647
+ po.obligation_id,
8648
+ COALESCE(po.reserved_balance, '0') AS reserved_balance
8426
8649
  FROM ${positions} p
8427
- LEFT JOIN position_offsets po
8650
+ LEFT JOIN per_obligation po
8428
8651
  ON po.chain_id = p.chain_id
8429
8652
  AND LOWER(po.contract) = LOWER(p.contract)
8430
8653
  AND LOWER(po."user") = LOWER(p."user")
8431
- LEFT JOIN position_consumed pc
8432
- ON pc.chain_id = p.chain_id
8433
- AND LOWER(pc.contract) = LOWER(p.contract)
8434
- AND LOWER(pc."user") = LOWER(p."user")
8435
- LEFT JOIN position_max_lot pml
8436
- ON pml.chain_id = p.chain_id
8437
- AND LOWER(pml.contract) = LOWER(p.contract)
8438
- AND LOWER(pml."user") = LOWER(p."user")
8439
8654
  WHERE LOWER(p."user") = LOWER(${user})
8440
8655
  AND p."user" != ${zeroAddress}
8441
- ${cursor !== null ? sql`AND (p.chain_id, p.contract) > (${cursor.chainId}, ${cursor.contract})` : sql``}
8442
- ORDER BY p.chain_id ASC, p.contract ASC
8656
+ ${cursor !== null ? sql`AND (p.chain_id, p.contract, COALESCE(po.obligation_id, '')) > (${cursor.chainId}, ${cursor.contract}, ${cursor.obligationId ?? ""})` : sql``}
8657
+ ORDER BY p.chain_id ASC, p.contract ASC, po.obligation_id ASC NULLS FIRST
8443
8658
  LIMIT ${limit}
8444
8659
  `);
8445
8660
  const nextCursor = raw.rows.length === limit ? Buffer.from(JSON.stringify({
8446
8661
  chainId: raw.rows[raw.rows.length - 1].chain_id.toString(),
8447
- contract: raw.rows[raw.rows.length - 1].contract
8662
+ contract: raw.rows[raw.rows.length - 1].contract,
8663
+ obligationId: raw.rows[raw.rows.length - 1].obligation_id
8448
8664
  })).toString("base64url") : null;
8449
8665
  return {
8450
8666
  positions: raw.rows.map((row) => ({
@@ -8452,6 +8668,7 @@ const create$5 = (db) => {
8452
8668
  contract: row.contract,
8453
8669
  user: row.user,
8454
8670
  blockNumber: row.block_number,
8671
+ obligationId: row.obligation_id,
8455
8672
  reserved: BigInt(row.reserved_balance.split(".")[0] ?? "0")
8456
8673
  })),
8457
8674
  nextCursor
@@ -8826,6 +9043,7 @@ function augmentWithDomains(base, chainRegistry) {
8826
9043
  return wrapped;
8827
9044
  }
8828
9045
  const InMemoryDbMap = /* @__PURE__ */ new Map();
9046
+ const LEGACY_SCHEMA_START_MINOR = 7;
8829
9047
  /**
8830
9048
  * Connect to the database.
8831
9049
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -8880,9 +9098,26 @@ function applyMigrations(kind, driver) {
8880
9098
  async function preMigrate(driver) {
8881
9099
  const tracer = getTracer("db.preMigrate");
8882
9100
  await startActiveSpan(tracer, "db.preMigrate", async () => {
8883
- await driver.execute(`create schema if not exists "${VERSION}"`);
9101
+ const schemaNames = getSchemaNamesForMigration(VERSION);
9102
+ for (const schemaName of schemaNames) await driver.execute(`create schema if not exists "${schemaName}"`);
8884
9103
  });
8885
9104
  }
9105
+ /**
9106
+ * Build the list of router schemas that should exist before running migrations.
9107
+ * @param version - Current schema version (e.g. `router_v1.8`).
9108
+ * @returns Ordered schema names from `router_v1.7` to current, or just current if parsing fails.
9109
+ */
9110
+ function getSchemaNamesForMigration(version) {
9111
+ const parsed = /^router_v(?<major>\d+)\.(?<minor>\d+)$/.exec(version);
9112
+ if (!parsed?.groups?.major || !parsed.groups.minor) return [version];
9113
+ const major = Number.parseInt(parsed.groups.major, 10);
9114
+ const currentMinor = Number.parseInt(parsed.groups.minor, 10);
9115
+ if (!Number.isInteger(major) || !Number.isInteger(currentMinor)) return [version];
9116
+ if (currentMinor < LEGACY_SCHEMA_START_MINOR) return [version];
9117
+ const schemaNames = [];
9118
+ for (let minor = LEGACY_SCHEMA_START_MINOR; minor <= currentMinor; minor += 1) schemaNames.push(`router_v${major}.${minor}`);
9119
+ return schemaNames;
9120
+ }
8886
9121
  async function postMigrate(driver) {
8887
9122
  const tracer = getTracer("db.postMigrate");
8888
9123
  await startActiveSpan(tracer, "db.postMigrate", async () => {
@@ -9152,15 +9387,16 @@ async function postMigrate(driver) {
9152
9387
  RETURNS trigger
9153
9388
  LANGUAGE plpgsql AS $$
9154
9389
  BEGIN
9155
- INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
9390
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", obligation_id, value)
9156
9391
  VALUES (
9157
9392
  OLD.chain_id,
9158
9393
  OLD."user",
9159
9394
  OLD.contract,
9160
9395
  OLD."group",
9396
+ OLD.obligation_id,
9161
9397
  OLD.upper::numeric - OLD.lower::numeric
9162
9398
  )
9163
- ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
9399
+ ON CONFLICT (chain_id, "user", contract, "group", obligation_id) DO NOTHING;
9164
9400
  RETURN OLD;
9165
9401
  END;
9166
9402
  $$;
@@ -9542,12 +9778,34 @@ async function getBook(params, db) {
9542
9778
  if (!result.success) return failure(result.error);
9543
9779
  const query = result.data;
9544
9780
  try {
9781
+ logger.debug({
9782
+ service: "api_controller",
9783
+ endpoint: "get_book",
9784
+ msg: "Loading book levels",
9785
+ obligation_id: query.obligation_id,
9786
+ side: query.side,
9787
+ limit: query.limit ?? null,
9788
+ has_cursor: query.cursor != null
9789
+ });
9545
9790
  const { levels, nextCursor } = await db.book.get({
9546
9791
  side: query.side,
9547
9792
  obligationId: query.obligation_id,
9548
9793
  cursor: query.cursor,
9549
9794
  limit: query.limit
9550
9795
  });
9796
+ const firstLevel = levels[0];
9797
+ logger.debug({
9798
+ service: "api_controller",
9799
+ endpoint: "get_book",
9800
+ msg: "Loaded book levels",
9801
+ obligation_id: query.obligation_id,
9802
+ side: query.side,
9803
+ levels_count: levels.length,
9804
+ has_next_cursor: nextCursor != null,
9805
+ first_level_tick: firstLevel?.tick ?? null,
9806
+ first_level_assets: firstLevel?.assets.toString() ?? null,
9807
+ first_level_count: firstLevel?.count ?? null
9808
+ });
9551
9809
  return success({
9552
9810
  data: levels.map(from$6),
9553
9811
  cursor: nextCursor
@@ -10049,16 +10307,57 @@ async function getOffersQuery(db, parameters) {
10049
10307
  '[]'::jsonb
10050
10308
  )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
10051
10309
  AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
10052
- const availableLateral = db.select({ available: sql`COALESCE(SUM(
10053
- CASE
10054
- WHEN ${positions.asset} IS NULL THEN 0
10055
- ELSE
10056
- CASE
10057
- WHEN ${callbacks.amount} IS NULL THEN COALESCE(${positions.balance}, 0)::numeric
10058
- ELSE LEAST(${callbacks.amount}::numeric, COALESCE(${positions.balance}, 0)::numeric)
10059
- END
10060
- END
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");
10310
+ const lotBalanceExpr = sql`GREATEST(0, LEAST(
10311
+ COALESCE(${positions.balance}, 0)::numeric
10312
+ + COALESCE((
10313
+ SELECT SUM(${offsets.value}::numeric)
10314
+ FROM ${offsets}
10315
+ WHERE ${offsets.chainId} = ${callbacks.positionChainId}
10316
+ AND LOWER(${offsets.contract}) = LOWER(${callbacks.positionContract})
10317
+ AND LOWER(${offsets.user}) = LOWER(${callbacks.positionUser})
10318
+ ), 0)
10319
+ - COALESCE(${lots.lower}::numeric, 0),
10320
+ (COALESCE(${lots.upper}::numeric, 0) - COALESCE(${lots.lower}::numeric, 0))
10321
+ - CASE
10322
+ WHEN ${offers.assets}::numeric > 0
10323
+ THEN COALESCE(${groups.consumed}::numeric, 0)
10324
+ * (COALESCE(${lots.upper}::numeric, 0) - COALESCE(${lots.lower}::numeric, 0))
10325
+ / ${offers.assets}::numeric
10326
+ ELSE 0
10327
+ END
10328
+ ))`;
10329
+ const contributionExpr = sql`CASE
10330
+ WHEN ${positions.asset} IS NULL OR ${lots.lower} IS NULL THEN 0
10331
+ ELSE LEAST(COALESCE(${callbacks.amount}::numeric, ${lotBalanceExpr}), ${lotBalanceExpr})
10332
+ END`;
10333
+ const availableExpr = sql`COALESCE((
10334
+ SELECT SUM(deduped.contribution)
10335
+ FROM (
10336
+ SELECT DISTINCT ON (
10337
+ ${callbacks.positionChainId},
10338
+ LOWER(${callbacks.positionContract}),
10339
+ LOWER(${callbacks.positionUser})
10340
+ )
10341
+ ${contributionExpr} AS contribution
10342
+ FROM ${offersCallbacks}
10343
+ INNER JOIN ${callbacks} ON ${offersCallbacks.callbackId} = ${callbacks.id}
10344
+ LEFT JOIN ${positions}
10345
+ ON ${positions.chainId} = ${callbacks.positionChainId}
10346
+ AND LOWER(${positions.contract}) = LOWER(${callbacks.positionContract})
10347
+ AND LOWER(${positions.user}) = LOWER(${callbacks.positionUser})
10348
+ LEFT JOIN ${lots}
10349
+ ON ${lots.chainId} = ${callbacks.positionChainId}
10350
+ AND LOWER(${lots.contract}) = LOWER(${callbacks.positionContract})
10351
+ AND LOWER(${lots.user}) = LOWER(${callbacks.positionUser})
10352
+ AND LOWER(${lots.group}) = LOWER(${offers.group})
10353
+ WHERE ${offersCallbacks.offerHash} = ${offers.hash}
10354
+ ORDER BY
10355
+ ${callbacks.positionChainId},
10356
+ LOWER(${callbacks.positionContract}),
10357
+ LOWER(${callbacks.positionUser}),
10358
+ ${contributionExpr} DESC
10359
+ ) deduped
10360
+ ), 0)`;
10062
10361
  const rows = (await db.select({
10063
10362
  hash: offers.hash,
10064
10363
  maker: offers.groupMaker,
@@ -10066,7 +10365,7 @@ async function getOffersQuery(db, parameters) {
10066
10365
  obligationUnits: offers.obligationUnits,
10067
10366
  obligationShares: offers.obligationShares,
10068
10367
  consumed: groups.consumed,
10069
- price: offers.price,
10368
+ tick: offers.tick,
10070
10369
  maturity: offers.maturity,
10071
10370
  expiry: offers.expiry,
10072
10371
  start: offers.start,
@@ -10079,22 +10378,22 @@ async function getOffersQuery(db, parameters) {
10079
10378
  callbackData: offers.callbackData,
10080
10379
  collaterals: collateralsLateral.collaterals,
10081
10380
  blockNumber: offers.blockNumber,
10082
- available: sql`COALESCE(${availableLateral.available}::numeric, 0)`.as("available"),
10381
+ available: sql`${availableExpr}::numeric`.as("available"),
10083
10382
  takeable: sql`FLOOR(GREATEST(0,
10084
10383
  CASE WHEN ${offers.buy} = false
10085
10384
  THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
10086
10385
  ELSE LEAST(
10087
10386
  ${offers.assets}::numeric - ${groups.consumed}::numeric,
10088
- COALESCE(${availableLateral.available}::numeric, 0)
10387
+ ${availableExpr}::numeric
10089
10388
  )
10090
10389
  END
10091
10390
  ))`.as("takeable")
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,
10391
+ }).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,
10093
10392
  CASE WHEN ${offers.buy} = false
10094
10393
  THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
10095
10394
  ELSE LEAST(
10096
10395
  ${offers.assets}::numeric - ${groups.consumed}::numeric,
10097
- COALESCE(${availableLateral.available}::numeric, 0)
10396
+ ${availableExpr}::numeric
10098
10397
  )
10099
10398
  END
10100
10399
  ) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
@@ -10104,7 +10403,7 @@ async function getOffersQuery(db, parameters) {
10104
10403
  assets: BigInt(row.assets),
10105
10404
  obligationUnits: BigInt(row.obligationUnits),
10106
10405
  obligationShares: BigInt(row.obligationShares),
10107
- price: BigInt(row.price),
10406
+ tick: row.tick,
10108
10407
  maturity: from$16(row.maturity),
10109
10408
  expiry: row.expiry,
10110
10409
  start: row.start,
@@ -10761,6 +11060,7 @@ function buildOfferAssociationsFromOffers(parameters) {
10761
11060
  positionContract: loanToken,
10762
11061
  positionUser: offer.maker,
10763
11062
  group: offer.group,
11063
+ obligationId: obligationId(offer),
10764
11064
  size: offer.assets
10765
11065
  });
10766
11066
  }