@morpho-dev/router 0.7.2 → 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 (65) hide show
  1. package/dist/cli.js +292 -122
  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/index.browser.d.mts +103 -33
  50. package/dist/index.browser.d.mts.map +1 -1
  51. package/dist/index.browser.d.ts +103 -33
  52. package/dist/index.browser.d.ts.map +1 -1
  53. package/dist/index.browser.js +298 -146
  54. package/dist/index.browser.js.map +1 -1
  55. package/dist/index.browser.mjs +293 -147
  56. package/dist/index.browser.mjs.map +1 -1
  57. package/dist/index.node.d.mts +182 -63
  58. package/dist/index.node.d.mts.map +1 -1
  59. package/dist/index.node.d.ts +182 -63
  60. package/dist/index.node.d.ts.map +1 -1
  61. package/dist/index.node.js +342 -127
  62. package/dist/index.node.js.map +1 -1
  63. package/dist/index.node.mjs +337 -128
  64. package/dist/index.node.mjs.map +1 -1
  65. 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.2";
155
+ var version = "0.8.0";
156
156
  var description = "Router package for Morpho protocol";
157
157
 
158
158
  //#endregion
@@ -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",
@@ -2126,7 +2153,7 @@ const OfferSchema = () => {
2126
2153
  assets: z$2.bigint({ coerce: true }).min(0n).max(maxUint256),
2127
2154
  obligationUnits: z$2.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2128
2155
  obligationShares: z$2.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2129
- price: z$2.bigint({ coerce: true }).min(0n).max(maxUint256),
2156
+ tick: z$2.coerce.number().int().min(0).max(990),
2130
2157
  maturity: MaturitySchema,
2131
2158
  expiry: z$2.number().int().max(Number.MAX_SAFE_INTEGER),
2132
2159
  start: z$2.number().int().max(Number.MAX_SAFE_INTEGER),
@@ -2198,7 +2225,7 @@ const serialize = (offer) => ({
2198
2225
  assets: offer.assets.toString(),
2199
2226
  obligationUnits: offer.obligationUnits.toString(),
2200
2227
  obligationShares: offer.obligationShares.toString(),
2201
- price: offer.price.toString(),
2228
+ tick: offer.tick,
2202
2229
  maturity: Number(offer.maturity),
2203
2230
  expiry: Number(offer.expiry),
2204
2231
  start: Number(offer.start),
@@ -2243,14 +2270,13 @@ function random(config) {
2243
2270
  [.98, 2]
2244
2271
  ]));
2245
2272
  const buy = config?.buy !== void 0 ? config.buy : bool();
2246
- const ONE = 1000000000000000000n;
2247
- const qMin = buy ? 16 : 4;
2248
- const len = (buy ? 32 : 16) - qMin + 1;
2249
- const pricePairs = Array.from({ length: len }, (_, idx) => {
2250
- const q = qMin + idx;
2251
- 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];
2252
2278
  });
2253
- const price = config?.price ?? weightedChoice(pricePairs);
2279
+ const tick = config?.tick ?? weightedChoice(tickPairs);
2254
2280
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
2255
2281
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2256
2282
  const amountBase = BigInt(100 + int(999901));
@@ -2264,7 +2290,7 @@ function random(config) {
2264
2290
  assets: assetsScaled,
2265
2291
  obligationUnits: config?.obligationUnits ?? 0n,
2266
2292
  obligationShares: config?.obligationShares ?? 0n,
2267
- price,
2293
+ tick,
2268
2294
  maturity,
2269
2295
  expiry: config?.expiry ?? maturity - 1,
2270
2296
  start: config?.start ?? maturity - 10,
@@ -2329,7 +2355,7 @@ const types = {
2329
2355
  type: "uint256"
2330
2356
  },
2331
2357
  {
2332
- name: "price",
2358
+ name: "tick",
2333
2359
  type: "uint256"
2334
2360
  },
2335
2361
  {
@@ -2397,7 +2423,7 @@ function hash(offer) {
2397
2423
  assets: offer.assets,
2398
2424
  obligationUnits: offer.obligationUnits,
2399
2425
  obligationShares: offer.obligationShares,
2400
- price: offer.price,
2426
+ tick: BigInt(offer.tick),
2401
2427
  maturity: BigInt(offer.maturity),
2402
2428
  expiry: BigInt(offer.expiry),
2403
2429
  group: offer.group,
@@ -2662,6 +2688,49 @@ var InvalidQuoteError = class extends BaseError {
2662
2688
  }
2663
2689
  };
2664
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
+
2665
2734
  //#endregion
2666
2735
  //#region src/core/TradingFee.ts
2667
2736
  /** WAD constant (1e18) for fee scaling. */
@@ -2950,15 +3019,25 @@ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy
2950
3019
  if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
2951
3020
  });
2952
3021
  /**
2953
- * A validation rule that checks if the offer's tokens are allowed for its chain.
2954
- * @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.
3024
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
3025
+ */
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.
2955
3034
  * @returns The issue that was found. If the offer is valid, this will be undefined.
2956
3035
  */
2957
- const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
2958
- const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
2959
- if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
2960
- if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
2961
- if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
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" };
2962
3041
  });
