@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
@@ -2080,7 +2080,7 @@ const OfferSchema = () => {
2080
2080
  assets: z$1.bigint({ coerce: true }).min(0n).max(maxUint256),
2081
2081
  obligationUnits: z$1.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2082
2082
  obligationShares: z$1.bigint({ coerce: true }).min(0n).max(maxUint256).optional().default(0n),
2083
- price: z$1.bigint({ coerce: true }).min(0n).max(maxUint256),
2083
+ tick: z$1.coerce.number().int().min(0).max(990),
2084
2084
  maturity: MaturitySchema,
2085
2085
  expiry: z$1.number().int().max(Number.MAX_SAFE_INTEGER),
2086
2086
  start: z$1.number().int().max(Number.MAX_SAFE_INTEGER),
@@ -2152,7 +2152,7 @@ const serialize = (offer) => ({
2152
2152
  assets: offer.assets.toString(),
2153
2153
  obligationUnits: offer.obligationUnits.toString(),
2154
2154
  obligationShares: offer.obligationShares.toString(),
2155
- price: offer.price.toString(),
2155
+ tick: offer.tick,
2156
2156
  maturity: Number(offer.maturity),
2157
2157
  expiry: Number(offer.expiry),
2158
2158
  start: Number(offer.start),
@@ -2197,14 +2197,13 @@ function random$1(config) {
2197
2197
  [.98, 2]
2198
2198
  ]));
2199
2199
  const buy = config?.buy !== void 0 ? config.buy : bool();
2200
- const ONE = 1000000000000000000n;
2201
- const qMin = buy ? 16 : 4;
2202
- const len = (buy ? 32 : 16) - qMin + 1;
2203
- const pricePairs = Array.from({ length: len }, (_, idx) => {
2204
- const q = qMin + idx;
2205
- return [BigInt(q) * (ONE / 4n), buy ? 1 + idx : 1 + (len - 1 - idx)];
2200
+ const tickMin = buy ? 0 : 495;
2201
+ const len = (buy ? 495 : 990) - tickMin + 1;
2202
+ const tickPairs = Array.from({ length: len }, (_, idx) => {
2203
+ const weight = buy ? 1 + idx : 1 + (len - 1 - idx);
2204
+ return [tickMin + idx, weight];
2206
2205
  });
2207
- const price = config?.price ?? weightedChoice(pricePairs);
2206
+ const tick = config?.tick ?? weightedChoice(tickPairs);
2208
2207
  const loanTokenDecimals = config?.assetsDecimals?.[loanToken] ?? 18;
2209
2208
  const unit = BigInt(10) ** BigInt(loanTokenDecimals);
2210
2209
  const amountBase = BigInt(100 + int(999901));
@@ -2218,7 +2217,7 @@ function random$1(config) {
2218
2217
  assets: assetsScaled,
2219
2218
  obligationUnits: config?.obligationUnits ?? 0n,
2220
2219
  obligationShares: config?.obligationShares ?? 0n,
2221
- price,
2220
+ tick,
2222
2221
  maturity,
2223
2222
  expiry: config?.expiry ?? maturity - 1,
2224
2223
  start: config?.start ?? maturity - 10,
@@ -2283,7 +2282,7 @@ const types = {
2283
2282
  type: "uint256"
2284
2283
  },
2285
2284
  {
2286
- name: "price",
2285
+ name: "tick",
2287
2286
  type: "uint256"
2288
2287
  },
2289
2288
  {
@@ -2351,7 +2350,7 @@ function hash(offer) {
2351
2350
  assets: offer.assets,
2352
2351
  obligationUnits: offer.obligationUnits,
2353
2352
  obligationShares: offer.obligationShares,
2354
- price: offer.price,
2353
+ tick: BigInt(offer.tick),
2355
2354
  maturity: BigInt(offer.maturity),
2356
2355
  expiry: BigInt(offer.expiry),
2357
2356
  group: offer.group,
@@ -2402,7 +2401,7 @@ const OfferAbi = [
2402
2401
  type: "uint256"
2403
2402
  },
2404
2403
  {
2405
- name: "price",
2404
+ name: "tick",
2406
2405
  type: "uint256"
2407
2406
  },
2408
2407
  {
@@ -2473,7 +2472,7 @@ function encode$1(offer) {
2473
2472
  offer.assets,
2474
2473
  offer.obligationUnits,
2475
2474
  offer.obligationShares,
2476
- offer.price,
2475
+ BigInt(offer.tick),
2477
2476
  BigInt(offer.maturity),
2478
2477
  BigInt(offer.expiry),
2479
2478
  offer.group,
@@ -2498,7 +2497,7 @@ function decode$1(data) {
2498
2497
  assets: decoded[1],
2499
2498
  obligationUnits: decoded[2],
2500
2499
  obligationShares: decoded[3],
2501
- price: decoded[4],
2500
+ tick: Number(decoded[4]),
2502
2501
  maturity: from$16(Number(decoded[5])),
2503
2502
  expiry: Number(decoded[6]),
2504
2503
  group: decoded[7],
@@ -2848,6 +2847,85 @@ var InvalidQuoteError = class extends BaseError {
2848
2847
  }
2849
2848
  };
2850
2849
 
2850
+ //#endregion
2851
+ //#region src/core/Tick.ts
2852
+ var Tick_exports = /* @__PURE__ */ __exportAll({
2853
+ InvalidPriceError: () => InvalidPriceError,
2854
+ InvalidTickError: () => InvalidTickError,
2855
+ MAX_PRICE: () => MAX_PRICE,
2856
+ TICK_RANGE: () => TICK_RANGE,
2857
+ priceToTick: () => priceToTick,
2858
+ tickToPrice: () => tickToPrice
2859
+ });
2860
+ /** ln(1 + 0.025), scaled by 1e18. Matches TickLib onchain constant. */
2861
+ const LN_ONE_PLUS_DELTA = 24692612590371501n;
2862
+ /** ln(2), scaled by 1e18. Matches TickLib onchain constant. */
2863
+ const LN2 = 693147180559945309n;
2864
+ const WAD$1 = 10n ** 18n;
2865
+ const WAD_SQUARED = 10n ** 36n;
2866
+ const PRICE_STEP = 10n ** 13n;
2867
+ const HALF_TICK_RANGE = 495n;
2868
+ /** Tick domain supported by Morpho V2. */
2869
+ const TICK_RANGE = 990;
2870
+ /** Max allowed price (1e18 in wad). */
2871
+ const MAX_PRICE = WAD$1;
2872
+ /**
2873
+ * Converts a tick to a wad price using the same approximation and rounding as TickLib.
2874
+ * @param tick - Tick value in the inclusive range [0, 990].
2875
+ * @returns The price in wad units.
2876
+ * @throws {@link InvalidTickError} If tick is not an integer in range [0, 990].
2877
+ */
2878
+ function tickToPrice(tick) {
2879
+ assertTick(tick);
2880
+ return divHalfDownUnchecked(divHalfDownUnchecked(WAD_SQUARED, WAD$1 + wExp(LN_ONE_PLUS_DELTA * (HALF_TICK_RANGE - BigInt(tick)))), PRICE_STEP) * PRICE_STEP;
2881
+ }
2882
+ /**
2883
+ * Returns the lowest tick with a higher-or-equal price.
2884
+ * @param price - Price in wad units.
2885
+ * @returns The first tick whose {@link tickToPrice} is greater than or equal to `price`.
2886
+ * @throws {@link InvalidPriceError} If price is outside [0, 1e18].
2887
+ */
2888
+ function priceToTick(price) {
2889
+ assertPrice(price);
2890
+ let low = 0;
2891
+ let high = TICK_RANGE;
2892
+ while (low !== high) {
2893
+ const mid = Math.floor((low + high) / 2);
2894
+ if (tickToPrice(mid) < price) low = mid + 1;
2895
+ else high = mid;
2896
+ }
2897
+ return low;
2898
+ }
2899
+ function divHalfDownUnchecked(x, d) {
2900
+ return (x + (d - 1n) / 2n) / d;
2901
+ }
2902
+ function wExp(x) {
2903
+ if (x < 0n) return WAD_SQUARED / wExp(-x);
2904
+ const q = (x + LN2 / 2n) / LN2;
2905
+ const r = x - q * LN2;
2906
+ const secondTerm = r * r / (2n * WAD$1);
2907
+ const thirdTerm = secondTerm * r / (3n * WAD$1);
2908
+ return WAD$1 + r + secondTerm + thirdTerm << q;
2909
+ }
2910
+ function assertTick(tick) {
2911
+ if (!Number.isInteger(tick) || tick < 0 || tick > TICK_RANGE) throw new InvalidTickError(tick);
2912
+ }
2913
+ function assertPrice(price) {
2914
+ if (price < 0n || price > MAX_PRICE) throw new InvalidPriceError(price);
2915
+ }
2916
+ var InvalidTickError = class extends BaseError {
2917
+ name = "Tick.InvalidTickError";
2918
+ constructor(tick) {
2919
+ super(`Invalid tick: ${tick}. Tick must be an integer between 0 and ${TICK_RANGE}.`);
2920
+ }
2921
+ };
2922
+ var InvalidPriceError = class extends BaseError {
2923
+ name = "Tick.InvalidPriceError";
2924
+ constructor(price) {
2925
+ super(`Invalid price: ${price}. Price must be between 0 and ${MAX_PRICE}.`);
2926
+ }
2927
+ };
2928
+
2851
2929
  //#endregion
2852
2930
  //#region src/core/TradingFee.ts
2853
2931
  var TradingFee_exports = /* @__PURE__ */ __exportAll({
@@ -3361,7 +3439,7 @@ const BrandTypeId = Symbol.for("mempool/Brand");
3361
3439
 
3362
3440
  //#endregion
3363
3441
  //#region src/database/drizzle/VERSION.ts
3364
- const VERSION = "router_v1.6";
3442
+ const VERSION = "router_v1.8";
3365
3443
 
3366
3444
  //#endregion
3367
3445
  //#region src/database/drizzle/schema.ts
@@ -3513,10 +3591,7 @@ const offers = s.table(EnumTableName.OFFERS, {
3513
3591
  precision: 78,
3514
3592
  scale: 0
3515
3593
  }).notNull().default("0"),
3516
- price: numeric("price", {
3517
- precision: 78,
3518
- scale: 0
3519
- }).notNull(),
3594
+ tick: integer("tick").notNull(),
3520
3595
  maturity: integer("maturity").notNull(),
3521
3596
  expiry: integer("expiry").notNull(),
3522
3597
  start: integer("start").notNull(),
@@ -3581,6 +3656,7 @@ const lots = s.table(EnumTableName.LOTS, {
3581
3656
  user: varchar("user", { length: 42 }).notNull(),
3582
3657
  contract: varchar("contract", { length: 42 }).notNull(),
3583
3658
  group: varchar("group", { length: 66 }).notNull(),
3659
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
3584
3660
  lower: numeric("lower", {
3585
3661
  precision: 78,
3586
3662
  scale: 0
@@ -3595,7 +3671,8 @@ const lots = s.table(EnumTableName.LOTS, {
3595
3671
  table.chainId,
3596
3672
  table.user,
3597
3673
  table.contract,
3598
- table.group
3674
+ table.group,
3675
+ table.obligationId
3599
3676
  ],
3600
3677
  name: "lots_pk"
3601
3678
  }),
@@ -3631,6 +3708,7 @@ const offsets = s.table(EnumTableName.OFFSETS, {
3631
3708
  user: varchar("user", { length: 42 }).notNull(),
3632
3709
  contract: varchar("contract", { length: 42 }).notNull(),
3633
3710
  group: varchar("group", { length: 66 }).notNull(),
3711
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
3634
3712
  value: numeric("value", {
3635
3713
  precision: 78,
3636
3714
  scale: 0
@@ -3640,7 +3718,8 @@ const offsets = s.table(EnumTableName.OFFSETS, {
3640
3718
  table.chainId,
3641
3719
  table.user,
3642
3720
  table.contract,
3643
- table.group
3721
+ table.group,
3722
+ table.obligationId
3644
3723
  ],
3645
3724
  name: "offsets_pk"
3646
3725
  }), foreignKey({
@@ -4231,6 +4310,7 @@ function decodeCallbacks(parameters) {
4231
4310
  positionContract: loanToken,
4232
4311
  positionUser: offer.maker,
4233
4312
  group: offer.group,
4313
+ obligationId: obligationId(offer),
4234
4314
  size: offer.assets
4235
4315
  });
4236
4316
  callbacks.push({
@@ -5169,8 +5249,10 @@ async function getRemoteBlockNumbers(healthClients) {
5169
5249
  //#region src/api/Schema/BookResponse.ts
5170
5250
  var BookResponse_exports = /* @__PURE__ */ __exportAll({ from: () => from$5 });
5171
5251
  function from$5(level) {
5252
+ const price = tickToPrice(level.tick);
5172
5253
  return {
5173
- price: level.price.toString(),
5254
+ tick: level.tick,
5255
+ price: price.toString(),
5174
5256
  assets: level.assets.toString(),
5175
5257
  count: level.count
5176
5258
  };
@@ -5275,7 +5357,7 @@ function from$3(input) {
5275
5357
  obligation_shares: input.obligationShares.toString(),
5276
5358
  start: input.start,
5277
5359
  expiry: input.expiry,
5278
- price: input.price.toString(),
5360
+ tick: input.tick,
5279
5361
  group: input.group,
5280
5362
  session: input.session,
5281
5363
  callback: input.callback.address,
@@ -5437,7 +5519,7 @@ const offerExample = {
5437
5519
  obligation_shares: "0",
5438
5520
  start: 1761922790,
5439
5521
  expiry: 1761922799,
5440
- price: "2750000000000000000",
5522
+ tick: 495,
5441
5523
  group: "0x000000000000000000000000000000000000000000000000000000000008b8f4",
5442
5524
  session: "0x0000000000000000000000000000000000000000000000000000000000000000",
5443
5525
  callback: "0x0000000000000000000000000000000000000000",
@@ -5478,7 +5560,7 @@ const validateOfferExample = {
5478
5560
  assets: "369216000000000000000000",
5479
5561
  obligation_units: "0",
5480
5562
  obligation_shares: "0",
5481
- price: "2750000000000000000",
5563
+ tick: 495,
5482
5564
  maturity: 1761922799,
5483
5565
  expiry: 1761922799,
5484
5566
  start: 1761922790,
@@ -5622,9 +5704,11 @@ __decorate([ApiProperty({
5622
5704
  example: offerExample.offer.expiry
5623
5705
  })], OfferDataResponse.prototype, "expiry", void 0);
5624
5706
  __decorate([ApiProperty({
5625
- type: "string",
5626
- example: offerExample.offer.price
5627
- })], OfferDataResponse.prototype, "price", void 0);
5707
+ type: "number",
5708
+ example: offerExample.offer.tick,
5709
+ minimum: 0,
5710
+ maximum: 990
5711
+ })], OfferDataResponse.prototype, "tick", void 0);
5628
5712
  __decorate([ApiProperty({
5629
5713
  type: "string",
5630
5714
  example: offerExample.offer.group
@@ -5865,9 +5949,11 @@ __decorate([ApiProperty({
5865
5949
  required: false
5866
5950
  })], ValidateOfferRequest.prototype, "obligation_shares", void 0);
5867
5951
  __decorate([ApiProperty({
5868
- type: "string",
5869
- example: validateOfferExample.price
5870
- })], ValidateOfferRequest.prototype, "price", void 0);
5952
+ type: "number",
5953
+ example: validateOfferExample.tick,
5954
+ minimum: 0,
5955
+ maximum: 990
5956
+ })], ValidateOfferRequest.prototype, "tick", void 0);
5871
5957
  __decorate([ApiProperty({
5872
5958
  type: "number",
5873
5959
  example: validateOfferExample.maturity
@@ -5967,9 +6053,16 @@ __decorate([ApiProperty({
5967
6053
  description: "List of validation issues. Returned when any offer fails validation."
5968
6054
  })], ValidationFailureResponse.prototype, "data", void 0);
5969
6055
  var BookLevelResponse = class {};
6056
+ __decorate([ApiProperty({
6057
+ type: "number",
6058
+ example: 495,
6059
+ minimum: 0,
6060
+ maximum: 990
6061
+ })], BookLevelResponse.prototype, "tick", void 0);
5970
6062
  __decorate([ApiProperty({
5971
6063
  type: "string",
5972
- example: "2750000000000000000"
6064
+ example: "500000000000000000",
6065
+ description: "Price derived from tick, scaled by 1e18."
5973
6066
  })], BookLevelResponse.prototype, "price", void 0);
5974
6067
  __decorate([ApiProperty({
5975
6068
  type: "string",
@@ -5983,6 +6076,7 @@ const positionExample = {
5983
6076
  chain_id: 1,
5984
6077
  contract: "0xC9A9C45C0eB717f8b5F193Af6bAa05A1c0Ac5078",
5985
6078
  user: "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
6079
+ obligation_id: "0x12590ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
5986
6080
  reserved: "200000000000000000000",
5987
6081
  block_number: 21345678
5988
6082
  };
@@ -5999,6 +6093,12 @@ __decorate([ApiProperty({
5999
6093
  type: "string",
6000
6094
  example: positionExample.user
6001
6095
  })], PositionListItemResponse.prototype, "user", void 0);
6096
+ __decorate([ApiProperty({
6097
+ type: "string",
6098
+ nullable: true,
6099
+ example: positionExample.obligation_id,
6100
+ description: "Obligation id this reserved amount belongs to, or null if no lots exist."
6101
+ })], PositionListItemResponse.prototype, "obligation_id", void 0);
6002
6102
  __decorate([ApiProperty({
6003
6103
  type: "string",
6004
6104
  example: positionExample.reserved
@@ -6026,7 +6126,7 @@ __decorate([ApiProperty({
6026
6126
  })], BookListResponse.prototype, "cursor", void 0);
6027
6127
  __decorate([ApiProperty({
6028
6128
  type: () => [BookLevelResponse],
6029
- description: "Aggregated book levels grouped by computed price."
6129
+ description: "Aggregated book levels grouped by offer tick."
6030
6130
  })], BookListResponse.prototype, "data", void 0);
6031
6131
  let BooksController = class BooksController {
6032
6132
  async getBook() {}
@@ -6036,7 +6136,7 @@ __decorate([
6036
6136
  methods: ["get"],
6037
6137
  path: "/v1/books/{obligationId}/{side}",
6038
6138
  summary: "Get aggregated book",
6039
- description: "Returns aggregated book data for a given obligation and side. Offers are grouped by computed price with summed takeable amounts. Book levels are sorted by price (ascending for buy side, descending for sell side)."
6139
+ description: "Returns aggregated book data for a given obligation and side. Offers are grouped by tick with summed takeable amounts, and each level includes the corresponding wad-scaled price. Book levels are sorted by tick (ascending for sell side, descending for buy side)."
6040
6140
  }),
6041
6141
  ApiParam({
6042
6142
  name: "obligationId",
@@ -6061,7 +6161,7 @@ __decorate([
6061
6161
  name: "limit",
6062
6162
  type: "number",
6063
6163
  example: 10,
6064
- description: "Maximum number of price levels to return."
6164
+ description: "Maximum number of tick levels to return."
6065
6165
  }),
6066
6166
  ApiResponse({
6067
6167
  status: 200,
@@ -6255,6 +6355,11 @@ const configRulesLoanTokenExample = {
6255
6355
  chain_id: 1,
6256
6356
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
6257
6357
  };
6358
+ const configRulesCollateralTokenExample = {
6359
+ type: "collateral_token",
6360
+ chain_id: 1,
6361
+ address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
6362
+ };
6258
6363
  const configRulesOracleExample = {
6259
6364
  type: "oracle",
6260
6365
  chain_id: 1,
@@ -6264,6 +6369,7 @@ const configRulesChecksumExample = "f1d2d2f924e986ac86fdf7b36c94bcdf";
6264
6369
  const configRulesPayloadExample = [
6265
6370
  configRulesMaturityExample,
6266
6371
  configRulesLoanTokenExample,
6372
+ configRulesCollateralTokenExample,
6267
6373
  configRulesOracleExample
6268
6374
  ];
6269
6375
  const configContractNames = [
@@ -6390,7 +6496,7 @@ __decorate([
6390
6496
  methods: ["get"],
6391
6497
  path: "/v1/config/rules",
6392
6498
  summary: "Get config rules",
6393
- description: "Returns configured rules (maturities, loan tokens, oracles) for supported chains."
6499
+ description: "Returns configured rules (maturities, loan tokens, collateral tokens, oracles) for supported chains."
6394
6500
  }),
6395
6501
  ApiQuery({
6396
6502
  name: "cursor",
@@ -6410,7 +6516,7 @@ __decorate([
6410
6516
  name: "types",
6411
6517
  type: ["string"],
6412
6518
  required: false,
6413
- example: "maturity,loan_token,oracle",
6519
+ example: "maturity,loan_token,collateral_token,oracle",
6414
6520
  description: "Filter by rule types (comma-separated).",
6415
6521
  style: "form",
6416
6522
  explode: false
@@ -6528,7 +6634,7 @@ __decorate([
6528
6634
  methods: ["get"],
6529
6635
  path: "/v1/users/{userAddress}/positions",
6530
6636
  summary: "Get user positions",
6531
- description: "Returns positions for a user with reserved balance. The reserved balance is the amount locked by active offers (max lot upper - offset - consumed)."
6637
+ description: "Returns positions for a user with reserved balance per obligation. Each (position, obligation) pair is returned as a separate row. Positions with no lots return a single row with obligation_id = null and reserved = 0."
6532
6638
  }),
6533
6639
  ApiParam({
6534
6640
  name: "userAddress",
@@ -6616,6 +6722,7 @@ function from$2(position) {
6616
6722
  chain_id: position.chainId,
6617
6723
  contract: position.contract,
6618
6724
  user: position.user,
6725
+ obligation_id: position.obligationId,
6619
6726
  reserved: position.reserved.toString(),
6620
6727
  block_number: position.blockNumber
6621
6728
  };
@@ -6669,10 +6776,11 @@ const ConfigRuleTypes = z$1.enum([
6669
6776
  "maturity",
6670
6777
  "callback",
6671
6778
  "loan_token",
6779
+ "collateral_token",
6672
6780
  "oracle"
6673
6781
  ]);
6674
6782
  const GetConfigRulesQueryParams = z$1.object({
6675
- cursor: z$1.string().regex(/^(maturity|callback|loan_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6783
+ cursor: z$1.string().regex(/^(maturity|callback|loan_token|collateral_token|oracle):[1-9]\d*:.+$/, { message: "Cursor must be in the format type:chain_id:<value>" }).optional().meta({
6676
6784
  description: "Pagination cursor in type:chain_id:<value> format",
6677
6785
  example: "maturity:1:1730415600:end_of_next_month"
6678
6786
  }),
@@ -6682,7 +6790,7 @@ const GetConfigRulesQueryParams = z$1.object({
6682
6790
  }),
6683
6791
  types: csvArray(ConfigRuleTypes).meta({
6684
6792
  description: "Filter by rule types (comma-separated).",
6685
- example: "maturity,loan_token,oracle"
6793
+ example: "maturity,loan_token,collateral_token,oracle"
6686
6794
  }),
6687
6795
  chains: csvArray(z$1.string().regex(/^[1-9]\d*$/, { message: "Chain must be a positive integer" }).transform((val) => Number.parseInt(val, 10))).meta({
6688
6796
  description: "Filter by chain IDs (comma-separated).",
@@ -6782,12 +6890,11 @@ const GetObligationParams = z$1.object({ obligation_id: z$1.string({ error: "Obl
6782
6890
  description: "Obligation id",
6783
6891
  example: "0x1234567890123456789012345678901234567890123456789012345678901234"
6784
6892
  }) });
6785
- /** Validate a book cursor format: {side, lastPrice, offersCursor} */
6893
+ /** Validate a book cursor format: {side, lastTick, offersCursor} */
6786
6894
  function isValidBookCursor(cursorString) {
6787
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
6788
6895
  try {
6789
6896
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
6790
- return (v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6897
+ return (v?.side === "buy" || v?.side === "sell") && typeof v?.lastTick === "number" && Number.isInteger(v.lastTick) && (v?.offersCursor === null || typeof v?.offersCursor === "string");
6791
6898
  } catch {
6792
6899
  return false;
6793
6900
  }
@@ -6885,7 +6992,7 @@ async function getBook(params, db) {
6885
6992
  side: query.side,
6886
6993
  levels_count: levels.length,
6887
6994
  has_next_cursor: nextCursor != null,
6888
- first_level_price: firstLevel?.price.toString() ?? null,
6995
+ first_level_tick: firstLevel?.tick ?? null,
6889
6996
  first_level_assets: firstLevel?.assets.toString() ?? null,
6890
6997
  first_level_count: firstLevel?.count ?? null
6891
6998
  });
@@ -7037,6 +7144,33 @@ const assets = {
7037
7144
  "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
7038
7145
  ]
7039
7146
  };
7147
+ const collateralAssets = {
7148
+ [ChainId.ETHEREUM.toString()]: [
7149
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
7150
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
7151
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
7152
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7153
+ ],
7154
+ [ChainId.BASE.toString()]: [
7155
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
7156
+ "0x4200000000000000000000000000000000000006",
7157
+ "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
7158
+ "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
7159
+ "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
7160
+ ],
7161
+ [ChainId["ETHEREUM-VIRTUAL-TESTNET"].toString()]: [
7162
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
7163
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
7164
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
7165
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7166
+ ],
7167
+ [ChainId.ANVIL.toString()]: [
7168
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
7169
+ "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
7170
+ "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
7171
+ "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
7172
+ ]
7173
+ };
7040
7174
  const oracles = {
7041
7175
  [ChainId.ETHEREUM.toString()]: [
7042
7176
  "0xDddd770BADd886dF3864029e4B377B5F6a2B6b83",
@@ -7096,7 +7230,7 @@ const configs = {
7096
7230
  //#endregion
7097
7231
  //#region src/gatekeeper/ConfigRules.ts
7098
7232
  /**
7099
- * Build the configured rules (maturities + callback addresses + loan tokens + oracles) for the provided chains.
7233
+ * Build the configured rules (maturities + callback addresses + loan tokens + collateral tokens + oracles) for the provided chains.
7100
7234
  * @param chains - Chains to include in the configured rules.
7101
7235
  * @returns Sorted list of config rules.
7102
7236
  */
@@ -7116,6 +7250,12 @@ function buildConfigRules(chains) {
7116
7250
  chain_id: chain.id,
7117
7251
  address: normalizeAddress(address)
7118
7252
  });
7253
+ const collateralTokens = collateralAssets[chain.id.toString()] ?? [];
7254
+ for (const address of collateralTokens) rules.push({
7255
+ type: "collateral_token",
7256
+ chain_id: chain.id,
7257
+ address: normalizeAddress(address)
7258
+ });
7119
7259
  const oracles$2 = oracles[chain.id.toString()] ?? [];
7120
7260
  for (const address of oracles$2) rules.push({
7121
7261
  type: "oracle",
@@ -7143,6 +7283,10 @@ function buildConfigRulesChecksum(rules) {
7143
7283
  hash.update(`callback:${rule.chain_id}:${rule.callback_type}:${rule.address}\n`);
7144
7284
  continue;
7145
7285
  }
7286
+ if (rule.type === "collateral_token") {
7287
+ hash.update(`collateral_token:${rule.chain_id}:${rule.address}\n`);
7288
+ continue;
7289
+ }
7146
7290
  if (rule.type === "oracle") {
7147
7291
  hash.update(`oracle:${rule.chain_id}:${rule.address}\n`);
7148
7292
  continue;
@@ -7163,6 +7307,7 @@ function compareConfigRules(left, right) {
7163
7307
  return left.address.localeCompare(right.address);
7164
7308
  }
7165
7309
  if (left.type === "loan_token" && right.type === "loan_token") return left.address.localeCompare(right.address);
7310
+ if (left.type === "collateral_token" && right.type === "collateral_token") return left.address.localeCompare(right.address);
7166
7311
  if (left.type === "oracle" && right.type === "oracle") return left.address.localeCompare(right.address);
7167
7312
  return 0;
7168
7313
  }
@@ -7209,6 +7354,7 @@ function formatCursor(rule) {
7209
7354
  if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
7210
7355
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
7211
7356
  if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
7357
+ if (rule.type === "collateral_token") return `collateral_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
7212
7358
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
7213
7359
  }
7214
7360
  function parseCursor(cursor) {
@@ -7241,7 +7387,7 @@ function parseCursor(cursor) {
7241
7387
  address: parseAddress(addressValue, "Cursor address")
7242
7388
  };
7243
7389
  }
7244
- if (type === "loan_token" || type === "oracle") {
7390
+ if (type === "loan_token" || type === "collateral_token" || type === "oracle") {
7245
7391
  const addressValue = rest.join(":");
7246
7392
  if (!addressValue) throw new BadRequestError(`Cursor must be in the format ${type}:chain_id:address`);
7247
7393
  return {
@@ -7268,7 +7414,7 @@ function parseAddress(address, label) {
7268
7414
  return address.toLowerCase();
7269
7415
  }
7270
7416
  function isConfigRuleType(value) {
7271
- return value === "maturity" || value === "callback" || value === "loan_token" || value === "oracle";
7417
+ return value === "maturity" || value === "callback" || value === "loan_token" || value === "collateral_token" || value === "oracle";
7272
7418
  }
7273
7419
  function isMaturityType(value) {
7274
7420
  return Object.values(MaturityType).includes(value);
@@ -7615,7 +7761,7 @@ function create$15(config) {
7615
7761
  assets: offers.assets,
7616
7762
  obligationUnits: offers.obligationUnits,
7617
7763
  obligationShares: offers.obligationShares,
7618
- price: offers.price,
7764
+ tick: offers.tick,
7619
7765
  maturity: offers.maturity,
7620
7766
  expiry: offers.expiry,
7621
7767
  start: offers.start,
@@ -7635,7 +7781,7 @@ function create$15(config) {
7635
7781
  assets: BigInt(row.assets),
7636
7782
  obligationUnits: BigInt(row.obligationUnits),
7637
7783
  obligationShares: BigInt(row.obligationShares),
7638
- price: BigInt(row.price),
7784
+ tick: row.tick,
7639
7785
  maturity: from$16(row.maturity),
7640
7786
  expiry: row.expiry,
7641
7787
  start: row.start,
@@ -7717,8 +7863,8 @@ function create$15(config) {
7717
7863
  const now$2 = now();
7718
7864
  const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
7719
7865
  obligationId: offers.obligationId,
7720
- price: offers.price
7721
- }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, eq(offers.hash, validations.offerHash)).leftJoin(status, eq(validations.statusId, status.id)).where(and(inArray(offers.obligationId, obligationIds), eq(offers.buy, side === "buy"), gte(offers.expiry, now$2), gte(offers.maturity, now$2), lte(offers.start, now$2), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(offers.obligationId, side === "buy" ? sql`${offers.price}::numeric ASC` : sql`${offers.price}::numeric DESC`);
7866
+ price: offers.tick
7867
+ }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, eq(offers.hash, validations.offerHash)).leftJoin(status, eq(validations.statusId, status.id)).where(and(inArray(offers.obligationId, obligationIds), eq(offers.buy, side === "buy"), gte(offers.expiry, now$2), gte(offers.maturity, now$2), lte(offers.start, now$2), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(offers.obligationId, side === "buy" ? sql`${offers.tick} ASC` : sql`${offers.tick} DESC`);
7722
7868
  const [bestBuys, bestSells] = await Promise.all([query({ side: "buy" }), query({ side: "sell" })]);
7723
7869
  const quotes = /* @__PURE__ */ new Map();
7724
7870
  for (const row of bestSells) quotes.set(row.obligationId, {
@@ -7834,7 +7980,7 @@ async function getOffersQuery(db, parameters) {
7834
7980
  obligationUnits: offers.obligationUnits,
7835
7981
  obligationShares: offers.obligationShares,
7836
7982
  consumed: groups.consumed,
7837
- price: offers.price,
7983
+ tick: offers.tick,
7838
7984
  maturity: offers.maturity,
7839
7985
  expiry: offers.expiry,
7840
7986
  start: offers.start,
@@ -7872,7 +8018,7 @@ async function getOffersQuery(db, parameters) {
7872
8018
  assets: BigInt(row.assets),
7873
8019
  obligationUnits: BigInt(row.obligationUnits),
7874
8020
  obligationShares: BigInt(row.obligationShares),
7875
- price: BigInt(row.price),
8021
+ tick: row.tick,
7876
8022
  maturity: from$16(row.maturity),
7877
8023
  expiry: row.expiry,
7878
8024
  start: row.start,
@@ -8271,7 +8417,7 @@ async function getOffers(apiClient, parameters) {
8271
8417
  assets: offerData.assets,
8272
8418
  obligation_units: offerData.obligation_units,
8273
8419
  obligation_shares: offerData.obligation_shares,
8274
- price: offerData.price,
8420
+ tick: offerData.tick,
8275
8421
  maturity: from$16(offerData.obligation.maturity),
8276
8422
  expiry: offerData.expiry,
8277
8423
  start: offerData.start,
@@ -8630,6 +8776,7 @@ function create$12(config) {
8630
8776
  return {
8631
8777
  get: async (parameters) => {
8632
8778
  const { side, obligationId, cursor: cursorString, limit = DEFAULT_LIMIT$2 } = parameters;
8779
+ const tickSortDirection = side === "sell" ? "asc" : "desc";
8633
8780
  const inputCursor = LevelCursor.decode(cursorString, logger);
8634
8781
  if (cursorString != null && inputCursor === null) return {
8635
8782
  levels: [],
@@ -8644,23 +8791,23 @@ function create$12(config) {
8644
8791
  cursor: inputCursor?.offersCursor ?? void 0,
8645
8792
  limit: fetchLimit
8646
8793
  });
8647
- const priceMap = /* @__PURE__ */ new Map();
8794
+ const tickMap = /* @__PURE__ */ new Map();
8648
8795
  for (const row of rows) {
8649
- const priceKey = row.price.toString();
8650
- const existing = priceMap.get(priceKey);
8796
+ const existing = tickMap.get(row.tick);
8651
8797
  if (existing) {
8652
8798
  existing.assets += row.takeable;
8653
8799
  existing.count += 1;
8654
- } else priceMap.set(priceKey, {
8800
+ } else tickMap.set(row.tick, {
8655
8801
  assets: row.takeable,
8656
8802
  count: 1
8657
8803
  });
8658
8804
  }
8659
- const levels = Array.from(priceMap.entries()).map(([price, data]) => ({
8660
- price: BigInt(price),
8661
- assets: data.assets,
8662
- count: data.count
8805
+ const levels = Array.from(tickMap.entries()).map(([tick, level]) => ({
8806
+ tick,
8807
+ assets: level.assets,
8808
+ count: level.count
8663
8809
  }));
8810
+ levels.sort((a, b) => tickSortDirection === "asc" ? a.tick - b.tick : b.tick - a.tick);
8664
8811
  const paginatedLevels = levels.slice(0, limit);
8665
8812
  const hasMore = levels.length > limit || offersNextCursor !== null;
8666
8813
  const lastLevel = paginatedLevels[paginatedLevels.length - 1];
@@ -8706,14 +8853,14 @@ async function _getOffers(db, params) {
8706
8853
  AND (s.code IS NULL OR s.code = ${Status.VALID})
8707
8854
  ORDER BY
8708
8855
  o.group_chain_id, o.group_maker, o."group_group",
8709
- o.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, o.block_number ASC, o.assets DESC, o.hash ASC
8856
+ o.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, o.block_number ASC, o.assets DESC, o.hash ASC
8710
8857
  ),
8711
8858
  enriched AS (
8712
8859
  SELECT
8713
8860
  w.*,
8714
8861
  g.consumed, g.chain_id, obl.loan_token,
8715
8862
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
8716
- THEN w.price::numeric ELSE -w.price::numeric END AS price_norm,
8863
+ THEN w.tick ELSE -w.tick END AS tick_norm,
8717
8864
  w.block_number AS block_norm,
8718
8865
  -w.assets AS assets_norm,
8719
8866
  w.hash AS hash_norm
@@ -8730,33 +8877,35 @@ async function _getOffers(db, params) {
8730
8877
  FROM enriched e
8731
8878
  ${cursor != null ? sql`
8732
8879
  WHERE
8733
- (e.price_norm, e.block_norm, e.assets_norm, e.hash_norm)
8880
+ (e.tick_norm, e.block_norm, e.assets_norm, e.hash_norm)
8734
8881
  > (
8735
8882
  CASE WHEN ${priceSortDirection === "asc" ? sql`TRUE` : sql`FALSE`}
8736
- THEN ${cursor.price}::numeric ELSE -${cursor.price}::numeric END,
8883
+ THEN ${cursor.tick}::integer ELSE -${cursor.tick}::integer END,
8737
8884
  ${cursor.blockNumber},
8738
8885
  -${cursor.assets}::numeric,
8739
8886
  ${cursor.hash}
8740
8887
  )` : sql``}
8741
- ORDER BY e.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
8888
+ ORDER BY e.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`}, e.block_number ASC, e.assets DESC, e.hash ASC
8742
8889
  LIMIT ${limit}
8743
8890
  ),
8744
- -- Compute sum of offsets per position
8891
+ -- Compute sum of offsets per position and obligation
8745
8892
  position_offsets AS (
8746
8893
  SELECT
8747
8894
  chain_id,
8748
8895
  "user",
8749
8896
  contract,
8897
+ obligation_id,
8750
8898
  SUM(value::numeric) AS total_offset
8751
8899
  FROM ${offsets}
8752
- GROUP BY chain_id, "user", contract
8900
+ GROUP BY chain_id, "user", contract, obligation_id
8753
8901
  ),
8754
- -- Compute position_consumed: sum of consumed from all groups with lots on each position (converted to lot terms)
8902
+ -- Compute position_consumed: sum of consumed from all groups with lots on each position+obligation (converted to lot terms)
8755
8903
  position_consumed AS (
8756
8904
  SELECT
8757
8905
  l.chain_id,
8758
8906
  l.contract,
8759
8907
  l."user",
8908
+ l.obligation_id,
8760
8909
  SUM(
8761
8910
  CASE
8762
8911
  WHEN wo.assets::numeric > 0
@@ -8773,7 +8922,7 @@ async function _getOffers(db, params) {
8773
8922
  ON wo.group_chain_id = g.chain_id
8774
8923
  AND LOWER(wo.group_maker) = LOWER(g.maker)
8775
8924
  AND wo.group_group = g."group"
8776
- GROUP BY l.chain_id, l.contract, l."user"
8925
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
8777
8926
  ),
8778
8927
  -- Compute callback contributions with lot balance
8779
8928
  callback_contributions AS (
@@ -8781,7 +8930,7 @@ async function _getOffers(db, params) {
8781
8930
  p.hash,
8782
8931
  p.obligation_id,
8783
8932
  p.assets,
8784
- p.price,
8933
+ p.tick,
8785
8934
  p.obligation_units,
8786
8935
  p.obligation_shares,
8787
8936
  p.maturity,
@@ -8826,6 +8975,7 @@ async function _getOffers(db, params) {
8826
8975
  AND LOWER(l.contract) = LOWER(c.position_contract)
8827
8976
  AND LOWER(l."user") = LOWER(c.position_user)
8828
8977
  AND l."group" = p.group_group
8978
+ AND l.obligation_id = p.obligation_id
8829
8979
  LEFT JOIN ${positions} pos
8830
8980
  ON pos.chain_id = c.position_chain_id
8831
8981
  AND LOWER(pos.contract) = LOWER(c.position_contract)
@@ -8834,10 +8984,12 @@ async function _getOffers(db, params) {
8834
8984
  ON pos_offsets.chain_id = c.position_chain_id
8835
8985
  AND LOWER(pos_offsets.contract) = LOWER(c.position_contract)
8836
8986
  AND LOWER(pos_offsets."user") = LOWER(c.position_user)
8987
+ AND pos_offsets.obligation_id = p.obligation_id
8837
8988
  LEFT JOIN position_consumed pc
8838
8989
  ON pc.chain_id = c.position_chain_id
8839
8990
  AND LOWER(pc.contract) = LOWER(c.position_contract)
8840
8991
  AND LOWER(pc."user") = LOWER(c.position_user)
8992
+ AND pc.obligation_id = p.obligation_id
8841
8993
  ),
8842
8994
  -- Compute contribution per callback in loan terms (loan token only — collateral positions are not indexed)
8843
8995
  callback_loan_contribution AS (
@@ -8855,7 +9007,7 @@ async function _getOffers(db, params) {
8855
9007
  hash,
8856
9008
  obligation_id,
8857
9009
  assets,
8858
- price,
9010
+ tick,
8859
9011
  obligation_units,
8860
9012
  obligation_shares,
8861
9013
  maturity,
@@ -8881,13 +9033,13 @@ async function _getOffers(db, params) {
8881
9033
  WHERE clc.callback_id IS NOT NULL
8882
9034
  ORDER BY clc.hash, clc.position_chain_id, clc.position_contract, clc.position_user, clc.contribution_in_loan DESC
8883
9035
  ) deduped
8884
- GROUP BY hash, obligation_id, assets, price, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
9036
+ GROUP BY hash, obligation_id, assets, tick, obligation_units, obligation_shares, maturity, expiry, start, group_group, buy,
8885
9037
  callback_address, callback_data, block_number, group_chain_id, group_maker,
8886
9038
  consumed, chain_id, loan_token, session
8887
9039
  UNION ALL
8888
9040
  -- Sell offers without callbacks: collateral positions not indexed, takeable = assets - consumed
8889
9041
  SELECT
8890
- p.hash, p.obligation_id, p.assets, p.price,
9042
+ p.hash, p.obligation_id, p.assets, p.tick,
8891
9043
  p.obligation_units, p.obligation_shares,
8892
9044
  p.maturity, p.expiry, p.start, p.group_group,
8893
9045
  p.buy, p.callback_address, p.callback_data,
@@ -8909,7 +9061,7 @@ async function _getOffers(db, params) {
8909
9061
  oc.obligation_units,
8910
9062
  oc.obligation_shares,
8911
9063
  oc.consumed,
8912
- oc.price,
9064
+ oc.tick,
8913
9065
  oc.maturity,
8914
9066
  oc.expiry,
8915
9067
  oc.start,
@@ -8941,7 +9093,7 @@ async function _getOffers(db, params) {
8941
9093
  ))
8942
9094
  END > 0
8943
9095
  ORDER BY
8944
- oc.price::numeric ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
9096
+ oc.tick ${priceSortDirection === "asc" ? sql`ASC` : sql`DESC`},
8945
9097
  oc.block_number ASC,
8946
9098
  oc.assets DESC,
8947
9099
  oc.hash ASC;
@@ -8954,7 +9106,7 @@ async function _getOffers(db, params) {
8954
9106
  assets: BigInt(row.assets),
8955
9107
  obligationUnits: BigInt(row.obligation_units ?? 0),
8956
9108
  obligationShares: BigInt(row.obligation_shares ?? 0),
8957
- price: BigInt(row.price),
9109
+ tick: row.tick,
8958
9110
  maturity: row.maturity,
8959
9111
  expiry: row.expiry,
8960
9112
  start: row.start,
@@ -8986,7 +9138,7 @@ let Cursor;
8986
9138
  function encode(row, totalReturned, now, side) {
8987
9139
  return Buffer.from(JSON.stringify({
8988
9140
  side,
8989
- price: row.price.toString(),
9141
+ tick: row.tick,
8990
9142
  blockNumber: row.blockNumber,
8991
9143
  assets: row.assets.toString(),
8992
9144
  hash: row.hash,
@@ -8997,10 +9149,9 @@ let Cursor;
8997
9149
  _Cursor.encode = encode;
8998
9150
  function decode(cursorString, logger) {
8999
9151
  if (cursorString == null) return null;
9000
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
9001
9152
  try {
9002
9153
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
9003
- if ((v?.side === "buy" || v?.side === "sell") && isNumericString(v?.price) && typeof v?.blockNumber === "number" && Number.isInteger(v.blockNumber) && isNumericString(v?.assets) && isHex(v?.hash) && typeof v?.totalReturned === "number" && Number.isInteger(v.totalReturned) && typeof v?.now === "number" && Number.isInteger(v.now)) return v;
9154
+ if ((v?.side === "buy" || v?.side === "sell") && typeof v?.tick === "number" && Number.isInteger(v.tick) && typeof v?.blockNumber === "number" && Number.isInteger(v.blockNumber) && typeof v?.assets === "string" && /^-?\d+$/.test(v.assets) && isHex(v?.hash) && typeof v?.totalReturned === "number" && Number.isInteger(v.totalReturned) && typeof v?.now === "number" && Number.isInteger(v.now)) return v;
9004
9155
  throw new Error("Invalid cursor");
9005
9156
  } catch {
9006
9157
  logger.error({
@@ -9018,7 +9169,7 @@ let LevelCursor;
9018
9169
  function encode(lastLevel, offersCursor, side, now) {
9019
9170
  return Buffer.from(JSON.stringify({
9020
9171
  side,
9021
- lastPrice: lastLevel.price.toString(),
9172
+ lastTick: lastLevel.tick,
9022
9173
  now,
9023
9174
  offersCursor
9024
9175
  })).toString("base64url");
@@ -9026,10 +9177,9 @@ let LevelCursor;
9026
9177
  _LevelCursor.encode = encode;
9027
9178
  function decode(cursorString, logger) {
9028
9179
  if (cursorString == null) return null;
9029
- const isNumericString = (value) => typeof value === "string" && /^-?\d+$/.test(value);
9030
9180
  try {
9031
9181
  const v = JSON.parse(Buffer.from(cursorString, "base64url").toString("utf8"));
9032
- if ((v?.side === "buy" || v?.side === "sell") && isNumericString(v?.lastPrice) && typeof v?.now === "number" && Number.isInteger(v.now) && (v?.offersCursor === null || typeof v?.offersCursor === "string")) return v;
9182
+ if ((v?.side === "buy" || v?.side === "sell") && typeof v?.lastTick === "number" && Number.isInteger(v.lastTick) && typeof v?.now === "number" && Number.isInteger(v.now) && (v?.offersCursor === null || typeof v?.offersCursor === "string")) return v;
9033
9183
  throw new Error("Invalid book cursor");
9034
9184
  } catch {
9035
9185
  logger.error({
@@ -9172,31 +9322,33 @@ function create$9(db) {
9172
9322
  function create$8(db) {
9173
9323
  return {
9174
9324
  get: async (parameters) => {
9175
- const { chainId, user, contract, group } = parameters ?? {};
9325
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
9176
9326
  const conditions = [];
9177
9327
  if (chainId !== void 0) conditions.push(eq(lots.chainId, chainId));
9178
9328
  if (user !== void 0) conditions.push(eq(lots.user, user.toLowerCase()));
9179
9329
  if (contract !== void 0) conditions.push(eq(lots.contract, contract.toLowerCase()));
9180
9330
  if (group !== void 0) conditions.push(eq(lots.group, group));
9331
+ if (obligationId !== void 0) conditions.push(eq(lots.obligationId, obligationId));
9181
9332
  return (await db.select().from(lots).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
9182
9333
  chainId: row.chainId,
9183
9334
  user: row.user,
9184
9335
  contract: row.contract,
9185
9336
  group: row.group,
9337
+ obligationId: row.obligationId,
9186
9338
  lower: BigInt(row.lower),
9187
9339
  upper: BigInt(row.upper)
9188
9340
  }));
9189
9341
  },
9190
9342
  create: async (parameters) => {
9191
9343
  if (parameters.length === 0) return;
9192
- const lotsByPositionGroup = /* @__PURE__ */ new Map();
9344
+ const lotsByKey = /* @__PURE__ */ new Map();
9193
9345
  for (const offer of parameters) {
9194
- const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}`.toLowerCase();
9195
- const existing = lotsByPositionGroup.get(key);
9196
- if (!existing || offer.size > existing.size) lotsByPositionGroup.set(key, offer);
9346
+ const key = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}-${offer.group}-${offer.obligationId}`.toLowerCase();
9347
+ const existing = lotsByKey.get(key);
9348
+ if (!existing || offer.size > existing.size) lotsByKey.set(key, offer);
9197
9349
  }
9198
- 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) {
9199
- const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase())));
9350
+ for (const offer of lotsByKey.values()) if ((await db.select().from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.group, offer.group.toLowerCase()), eq(lots.obligationId, offer.obligationId.toLowerCase()))).limit(1)).length === 0) {
9351
+ const maxUpperResult = await db.select({ maxUpper: sql`COALESCE(MAX(${lots.upper}::numeric), 0)` }).from(lots).where(and(eq(lots.chainId, offer.positionChainId), eq(lots.contract, offer.positionContract.toLowerCase()), eq(lots.user, offer.positionUser.toLowerCase()), eq(lots.obligationId, offer.obligationId.toLowerCase())));
9200
9352
  const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
9201
9353
  const newUpper = newLower + offer.size;
9202
9354
  await db.insert(lots).values({
@@ -9204,6 +9356,7 @@ function create$8(db) {
9204
9356
  user: offer.positionUser.toLowerCase(),
9205
9357
  contract: offer.positionContract.toLowerCase(),
9206
9358
  group: offer.group.toLowerCase(),
9359
+ obligationId: offer.obligationId.toLowerCase(),
9207
9360
  lower: newLower.toString(),
9208
9361
  upper: newUpper.toString()
9209
9362
  });
@@ -9258,17 +9411,19 @@ function create$7(db) {
9258
9411
  //#region src/database/domains/Offsets.ts
9259
9412
  function create$6(db) {
9260
9413
  return { get: async (parameters) => {
9261
- const { chainId, user, contract, group } = parameters ?? {};
9414
+ const { chainId, user, contract, group, obligationId } = parameters ?? {};
9262
9415
  const conditions = [];
9263
9416
  if (chainId !== void 0) conditions.push(eq(offsets.chainId, chainId));
9264
9417
  if (user !== void 0) conditions.push(eq(offsets.user, user.toLowerCase()));
9265
9418
  if (contract !== void 0) conditions.push(eq(offsets.contract, contract.toLowerCase()));
9266
9419
  if (group !== void 0) conditions.push(eq(offsets.group, group));
9420
+ if (obligationId !== void 0) conditions.push(eq(offsets.obligationId, obligationId));
9267
9421
  return (await db.select().from(offsets).where(conditions.length > 0 ? and(...conditions) : void 0)).map((row) => ({
9268
9422
  chainId: row.chainId,
9269
9423
  user: row.user,
9270
9424
  contract: row.contract,
9271
9425
  group: row.group,
9426
+ obligationId: row.obligationId,
9272
9427
  value: BigInt(row.value)
9273
9428
  }));
9274
9429
  } };
@@ -9418,7 +9573,8 @@ const create$4 = (db) => {
9418
9573
  if (!parsed.chainId || !parsed.contract) throw new Error("Invalid cursor format");
9419
9574
  cursor = {
9420
9575
  chainId: parsed.chainId,
9421
- contract: parsed.contract
9576
+ contract: parsed.contract,
9577
+ obligationId: parsed.obligationId ?? null
9422
9578
  };
9423
9579
  }
9424
9580
  const raw = await db.execute(sql`
@@ -9427,16 +9583,18 @@ const create$4 = (db) => {
9427
9583
  chain_id,
9428
9584
  "user",
9429
9585
  contract,
9586
+ obligation_id,
9430
9587
  SUM(value::numeric) AS total_offset
9431
9588
  FROM ${offsets}
9432
9589
  WHERE LOWER("user") = LOWER(${user})
9433
- GROUP BY chain_id, "user", contract
9590
+ GROUP BY chain_id, "user", contract, obligation_id
9434
9591
  ),
9435
9592
  position_consumed AS (
9436
9593
  SELECT
9437
9594
  l.chain_id,
9438
9595
  l.contract,
9439
9596
  l."user",
9597
+ l.obligation_id,
9440
9598
  SUM(
9441
9599
  CASE
9442
9600
  WHEN offer_agg.assets > 0
@@ -9462,50 +9620,64 @@ const create$4 = (db) => {
9462
9620
  AND LOWER(offer_agg.group_maker) = LOWER(g.maker)
9463
9621
  AND offer_agg."group_group" = g."group"
9464
9622
  WHERE LOWER(l."user") = LOWER(${user})
9465
- GROUP BY l.chain_id, l.contract, l."user"
9623
+ GROUP BY l.chain_id, l.contract, l."user", l.obligation_id
9466
9624
  ),
9467
9625
  position_max_lot AS (
9468
9626
  SELECT
9469
9627
  chain_id,
9470
9628
  contract,
9471
9629
  "user",
9630
+ obligation_id,
9472
9631
  MAX(upper::numeric) AS max_upper
9473
9632
  FROM ${lots}
9474
9633
  WHERE LOWER("user") = LOWER(${user})
9475
- GROUP BY chain_id, contract, "user"
9634
+ GROUP BY chain_id, contract, "user", obligation_id
9635
+ ),
9636
+ per_obligation AS (
9637
+ SELECT
9638
+ pml.chain_id,
9639
+ pml.contract,
9640
+ pml."user",
9641
+ pml.obligation_id,
9642
+ GREATEST(0,
9643
+ COALESCE(pml.max_upper, 0)
9644
+ - COALESCE(po.total_offset, 0)
9645
+ - COALESCE(pc.consumed, 0)
9646
+ )::text AS reserved_balance
9647
+ FROM position_max_lot pml
9648
+ LEFT JOIN position_offsets po
9649
+ ON po.chain_id = pml.chain_id
9650
+ AND LOWER(po.contract) = LOWER(pml.contract)
9651
+ AND LOWER(po."user") = LOWER(pml."user")
9652
+ AND po.obligation_id = pml.obligation_id
9653
+ LEFT JOIN position_consumed pc
9654
+ ON pc.chain_id = pml.chain_id
9655
+ AND LOWER(pc.contract) = LOWER(pml.contract)
9656
+ AND LOWER(pc."user") = LOWER(pml."user")
9657
+ AND pc.obligation_id = pml.obligation_id
9476
9658
  )
9477
9659
  SELECT
9478
9660
  p.chain_id,
9479
9661
  p.contract,
9480
9662
  p."user",
9481
9663
  p.block_number,
9482
- GREATEST(0,
9483
- COALESCE(pml.max_upper, 0)
9484
- - COALESCE(po.total_offset, 0)
9485
- - COALESCE(pc.consumed, 0)
9486
- )::text AS reserved_balance
9664
+ po.obligation_id,
9665
+ COALESCE(po.reserved_balance, '0') AS reserved_balance
9487
9666
  FROM ${positions} p
9488
- LEFT JOIN position_offsets po
9667
+ LEFT JOIN per_obligation po
9489
9668
  ON po.chain_id = p.chain_id
9490
9669
  AND LOWER(po.contract) = LOWER(p.contract)
9491
9670
  AND LOWER(po."user") = LOWER(p."user")
9492
- LEFT JOIN position_consumed pc
9493
- ON pc.chain_id = p.chain_id
9494
- AND LOWER(pc.contract) = LOWER(p.contract)
9495
- AND LOWER(pc."user") = LOWER(p."user")
9496
- LEFT JOIN position_max_lot pml
9497
- ON pml.chain_id = p.chain_id
9498
- AND LOWER(pml.contract) = LOWER(p.contract)
9499
- AND LOWER(pml."user") = LOWER(p."user")
9500
9671
  WHERE LOWER(p."user") = LOWER(${user})
9501
9672
  AND p."user" != ${zeroAddress}
9502
- ${cursor !== null ? sql`AND (p.chain_id, p.contract) > (${cursor.chainId}, ${cursor.contract})` : sql``}
9503
- ORDER BY p.chain_id ASC, p.contract ASC
9673
+ ${cursor !== null ? sql`AND (p.chain_id, p.contract, COALESCE(po.obligation_id, '')) > (${cursor.chainId}, ${cursor.contract}, ${cursor.obligationId ?? ""})` : sql``}
9674
+ ORDER BY p.chain_id ASC, p.contract ASC, po.obligation_id ASC NULLS FIRST
9504
9675
  LIMIT ${limit}
9505
9676
  `);
9506
9677
  const nextCursor = raw.rows.length === limit ? Buffer.from(JSON.stringify({
9507
9678
  chainId: raw.rows[raw.rows.length - 1].chain_id.toString(),
9508
- contract: raw.rows[raw.rows.length - 1].contract
9679
+ contract: raw.rows[raw.rows.length - 1].contract,
9680
+ obligationId: raw.rows[raw.rows.length - 1].obligation_id
9509
9681
  })).toString("base64url") : null;
9510
9682
  return {
9511
9683
  positions: raw.rows.map((row) => ({
@@ -9513,6 +9685,7 @@ const create$4 = (db) => {
9513
9685
  contract: row.contract,
9514
9686
  user: row.user,
9515
9687
  blockNumber: row.block_number,
9688
+ obligationId: row.obligation_id,
9516
9689
  reserved: BigInt(row.reserved_balance.split(".")[0] ?? "0")
9517
9690
  })),
9518
9691
  nextCursor
@@ -9791,7 +9964,10 @@ function create$1(db) {
9791
9964
 
9792
9965
  //#endregion
9793
9966
  //#region src/database/Database.ts
9794
- var Database_exports = /* @__PURE__ */ __exportAll({ connect: () => connect$1 });
9967
+ var Database_exports = /* @__PURE__ */ __exportAll({
9968
+ connect: () => connect$1,
9969
+ getSchemaNamesForMigration: () => getSchemaNamesForMigration
9970
+ });
9795
9971
  function createDomains(core, chainRegistry) {
9796
9972
  return {
9797
9973
  book: create$12({ db: core }),
@@ -9888,6 +10064,7 @@ function augmentWithDomains(base, chainRegistry) {
9888
10064
  return wrapped;
9889
10065
  }
9890
10066
  const InMemoryDbMap = /* @__PURE__ */ new Map();
10067
+ const LEGACY_SCHEMA_START_MINOR = 7;
9891
10068
  /**
9892
10069
  * Connect to the database.
9893
10070
  * @notice If no connection string is provided, an in-process PGLite database is created.
@@ -9942,9 +10119,26 @@ function applyMigrations(kind, driver) {
9942
10119
  async function preMigrate(driver) {
9943
10120
  const tracer = getTracer("db.preMigrate");
9944
10121
  await startActiveSpan(tracer, "db.preMigrate", async () => {
9945
- await driver.execute(`create schema if not exists "${VERSION}"`);
10122
+ const schemaNames = getSchemaNamesForMigration(VERSION);
10123
+ for (const schemaName of schemaNames) await driver.execute(`create schema if not exists "${schemaName}"`);
9946
10124
  });
9947
10125
  }
10126
+ /**
10127
+ * Build the list of router schemas that should exist before running migrations.
10128
+ * @param version - Current schema version (e.g. `router_v1.8`).
10129
+ * @returns Ordered schema names from `router_v1.7` to current, or just current if parsing fails.
10130
+ */
10131
+ function getSchemaNamesForMigration(version) {
10132
+ const parsed = /^router_v(?<major>\d+)\.(?<minor>\d+)$/.exec(version);
10133
+ if (!parsed?.groups?.major || !parsed.groups.minor) return [version];
10134
+ const major = Number.parseInt(parsed.groups.major, 10);
10135
+ const currentMinor = Number.parseInt(parsed.groups.minor, 10);
10136
+ if (!Number.isInteger(major) || !Number.isInteger(currentMinor)) return [version];
10137
+ if (currentMinor < LEGACY_SCHEMA_START_MINOR) return [version];
10138
+ const schemaNames = [];
10139
+ for (let minor = LEGACY_SCHEMA_START_MINOR; minor <= currentMinor; minor += 1) schemaNames.push(`router_v${major}.${minor}`);
10140
+ return schemaNames;
10141
+ }
9948
10142
  async function postMigrate(driver) {
9949
10143
  const tracer = getTracer("db.postMigrate");
9950
10144
  await startActiveSpan(tracer, "db.postMigrate", async () => {
@@ -10214,15 +10408,16 @@ async function postMigrate(driver) {
10214
10408
  RETURNS trigger
10215
10409
  LANGUAGE plpgsql AS $$
10216
10410
  BEGIN
10217
- INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", value)
10411
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", obligation_id, value)
10218
10412
  VALUES (
10219
10413
  OLD.chain_id,
10220
10414
  OLD."user",
10221
10415
  OLD.contract,
10222
10416
  OLD."group",
10417
+ OLD.obligation_id,
10223
10418
  OLD.upper::numeric - OLD.lower::numeric
10224
10419
  )
10225
- ON CONFLICT (chain_id, "user", contract, "group") DO NOTHING;
10420
+ ON CONFLICT (chain_id, "user", contract, "group", obligation_id) DO NOTHING;
10226
10421
  RETURN OLD;
10227
10422
  END;
10228
10423
  $$;
@@ -10476,10 +10671,11 @@ var Rules_exports = /* @__PURE__ */ __exportAll({
10476
10671
  amountMutualExclusivity: () => amountMutualExclusivity,
10477
10672
  callback: () => callback,
10478
10673
  chains: () => chains,
10674
+ collateralToken: () => collateralToken,
10675
+ loanToken: () => loanToken,
10479
10676
  maturity: () => maturity,
10480
10677
  oracle: () => oracle,
10481
10678
  sameMaker: () => sameMaker,
10482
- token: () => token,
10483
10679
  validity: () => validity
10484
10680
  });
10485
10681
  /**
@@ -10507,15 +10703,25 @@ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy
10507
10703
  if (isEmptyCallback(offer) && !offer.buy && !callbacks.includes(Type$1.SellWithEmptyCallback)) return { message: "Sell offers with empty callback not allowed." };
10508
10704
  });
10509
10705
  /**
10510
- * A validation rule that checks if the offer's tokens are allowed for its chain.
10511
- * @param assetsByChainId - Allowed assets indexed by chain id.
10706
+ * A validation rule that checks if the offer's loan token is allowed for its chain.
10707
+ * @param assetsByChainId - Allowed loan tokens indexed by chain id.
10708
+ * @returns The issue that was found. If the offer is valid, this will be undefined.
10709
+ */
10710
+ const loanToken = ({ assetsByChainId }) => single("loan_token", "Validates that offer loan token is in the allowed token list for the offer chain", (offer) => {
10711
+ const allowedLoanTokens = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
10712
+ if (!allowedLoanTokens || allowedLoanTokens.length === 0) return { message: `No allowed loan tokens for chain ${offer.chainId}` };
10713
+ if (!allowedLoanTokens.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
10714
+ });
10715
+ /**
10716
+ * A validation rule that checks if the offer's collateral tokens are allowed for its chain.
10717
+ * @param collateralAssetsByChainId - Allowed collateral tokens indexed by chain id.
10512
10718
  * @returns The issue that was found. If the offer is valid, this will be undefined.
10513
10719
  */
10514
- const token = ({ assetsByChainId }) => single("token", "Validates that offer loan token and collateral tokens are in the allowed assets list for the offer chain", (offer) => {
10515
- const allowedAssets = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
10516
- if (!allowedAssets || allowedAssets.length === 0) return { message: `No allowed assets for chain ${offer.chainId}` };
10517
- if (!allowedAssets.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
10518
- if (offer.collaterals.some((collateral) => !allowedAssets.includes(collateral.asset.toLowerCase()))) return { message: "Collateral is not allowed" };
10720
+ const collateralToken = ({ collateralAssetsByChainId }) => single("collateral_token", "Validates that offer collateral tokens are in the allowed token list for the offer chain", (offer) => {
10721
+ const allowedCollateralTokens = collateralAssetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase()) ?? [];
10722
+ if (allowedCollateralTokens.length === 0) return { message: `No allowed collateral tokens for chain ${offer.chainId}` };
10723
+ if (offer.collaterals.length === 0) return { message: "At least one collateral token is required" };
10724
+ if (offer.collaterals.some((collateral) => !allowedCollateralTokens.includes(collateral.asset.toLowerCase()))) return { message: "Collateral token is not allowed" };
10519
10725
  });
10520
10726
  /**
10521
10727
  * A validation rule that checks if the offer's oracle addresses are allowed for its chain.
@@ -10558,9 +10764,11 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
10558
10764
  //#region src/gatekeeper/morphoRules.ts
10559
10765
  const morphoRules = (chains$3) => {
10560
10766
  const assetsByChainId = {};
10767
+ const collateralAssetsByChainId = {};
10561
10768
  const oraclesByChainId = {};
10562
10769
  for (const chain of chains$3) {
10563
10770
  assetsByChainId[chain.id] = assets[chain.id.toString()] ?? [];
10771
+ collateralAssetsByChainId[chain.id] = collateralAssets[chain.id.toString()] ?? [];
10564
10772
  oraclesByChainId[chain.id] = oracles[chain.id.toString()] ?? [];
10565
10773
  }
10566
10774
  return [
@@ -10572,7 +10780,8 @@ const morphoRules = (chains$3) => {
10572
10780
  callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
10573
10781
  allowedAddresses: []
10574
10782
  }),
10575
- token({ assetsByChainId }),
10783
+ loanToken({ assetsByChainId }),
10784
+ collateralToken({ collateralAssetsByChainId }),
10576
10785
  oracle({ oraclesByChainId })
10577
10786
  ];
10578
10787
  };
@@ -10755,5 +10964,5 @@ var mempool_exports = /* @__PURE__ */ __exportAll({
10755
10964
  });
10756
10965
 
10757
10966
  //#endregion
10758
- export { Abi_exports as Abi, BookResponse_exports as BookResponse, BooksController, BrandTypeId, Callback_exports as Callback, Chain_exports as Chain, ChainHealth, ChainRegistry_exports as ChainRegistry, ChainsHealthResponse, Collateral_exports as Collateral, CollectorHealth, CollectorsHealthResponse, ConfigContractsController, ConfigRulesController, Database_exports as Database, ERC4626_exports as ERC4626, Errors_exports as Errors, Format_exports as Format, Gatekeeper_exports as Gatekeeper, Client_exports as GatekeeperClient, Health_exports as Health, HealthController, Indexer_exports as Indexer, LLTV_exports as LLTV, Liquidity_exports as Liquidity, Logger_exports as Logger, Maturity_exports as Maturity, mempool_exports as Mempool, Obligation_exports as Obligation, ObligationResponse_exports as ObligationResponse, ObligationsController, Offer_exports as Offer, OfferResponse_exports as OfferResponse, OffersController, drizzle_exports as OffersSchema, OpenApi, Oracle_exports as Oracle, Position_exports as Position, PositionResponse_exports as PositionResponse, Quote_exports as Quote, RouterApi_exports as RouterApi, Client_exports$1 as RouterClient, RouterStatusResponse, Rules_exports as Rules, time_exports as Time, TradingFee_exports as TradingFee, Transfer_exports as Transfer, Tree_exports as Tree, UsersController, utils_exports as Utils, ValidateController, Gate_exports as Validation, morphoRules, parse, safeParse };
10967
+ export { Abi_exports as Abi, BookResponse_exports as BookResponse, BooksController, BrandTypeId, Callback_exports as Callback, Chain_exports as Chain, ChainHealth, ChainRegistry_exports as ChainRegistry, ChainsHealthResponse, Collateral_exports as Collateral, CollectorHealth, CollectorsHealthResponse, ConfigContractsController, ConfigRulesController, Database_exports as Database, ERC4626_exports as ERC4626, Errors_exports as Errors, Format_exports as Format, Gatekeeper_exports as Gatekeeper, Client_exports as GatekeeperClient, Health_exports as Health, HealthController, Indexer_exports as Indexer, LLTV_exports as LLTV, Liquidity_exports as Liquidity, Logger_exports as Logger, Maturity_exports as Maturity, mempool_exports as Mempool, Obligation_exports as Obligation, ObligationResponse_exports as ObligationResponse, ObligationsController, Offer_exports as Offer, OfferResponse_exports as OfferResponse, OffersController, drizzle_exports as OffersSchema, OpenApi, Oracle_exports as Oracle, Position_exports as Position, PositionResponse_exports as PositionResponse, Quote_exports as Quote, RouterApi_exports as RouterApi, Client_exports$1 as RouterClient, RouterStatusResponse, Rules_exports as Rules, Tick_exports as Tick, time_exports as Time, TradingFee_exports as TradingFee, Transfer_exports as Transfer, Tree_exports as Tree, UsersController, utils_exports as Utils, ValidateController, Gate_exports as Validation, morphoRules, parse, safeParse };
10759
10968
  //# sourceMappingURL=index.node.mjs.map