2963
3042
  /**
2964
3043
  * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
@@ -3001,9 +3080,11 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
3001
3080
  //#region src/gatekeeper/morphoRules.ts
3002
3081
  const morphoRules = (chains) => {
3003
3082
  const assetsByChainId = {};
3083
+ const collateralAssetsByChainId = {};
3004
3084
  const oraclesByChainId = {};
3005
3085
  for (const chain of chains) {
3006
3086
  assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
3087
+ collateralAssetsByChainId[chain.id] = collateralAssets[chain.id.toString()] ?? [];
3007
3088
  oraclesByChainId[chain.id] = oracles$1[chain.id.toString()] ?? [];
3008
3089
  }
3009
3090
  return [
@@ -3015,7 +3096,8 @@ const morphoRules = (chains) => {
3015
3096
  callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
3016
3097
  allowedAddresses: []
3017
3098
  }),
3018
- token({ assetsByChainId }),
3099
+ loanToken({ assetsByChainId }),
3100
+ collateralToken({ collateralAssetsByChainId }),
3019
3101
  oracle({ oraclesByChainId })
3020
3102
  ];
3021
3103
  };
@@ -3023,7 +3105,7 @@ const morphoRules = (chains) => {
3023
3105
  //#endregion
3024
3106
  //#region src/gatekeeper/ConfigRules.ts
3025
3107
  /**
3026
- * 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.
3027
3109
  * @param chains - Chains to include in the configured rules.
3028
3110
  * @returns Sorted list of config rules.
3029
3111
  */
@@ -3043,6 +3125,12 @@ function buildConfigRules(chains) {
3043
3125
  chain_id: chain.id,
3044
3126
  address: normalizeAddress(address)
3045
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
+ });
3046
3134
  const oracles = oracles$1[chain.id.toString()] ?? [];
3047
3135
  for (const address of oracles) rules.push({
3048
3136
  type: "oracle",
@@ -3070,6 +3158,10 @@ function buildConfigRulesChecksum(rules) {
3070
3158
  hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
3071
3159
  continue;
3072
3160
  }
3161
+ if (rule.type === "collateral_token") {
3162
+ hash.update(`collateral_token:${rule.chain_id}:${rule.address}\n`);
3163
+ continue;
3164
+ }
3073
3165
  if (rule.type === "oracle") {
3074
3166
  hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
3075
3167
  continue;
@@ -3090,6 +3182,7 @@ function compareConfigRules(left, right) {
3090
3182
  return left.address.localeCompare(right.address);
3091
3183
  }
3092
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);
3093
3186
  if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
3094
3187
  return 0;
3095
3188
  }
@@ -3097,8 +3190,10 @@ function compareConfigRules(left, right) {
3097
3190
  //#endregion
3098
3191
  //#region src/api/Schema/BookResponse.ts
3099
3192
  function from$6(level) {
3193
+ const price = tickToPrice(level.tick);
3100
3194
  return {
3101
- price: level.price.toString(),
3195
+ tick: level.tick,
3196
+ price: price.toString(),
3102
3197
  assets: level.assets.toString(),
3103
3198
  count: level.count
3104
3199
  };
@@ -3201,7 +3296,7 @@ function from$4(input) {
3201
3296
  obligation_shares: input.obligationShares.toString(),
3202
3297
  start: input.start,
3203
3298
  expiry: input.expiry,
3204
- price: input.price.toString(),
3299
+ tick: input.tick,
3205
3300
  group: input.group,
3206
3301
  session: input.session,
3207
3302
  callback: input.callback.address,
@@ -3363,7 +3458,7 @@ const offerExample = {
3363
3458
  obligation_shares: "0",
3364
3459
  start: 1761922790,
3365
3460
  expiry: 1761922799,
3366
- price: "2750000000000000000",
3461
+ tick: 495,
3367
3462
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
3368
3463
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
3369
3464
  callback: "0x0000000000000000000000000000000000000000",
@@ -3404,7 +3499,7 @@ const validateOfferExample = {
3404
3499
  assets: "369216000000000000000000",
3405
3500
  obligation_units: "0",
3406
3501
  obligation_shares: "0",
3407
- price: "2750000000000000000",
3502
+ tick: 495,
3408
3503
  maturity: 1761922799,
3409
3504
  expiry: 1761922799,
3410
3505
  start: 1761922790,
@@ -3548,9 +3643,11 @@ __decorate([ApiProperty({
3548
3643
  example: offerExample.offer.expiry
3549
3644
  })], OfferDataResponse.prototype, "expiry", void 0);
3550
3645
  __decorate([ApiProperty({
3551
- type: "string",
3552
- example: offerExample.offer.price
3553
- })], 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);
3554
3651
  __decorate([ApiProperty({
3555
3652
  type: "string",
3556
3653
  example: offerExample.offer.group
@@ -3791,9 +3888,11 @@ __decorate([ApiProperty({
3791
3888
  required: false
3792
3889
  })], ValidateOfferRequest.prototype, "obligation_shares", void 0);
3793
3890
  __decorate([ApiProperty({
3794
- type: "string",
3795
- example: validateOfferExample.price
3796
- })], ValidateOfferRequest.prototype, "price", void 0);
3891
+ type: "number",
3892
+ example: validateOfferExample.tick,
3893
+ minimum: 0,
3894
+ maximum: 990
3895
+ })], ValidateOfferRequest.prototype, "tick", void 0);
3797
3896
  __decorate([ApiProperty({
3798
3897
  type: "number",
3799
3898
  example: validateOfferExample.maturity
@@ -3893,9 +3992,16 @@ __decorate([ApiProperty({
3893
3992
  description: "List of validation issues. Returned when any offer fails validation."
3894
3993
  })], ValidationFailureResponse.prototype, "data", void 0);
3895
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);
3896
4001
  __decorate([ApiProperty({
3897
4002
  type: "string",
3898
- example: "2750000000000000000"
4003
+ example: "500000000000000000",
4004
+ description: "Price derived from tick, scaled by 1e18."
3899
4005
  })], BookLevelResponse.prototype, "price", void 0);
3900
4006
  __decorate([ApiProperty({
3901
4007
  type: "string",
@@ -3909,6 +4015,7 @@ const positionExample = {
3909
4015
  chain_id: 1,
3910
4016
  contract: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
3911
4017
  user: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
4018
+ obligation_id: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
3912
4019
  reserved: "200000000000000000000",
3913
4020
  block_number: 21345678
3914
4021
  };
@@ -3925,6 +4032,12 @@ __decorate([ApiProperty({
3925
4032
  type: "string",
3926
4033
  example: positionExample.user
3927
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);
3928
4041
  __decorate([ApiProperty({
3929
4042
  type: "string",
3930
4043
  example: positionExample.reserved
@@ -3952,7 +4065,7 @@ __decorate([ApiProperty({
3952
4065
  })], BookListResponse.prototype, "cursor", void 0);
3953
4066
  __decorate([ApiProperty({
3954
4067
  type: () => [BookLevelResponse],
3955
- description: "Aggregated book levels grouped by computed price."
4068
+ description: "Aggregated book levels grouped by offer tick."
3956
4069
  })], BookListResponse.prototype, "data", void 0);
3957
4070
  let BooksController = class BooksController {
3958
4071
  async getBook() {}
@@ -3962,7 +4075,7 @@ __decorate([
3962
4075
  methods: ["get"],
3963
4076
  path: "/v1/books/{obligationId}/{side}",
3964
4077
  summary: "Get aggregated book",
3965
- 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)."
3966
4079
  }),
3967
4080
  ApiParam({
3968
4081
  name: "obligationId",
@@ -3987,7 +4100,7 @@ __decorate([
3987
4100
  name: "limit",
3988
4101
  type: "number",
3989
4102
  example: 10,
3990
- description: "Maximum number of price levels to return."
4103
+ description: "Maximum number of tick levels to return."
3991
4104
  }),
3992
4105
  ApiResponse({
3993
4106
  status: 200,
@@ -4181,6 +4294,11 @@ const configRulesLoanTokenExample = {
4181
4294
  chain_id: 1,
4182
4295
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
4183
4296
  };
4297
+ const configRulesCollateralTokenExample = {
4298
+ type: "collateral_token",
4299
+ chain_id: 1,
4300
+ address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
4301
+ };
4184
4302
  const configRulesOracleExample = {
4185
4303
  type: "oracle",
4186
4304
  chain_id: 1,
@@ -4190,6 +4308,7 @@ const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
4190
4308
  const configRulesPayloadExample = [
4191
4309
  configRulesMaturityExample,
4192
4310
  configRulesLoanTokenExample,
4311
+ configRulesCollateralTokenExample,
4193
4312
  configRulesOracleExample
4194
4313
  ];
4195
4314
  const configContractNames = [
@@ -4316,7 +4435,7 @@ __decorate([
4316
4435
  methods: ["get"],
4317
4436
  path: "/v1/config/rules",
4318
4437
  summary: "Get config rules",
4319
- 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."
4320
4439
  }),
4321
4440
  ApiQuery({
4322
4441
  name: "cursor",
@@ -4336,7 +4455,7 @@ __decorate([
4336
4455
  name: "types",
4337
4456
  type: ["string"],
4338
4457
  required: false,
4339
- example: "maturity,loan_token,oracle",
4458
+ example: "maturity,loan_token,collateral_token,oracle",
4340
4459
  description: "Filter by rule types (comma-separated).",
4341
4460
  style: "form",
4342
4461
  explode: false
@@ -4454,7 +4573,7 @@ __decorate([
4454
4573
  methods: ["get"],
4455
4574
  path: "/v1/users/{userAddress}/positions",
4456
4575
  summary: "Get user positions",
4457
- 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."
4458
4577
  }),
4459
4578
  ApiParam({
4460
4579
  name: "userAddress",
@@ -4541,6 +4660,7 @@ function from$3(position) {
4541
4660
  chain_id: position.chainId,
4542
4661
  contract: position.contract,
4543
4662
  user: position.user,
4663
+ obligation_id: position.obligationId,
4544
4664
  reserved: position.reserved.toString(),
4545
4665
  block_number: position.blockNumber
4546
4666
  };
@@ -4594,10 +4714,11 @@ const ConfigRuleTypes = z$2.enum([
4594
4714
  "maturity",
4595
4715
  "callback",
4596
4716
  "loan_token",
4717
+ "collateral_token",
4597
4718
  "oracle"
4598
4719
  ]);
4599
4720
  const GetConfigRulesQueryParams = z$2.object({
4600
- 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({
4601
4722
  description: "Pagination cursor in type:chain_id:<value> format",
4602
4723
  example: "maturity:1:1730415600:end_of_next_month"
4603
4724
  }),
@@ -4607,7 +4728,7 @@ const GetConfigRulesQueryParams = z$2.object({
4607
4728
  }),
4608
4729
  types: csvArray(ConfigRuleTypes).meta({
4609
4730
  description: "Filter by rule types (comma-separated).",
4610
- example: "maturity,loan_token,oracle"
4731
+ example: "maturity,loan_token,collateral_token,oracle"
4611
4732
  }),
4612
4733
  chains: csvArray(z$2.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
4613
4734
  description: "Filter by chain IDs (comma-separated).",
@@ -4707,12 +4828,11 @@ const GetObligationParams = z$2.object({ obligation_id: z$2.string({ error: "Obl
4707
4828
  description: "Obligation id",
4708
4829
  example: "0x1234567890123456789012345678901234567890123456789012345678901234"
4709
4830
  }) });
4710
- /** Validate a book cursor format: {side, lastPrice, offersCursor} */
4831
+ /** Validate a book cursor format: {side, lastTick, offersCursor} */
4711
4832
  function isValidBookCursor(cursorString) {
4712
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
4713
4833
  try {
4714
4834
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
4715
- 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");
4716
4836
  } catch {
4717
4837
  return false;
4718
4838
  }
@@ -4817,6 +4937,7 @@ function formatCursor$1(rule) {
4817
4937
  if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
4818
4938
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
4819
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()}`;
4820
4941
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
4821
4942
  }
4822
4943
  function parseCursor$1(cursor) {
@@ -4849,7 +4970,7 @@ function parseCursor$1(cursor) {
4849
4970
  address: parseAddress(addressValue, "Cursor address")
4850
4971
  };
4851
4972
  }
4852
- if (type === "loan_token" || type === "oracle") {
4973
+ if (type === "loan_token" || type === "collateral_token" || type === "oracle") {
4853
4974
  const addressValue = rest.join(":");
4854
4975
  if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
4855
4976
  return {
@@ -4876,7 +4997,7 @@ function parseAddress(address, label) {
4876
4997
  return address.toLowerCase();
4877
4998
  }
4878
4999
  function isConfigRuleType(value) {
4879
- return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
5000
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "collateral_token" || value === "oracle";
4880
5001
  }
4881
5002
  function isMaturityType(value) {
4882
5003
  return Object.values(MaturityType).includes(value);
@@ -5149,7 +5270,7 @@ function now() {
5149
5270
 
5150
5271
  //#endregion
5151
5272
  //#region src/database/drizzle/VERSION.ts
5152
- const VERSION = "router_v1.6";
5273
+ const VERSION = "router_v1.8";
5153
5274
 
5154
5275
  //#endregion
5155
5276
  //#region src/database/drizzle/schema.ts
@@ -5301,10 +5422,7 @@ const offers = s.table(EnumTableName.OFFERS, {
5301
5422
  precision: 78,
5302
5423
  scale: 0
5303
5424
  }).notNull().default("0"),
5304
- price: numeric("price", {
5305
- precision: 78,
5306
- scale: 0
5307
- }).notNull(),
5425
+ tick: integer("tick").notNull(),
5308
5426
  maturity: integer("maturity").notNull(),
5309
5427
  expiry: integer("expiry").notNull(),
5310
5428
  start: integer("start").notNull(),
@@ -5369,6 +5487,7 @@ const lots = s.table(EnumTableName.LOTS, {
5369
5487
  user: varchar("user", { length: 42 }).notNull(),
5370
5488
  contract: varchar("contract", { length: 42 }).notNull(),
5371
5489
  group: varchar("group", { length: 66 }).notNull(),
5490
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5372
5491
  lower: numeric("lower", {
5373
5492
  precision: 78,
5374
5493
  scale: 0
@@ -5383,7 +5502,8 @@ const lots = s.table(EnumTableName.LOTS, {
5383
5502
  table.chainId,
5384
5503
  table.user,
5385
5504
  table.contract,
5386
- table.group
5505
+ table.group,
5506
+ table.obligationId
5387
5507
  ],
5388
5508
  name: "lots_pk"
5389
5509
  }),
@@ -5419,6 +5539,7 @@ const offsets = s.table(EnumTableName.OFFSETS, {
5419
5539
  user: varchar("user", { length: 42 }).notNull(),
5420
5540
  contract: varchar("contract", { length: 42 }).notNull(),
5421
5541
  group: varchar("group", { length: 66 }).notNull(),
5542
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5422
5543
  value: numeric("value", {
5423
5544
  precision: 78,
5424
5545
  scale: 0
@@ -5428,7 +5549,8 @@ const offsets = s.table(EnumTableName.OFFSETS, {
5428
5549
  table.chainId,
5429
5550
  table.user,
5430
5551
  table.contract,
5431
- table.group
5552
+ table.group,
5553
+ table.obligationId
5432
5554
  ],
5433
5555
  name: "offsets_pk"
5434
5556
  }), foreignKey({
@@ -6019,6 +6141,7 @@ function decodeCallbacks(parameters) {
6019
6141
  positionContract: loanToken,
6020
6142
  positionUser: offer.maker,
6021
6143
  group: offer.group,
6144
+ obligationId: obligationId(offer),
6022
6145
  size: offer.assets
6023
6146
  });
6024
6147
  callbacks.push({
@@ -7421,6 +7544,7 @@ function create$14(config) {
7421
7544
  return {
7422
7545
  get: async (parameters) => {
7423
7546
  const { side, obligationId, cursor: cursorString, limit = DEFAULT_LIMIT$3 } = parameters;
7547
+ const tickSortDirection = side === "sell" ? "asc" : "desc";
7424
7548
  const inputCursor = LevelCursor.decode(cursorString, logger);
7425
7549
  if (cursorString != null && inputCursor === null) return {
7426
7550
  levels: [],
@@ -7435,23 +7559,23 @@ function create$14(config) {
7435
7559
  cursor: inputCursor?.offersCursor ?? void 0,
7436
7560
  limit: fetchLimit
7437
7561
  });
7438
- const priceMap = /* @__PURE__ */ new Map();
7562
+ const tickMap = /* @__PURE__ */ new Map();
7439
7563
  for (const row of rows) {
7440
- const priceKey = row.price.toString();
7441
- const existing = priceMap.get(priceKey);
7564
+ const existing = tickMap.get(row.tick);
7442
7565
  if (existing) {
7443
7566
  existing.assets += row.takeable;
7444
7567
  existing.count += 1;
7445
- } else priceMap.set(priceKey, {
7568
+ } else tickMap.set(row.tick, {
7446
7569
  assets: row.takeable,
7447
7570
  count: 1
7448
7571
  });
7449
7572
  }
7450
- const levels = Array.from(priceMap.entries()).map(([price, data]) => ({
7451
- price: BigInt(price),
7452
- assets: data.assets,
7453
- count: data.count
7573
+ const levels = Array.from(tickMap.entries()).map(([tick, level]) => ({
7574
+ tick,
7575
+ assets: level.assets,
7576
+ count: level.count
7454
7577
  }));
7578
+ levels.sort((a, b) => tickSortDirection === "asc" ? a.tick - b.tick : b.tick - a.tick);
7455
7579
  const paginatedLevels = levels.slice(0, limit);
7456
7580
  const hasMore = levels.length > limit || offersNextCursor !== null;
7457
7581
  const lastLevel = paginatedLevels[paginatedLevels.length - 1];
@@ -7497,14 +7621,14 @@ async function _getOffers(db, params) {
7497
7621
  AND (s.code IS NULL OR s.code = ${Status.VALID})
7498
7622
  ORDER BY
7499
7623
  o.group_chain_id, o.group_maker, o."group_group",
7500
- 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
7501
7625
  ),
7502
7626
  enriched AS (
7503
7627
  SELECT
7504
7628
  w.*,
7505
7629
  g.consumed, g.chain_id, obl.loan_token,
7506
7630
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
7507
- THEN w.price::numeric ELSE -w.price::numeric END AS price_norm,
7631
+ THEN w.tick ELSE -w.tick END AS tick_norm,
7508
7632
  w.block_number AS block_norm,
7509
7633
  -w.assets AS assets_norm,
7510
7634
  w.hash AS hash_norm
@@ -7521,33 +7645,35 @@ async function _getOffers(db, params) {
7521
7645
  FROM enriched e
7522
7646
  ${cursor != null ? sql`
7523
7647
  WHERE
7524
- (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)
7525
7649
  > (
7526
7650
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
7527
- THEN ${cursor.price}::numeric ELSE -${cursor.price}::numeric END,
7651
+ THEN ${cursor.tick}::integer ELSE -${cursor.tick}::integer END,
7528
7652
  ${cursor.blockNumber},
7529
7653
  -${cursor.assets}::numeric,
7530
7654
  ${cursor.hash}
7531
7655
  )` : sql``}
7532
- 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
7533
7657
  LIMIT ${limit}
7534
7658
  ),
7535
- -- Compute sum of offsets per position
7659
+ -- Compute sum of offsets per position and obligation
7536
7660
  position_offsets AS (
7537
7661
  SELECT
7538
7662
  chain_id,
7539
7663
  "user",
7540
7664
  contract,
7665
+ obligation_id,
7541
7666
  SUM(value::numeric) AS total_offset
7542
7667
  FROM ${offsets}
7543
- GROUP BY chain_id, "user", contract
7668
+ GROUP BY chain_id, "user", contract, obligation_id
7544
7669
  ),
7545
- -- 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)
7546
7671
  position_consumed AS (
7547
7672
  SELECT
7548
7673
  l.chain_id,
7549
7674
  l.contract,
7550
7675
  l."user",
7676
+ l.obligation_id,
7551
7677
  SUM(
7552
7678
  CASE
7553
7679
  WHEN wo.assets::numeric > 0
@@ -7564,7 +7690,7 @@ async function _getOffers(db, params) {
7564
7690
  ON wo.group_chain_id = g.chain_id
7565
7691
  AND LOWER(wo.group_maker) = LOWER(g.maker)
7566
7692
  AND wo.group_group = g."group"
7567
- GROUP BY l.chain_id, l.contract, l."user"
7693
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
7568
7694
  ),
7569
7695
  -- Compute callback contributions with lot balance
7570
7696
  callback_contributions AS (
@@ -7572,7 +7698,7 @@ async function _getOffers(db, params) {
7572
7698
  p.hash,
7573
7699
  p.obligation_id,
7574
7700
  p.assets,
7575
- p.price,
7701
+ p.tick,
7576
7702
  p.obligation_units,
7577
7703
  p.obligation_shares,
7578
7704
  p.maturity,
@@ -7617,6 +7743,7 @@ async function _getOffers(db, params) {
7617
7743
  AND LOWER(l.contract) = LOWER(c.position_contract)
7618
7744
  AND LOWER(l."user") = LOWER(c.position_user)
7619
7745
  AND l."group" = p.group_group
7746
+ AND l.obligation_id = p.obligation_id
7620
7747
  LEFT JOIN ${positions} pos
7621
7748
  ON pos.chain_id = c.position_chain_id
7622
7749
  AND LOWER(pos.contract) = LOWER(c.position_contract)
@@ -7625,10 +7752,12 @@ async function _getOffers(db, params) {
7625
7752
  ON pos_offsets.chain_id = c.position_chain_id
7626
7753
  AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
7627
7754
  AND LOWER(pos_offsets."user") = LOWER(c.position_user)
7755
+ AND pos_offsets.obligation_id = p.obligation_id
7628
7756
  LEFT JOIN position_consumed pc
7629
7757
  ON pc.chain_id = c.position_chain_id
7630
7758
  AND LOWER(pc.contract) = LOWER(c.position_contract)
7631
7759
  AND LOWER(pc."user") = LOWER(c.position_user)
7760
+ AND pc.obligation_id = p.obligation_id
7632
7761
  ),
7633
7762
  -- Compute contribution per callback in loan terms (loan token only — collateral positions are not indexed)
7634
7763
  callback_loan_contribution AS (
@@ -7646,7 +7775,7 @@ async function _getOffers(db, params) {
7646
7775
  hash,
7647
7776
  obligation_id,
7648
7777
  assets,
7649
- price,
7778
+ tick,
7650
7779
  obligation_units,
7651
7780
  obligation_shares,
7652
7781
  maturity,
@@ -7672,13 +7801,13 @@ async function _getOffers(db, params) {
7672
7801
  WHERE clc.callback_id IS NOT NULL
7673
7802
  ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
7674
7803
  ) deduped
7675
- 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,
7676
7805
  callback_address, callback_data, block_number, group_chain_id, group_maker,
7677
7806
  consumed, chain_id, loan_token, session
7678
7807
  UNION ALL
7679
7808
  -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
7680
7809
  SELECT
7681
- p.hash, p.obligation_id, p.assets, p.price,
7810
+ p.hash, p.obligation_id, p.assets, p.tick,
7682
7811
  p.obligation_units, p.obligation_shares,
7683
7812
  p.maturity, p.expiry, p.start, p.group_group,
7684
7813
  p.buy, p.callback_address, p.callback_data,
@@ -7700,7 +7829,7 @@ async function _getOffers(db, params) {
7700
7829
  oc.obligation_units,
7701
7830
  oc.obligation_shares,
7702
7831
  oc.consumed,
7703
- oc.price,
7832
+ oc.tick,
7704
7833
  oc.maturity,
7705
7834
  oc.expiry,
7706
7835
  oc.start,
@@ -7732,7 +7861,7 @@ async function _getOffers(db, params) {
7732
7861
  ))
7733
7862
  END > 0
7734
7863
  ORDER BY
7735
- oc.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
7864
+ oc.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
7736
7865
  oc.block_number ASC,
7737
7866
  oc.assets DESC,
7738
7867
  oc.hash ASC;
@@ -7745,7 +7874,7 @@ async function _getOffers(db, params) {
7745
7874
  assets: BigInt(row.assets),
7746
7875
  obligationUnits: BigInt(row.obligation_units ?? 0),
7747
7876
  obligationShares: BigInt(row.obligation_shares ?? 0),
7748
- price: BigInt(row.price),
7877
+ tick: row.tick,
7749
7878
  maturity: row.maturity,
7750
7879
  expiry: row.expiry,
7751
7880
  start: row.start,
@@ -7777,7 +7906,7 @@ let Cursor;
7777
7906
  function encode(row, totalReturned, now, side) {
7778
7907
  return Buffer.from(JSON.stringify({
7779
7908
  side,
7780
- price: row.price.toString(),
7909
+ tick: row.tick,
7781
7910
  blockNumber: row.blockNumber,
7782
7911
  assets: row.assets.toString(),
7783
7912
  hash: row.hash,
@@ -7788,10 +7917,9 @@ let Cursor;
7788
7917
  _Cursor.encode = encode;
7789
7918
  function decode(cursorString, logger) {
7790
7919
  if (cursorString == null) return null;
7791
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
7792
7920
  try {
7793
7921
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
7794
- 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;
7795
7923
  throw new Error("Invalid cursor");
7796
7924
  } catch {
7797
7925
  logger.error({
@@ -7809,7 +7937,7 @@ let LevelCursor;
7809
7937
  function encode(lastLevel, offersCursor, side, now) {
7810
7938
  return Buffer.from(JSON.stringify({
7811
7939
  side,
7812
- lastPrice: lastLevel.price.toString(),
7940
+ lastTick: lastLevel.tick,
7813
7941
  now,
7814
7942
  offersCursor
7815
7943
  })).toString("base64url");
@@ -7817,10 +7945,9 @@ let LevelCursor;
7817
7945
  _LevelCursor.encode = encode;
7818
7946
  function decode(cursorString, logger) {
7819
7947
  if (cursorString == null) return null;
7820
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
7821
7948
  try {
7822
7949
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
7823
- 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;
7824
7951
  throw new Error("Invalid book cursor");
7825
7952
  } catch {
7826
7953
  logger.error({
@@ -7978,31 +8105,33 @@ function create$11(db) {
7978
8105
  function create$10(db) {
7979
8106
  return {
7980
8107
  get: async (parameters) => {
7981
- const { chainId, user, contract, group } = parameters ?? {};
8108
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
7982
8109
  const conditions = [];
7983
8110
  if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
7984
8111
  if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
7985
8112
  if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
7986
8113
  if (group !== void 0) conditions.push(eq(lots.group, group));
8114
+ if (obligationId !== void 0) conditions.push(eq(lots.obligationId, obligationId));
7987
8115
  return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
7988
8116
  chainId: row.chainId,
7989
8117
  user: row.user,
7990
8118
  contract: row.contract,
7991
8119
  group: row.group,
8120
+ obligationId: row.obligationId,
7992
8121
  lower: BigInt(row.lower),
7993
8122
  upper: BigInt(row.upper)
7994
8123
  }));
7995
8124
  },
7996
8125
  create: async (parameters) => {
7997
8126
  if (parameters.length === 0) return;
7998
- const lotsByPositionGroup = /* @__PURE__ */ new Map();
8127
+ const lotsByKey = /* @__PURE__ */ new Map();
7999
8128
  for (const offer of parameters) {
8000
- const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
8001
- const existing = lotsByPositionGroup.get(key);
8002
- 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);
8003
8132
  }
8004
- 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) {
8005
- 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())));
8006
8135
  const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
8007
8136
  const newUpper = newLower + offer.size;
8008
8137
  await db.insert(lots).values({
@@ -8010,6 +8139,7 @@ function create$10(db) {
8010
8139
  user: offer.positionUser.toLowerCase(),
8011
8140
  contract: offer.positionContract.toLowerCase(),
8012
8141
  group: offer.group.toLowerCase(),
8142
+ obligationId: offer.obligationId.toLowerCase(),
8013
8143
  lower: newLower.toString(),
8014
8144
  upper: newUpper.toString()
8015
8145
  });
@@ -8126,7 +8256,7 @@ function create$8(config) {
8126
8256
  assets: offers.assets,
8127
8257
  obligationUnits: offers.obligationUnits,
8128
8258
  obligationShares: offers.obligationShares,
8129
- price: offers.price,
8259
+ tick: offers.tick,
8130
8260
  maturity: offers.maturity,
8131
8261
  expiry: offers.expiry,
8132
8262
  start: offers.start,
@@ -8146,7 +8276,7 @@ function create$8(config) {
8146
8276
  assets: BigInt(row.assets),
8147
8277
  obligationUnits: BigInt(row.obligationUnits),
8148
8278
  obligationShares: BigInt(row.obligationShares),
8149
- price: BigInt(row.price),
8279
+ tick: row.tick,
8150
8280
  maturity: from$16(row.maturity),
8151
8281
  expiry: row.expiry,
8152
8282
  start: row.start,
@@ -8228,8 +8358,8 @@ function create$8(config) {
8228
8358
  const now$2 = now();
8229
8359
  const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
8230
8360
  obligationId: offers.obligationId,
8231
- price: offers.price
8232
- }).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`);
8233
8363
  const [bestBuys, bestSells] = await Promise.all([query({ side: "buy" }), query({ side: "sell" })]);
8234
8364
  const quotes = /* @__PURE__ */ new Map();
8235
8365
  for (const row of bestSells) quotes.set(row.obligationId, {
@@ -8264,17 +8394,19 @@ function create$8(config) {
8264
8394
  //#region src/database/domains/Offsets.ts
8265
8395
  function create$7(db) {
8266
8396
  return { get: async (parameters) => {
8267
- const { chainId, user, contract, group } = parameters ?? {};
8397
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
8268
8398
  const conditions = [];
8269
8399
  if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
8270
8400
  if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
8271
8401
  if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
8272
8402
  if (group !== void 0) conditions.push(eq(offsets.group, group));
8403
+ if (obligationId !== void 0) conditions.push(eq(offsets.obligationId, obligationId));
8273
8404
  return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
8274
8405
  chainId: row.chainId,
8275
8406
  user: row.user,
8276
8407
  contract: row.contract,
8277
8408
  group: row.group,
8409
+ obligationId: row.obligationId,
8278
8410
  value: BigInt(row.value)
8279
8411
  }));
8280
8412
  } };
@@ -8424,7 +8556,8 @@ const create$5 = (db) => {
8424
8556
  if (!parsed.chainId || !parsed.contract) throw new Error("Invalid cursor format");
8425
8557
  cursor = {
8426
8558
  chainId: parsed.chainId,
8427
- contract: parsed.contract
8559
+ contract: parsed.contract,
8560
+ obligationId: parsed.obligationId ?? null
8428
8561
  };
8429
8562
  }
8430
8563
  const raw = await db.execute(sql`
@@ -8433,16 +8566,18 @@ const create$5 = (db) => {
8433
8566
  chain_id,
8434
8567
  "user",
8435
8568
  contract,
8569
+ obligation_id,
8436
8570
  SUM(value::numeric) AS total_offset
8437
8571
  FROM ${offsets}
8438
8572
  WHERE LOWER("user") = LOWER(${user})
8439
- GROUP BY chain_id, "user", contract
8573
+ GROUP BY chain_id, "user", contract, obligation_id
8440
8574
  ),
8441
8575
  position_consumed AS (
8442
8576
  SELECT
8443
8577
  l.chain_id,
8444
8578
  l.contract,
8445
8579
  l."user",
8580
+ l.obligation_id,
8446
8581
  SUM(
8447
8582
  CASE
8448
8583
  WHEN offer_agg.assets > 0
@@ -8468,50 +8603,64 @@ const create$5 = (db) => {
8468
8603
  AND LOWER(offer_agg.group_maker) = LOWER(g.maker)
8469
8604
  AND offer_agg."group_group" = g."group"
8470
8605
  WHERE LOWER(l."user") = LOWER(${user})
8471
- GROUP BY l.chain_id, l.contract, l."user"
8606
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
8472
8607
  ),
8473
8608
  position_max_lot AS (
8474
8609
  SELECT
8475
8610
  chain_id,
8476
8611
  contract,
8477
8612
  "user",
8613
+ obligation_id,
8478
8614
  MAX(upper::numeric) AS max_upper
8479
8615
  FROM ${lots}
8480
8616
  WHERE LOWER("user") = LOWER(${user})
8481
- 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
8482
8641
  )
8483
8642
  SELECT
8484
8643
  p.chain_id,
8485
8644
  p.contract,
8486
8645
  p."user",
8487
8646
  p.block_number,
8488
- GREATEST(0,
8489
- COALESCE(pml.max_upper, 0)
8490
- - COALESCE(po.total_offset, 0)
8491
- - COALESCE(pc.consumed, 0)
8492
- )::text AS reserved_balance
8647
+ po.obligation_id,
8648
+ COALESCE(po.reserved_balance, '0') AS reserved_balance
8493
8649
  FROM ${positions} p
8494
- LEFT JOIN position_offsets po
8650
+ LEFT JOIN per_obligation po
8495
8651
  ON po.chain_id = p.chain_id
8496
8652
  AND LOWER(po.contract) = LOWER(p.contract)
8497
8653
  AND LOWER(po."user") = LOWER(p."user")
8498
- LEFT JOIN position_consumed pc
8499
- ON pc.chain_id = p.chain_id
8500
- AND LOWER(pc.contract) = LOWER(p.contract)
8501
- AND LOWER(pc."user") = LOWER(p."user")
8502
- LEFT JOIN position_max_lot pml
8503
- ON pml.chain_id = p.chain_id
8504
- AND LOWER(pml.contract) = LOWER(p.contract)
8505
- AND LOWER(pml."user") = LOWER(p."user")
8506
8654
  WHERE LOWER(p."user") = LOWER(${user})
8507
8655
  AND p."user" != ${zeroAddress}
8508
- ${cursor !== null ? sql`AND (p.chain_id, p.contract) > (${cursor.chainId}, ${cursor.contract})` : sql``}
8509
- 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
8510
8658
  LIMIT ${limit}
8511
8659
  `);
8512
8660
  const nextCursor = raw.rows.length === limit ? Buffer.from(JSON.stringify({
8513
8661
  chainId: raw.rows[raw.rows.length - 1].chain_id.toString(),
8514
- 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
8515
8664
  })).toString("base64url") : null;
8516
8665
  return {
8517
8666
  positions: raw.rows.map((row) => ({
@@ -8519,6 +8668,7 @@ const create$5 = (db) => {
8519
8668
  contract: row.contract,
8520
8669
  user: row.user,
8521
8670
  blockNumber: row.block_number,
8671
+ obligationId: row.obligation_id,
8522
8672
  reserved: BigInt(row.reserved_balance.split(".")[0] ?? "0")
8523
8673
  })),
8524
8674
  nextCursor
@@ -8893,6 +9043,7 @@ function augmentWithDomains(base, chainRegistry) {
8893
9043
  return wrapped;
8894
9044
  }
8895
9045
  const InMemoryDbMap = /* @__PURE__ */ new Map();
9046
+ const LEGACY_SCHEMA_START_MINOR = 7;
8896
9047
  /**
8897
9048
  * Connect to the database.
8898
9049
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -8947,9 +9098,26 @@ function applyMigrations(kind, driver) {
8947
9098
  async function preMigrate(driver) {
8948
9099
  const tracer = getTracer("db.preMigrate");
8949
9100
  await startActiveSpan(tracer, "db.preMigrate", async () => {
8950
- 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}"`);
8951
9103
  });
8952
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
+ }
8953
9121
  async function postMigrate(driver) {
8954
9122
  const tracer = getTracer("db.postMigrate");
8955
9123
  await startActiveSpan(tracer, "db.postMigrate", async () => {
@@ -9219,15 +9387,16 @@ async function postMigrate(driver) {
9219
9387
  RETURNS trigger
9220
9388
  LANGUAGE plpgsql AS $$
9221
9389
  BEGIN
9222
- INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
9390
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", obligation_id, value)
9223
9391
  VALUES (
9224
9392
  OLD.chain_id,
9225
9393
  OLD."user",
9226
9394
  OLD.contract,
9227
9395
  OLD."group",
9396
+ OLD.obligation_id,
9228
9397
  OLD.upper::numeric - OLD.lower::numeric
9229
9398
  )
9230
- ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
9399
+ ON CONFLICT (chain_id, "user", contract, "group", obligation_id) DO NOTHING;
9231
9400
  RETURN OLD;
9232
9401
  END;
9233
9402
  $$;
@@ -9633,7 +9802,7 @@ async function getBook(params, db) {
9633
9802
  side: query.side,
9634
9803
  levels_count: levels.length,
9635
9804
  has_next_cursor: nextCursor != null,
9636
- first_level_price: firstLevel?.price.toString() ?? null,
9805
+ first_level_tick: firstLevel?.tick ?? null,
9637
9806
  first_level_assets: firstLevel?.assets.toString() ?? null,
9638
9807
  first_level_count: firstLevel?.count ?? null
9639
9808
  });
@@ -10196,7 +10365,7 @@ async function getOffersQuery(db, parameters) {
10196
10365
  obligationUnits: offers.obligationUnits,
10197
10366
  obligationShares: offers.obligationShares,
10198
10367
  consumed: groups.consumed,
10199
- price: offers.price,
10368
+ tick: offers.tick,
10200
10369
  maturity: offers.maturity,
10201
10370
  expiry: offers.expiry,
10202
10371
  start: offers.start,
@@ -10234,7 +10403,7 @@ async function getOffersQuery(db, parameters) {
10234
10403
  assets: BigInt(row.assets),
10235
10404
  obligationUnits: BigInt(row.obligationUnits),
10236
10405
  obligationShares: BigInt(row.obligationShares),
10237
- price: BigInt(row.price),
10406
+ tick: row.tick,
10238
10407
  maturity: from$16(row.maturity),
10239
10408
  expiry: row.expiry,
10240
10409
  start: row.start,
@@ -10891,6 +11060,7 @@ function buildOfferAssociationsFromOffers(parameters) {
10891
11060
  positionContract: loanToken,
10892
11061
  positionUser: offer.maker,
10893
11062
  group: offer.group,
11063
+ obligationId: obligationId(offer),
10894
11064
  size: offer.assets
10895
11065
  });
10896
11066
  }