@morpho-dev/router 0.9.0 → 0.10.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.
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import os from "node:os";
16
16
  import path, { dirname, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
19
- import { bytesToHex, createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, numberToHex, pad, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, toHex, zeroAddress } from "viem";
19
+ import { bytesToHex, concatHex, createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, erc20Abi, getAddress, hashTypedData, hexToBytes, http, isAddress, isHex, keccak256, maxUint256, numberToHex, pad, parseAbi, parseEventLogs, publicActions, recoverAddress, stringify, toHex, zeroAddress } from "viem";
20
20
  import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts";
21
21
  import { getBlock, getBlockNumber, getLogs, multicall } from "viem/actions";
22
22
  import { anvil, base, mainnet } from "viem/chains";
@@ -39,7 +39,7 @@ import { migrate } from "drizzle-orm/node-postgres/migrator";
39
39
  import { drizzle as drizzle$1 } from "drizzle-orm/pglite";
40
40
  import { migrate as migrate$1 } from "drizzle-orm/pglite/migrator";
41
41
  import { Pool } from "pg";
42
- import { and, asc, desc, eq, gt, gte, inArray, lte, ne, sql } from "drizzle-orm";
42
+ import { and, asc, desc, eq, gt, gte, inArray, lte, ne, or, sql } from "drizzle-orm";
43
43
  import { bigint, boolean, foreignKey, index, integer, numeric, pgSchema, primaryKey, serial, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
44
44
  import { parse } from "smol-toml";
45
45
  import { cors } from "hono/cors";
@@ -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.9.0";
155
+ var version = "0.10.0";
156
156
  var description = "Router package for Morpho protocol";
157
157
 
158
158
  //#endregion
@@ -264,7 +264,7 @@ const ChainId = {
264
264
  const chainNames = Object.keys(ChainId).map((key) => key.toLowerCase());
265
265
  const chainIds = Object.values(ChainId);
266
266
  const chainNameLookup = new Map(Object.entries(ChainId).map(([key, value]) => [value, key.toLowerCase()]));
267
- const chains$2 = {
267
+ const chains$1 = {
268
268
  ethereum: {
269
269
  ...mainnet,
270
270
  id: ChainId.ETHEREUM,
@@ -301,16 +301,16 @@ const chains$2 = {
301
301
  name: "base",
302
302
  custom: {
303
303
  morpho: {
304
- address: "0x0000000000000000000000000000000000000000",
305
- blockCreated: 0
304
+ address: "0x3F067BC9D8898F6ec02D6480c3fF1026E512BcBF",
305
+ blockCreated: 41799989
306
306
  },
307
307
  morphoBlue: {
308
308
  address: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
309
309
  blockCreated: 0
310
310
  },
311
311
  mempool: {
312
- address: "0x0000000000000000000000000000000000000000",
313
- blockCreated: 35449942
312
+ address: "0xd25c7512EA5035bef4F18c708C0862E1B6151765",
313
+ blockCreated: 41800226
314
314
  },
315
315
  vaults: { factories: {
316
316
  v1_0: {
@@ -513,7 +513,7 @@ async function deployContracts(parameters) {
513
513
  const deployClient = client ?? createWalletClient({
514
514
  account,
515
515
  chain: {
516
- ...chains$2.anvil,
516
+ ...chains$1.anvil,
517
517
  id: chainId
518
518
  },
519
519
  transport: http(rpcUrl)
@@ -1081,7 +1081,7 @@ async function readLogs(parameters) {
1081
1081
  return readFile(logPath, "utf8");
1082
1082
  }
1083
1083
  async function resolveForkContracts(parameters) {
1084
- const chain = parameters.forkChain ? chains$2[parameters.forkChain] : void 0;
1084
+ const chain = parameters.forkChain ? chains$1[parameters.forkChain] : void 0;
1085
1085
  const mempoolAddress = parameters.mempoolAddress ?? chain?.custom.mempool.address;
1086
1086
  const morphoAddress = parameters.morphoAddress ?? chain?.custom.morpho.address;
1087
1087
  const multicall3Address = parameters.multicall3Address ?? chain?.contracts?.multicall3?.address;
@@ -1112,8 +1112,8 @@ async function resolveForkContracts(parameters) {
1112
1112
  }
1113
1113
  async function validateForkContracts(parameters) {
1114
1114
  const client = createPublicClient({
1115
- chain: parameters.forkChain ? chains$2[parameters.forkChain] : {
1116
- ...chains$2.anvil,
1115
+ chain: parameters.forkChain ? chains$1[parameters.forkChain] : {
1116
+ ...chains$1.anvil,
1117
1117
  id: parameters.chainId
1118
1118
  },
1119
1119
  transport: http(parameters.rpcUrl)
@@ -1470,11 +1470,12 @@ async function run(parameters) {
1470
1470
  * @returns Gatekeeper instance. {@link Gatekeeper}
1471
1471
  */
1472
1472
  function create$21(parameters) {
1473
- const { rules } = parameters;
1474
- return { isAllowed: async (offers) => {
1473
+ const { rules: rulesFactory } = parameters;
1474
+ return { isAllowed: async (parameters) => {
1475
+ const { offers, chainId } = parameters;
1475
1476
  return await run({
1476
1477
  items: offers,
1477
- rules
1478
+ rules: rulesFactory(chainId)
1478
1479
  });
1479
1480
  } };
1480
1481
  }
@@ -1918,6 +1919,20 @@ const LLTVSchema = z$2.bigint({ coerce: true }).refine((lltv) => {
1918
1919
 
1919
1920
  //#endregion
1920
1921
  //#region src/core/Collateral.ts
1922
+ const abi$1 = [
1923
+ {
1924
+ type: "address",
1925
+ name: "token"
1926
+ },
1927
+ {
1928
+ type: "uint256",
1929
+ name: "lltv"
1930
+ },
1931
+ {
1932
+ type: "address",
1933
+ name: "oracle"
1934
+ }
1935
+ ];
1921
1936
  const CollateralSchema = z$2.object({
1922
1937
  asset: z$2.string().transform(transformAddress),
1923
1938
  oracle: z$2.string().transform(transformAddress),
@@ -2028,11 +2043,29 @@ function stringifyBigint(value) {
2028
2043
  //#endregion
2029
2044
  //#region src/core/Obligation.ts
2030
2045
  const ObligationSchema = z$2.object({
2031
- chainId: z$2.number().min(0).max(Number.MAX_SAFE_INTEGER),
2032
2046
  loanToken: z$2.string().transform(transformAddress),
2033
2047
  collaterals: CollateralsSchema,
2034
2048
  maturity: MaturitySchema
2035
2049
  });
2050
+ const abi = [
2051
+ {
2052
+ type: "address",
2053
+ name: "loanToken"
2054
+ },
2055
+ {
2056
+ type: "tuple[]",
2057
+ name: "collaterals",
2058
+ components: abi$1
2059
+ },
2060
+ {
2061
+ type: "uint256",
2062
+ name: "maturity"
2063
+ }
2064
+ ];
2065
+ const tupleAbi = [{
2066
+ type: "tuple",
2067
+ components: abi
2068
+ }];
2036
2069
  /**
2037
2070
  * Creates an obligation from the given parameters.
2038
2071
  * @constructor
@@ -2043,7 +2076,6 @@ const ObligationSchema = z$2.object({
2043
2076
  * @example
2044
2077
  * ```ts
2045
2078
  * const obligation = Obligation.from({
2046
- * chainId: 1,
2047
2079
  * loanToken: privateKeyToAccount(generatePrivateKey()).address,
2048
2080
  * collaterals: [
2049
2081
  * Collateral.from({
@@ -2063,7 +2095,6 @@ function from$13(parameters) {
2063
2095
  maturity: from$16(parameters.maturity)
2064
2096
  });
2065
2097
  return {
2066
- chainId: parsedObligation.chainId,
2067
2098
  loanToken: parsedObligation.loanToken.toLowerCase(),
2068
2099
  collaterals: parsedObligation.collaterals.sort((a, b) => a.asset.localeCompare(b.asset)),
2069
2100
  maturity: parsedObligation.maturity
@@ -2073,49 +2104,27 @@ function from$13(parameters) {
2073
2104
  }
2074
2105
  }
2075
2106
  /**
2076
- * Calculates the obligation id based on the smart contract's Obligation struct.
2077
- * The id is computed as keccak256(abi.encode(chainId, loanToken, collaterals, maturity)).
2107
+ * Calculates a canonical key for an obligation payload.
2108
+ * The key is computed as keccak256(abi.encode(loanToken, collaterals, maturity)).
2078
2109
  * @throws If the collaterals are not sorted alphabetically by address. {@link CollateralsAreNotSortedError}
2079
- * @param parameters - {@link id.Parameters}
2080
- * @returns The obligation id as a 32-byte hex string. {@link id.ReturnType}
2110
+ * @param parameters - {@link key.Parameters}
2111
+ * @returns The obligation key as a 32-byte hex string. {@link key.ReturnType}
2081
2112
  *
2082
2113
  * @example
2083
2114
  * ```ts
2084
2115
  * const obligation = Obligation.random();
2085
- * const id = Obligation.id(obligation);
2086
- * console.log(id); // 0x1234567890123456789012345678901234567890123456789012345678901234
2116
+ * const key = Obligation.key(obligation);
2117
+ * console.log(key); // 0x1234567890123456789012345678901234567890123456789012345678901234
2087
2118
  * ```
2088
2119
  */
2089
- function id(parameters) {
2120
+ function key(parameters) {
2090
2121
  let lastAsset = "";
2091
2122
  for (const collateral of parameters.collaterals) {
2092
2123
  const newAsset = collateral.asset.toLowerCase();
2093
2124
  if (newAsset.localeCompare(lastAsset) < 0) throw new CollateralsAreNotSortedError();
2094
2125
  lastAsset = newAsset;
2095
2126
  }
2096
- return keccak256(encodeAbiParameters([
2097
- { type: "uint256" },
2098
- { type: "address" },
2099
- {
2100
- type: "tuple[]",
2101
- components: [
2102
- {
2103
- type: "address",
2104
- name: "token"
2105
- },
2106
- {
2107
- type: "uint256",
2108
- name: "lltv"
2109
- },
2110
- {
2111
- type: "address",
2112
- name: "oracle"
2113
- }
2114
- ]
2115
- },
2116
- { type: "uint256" }
2117
- ], [
2118
- BigInt(parameters.chainId),
2127
+ return keccak256(encodeAbiParameters(abi, [
2119
2128
  parameters.loanToken.toLowerCase(),
2120
2129
  parameters.collaterals.map((c) => ({
2121
2130
  token: c.asset.toLowerCase(),
@@ -2138,6 +2147,43 @@ var CollateralsAreNotSortedError = class extends BaseError {
2138
2147
  }
2139
2148
  };
2140
2149
 
2150
+ //#endregion
2151
+ //#region src/core/Id.ts
2152
+ const CREATION_CODE_PREFIX = "0x603f380380603f5f395ff3";
2153
+ /**
2154
+ * Builds the same creation code as `IdLib.creationCode` in Solidity.
2155
+ *
2156
+ * Layout: `prefix (11 bytes) + chainId (32 bytes) + morphoV2 (20 bytes) + abi.encode(obligation)`.
2157
+ *
2158
+ * @param parameters - {@link creationCode.Parameters}
2159
+ * @returns The CREATE2 init code bytes. {@link creationCode.ReturnType}
2160
+ */
2161
+ function creationCode(parameters) {
2162
+ const encodedObligation = encodeAbiParameters(tupleAbi, [{
2163
+ loanToken: parameters.obligation.loanToken.toLowerCase(),
2164
+ collaterals: parameters.obligation.collaterals.map((collateral) => ({
2165
+ token: collateral.asset.toLowerCase(),
2166
+ lltv: collateral.lltv,
2167
+ oracle: collateral.oracle.toLowerCase()
2168
+ })),
2169
+ maturity: BigInt(parameters.obligation.maturity)
2170
+ }]);
2171
+ return concatHex([
2172
+ CREATION_CODE_PREFIX,
2173
+ numberToHex(BigInt(parameters.chainId), { size: 32 }),
2174
+ parameters.morphoV2.toLowerCase(),
2175
+ encodedObligation
2176
+ ]);
2177
+ }
2178
+ /**
2179
+ * Computes the same id as `IdLib.toId` in Solidity.
2180
+ * @param parameters - {@link toId.Parameters}
2181
+ * @returns The obligation id. {@link toId.ReturnType}
2182
+ */
2183
+ function toId(parameters) {
2184
+ return keccak256(creationCode(parameters));
2185
+ }
2186
+
2141
2187
  //#endregion
2142
2188
  //#region src/core/Offer.ts
2143
2189
  /** Internal symbol for caching the computed hash. */
@@ -2168,7 +2214,6 @@ const OfferSchema = () => {
2168
2214
  z$2.bigint()
2169
2215
  ]).optional().default("0x0000000000000000000000000000000000000000000000000000000000000000").transform(transformBytes32),
2170
2216
  buy: z$2.boolean(),
2171
- chainId: z$2.number().min(0).max(Number.MAX_SAFE_INTEGER),
2172
2217
  loanToken: z$2.string().transform(transformAddress),
2173
2218
  collaterals: CollateralsSchema,
2174
2219
  callback: z$2.object({
@@ -2237,7 +2282,6 @@ const serialize = (offer) => ({
2237
2282
  group: offer.group,
2238
2283
  session: offer.session,
2239
2284
  buy: offer.buy,
2240
- chainId: offer.chainId,
2241
2285
  loanToken: offer.loanToken,
2242
2286
  collaterals: offer.collaterals.map((c) => ({
2243
2287
  asset: c.asset,
@@ -2258,7 +2302,6 @@ const serialize = (offer) => ({
2258
2302
  * @returns {Offer} A randomly generated Offer object.
2259
2303
  */
2260
2304
  function random(config) {
2261
- const chain = config?.chains ? config.chains[int(config.chains.length)] : chains$2.ethereum;
2262
2305
  const loanToken = config?.loanTokens ? config.loanTokens[int(config.loanTokens.length)] : address();
2263
2306
  const collateralCandidates = config?.collateralTokens ? config.collateralTokens.filter((a) => a !== loanToken) : [address()];
2264
2307
  collateralCandidates[int(collateralCandidates.length)];
@@ -2304,7 +2347,6 @@ function random(config) {
2304
2347
  group: config?.group ?? hex(32),
2305
2348
  session: config?.session ?? hex(32),
2306
2349
  buy,
2307
- chainId: chain.id,
2308
2350
  loanToken,
2309
2351
  collaterals: config?.collaterals ?? Array.from({ length: int(3) + 1 }, () => ({
2310
2352
  ...random$1(),
@@ -2324,150 +2366,138 @@ const weightedChoice = (pairs) => {
2324
2366
  return pairs[0][0];
2325
2367
  };
2326
2368
  /**
2327
- * Creates an EIP-712 domain object.
2328
- * @param chainId - The chain ID.
2329
- * @returns The EIP-712 domain object.
2330
- */
2331
- const domain = (chainId) => ({
2332
- chainId: BigInt(chainId),
2333
- verifyingContract: zeroAddress
2334
- });
2335
- /**
2336
- * The EIP-712 types for the offer.
2337
- * @warning The ordering of the types should NEVER be changed. The offer hash is computed based on the order of the types.
2338
- * @returns The EIP-712 types.
2369
+ * Computes the canonical chain-agnostic offer hash.
2370
+ * The hash is `keccak256(abi.encode(offer))` using {@link encode}.
2371
+ *
2372
+ * @param offer - Offer payload to hash.
2373
+ * @returns 32-byte offer hash.
2339
2374
  */
2340
- const types = {
2341
- EIP712Domain: [{
2342
- name: "chainId",
2343
- type: "uint256"
2344
- }, {
2345
- name: "verifyingContract",
2346
- type: "address"
2347
- }],
2348
- Offer: [
2349
- {
2350
- name: "maker",
2351
- type: "address"
2352
- },
2353
- {
2354
- name: "assets",
2355
- type: "uint256"
2356
- },
2357
- {
2358
- name: "obligationUnits",
2359
- type: "uint256"
2360
- },
2361
- {
2362
- name: "obligationShares",
2363
- type: "uint256"
2364
- },
2365
- {
2366
- name: "tick",
2367
- type: "uint256"
2368
- },
2369
- {
2370
- name: "maturity",
2371
- type: "uint256"
2372
- },
2373
- {
2374
- name: "expiry",
2375
- type: "uint256"
2376
- },
2377
- {
2378
- name: "group",
2379
- type: "bytes32"
2380
- },
2381
- {
2382
- name: "session",
2383
- type: "bytes32"
2384
- },
2385
- {
2386
- name: "buy",
2387
- type: "bool"
2388
- },
2389
- {
2390
- name: "loanToken",
2391
- type: "address"
2392
- },
2393
- {
2394
- name: "collaterals",
2395
- type: "Collateral[]"
2396
- },
2397
- {
2398
- name: "callback",
2399
- type: "Callback"
2400
- },
2401
- {
2402
- name: "receiverIfMakerIsSeller",
2403
- type: "address"
2404
- }
2405
- ],
2406
- Collateral: [
2407
- {
2408
- name: "asset",
2409
- type: "address"
2410
- },
2411
- {
2412
- name: "oracle",
2413
- type: "address"
2414
- },
2415
- {
2416
- name: "lltv",
2417
- type: "uint256"
2418
- }
2419
- ],
2420
- Callback: [{
2421
- name: "address",
2422
- type: "address"
2423
- }, {
2424
- name: "data",
2425
- type: "bytes"
2426
- }]
2427
- };
2428
2375
  function hash(offer) {
2429
2376
  const cached = offer[HASH_CACHE];
2430
2377
  if (cached) return cached;
2431
- const computed = hashTypedData({
2432
- domain: domain(offer.chainId),
2433
- message: {
2434
- maker: offer.maker.toLowerCase(),
2435
- assets: offer.assets,
2436
- obligationUnits: offer.obligationUnits,
2437
- obligationShares: offer.obligationShares,
2438
- tick: BigInt(offer.tick),
2439
- maturity: BigInt(offer.maturity),
2440
- expiry: BigInt(offer.expiry),
2441
- group: offer.group,
2442
- session: offer.session,
2443
- buy: offer.buy,
2444
- loanToken: offer.loanToken.toLowerCase(),
2445
- collaterals: offer.collaterals,
2446
- callback: {
2447
- address: offer.callback.address.toLowerCase(),
2448
- data: offer.callback.data
2449
- },
2450
- receiverIfMakerIsSeller: offer.receiverIfMakerIsSeller.toLowerCase()
2451
- },
2452
- primaryType: "Offer",
2453
- types
2454
- });
2378
+ const computed = keccak256(encode(offer));
2455
2379
  offer[HASH_CACHE] = computed;
2456
2380
  return computed;
2457
2381
  }
2458
2382
  /**
2459
- * Calculates the obligation id for an offer based on the smart contract's Obligation struct.
2460
- * The id is computed as keccak256(abi.encode(chainId, loanToken, collaterals (sorted by token address), maturity)).
2383
+ * Calculates the onchain obligation id for an offer.
2384
+ * The id is computed with {@link Id.toId}.
2461
2385
  * @param offer - The offer to calculate the obligation id for.
2386
+ * @param parameters - The chain context used by the onchain id function.
2462
2387
  * @returns The obligation id as a 32-byte hex string.
2463
2388
  */
2464
- function obligationId(offer) {
2465
- return id(from$13({
2466
- chainId: offer.chainId,
2467
- loanToken: offer.loanToken,
2468
- collaterals: offer.collaterals,
2469
- maturity: offer.maturity
2470
- }));
2389
+ function obligationId(offer, parameters) {
2390
+ return toId({
2391
+ chainId: parameters.chainId,
2392
+ morphoV2: parameters.morphoV2,
2393
+ obligation: from$13({
2394
+ loanToken: offer.loanToken,
2395
+ collaterals: offer.collaterals,
2396
+ maturity: offer.maturity
2397
+ })
2398
+ });
2399
+ }
2400
+ const OfferAbi = [
2401
+ {
2402
+ name: "maker",
2403
+ type: "address"
2404
+ },
2405
+ {
2406
+ name: "assets",
2407
+ type: "uint256"
2408
+ },
2409
+ {
2410
+ name: "obligationUnits",
2411
+ type: "uint256"
2412
+ },
2413
+ {
2414
+ name: "obligationShares",
2415
+ type: "uint256"
2416
+ },
2417
+ {
2418
+ name: "tick",
2419
+ type: "uint256"
2420
+ },
2421
+ {
2422
+ name: "maturity",
2423
+ type: "uint256"
2424
+ },
2425
+ {
2426
+ name: "expiry",
2427
+ type: "uint256"
2428
+ },
2429
+ {
2430
+ name: "group",
2431
+ type: "bytes32"
2432
+ },
2433
+ {
2434
+ name: "session",
2435
+ type: "bytes32"
2436
+ },
2437
+ {
2438
+ name: "buy",
2439
+ type: "bool"
2440
+ },
2441
+ {
2442
+ name: "loanToken",
2443
+ type: "address"
2444
+ },
2445
+ {
2446
+ name: "start",
2447
+ type: "uint256"
2448
+ },
2449
+ {
2450
+ name: "collaterals",
2451
+ type: "tuple[]",
2452
+ components: [
2453
+ {
2454
+ name: "asset",
2455
+ type: "address"
2456
+ },
2457
+ {
2458
+ name: "oracle",
2459
+ type: "address"
2460
+ },
2461
+ {
2462
+ name: "lltv",
2463
+ type: "uint256"
2464
+ }
2465
+ ]
2466
+ },
2467
+ {
2468
+ name: "callback",
2469
+ type: "tuple",
2470
+ components: [{
2471
+ name: "address",
2472
+ type: "address"
2473
+ }, {
2474
+ name: "data",
2475
+ type: "bytes"
2476
+ }]
2477
+ },
2478
+ {
2479
+ name: "receiverIfMakerIsSeller",
2480
+ type: "address"
2481
+ }
2482
+ ];
2483
+ function encode(offer) {
2484
+ return encodeAbiParameters(OfferAbi, [
2485
+ offer.maker,
2486
+ offer.assets,
2487
+ offer.obligationUnits,
2488
+ offer.obligationShares,
2489
+ BigInt(offer.tick),
2490
+ BigInt(offer.maturity),
2491
+ BigInt(offer.expiry),
2492
+ offer.group,
2493
+ offer.session,
2494
+ offer.buy,
2495
+ offer.loanToken,
2496
+ BigInt(offer.start),
2497
+ offer.collaterals,
2498
+ offer.callback,
2499
+ offer.receiverIfMakerIsSeller
2500
+ ]);
2471
2501
  }
2472
2502
  /**
2473
2503
  * ABI for the Take event emitted by the Morpho V2 contract.
@@ -2591,6 +2621,189 @@ const consumedEvent = {
2591
2621
  ],
2592
2622
  anonymous: false
2593
2623
  };
2624
+ /**
2625
+ * ABI for the Repay event emitted by the MorphoV2 contract.
2626
+ */
2627
+ const repayEvent = {
2628
+ type: "event",
2629
+ name: "Repay",
2630
+ inputs: [
2631
+ {
2632
+ name: "caller",
2633
+ type: "address",
2634
+ indexed: true,
2635
+ internalType: "address"
2636
+ },
2637
+ {
2638
+ name: "id",
2639
+ type: "bytes32",
2640
+ indexed: true,
2641
+ internalType: "bytes32"
2642
+ },
2643
+ {
2644
+ name: "obligationUnits",
2645
+ type: "uint256",
2646
+ indexed: false,
2647
+ internalType: "uint256"
2648
+ },
2649
+ {
2650
+ name: "onBehalf",
2651
+ type: "address",
2652
+ indexed: true,
2653
+ internalType: "address"
2654
+ }
2655
+ ],
2656
+ anonymous: false
2657
+ };
2658
+ /**
2659
+ * ABI for the Liquidate event emitted by the MorphoV2 contract.
2660
+ */
2661
+ const liquidateEvent = {
2662
+ type: "event",
2663
+ name: "Liquidate",
2664
+ inputs: [
2665
+ {
2666
+ name: "caller",
2667
+ type: "address",
2668
+ indexed: true,
2669
+ internalType: "address"
2670
+ },
2671
+ {
2672
+ name: "id",
2673
+ type: "bytes32",
2674
+ indexed: true,
2675
+ internalType: "bytes32"
2676
+ },
2677
+ {
2678
+ name: "seizures",
2679
+ type: "tuple[]",
2680
+ indexed: false,
2681
+ internalType: "struct IMorphoV2.Seizure[]",
2682
+ components: [
2683
+ {
2684
+ name: "collateralIndex",
2685
+ type: "uint256",
2686
+ internalType: "uint256"
2687
+ },
2688
+ {
2689
+ name: "repaid",
2690
+ type: "uint256",
2691
+ internalType: "uint256"
2692
+ },
2693
+ {
2694
+ name: "seized",
2695
+ type: "uint256",
2696
+ internalType: "uint256"
2697
+ }
2698
+ ]
2699
+ },
2700
+ {
2701
+ name: "borrower",
2702
+ type: "address",
2703
+ indexed: true,
2704
+ internalType: "address"
2705
+ },
2706
+ {
2707
+ name: "totalRepaid",
2708
+ type: "uint256",
2709
+ indexed: false,
2710
+ internalType: "uint256"
2711
+ },
2712
+ {
2713
+ name: "badDebt",
2714
+ type: "uint256",
2715
+ indexed: false,
2716
+ internalType: "uint256"
2717
+ }
2718
+ ],
2719
+ anonymous: false
2720
+ };
2721
+ /**
2722
+ * ABI for the SupplyCollateral event emitted by the MorphoV2 contract.
2723
+ */
2724
+ const supplyCollateralEvent = {
2725
+ type: "event",
2726
+ name: "SupplyCollateral",
2727
+ inputs: [
2728
+ {
2729
+ name: "caller",
2730
+ type: "address",
2731
+ indexed: false,
2732
+ internalType: "address"
2733
+ },
2734
+ {
2735
+ name: "id",
2736
+ type: "bytes32",
2737
+ indexed: true,
2738
+ internalType: "bytes32"
2739
+ },
2740
+ {
2741
+ name: "collateral",
2742
+ type: "address",
2743
+ indexed: true,
2744
+ internalType: "address"
2745
+ },
2746
+ {
2747
+ name: "assets",
2748
+ type: "uint256",
2749
+ indexed: false,
2750
+ internalType: "uint256"
2751
+ },
2752
+ {
2753
+ name: "onBehalf",
2754
+ type: "address",
2755
+ indexed: true,
2756
+ internalType: "address"
2757
+ }
2758
+ ],
2759
+ anonymous: false
2760
+ };
2761
+ /**
2762
+ * ABI for the WithdrawCollateral event emitted by the MorphoV2 contract.
2763
+ */
2764
+ const withdrawCollateralEvent = {
2765
+ type: "event",
2766
+ name: "WithdrawCollateral",
2767
+ inputs: [
2768
+ {
2769
+ name: "caller",
2770
+ type: "address",
2771
+ indexed: false,
2772
+ internalType: "address"
2773
+ },
2774
+ {
2775
+ name: "id",
2776
+ type: "bytes32",
2777
+ indexed: true,
2778
+ internalType: "bytes32"
2779
+ },
2780
+ {
2781
+ name: "collateral",
2782
+ type: "address",
2783
+ indexed: true,
2784
+ internalType: "address"
2785
+ },
2786
+ {
2787
+ name: "assets",
2788
+ type: "uint256",
2789
+ indexed: false,
2790
+ internalType: "uint256"
2791
+ },
2792
+ {
2793
+ name: "onBehalf",
2794
+ type: "address",
2795
+ indexed: true,
2796
+ internalType: "address"
2797
+ },
2798
+ {
2799
+ name: "receiver",
2800
+ type: "address",
2801
+ indexed: false,
2802
+ internalType: "address"
2803
+ }
2804
+ ],
2805
+ anonymous: false
2806
+ };
2594
2807
  var InvalidOfferError = class InvalidOfferError extends BaseError {
2595
2808
  name = "Offer.InvalidOfferError";
2596
2809
  constructor(error) {
@@ -2649,6 +2862,8 @@ let Conversion;
2649
2862
  let Type = /* @__PURE__ */ function(Type) {
2650
2863
  Type["ERC20"] = "erc20";
2651
2864
  Type["VAULT_V1"] = "vault_v1";
2865
+ Type["DEBT_OF"] = "debtOf";
2866
+ Type["COLLATERAL_OF"] = "collateralOf";
2652
2867
  return Type;
2653
2868
  }({});
2654
2869
  /**
@@ -2664,10 +2879,16 @@ function from$10(parameters) {
2664
2879
  user: parameters.user.toLowerCase(),
2665
2880
  type: parameters.type,
2666
2881
  balance: parameters.balance,
2667
- ...parameters.asset !== void 0 ? { asset: parameters.asset.toLowerCase() } : {},
2882
+ asset: parameters.asset.toLowerCase(),
2668
2883
  blockNumber: parameters.blockNumber
2669
2884
  };
2670
2885
  }
2886
+ /**
2887
+ * Maps a {@link Type} enum value to its 1-based integer ID used in the database.
2888
+ * @param type - The position type.
2889
+ * @returns The 1-based integer ID.
2890
+ */
2891
+ const positionTypeId = (type) => Object.values(Type).indexOf(type) + 1;
2671
2892
 
2672
2893
  //#endregion
2673
2894
  //#region src/core/Tick.ts
@@ -2784,6 +3005,8 @@ function from$8(parameters) {
2784
3005
  from: parameters.from.toLowerCase(),
2785
3006
  to: parameters.to.toLowerCase(),
2786
3007
  value: parameters.value,
3008
+ type: parameters.type,
3009
+ asset: parameters.asset.toLowerCase(),
2787
3010
  blockNumber: parameters.blockNumber
2788
3011
  };
2789
3012
  }
@@ -2912,14 +3135,10 @@ const encodeUnsigned = (tree) => {
2912
3135
  validateTreeForEncoding(tree);
2913
3136
  return bytesToHex(encodeUnsignedBytes(tree));
2914
3137
  };
2915
- const validateTreeForEncoding = (tree, domain) => {
3138
+ const validateTreeForEncoding = (tree) => {
2916
3139
  if (VERSION$1 > 255) throw new EncodeError(`version overflow: ${VERSION$1} exceeds 255`);
2917
3140
  const computed = from$7(tree.offers);
2918
3141
  if (tree.root !== computed.root) throw new EncodeError(`root mismatch: expected ${computed.root}, got ${tree.root}`);
2919
- if (domain) {
2920
- const mismatched = tree.offers.find((offer) => BigInt(offer.chainId) !== domain.chainId);
2921
- if (mismatched) throw new EncodeError(`chainId mismatch: expected ${domain.chainId}, got ${mismatched.chainId}`);
2922
- }
2923
3142
  };
2924
3143
  const encodeUnsignedBytes = (tree) => {
2925
3144
  const offersPayload = tree.offers.map(serialize);
@@ -2986,8 +3205,6 @@ const decode = async (encoded, domain) => {
2986
3205
  }
2987
3206
  const tree = from$7(rawOffers.map((o) => OfferSchema().parse(o)));
2988
3207
  if (root !== tree.root) throw new DecodeError(`root mismatch: expected ${tree.root}, got ${root}`);
2989
- const chainIdMismatch = tree.offers.find((offer) => BigInt(offer.chainId) !== normalizedDomain.chainId);
2990
- if (chainIdMismatch) throw new DecodeError(`chainId mismatch: expected ${normalizedDomain.chainId}, got ${chainIdMismatch.chainId}`);
2991
3208
  return {
2992
3209
  tree,
2993
3210
  signature,
@@ -3031,10 +3248,6 @@ const BrandTypeId = Symbol.for("mempool/Brand");
3031
3248
 
3032
3249
  //#endregion
3033
3250
  //#region src/gatekeeper/Rules.ts
3034
- const chains$1 = ({ chains }) => single("chain_ids", `Validates that offer chain is one of: [${chains.map((c) => c.id).join(", ")}]`, (offer) => {
3035
- const allowedChainIds = chains.map((c) => c.id);
3036
- if (!allowedChainIds.some((id) => id === offer.chainId)) return { message: `Chain ID ${offer.chainId} is not in the allowed chains (${allowedChainIds.join(", ")})` };
3037
- });
3038
3251
  const maturity = ({ maturities }) => single("maturity", `Validates that offer maturity is one of: [${maturities.join(", ")}]`, (offer) => {
3039
3252
  const allowedMaturities = maturities.map((m) => from$16(m));
3040
3253
  if (!allowedMaturities.includes(offer.maturity)) return { message: `Maturity must be one of (${allowedMaturities.join(", ")}). Got: ${offer.maturity}` };
@@ -3049,9 +3262,9 @@ const callback = ({ callbacks }) => single("callback", `Validates callbacks: buy
3049
3262
  * @param assetsByChainId - Allowed loan tokens indexed by chain id.
3050
3263
  * @returns The issue that was found. If the offer is valid, this will be undefined.
3051
3264
  */
3052
- const loanToken = ({ assetsByChainId }) => single("loan_token", "Validates that offer loan token is in the allowed token list for the offer chain", (offer) => {
3053
- const allowedLoanTokens = assetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase());
3054
- if (!allowedLoanTokens || allowedLoanTokens.length === 0) return { message: `No allowed loan tokens for chain ${offer.chainId}` };
3265
+ const loanToken = ({ assetsByChainId, chainId }) => single("loan_token", "Validates that offer loan token is in the allowed token list for the request chain", (offer) => {
3266
+ const allowedLoanTokens = assetsByChainId[chainId]?.map((asset) => asset.toLowerCase());
3267
+ if (!allowedLoanTokens || allowedLoanTokens.length === 0) return { message: `No allowed loan tokens for chain ${chainId}` };
3055
3268
  if (!allowedLoanTokens.includes(offer.loanToken.toLowerCase())) return { message: "Loan token is not allowed" };
3056
3269
  });
3057
3270
  /**
@@ -3059,9 +3272,9 @@ const loanToken = ({ assetsByChainId }) => single("loan_token", "Validates that
3059
3272
  * @param collateralAssetsByChainId - Allowed collateral tokens indexed by chain id.
3060
3273
  * @returns The issue that was found. If the offer is valid, this will be undefined.
3061
3274
  */
3062
- const collateralToken = ({ collateralAssetsByChainId }) => single("collateral_token", "Validates that offer collateral tokens are in the allowed token list for the offer chain", (offer) => {
3063
- const allowedCollateralTokens = collateralAssetsByChainId[offer.chainId]?.map((asset) => asset.toLowerCase()) ?? [];
3064
- if (allowedCollateralTokens.length === 0) return { message: `No allowed collateral tokens for chain ${offer.chainId}` };
3275
+ const collateralToken = ({ collateralAssetsByChainId, chainId }) => single("collateral_token", "Validates that offer collateral tokens are in the allowed token list for the request chain", (offer) => {
3276
+ const allowedCollateralTokens = collateralAssetsByChainId[chainId]?.map((asset) => asset.toLowerCase()) ?? [];
3277
+ if (allowedCollateralTokens.length === 0) return { message: `No allowed collateral tokens for chain ${chainId}` };
3065
3278
  if (offer.collaterals.length === 0) return { message: "At least one collateral token is required" };
3066
3279
  if (offer.collaterals.some((collateral) => !allowedCollateralTokens.includes(collateral.asset.toLowerCase()))) return { message: "Collateral token is not allowed" };
3067
3280
  });
@@ -3070,9 +3283,9 @@ const collateralToken = ({ collateralAssetsByChainId }) => single("collateral_to
3070
3283
  * @param oraclesByChainId - Allowed oracles indexed by chain id.
3071
3284
  * @returns The issue that was found. If the offer is valid, this will be undefined.
3072
3285
  */
3073
- const oracle = ({ oraclesByChainId }) => single("oracle", "Validates that offer collateral oracles are in the allowed oracle list for the offer chain", (offer) => {
3074
- const allowedOracles = oraclesByChainId[offer.chainId]?.map((oracle) => oracle.toLowerCase());
3075
- if (!allowedOracles || allowedOracles.length === 0) return { message: `No allowed oracles for chain ${offer.chainId}` };
3286
+ const oracle = ({ oraclesByChainId, chainId }) => single("oracle", "Validates that offer collateral oracles are in the allowed oracle list for the request chain", (offer) => {
3287
+ const allowedOracles = oraclesByChainId[chainId]?.map((oracle) => oracle.toLowerCase());
3288
+ if (!allowedOracles || allowedOracles.length === 0) return { message: `No allowed oracles for chain ${chainId}` };
3076
3289
  if (offer.collaterals.some((collateral) => !allowedOracles.includes(collateral.oracle.toLowerCase()))) return { message: "Oracle is not allowed" };
3077
3290
  });
3078
3291
  /**
@@ -3104,7 +3317,8 @@ const amountMutualExclusivity = () => single("amount_mutual_exclusivity", "Valid
3104
3317
 
3105
3318
  //#endregion
3106
3319
  //#region src/gatekeeper/morphoRules.ts
3107
- const morphoRules = (chains) => {
3320
+ const morphoRules = (parameters) => {
3321
+ const { chains, chainId } = parameters;
3108
3322
  const assetsByChainId = {};
3109
3323
  const collateralAssetsByChainId = {};
3110
3324
  const oraclesByChainId = {};
@@ -3116,15 +3330,23 @@ const morphoRules = (chains) => {
3116
3330
  return [
3117
3331
  sameMaker(),
3118
3332
  amountMutualExclusivity(),
3119
- chains$1({ chains }),
3120
3333
  maturity({ maturities: [MaturityType.EndOfWeek, MaturityType.EndOfNextWeek] }),
3121
3334
  callback({
3122
3335
  callbacks: [Type$1.BuyWithEmptyCallback, Type$1.SellWithEmptyCallback],
3123
3336
  allowedAddresses: []
3124
3337
  }),
3125
- loanToken({ assetsByChainId }),
3126
- collateralToken({ collateralAssetsByChainId }),
3127
- oracle({ oraclesByChainId })
3338
+ loanToken({
3339
+ assetsByChainId,
3340
+ chainId
3341
+ }),
3342
+ collateralToken({
3343
+ collateralAssetsByChainId,
3344
+ chainId
3345
+ }),
3346
+ oracle({
3347
+ oraclesByChainId,
3348
+ chainId
3349
+ })
3128
3350
  ];
3129
3351
  };
3130
3352
 
@@ -3266,12 +3488,13 @@ const RouterStatusResponse = z$1.object({
3266
3488
  * @constructor
3267
3489
  * @param obligation - {@link Obligation}
3268
3490
  * @param quote - {@link Quote}
3491
+ * @param chainId - The chain id used to compute `id`.
3269
3492
  * @returns The created `ObligationResponse`. {@link ObligationResponse}
3270
3493
  */
3271
- function from$5(obligation, quote) {
3494
+ function from$5(obligation, quote, chainId) {
3272
3495
  return {
3273
3496
  id: quote.obligationId,
3274
- chain_id: obligation.chainId,
3497
+ chain_id: chainId,
3275
3498
  loan_token: obligation.loanToken,
3276
3499
  collaterals: obligation.collaterals.map((c) => ({
3277
3500
  token: c.asset,
@@ -3337,12 +3560,7 @@ function from$4(input) {
3337
3560
  receiver_if_maker_is_seller: input.receiverIfMakerIsSeller
3338
3561
  },
3339
3562
  offer_hash: input.hash,
3340
- obligation_id: id({
3341
- chainId,
3342
- loanToken: input.loanToken,
3343
- collaterals: [...input.collaterals],
3344
- maturity: input.maturity
3345
- }),
3563
+ obligation_id: input.obligationId,
3346
3564
  chain_id: chainId,
3347
3565
  consumed: input.consumed.toString(),
3348
3566
  takeable: input.takeable.toString(),
@@ -3969,10 +4187,6 @@ __decorate([ApiProperty({
3969
4187
  type: "boolean",
3970
4188
  example: validateOfferExample.buy
3971
4189
  })], ValidateOfferRequest.prototype, "buy", void 0);
3972
- __decorate([ApiProperty({
3973
- type: "number",
3974
- example: validateOfferExample.chain_id
3975
- })], ValidateOfferRequest.prototype, "chain_id", void 0);
3976
4190
  __decorate([ApiProperty({
3977
4191
  type: "string",
3978
4192
  example: validateOfferExample.loan_token
@@ -3990,6 +4204,11 @@ __decorate([ApiProperty({
3990
4204
  example: validateOfferExample.receiver_if_maker_is_seller
3991
4205
  })], ValidateOfferRequest.prototype, "receiver_if_maker_is_seller", void 0);
3992
4206
  var ValidateOffersRequest = class {};
4207
+ __decorate([ApiProperty({
4208
+ type: "number",
4209
+ description: "Chain id used for chain-scoped validation rules.",
4210
+ example: validateOfferExample.chain_id
4211
+ })], ValidateOffersRequest.prototype, "chain_id", void 0);
3993
4212
  __decorate([ApiProperty({
3994
4213
  type: () => [ValidateOfferRequest],
3995
4214
  description: "Array of offers in snake_case format. Required, non-empty.",
@@ -4755,6 +4974,12 @@ function isValidBase64urlJson(val) {
4755
4974
  function isValidOfferHashCursor(val) {
4756
4975
  return /^0x[a-f0-9]{64}$/i.test(val);
4757
4976
  }
4977
+ function isValidOfferCursor(val) {
4978
+ const [hash, obligationId, ...rest] = val.split(":");
4979
+ if (rest.length !== 0) return false;
4980
+ if (!hash || !obligationId) return false;
4981
+ return isValidOfferHashCursor(hash) && isValidOfferHashCursor(obligationId);
4982
+ }
4758
4983
  const csvArray = (schema) => z$2.preprocess((value) => {
4759
4984
  if (value === void 0) return void 0;
4760
4985
  if (Array.isArray(value)) {
@@ -4818,7 +5043,7 @@ const GetConfigContractsQueryParams = z$2.object({
4818
5043
  });
4819
5044
  const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend({
4820
5045
  cursor: z$2.string().optional().meta({
4821
- description: "Pagination cursor. Use offer hash (0x...) for maker queries, base64url for obligation queries.",
5046
+ description: "Pagination cursor. Use offer hash:obligation_id for maker queries, base64url for obligation queries.",
4822
5047
  example: "eyJzaWRlIjoic2VsbCIsImN1cnJlbnRQcmljZSI6IjEwMDAwMDAwMDAwMDAwMDAwMDAiLCJibG9ja051bWJlciI6MSwiYXNzZXRzIjoiMTAwMDAwMDAwMDAwMDAwMDAwMCIsImhhc2giOiIweGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIiLCJ0b3RhbFJldHVybmVkIjoxMCwibm93IjoxNjAwMDAwMDAwfQ"
4823
5048
  }),
4824
5049
  side: z$2.enum(["buy", "sell"]).optional().meta({
@@ -4845,10 +5070,10 @@ const GetOffersQueryParams = PaginationQueryParams.omit({ cursor: true }).extend
4845
5070
  return;
4846
5071
  }
4847
5072
  if (hasMaker) {
4848
- if (val.cursor !== void 0 && !isValidOfferHashCursor(val.cursor)) ctx.addIssue({
5073
+ if (val.cursor !== void 0 && !isValidOfferCursor(val.cursor)) ctx.addIssue({
4849
5074
  code: "custom",
4850
5075
  path: ["cursor"],
4851
- message: "Cursor must be a 32-byte hex offer hash when filtering by maker"
5076
+ message: "Cursor must be in the offer hash:obligation_id format when filtering by maker"
4852
5077
  });
4853
5078
  return;
4854
5079
  }
@@ -4958,7 +5183,13 @@ const GetBookParams = z$2.object({
4958
5183
  example: "buy"
4959
5184
  })
4960
5185
  });
4961
- const ValidateOffersBody = z$2.object({ offers: z$2.array(z$2.unknown()).min(1, { message: "'offers' must contain at least 1 offer" }) }).strict();
5186
+ const ValidateOffersBody = z$2.object({
5187
+ chain_id: z$2.number().int().positive("chain_id must be a positive integer").meta({
5188
+ description: "Chain id used for chain-scoped validation rules.",
5189
+ example: 1
5190
+ }),
5191
+ offers: z$2.array(z$2.unknown()).min(1, { message: "'offers' must contain at least 1 offer" })
5192
+ }).strict();
4962
5193
  const GetUserPositionsParams = z$2.object({
4963
5194
  ...PaginationQueryParams.shape,
4964
5195
  user_address: z$2.string().regex(/^0x[a-fA-F0-9]{40}$/, { error: "User address must be a valid 20-byte address" }).transform((val) => val.toLowerCase()).meta({
@@ -5005,7 +5236,7 @@ async function getConfigRules(query, chains) {
5005
5236
  const checksum = buildConfigRulesChecksum(filteredRules);
5006
5237
  let cursorRule = null;
5007
5238
  if (cursor) try {
5008
- cursorRule = parseCursor$1(cursor);
5239
+ cursorRule = parseCursor$3(cursor);
5009
5240
  } catch (err) {
5010
5241
  return failure(err);
5011
5242
  }
@@ -5013,7 +5244,7 @@ async function getConfigRules(query, chains) {
5013
5244
  if (cursorRule && chainFilter && !chainFilter.has(cursorRule.chain_id)) return failure(new BadRequestError$1("Cursor chain_id must match requested chains"));
5014
5245
  const startIndex = cursorRule ? findStartIndex$1(filteredRules, cursorRule) : 0;
5015
5246
  const page = filteredRules.slice(startIndex, startIndex + limit);
5016
- const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor$1(page.at(-1)) : null;
5247
+ const nextCursor = startIndex + limit < filteredRules.length && page.length > 0 ? formatCursor$3(page.at(-1)) : null;
5017
5248
  const response = success({
5018
5249
  data: page,
5019
5250
  cursor: nextCursor
@@ -5021,14 +5252,14 @@ async function getConfigRules(query, chains) {
5021
5252
  response.body.meta.checksum = checksum;
5022
5253
  return response;
5023
5254
  }
5024
- function formatCursor$1(rule) {
5255
+ function formatCursor$3(rule) {
5025
5256
  if (rule.type === "maturity") return `maturity:${rule.chain_id}:${rule.timestamp}:${rule.name}`;
5026
5257
  if (rule.type === "callback") return `callback:${rule.chain_id}:${rule.callback_type}:${rule.address.toLowerCase()}`;
5027
5258
  if (rule.type === "oracle") return `oracle:${rule.chain_id}:${rule.address.toLowerCase()}`;
5028
5259
  if (rule.type === "collateral_token") return `collateral_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
5029
5260
  return `loan_token:${rule.chain_id}:${rule.address.toLowerCase()}`;
5030
5261
  }
5031
- function parseCursor$1(cursor) {
5262
+ function parseCursor$3(cursor) {
5032
5263
  const [type, chain, ...rest] = cursor.split(":");
5033
5264
  if (!type || !chain || rest.length === 0) throw new BadRequestError$1("Cursor must be in the format type:chain_id:<value>");
5034
5265
  if (!isConfigRuleType(type)) throw new BadRequestError$1("Cursor has an invalid rule type");
@@ -5178,16 +5409,17 @@ async function validateOffers(body, gatekeeper) {
5178
5409
  const logger = getLogger();
5179
5410
  const result = safeParse("validate_offers", body, (issue) => issue.message);
5180
5411
  if (!result.success) return failure(new BadRequestError$1(result.error.issues[0]?.message ?? "Invalid request body"));
5181
- const { offers: rawOffers } = result.data;
5412
+ const { offers: rawOffers, chain_id: rawChainId } = result.data;
5413
+ const chainId = rawChainId;
5182
5414
  const parsedOffers = [];
5183
5415
  const offerIndexByHash = /* @__PURE__ */ new Map();
5184
5416
  for (let i = 0; i < rawOffers.length; i++) {
5185
5417
  const rawOffer = rawOffers[i];
5186
5418
  try {
5187
5419
  const offer = fromSnakeCase(rawOffer);
5188
- const hash$3 = hash(offer);
5189
- if (!offerIndexByHash.has(hash$3)) {
5190
- offerIndexByHash.set(hash$3, i);
5420
+ const hash$1 = hash(offer);
5421
+ if (!offerIndexByHash.has(hash$1)) {
5422
+ offerIndexByHash.set(hash$1, i);
5191
5423
  parsedOffers.push(offer);
5192
5424
  }
5193
5425
  } catch (err) {
@@ -5197,7 +5429,10 @@ async function validateOffers(body, gatekeeper) {
5197
5429
  }
5198
5430
  }
5199
5431
  try {
5200
- const { issues } = await gatekeeper.isAllowed(parsedOffers);
5432
+ const { issues } = await gatekeeper.isAllowed({
5433
+ offers: parsedOffers,
5434
+ chainId
5435
+ });
5201
5436
  if (issues.length > 0) {
5202
5437
  const mappedIssues = issues.map((issue) => {
5203
5438
  const index = offerIndexByHash.get(hash(issue.item));
@@ -5358,7 +5593,7 @@ function now() {
5358
5593
 
5359
5594
  //#endregion
5360
5595
  //#region src/database/drizzle/VERSION.ts
5361
- const VERSION = "router_v1.8";
5596
+ const VERSION = "router_v1.11";
5362
5597
 
5363
5598
  //#endregion
5364
5599
  //#region src/database/drizzle/schema.ts
@@ -5373,8 +5608,10 @@ var schema_exports = /* @__PURE__ */ __exportAll({
5373
5608
  consumedEvents: () => consumedEvents,
5374
5609
  groups: () => groups,
5375
5610
  lots: () => lots,
5611
+ lotsPositions: () => lotsPositions,
5376
5612
  merklePaths: () => merklePaths,
5377
5613
  obligationCollateralsV2: () => obligationCollateralsV2,
5614
+ obligationIdKeys: () => obligationIdKeys,
5378
5615
  obligations: () => obligations,
5379
5616
  offers: () => offers,
5380
5617
  offersCallbacks: () => offersCallbacks,
@@ -5390,6 +5627,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
5390
5627
  const s = pgSchema(VERSION);
5391
5628
  var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
5392
5629
  EnumTableName["OBLIGATIONS"] = "obligations";
5630
+ EnumTableName["OBLIGATION_ID_KEYS"] = "obligation_id_keys";
5393
5631
  EnumTableName["GROUPS"] = "groups";
5394
5632
  EnumTableName["CONSUMED_EVENTS"] = "consumed_events";
5395
5633
  EnumTableName["OBLIGATION_COLLATERALS_V2"] = "obligation_collaterals_v2";
@@ -5403,6 +5641,7 @@ var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
5403
5641
  EnumTableName["COLLECTORS"] = "collectors";
5404
5642
  EnumTableName["CHAINS"] = "chains";
5405
5643
  EnumTableName["LOTS"] = "lots";
5644
+ EnumTableName["LOTS_POSITIONS"] = "lots_positions";
5406
5645
  EnumTableName["OFFSETS"] = "offsets";
5407
5646
  EnumTableName["TREES"] = "trees";
5408
5647
  EnumTableName["MERKLE_PATHS"] = "merkle_paths";
@@ -5411,11 +5650,16 @@ var EnumTableName = /* @__PURE__ */ function(EnumTableName) {
5411
5650
  const TABLE_NAMES = Object.values(EnumTableName);
5412
5651
  const VERSIONED_TABLE_NAMES = TABLE_NAMES.map((table) => `"${VERSION}"."${table}"`);
5413
5652
  const obligations = s.table(EnumTableName.OBLIGATIONS, {
5414
- obligationId: varchar("obligation_id", { length: 66 }).primaryKey(),
5415
- chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5653
+ obligationKey: varchar("obligation_key", { length: 66 }).primaryKey(),
5416
5654
  loanToken: varchar("loan_token", { length: 42 }).notNull(),
5417
5655
  maturity: integer("maturity").notNull()
5418
5656
  });
5657
+ const obligationIdKeys = s.table(EnumTableName.OBLIGATION_ID_KEYS, {
5658
+ obligationId: varchar("obligation_id", { length: 66 }).primaryKey(),
5659
+ obligationKey: varchar("obligation_key", { length: 66 }).notNull().references(() => obligations.obligationKey, { onDelete: "cascade" }),
5660
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5661
+ morphoV2: varchar("morpho_v2", { length: 42 }).notNull()
5662
+ }, (table) => [index("obligation_id_keys_obligation_key_idx").on(table.obligationKey), index("obligation_id_keys_chain_id_idx").on(table.chainId)]);
5419
5663
  const groups = s.table(EnumTableName.GROUPS, {
5420
5664
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5421
5665
  maker: varchar("maker", { length: 42 }).notNull(),
@@ -5463,24 +5707,19 @@ const consumedEvents = s.table(EnumTableName.CONSUMED_EVENTS, {
5463
5707
  index("consumed_events_block_number_idx").on(t.blockNumber)
5464
5708
  ]);
5465
5709
  const obligationCollateralsV2 = s.table(EnumTableName.OBLIGATION_COLLATERALS_V2, {
5466
- obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
5710
+ obligationKey: varchar("obligation_key", { length: 66 }).notNull().references(() => obligations.obligationKey, { onDelete: "cascade" }),
5467
5711
  asset: varchar("asset", { length: 42 }).notNull(),
5468
- oracleChainId: bigint("oracle_chain_id", { mode: "number" }).$type().notNull(),
5469
5712
  oracleAddress: varchar("oracle_address", { length: 42 }).notNull(),
5470
5713
  lltv: bigint("lltv", { mode: "bigint" }).notNull(),
5714
+ collateralIndex: integer("collateral_index").notNull(),
5471
5715
  updatedAt: timestamp("updated_at").defaultNow().notNull()
5472
5716
  }, (table) => [
5473
5717
  primaryKey({
5474
- columns: [table.obligationId, table.asset],
5718
+ columns: [table.obligationKey, table.asset],
5475
5719
  name: "obligation_collaterals_v2_pk"
5476
5720
  }),
5477
- foreignKey({
5478
- columns: [table.oracleChainId, table.oracleAddress],
5479
- foreignColumns: [oracles.chainId, oracles.address],
5480
- name: "obligation_collaterals_v2_oracles_fk"
5481
- }),
5482
- index("obligation_collaterals_v2_obligation_id_idx").on(table.obligationId),
5483
- index("obligation_collaterals_v2_oracle_fk_idx").on(table.oracleChainId, table.oracleAddress)
5721
+ index("obligation_collaterals_v2_obligation_key_idx").on(table.obligationKey),
5722
+ index("obligation_collaterals_v2_oracle_address_idx").on(table.oracleAddress)
5484
5723
  ]);
5485
5724
  const oracles = s.table(EnumTableName.ORACLES, {
5486
5725
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
@@ -5496,8 +5735,8 @@ const oracles = s.table(EnumTableName.ORACLES, {
5496
5735
  name: "oracles_pk"
5497
5736
  })]);
5498
5737
  const offers = s.table(EnumTableName.OFFERS, {
5499
- hash: varchar("hash", { length: 66 }).primaryKey(),
5500
- obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligations.obligationId, { onDelete: "cascade" }),
5738
+ hash: varchar("hash", { length: 66 }).notNull(),
5739
+ obligationId: varchar("obligation_id", { length: 66 }).notNull().references(() => obligationIdKeys.obligationId, { onDelete: "cascade" }),
5501
5740
  assets: numeric("assets", {
5502
5741
  precision: 78,
5503
5742
  scale: 0
@@ -5525,6 +5764,10 @@ const offers = s.table(EnumTableName.OFFERS, {
5525
5764
  blockNumber: bigint("block_number", { mode: "number" }).notNull(),
5526
5765
  updatedAt: timestamp("updated_at").defaultNow().notNull()
5527
5766
  }, (table) => [
5767
+ primaryKey({
5768
+ columns: [table.hash, table.obligationId],
5769
+ name: "offers_pk"
5770
+ }),
5528
5771
  foreignKey({
5529
5772
  columns: [
5530
5773
  table.groupChainId,
@@ -5543,10 +5786,19 @@ const offers = s.table(EnumTableName.OFFERS, {
5543
5786
  index("offers_obligation_id_side_idx").on(table.obligationId, table.buy)
5544
5787
  ]);
5545
5788
  const offersCallbacks = s.table(EnumTableName.OFFERS_CALLBACKS, {
5546
- offerHash: varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
5789
+ offerHash: varchar("offer_hash", { length: 66 }).notNull(),
5790
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5547
5791
  callbackId: varchar("callback_id", { length: 66 })
5548
- }, (table) => [primaryKey({
5549
- columns: [table.offerHash, table.callbackId],
5792
+ }, (table) => [foreignKey({
5793
+ columns: [table.offerHash, table.obligationId],
5794
+ foreignColumns: [offers.hash, offers.obligationId],
5795
+ name: "offers_callbacks_offer_fk"
5796
+ }).onDelete("cascade"), primaryKey({
5797
+ columns: [
5798
+ table.offerHash,
5799
+ table.obligationId,
5800
+ table.callbackId
5801
+ ],
5550
5802
  name: "offers_callbacks_pk"
5551
5803
  })]);
5552
5804
  const callbacks = s.table(EnumTableName.CALLBACKS, {
@@ -5554,23 +5806,25 @@ const callbacks = s.table(EnumTableName.CALLBACKS, {
5554
5806
  positionChainId: bigint("position_chain_id", { mode: "number" }).$type().notNull(),
5555
5807
  positionContract: varchar("position_contract", { length: 42 }).notNull(),
5556
5808
  positionUser: varchar("position_user", { length: 42 }).notNull(),
5809
+ positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
5557
5810
  amount: numeric("amount", {
5558
5811
  precision: 78,
5559
5812
  scale: 0
5560
5813
  })
5561
- }, (table) => [foreignKey({
5814
+ });
5815
+ const lotsPositions = s.table(EnumTableName.LOTS_POSITIONS, {
5816
+ chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5817
+ contract: varchar("contract", { length: 42 }).notNull(),
5818
+ user: varchar("user", { length: 42 }).notNull(),
5819
+ positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" })
5820
+ }, (table) => [primaryKey({
5562
5821
  columns: [
5563
- table.positionChainId,
5564
- table.positionContract,
5565
- table.positionUser
5566
- ],
5567
- foreignColumns: [
5568
- positions.chainId,
5569
- positions.contract,
5570
- positions.user
5822
+ table.chainId,
5823
+ table.contract,
5824
+ table.user
5571
5825
  ],
5572
- name: "callbacks_positions_fk"
5573
- }).onDelete("cascade")]);
5826
+ name: "lots_positions_pk"
5827
+ })]);
5574
5828
  const lots = s.table(EnumTableName.LOTS, {
5575
5829
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5576
5830
  user: varchar("user", { length: 42 }).notNull(),
@@ -5603,11 +5857,11 @@ const lots = s.table(EnumTableName.LOTS, {
5603
5857
  table.user
5604
5858
  ],
5605
5859
  foreignColumns: [
5606
- positions.chainId,
5607
- positions.contract,
5608
- positions.user
5860
+ lotsPositions.chainId,
5861
+ lotsPositions.contract,
5862
+ lotsPositions.user
5609
5863
  ],
5610
- name: "lots_positions_fk"
5864
+ name: "lots_lots_positions_fk"
5611
5865
  }).onDelete("cascade"),
5612
5866
  foreignKey({
5613
5867
  columns: [
@@ -5649,11 +5903,11 @@ const offsets = s.table(EnumTableName.OFFSETS, {
5649
5903
  table.user
5650
5904
  ],
5651
5905
  foreignColumns: [
5652
- positions.chainId,
5653
- positions.contract,
5654
- positions.user
5906
+ lotsPositions.chainId,
5907
+ lotsPositions.contract,
5908
+ lotsPositions.user
5655
5909
  ],
5656
- name: "offsets_positions_fk"
5910
+ name: "offsets_lots_positions_fk"
5657
5911
  }).onDelete("cascade")]);
5658
5912
  const PositionTypes = s.enum("position_type", Object.values(Type));
5659
5913
  const positionTypes = s.table("position_types", {
@@ -5662,34 +5916,38 @@ const positionTypes = s.table("position_types", {
5662
5916
  });
5663
5917
  const positions = s.table(EnumTableName.POSITIONS, {
5664
5918
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5665
- contract: varchar("contract", { length: 42 }).notNull(),
5919
+ contract: varchar("contract", { length: 66 }).notNull(),
5666
5920
  user: varchar("user", { length: 42 }).notNull(),
5667
5921
  positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
5668
5922
  balance: numeric("balance", {
5669
5923
  precision: 78,
5670
5924
  scale: 0
5671
5925
  }),
5672
- asset: varchar("asset", { length: 42 }),
5926
+ asset: varchar("asset", { length: 42 }).notNull(),
5673
5927
  blockNumber: bigint("block_number", { mode: "number" }).notNull(),
5674
5928
  updatedAt: timestamp("updated_at").defaultNow().notNull()
5675
5929
  }, (table) => [primaryKey({
5676
5930
  columns: [
5677
5931
  table.chainId,
5678
5932
  table.contract,
5679
- table.user
5933
+ table.user,
5934
+ table.positionTypeId,
5935
+ table.asset
5680
5936
  ],
5681
5937
  name: "positions_pk"
5682
5938
  })]);
5683
5939
  const transfers = s.table(EnumTableName.TRANSFERS, {
5684
5940
  eventId: varchar("event_id", { length: 128 }).primaryKey(),
5685
5941
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull(),
5686
- contract: varchar("contract", { length: 42 }).notNull(),
5942
+ contract: varchar("contract", { length: 66 }).notNull(),
5687
5943
  from: varchar("from", { length: 42 }).notNull(),
5688
5944
  to: varchar("to", { length: 42 }).notNull(),
5689
5945
  value: numeric("value", {
5690
5946
  precision: 78,
5691
5947
  scale: 0
5692
5948
  }).notNull(),
5949
+ positionTypeId: integer("position_type_id").notNull().references(() => positionTypes.id, { onDelete: "no action" }),
5950
+ asset: varchar("asset", { length: 42 }).notNull(),
5693
5951
  blockNumber: bigint("block_number", { mode: "number" }).notNull(),
5694
5952
  createdAt: timestamp("created_at").defaultNow().notNull()
5695
5953
  }, (table) => [
@@ -5697,12 +5955,16 @@ const transfers = s.table(EnumTableName.TRANSFERS, {
5697
5955
  columns: [
5698
5956
  table.chainId,
5699
5957
  table.contract,
5700
- table.from
5958
+ table.from,
5959
+ table.positionTypeId,
5960
+ table.asset
5701
5961
  ],
5702
5962
  foreignColumns: [
5703
5963
  positions.chainId,
5704
5964
  positions.contract,
5705
- positions.user
5965
+ positions.user,
5966
+ positions.positionTypeId,
5967
+ positions.asset
5706
5968
  ],
5707
5969
  name: "transfers_positions_from_fk"
5708
5970
  }).onDelete("cascade"),
@@ -5710,16 +5972,21 @@ const transfers = s.table(EnumTableName.TRANSFERS, {
5710
5972
  columns: [
5711
5973
  table.chainId,
5712
5974
  table.contract,
5713
- table.to
5975
+ table.to,
5976
+ table.positionTypeId,
5977
+ table.asset
5714
5978
  ],
5715
5979
  foreignColumns: [
5716
5980
  positions.chainId,
5717
5981
  positions.contract,
5718
- positions.user
5982
+ positions.user,
5983
+ positions.positionTypeId,
5984
+ positions.asset
5719
5985
  ],
5720
5986
  name: "transfers_positions_to_fk"
5721
5987
  }).onDelete("cascade"),
5722
- index("transfers_chain_contract_user_idx").on(table.chainId, table.contract, table.from, table.to, table.blockNumber)
5988
+ index("transfers_chain_contract_user_idx").on(table.chainId, table.contract, table.from, table.to, table.blockNumber),
5989
+ index("transfers_chain_type_block_idx").on(table.chainId, table.positionTypeId, table.blockNumber)
5723
5990
  ]);
5724
5991
  const StatusCode = s.enum("status_code", Object.values(Status));
5725
5992
  const status = s.table("status", {
@@ -5727,10 +5994,18 @@ const status = s.table("status", {
5727
5994
  code: StatusCode("code").unique()
5728
5995
  });
5729
5996
  const validations = s.table("validations", {
5730
- offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
5997
+ offerHash: varchar("offer_hash", { length: 66 }).notNull(),
5998
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5731
5999
  statusId: integer("status_id").notNull().references(() => status.id, { onDelete: "no action" }),
5732
6000
  updatedAt: timestamp("updated_at").defaultNow().notNull()
5733
- });
6001
+ }, (table) => [primaryKey({
6002
+ columns: [table.offerHash, table.obligationId],
6003
+ name: "validations_pk"
6004
+ }), foreignKey({
6005
+ columns: [table.offerHash, table.obligationId],
6006
+ foreignColumns: [offers.hash, offers.obligationId],
6007
+ name: "validations_offer_fk"
6008
+ }).onDelete("cascade")]);
5734
6009
  const collectors = s.table(EnumTableName.COLLECTORS, {
5735
6010
  chainId: bigint("chain_id", { mode: "number" }).$type().notNull().references(() => chains.chainId, { onDelete: "no action" }),
5736
6011
  name: text("name").$type().notNull(),
@@ -5756,18 +6031,352 @@ const trees = s.table(EnumTableName.TREES, {
5756
6031
  createdAt: timestamp("created_at").defaultNow().notNull()
5757
6032
  });
5758
6033
  const merklePaths = s.table(EnumTableName.MERKLE_PATHS, {
5759
- offerHash: varchar("offer_hash", { length: 66 }).primaryKey().references(() => offers.hash, { onDelete: "cascade" }),
6034
+ offerHash: varchar("offer_hash", { length: 66 }).notNull(),
6035
+ obligationId: varchar("obligation_id", { length: 66 }).notNull(),
5760
6036
  treeRoot: varchar("tree_root", { length: 66 }).notNull().references(() => trees.root, { onDelete: "cascade" }),
5761
6037
  proofNodes: text("proof_nodes").notNull(),
5762
6038
  createdAt: timestamp("created_at").defaultNow().notNull()
5763
- }, (table) => [index("merkle_paths_tree_root_idx").on(table.treeRoot)]);
6039
+ }, (table) => [
6040
+ primaryKey({
6041
+ columns: [table.offerHash, table.obligationId],
6042
+ name: "merkle_paths_pk"
6043
+ }),
6044
+ foreignKey({
6045
+ columns: [table.offerHash, table.obligationId],
6046
+ foreignColumns: [offers.hash, offers.obligationId],
6047
+ name: "merkle_paths_offer_fk"
6048
+ }).onDelete("cascade"),
6049
+ index("merkle_paths_tree_root_idx").on(table.treeRoot)
6050
+ ]);
6051
+
6052
+ //#endregion
6053
+ //#region src/indexer/collectors/CollectFunctions/processors/processCollateralSeizures.ts
6054
+ /**
6055
+ * Parse raw MorphoV2 liquidate logs and compute collateral seizures.
6056
+ *
6057
+ * Seizures only expose `(obligationId, collateralIndex)` and must be resolved
6058
+ * to collateral token addresses before creating transfer rows.
6059
+ *
6060
+ * @param parameters - The parsed event logs and chain ID.
6061
+ * @param parameters.logs - Parsed event logs from MorphoV2.
6062
+ * @param parameters.chainId - Chain ID for event attribution.
6063
+ * @returns Seizure events pending collateral address resolution.
6064
+ */
6065
+ function processCollateralSeizures(parameters) {
6066
+ const { logs, chainId } = parameters;
6067
+ const logger = getLogger();
6068
+ const seizureEvents = [];
6069
+ for (const rawLog of logs) {
6070
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
6071
+ logger.debug({
6072
+ chainId,
6073
+ msg: "Skipping collateral log because it is missing required fields"
6074
+ });
6075
+ continue;
6076
+ }
6077
+ if (rawLog.eventName !== liquidateEvent.name) continue;
6078
+ const args = rawLog.args;
6079
+ if (args?.id === void 0 || args?.borrower === void 0 || args?.seizures === void 0) {
6080
+ logger.debug({
6081
+ chainId,
6082
+ msg: "Skipping Liquidate log for collateral because it is missing required args"
6083
+ });
6084
+ continue;
6085
+ }
6086
+ const baseId = `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`;
6087
+ for (let seizureIndex = 0; seizureIndex < args.seizures.length; seizureIndex++) {
6088
+ const seizure = args.seizures[seizureIndex];
6089
+ if (seizure.seized === 0n) continue;
6090
+ seizureEvents.push({
6091
+ id: `${baseId}-collateral-liquidate-${seizureIndex}`,
6092
+ chainId,
6093
+ obligationId: args.id,
6094
+ collateralIndex: Number(seizure.collateralIndex),
6095
+ user: args.borrower,
6096
+ amount: seizure.seized,
6097
+ blockNumber: Number(rawLog.blockNumber)
6098
+ });
6099
+ }
6100
+ }
6101
+ return seizureEvents;
6102
+ }
6103
+
6104
+ //#endregion
6105
+ //#region src/indexer/collectors/CollectFunctions/processors/processCollateralTransfers.ts
6106
+ /**
6107
+ * Parse raw MorphoV2 logs and compute collateral transfers.
6108
+ *
6109
+ * A collateral position uses `contract = obligationId` and `asset = collateral token address`.
6110
+ * The 5-col PK `(chainId, contract, user, positionTypeId, asset)` distinguishes per-token positions.
6111
+ *
6112
+ * **SupplyCollateral**: Transfer from zeroAddress to onBehalf (supply)
6113
+ * **WithdrawCollateral**: Transfer from onBehalf to zeroAddress (withdrawal)
6114
+ *
6115
+ * @param parameters - The parsed event logs and chain ID.
6116
+ * @param parameters.logs - Parsed event logs from MorphoV2.
6117
+ * @param parameters.chainId - Chain ID for event attribution.
6118
+ * @returns Collateral transfers from SupplyCollateral/WithdrawCollateral.
6119
+ */
6120
+ function processCollateralTransfers(parameters) {
6121
+ const { logs, chainId } = parameters;
6122
+ const logger = getLogger();
6123
+ const transfers = [];
6124
+ for (const rawLog of logs) {
6125
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
6126
+ logger.debug({
6127
+ chainId,
6128
+ msg: "Skipping collateral log because it is missing required fields"
6129
+ });
6130
+ continue;
6131
+ }
6132
+ const eventName = rawLog.eventName;
6133
+ const baseId = `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`;
6134
+ if (eventName === supplyCollateralEvent.name) {
6135
+ const args = rawLog.args;
6136
+ if (args?.id === void 0 || args?.collateral === void 0 || args?.assets === void 0 || args?.onBehalf === void 0) {
6137
+ logger.debug({
6138
+ chainId,
6139
+ msg: "Skipping SupplyCollateral log because it is missing required args"
6140
+ });
6141
+ continue;
6142
+ }
6143
+ if (args.assets === 0n) continue;
6144
+ transfers.push(from$8({
6145
+ id: `${baseId}-collateral-supply`,
6146
+ chainId,
6147
+ contract: args.id,
6148
+ from: zeroAddress,
6149
+ to: args.onBehalf,
6150
+ value: args.assets,
6151
+ type: Type.COLLATERAL_OF,
6152
+ asset: args.collateral,
6153
+ blockNumber: Number(rawLog.blockNumber)
6154
+ }));
6155
+ continue;
6156
+ }
6157
+ if (eventName === withdrawCollateralEvent.name) {
6158
+ const args = rawLog.args;
6159
+ if (args?.id === void 0 || args?.collateral === void 0 || args?.assets === void 0 || args?.onBehalf === void 0) {
6160
+ logger.debug({
6161
+ chainId,
6162
+ msg: "Skipping WithdrawCollateral log because it is missing required args"
6163
+ });
6164
+ continue;
6165
+ }
6166
+ if (args.assets === 0n) continue;
6167
+ transfers.push(from$8({
6168
+ id: `${baseId}-collateral-withdraw`,
6169
+ chainId,
6170
+ contract: args.id,
6171
+ from: args.onBehalf,
6172
+ to: zeroAddress,
6173
+ value: args.assets,
6174
+ type: Type.COLLATERAL_OF,
6175
+ asset: args.collateral,
6176
+ blockNumber: Number(rawLog.blockNumber)
6177
+ }));
6178
+ }
6179
+ }
6180
+ return transfers;
6181
+ }
5764
6182
 
5765
6183
  //#endregion
5766
- //#region src/indexer/collectors/CollectFunctions/collectConsumedEvents.ts
6184
+ //#region src/indexer/collectors/CollectFunctions/processors/processConsumedLogs.ts
5767
6185
  const buildGroupKey = (parameters) => {
5768
6186
  return `${parameters.chainId}-${parameters.maker.toLowerCase()}-${parameters.group.toLowerCase()}`;
5769
6187
  };
5770
- async function* collectConsumedEvents(parameters) {
6188
+ /** Parse raw MorphoV2 logs and produce normalized consumed events.
6189
+ * @param parameters - The parsed event logs and chain ID.
6190
+ * @param parameters.logs - Parsed event logs from MorphoV2 (Consume and Take events).
6191
+ * @param parameters.chainId - Chain ID for event attribution.
6192
+ * @returns Flat array of consumed events.
6193
+ */
6194
+ function processConsumedLogs(parameters) {
6195
+ const { logs, chainId } = parameters;
6196
+ const logger = getLogger();
6197
+ const consumedEvents = [];
6198
+ for (const rawLog of logs) {
6199
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
6200
+ logger.debug({
6201
+ chainId,
6202
+ msg: "Skipping log because it is missing required fields"
6203
+ });
6204
+ continue;
6205
+ }
6206
+ const eventName = rawLog.eventName;
6207
+ if (eventName === consumedEvent.name) {
6208
+ const consumeArgs = rawLog.args;
6209
+ if (consumeArgs?.user === void 0 || consumeArgs?.group === void 0 || consumeArgs?.amount === void 0) {
6210
+ logger.debug({
6211
+ chainId,
6212
+ msg: "Skipping Consume log because it is missing required args"
6213
+ });
6214
+ continue;
6215
+ }
6216
+ consumedEvents.push({
6217
+ kind: "consume",
6218
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`,
6219
+ chainId,
6220
+ maker: consumeArgs.user,
6221
+ group: consumeArgs.group,
6222
+ amount: consumeArgs.amount,
6223
+ blockNumber: Number(rawLog.blockNumber)
6224
+ });
6225
+ continue;
6226
+ }
6227
+ if (eventName === takeEvent.name) {
6228
+ const takeArgs = rawLog.args;
6229
+ if (takeArgs?.maker === void 0 || takeArgs?.group === void 0 || takeArgs?.consumed === void 0) {
6230
+ logger.debug({
6231
+ chainId,
6232
+ msg: "Skipping Take log because it is missing required args for consumed"
6233
+ });
6234
+ continue;
6235
+ }
6236
+ consumedEvents.push({
6237
+ kind: "take",
6238
+ id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`,
6239
+ chainId,
6240
+ maker: takeArgs.maker,
6241
+ group: takeArgs.group,
6242
+ consumed: takeArgs.consumed,
6243
+ blockNumber: Number(rawLog.blockNumber)
6244
+ });
6245
+ }
6246
+ }
6247
+ return consumedEvents;
6248
+ }
6249
+
6250
+ //#endregion
6251
+ //#region src/indexer/collectors/CollectFunctions/processors/processDebtTransfers.ts
6252
+ /**
6253
+ * Parse raw MorphoV2 logs and compute debt transfers.
6254
+ *
6255
+ * A debt is modeled as a negative position: borrowing = transfer FROM user TO zeroAddress,
6256
+ * repayment = FROM zeroAddress TO user. The contract field is the obligationId.
6257
+ *
6258
+ * Applies the 4-case Take matrix, Repay, and Liquidate attribution rules:
6259
+ *
6260
+ * **Take event** — Buyer = maker if offerIsBuy, taker otherwise. Seller = the other.
6261
+ * | buyerIsLender | sellerIsBorrower | Buyer transfer | Seller transfer |
6262
+ * |---------------|------------------|------------------------------|------------------------------|
6263
+ * | true | true | none | from: seller → to: 0x0 |
6264
+ * | true | false | none | none |
6265
+ * | false | true | from: 0x0 → to: buyer | from: seller → to: 0x0 |
6266
+ * | false | false | from: 0x0 → to: buyer | none |
6267
+ *
6268
+ * **Repay**: from: 0x0 → to: onBehalf
6269
+ * **Liquidate**: from: 0x0 → to: borrower (value = totalRepaid + badDebt)
6270
+ *
6271
+ * @param parameters - The parsed event logs and chain ID.
6272
+ * @param parameters.logs - Parsed event logs from MorphoV2 (Take, Repay, Liquidate events).
6273
+ * @param parameters.chainId - Chain ID for event attribution.
6274
+ * @returns Transfer events ready for DB insertion.
6275
+ */
6276
+ function processDebtTransfers(parameters) {
6277
+ const { logs, chainId } = parameters;
6278
+ const logger = getLogger();
6279
+ const transfers = [];
6280
+ for (const rawLog of logs) {
6281
+ if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
6282
+ logger.debug({
6283
+ chainId,
6284
+ msg: "Skipping debt log because it is missing required fields"
6285
+ });
6286
+ continue;
6287
+ }
6288
+ const eventName = rawLog.eventName;
6289
+ const baseId = `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${chainId}-${rawLog.transactionHash}`;
6290
+ if (eventName === takeEvent.name) {
6291
+ const args = rawLog.args;
6292
+ if (args?.id === void 0 || args?.maker === void 0 || args?.taker === void 0 || args?.offerIsBuy === void 0 || args?.obligationUnits === void 0 || args?.buyerIsLender === void 0 || args?.sellerIsBorrower === void 0) {
6293
+ logger.debug({
6294
+ chainId,
6295
+ msg: "Skipping Take log because it is missing required args for debt"
6296
+ });
6297
+ continue;
6298
+ }
6299
+ if (args.obligationUnits === 0n) continue;
6300
+ const buyer = args.offerIsBuy ? args.maker : args.taker;
6301
+ const seller = args.offerIsBuy ? args.taker : args.maker;
6302
+ const blockNumber = Number(rawLog.blockNumber);
6303
+ if (!args.buyerIsLender) transfers.push(from$8({
6304
+ id: `${baseId}-debt-buyer`,
6305
+ chainId,
6306
+ contract: args.id,
6307
+ from: zeroAddress,
6308
+ to: buyer,
6309
+ value: args.obligationUnits,
6310
+ type: Type.DEBT_OF,
6311
+ asset: zeroAddress,
6312
+ blockNumber
6313
+ }));
6314
+ if (args.sellerIsBorrower) transfers.push(from$8({
6315
+ id: `${baseId}-debt-seller`,
6316
+ chainId,
6317
+ contract: args.id,
6318
+ from: seller,
6319
+ to: zeroAddress,
6320
+ value: args.obligationUnits,
6321
+ type: Type.DEBT_OF,
6322
+ asset: zeroAddress,
6323
+ blockNumber
6324
+ }));
6325
+ continue;
6326
+ }
6327
+ if (eventName === repayEvent.name) {
6328
+ const args = rawLog.args;
6329
+ if (args?.id === void 0 || args?.obligationUnits === void 0 || args?.onBehalf === void 0) {
6330
+ logger.debug({
6331
+ chainId,
6332
+ msg: "Skipping Repay log because it is missing required args"
6333
+ });
6334
+ continue;
6335
+ }
6336
+ if (args.obligationUnits === 0n) continue;
6337
+ transfers.push(from$8({
6338
+ id: `${baseId}-debt-repay`,
6339
+ chainId,
6340
+ contract: args.id,
6341
+ from: zeroAddress,
6342
+ to: args.onBehalf,
6343
+ value: args.obligationUnits,
6344
+ type: Type.DEBT_OF,
6345
+ asset: zeroAddress,
6346
+ blockNumber: Number(rawLog.blockNumber)
6347
+ }));
6348
+ continue;
6349
+ }
6350
+ if (eventName === liquidateEvent.name) {
6351
+ const args = rawLog.args;
6352
+ if (args?.id === void 0 || args?.borrower === void 0 || args?.totalRepaid === void 0 || args?.badDebt === void 0) {
6353
+ logger.debug({
6354
+ chainId,
6355
+ msg: "Skipping Liquidate log because it is missing required args"
6356
+ });
6357
+ continue;
6358
+ }
6359
+ const totalReduction = args.totalRepaid + args.badDebt;
6360
+ if (totalReduction === 0n) continue;
6361
+ transfers.push(from$8({
6362
+ id: `${baseId}-debt-liquidate`,
6363
+ chainId,
6364
+ contract: args.id,
6365
+ from: zeroAddress,
6366
+ to: args.borrower,
6367
+ value: totalReduction,
6368
+ type: Type.DEBT_OF,
6369
+ asset: zeroAddress,
6370
+ blockNumber: Number(rawLog.blockNumber)
6371
+ }));
6372
+ }
6373
+ }
6374
+ return transfers;
6375
+ }
6376
+
6377
+ //#endregion
6378
+ //#region src/indexer/collectors/CollectFunctions/collectMorphoV2.ts
6379
+ async function* collectMorphoV2(parameters) {
5771
6380
  let { db, collector, client, lastBlockNumber: blockNumber, epoch, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
5772
6381
  const logger = getLogger();
5773
6382
  let startBlock = blockNumber;
@@ -5786,179 +6395,63 @@ async function* collectConsumedEvents(parameters) {
5786
6395
  });
5787
6396
  for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
5788
6397
  const parsedLogs = parseEventLogs({
5789
- abi: [consumedEvent, takeEvent],
6398
+ abi: [
6399
+ consumedEvent,
6400
+ takeEvent,
6401
+ repayEvent,
6402
+ liquidateEvent,
6403
+ supplyCollateralEvent,
6404
+ withdrawCollateralEvent
6405
+ ],
5790
6406
  logs,
5791
6407
  strict: false
5792
6408
  });
5793
- const normalizedLogs = [];
5794
- const groups$3 = /* @__PURE__ */ new Map();
5795
- const eventIds = /* @__PURE__ */ new Set();
5796
- const recordLog = (log) => {
5797
- normalizedLogs.push(log);
5798
- eventIds.add(log.id);
5799
- const groupKey = buildGroupKey({
5800
- chainId: log.chainId,
5801
- maker: log.maker,
5802
- group: log.group
5803
- });
5804
- if (!groups$3.has(groupKey)) groups$3.set(groupKey, {
5805
- chainId: log.chainId,
5806
- maker: log.maker.toLowerCase(),
5807
- group: log.group.toLowerCase()
5808
- });
5809
- };
5810
- for (const rawLog of parsedLogs) {
5811
- if (rawLog.blockNumber === null || rawLog.logIndex === null || rawLog.transactionHash === null) {
5812
- logger.debug({
5813
- collector,
5814
- chainId: client.chain.id,
5815
- msg: "Skipping log because it is missing required fields"
5816
- });
5817
- continue;
5818
- }
5819
- if (rawLog.eventName === consumedEvent.name) {
5820
- const consumeArgs = rawLog.args;
5821
- if (consumeArgs.user === void 0 || consumeArgs.group === void 0 || consumeArgs.amount === void 0) {
5822
- logger.debug({
5823
- collector,
5824
- chainId: client.chain.id,
5825
- msg: "Skipping Consume log because it is missing required args"
5826
- });
5827
- continue;
5828
- }
5829
- recordLog({
5830
- kind: "consume",
5831
- id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
5832
- chainId: client.chain.id,
5833
- maker: consumeArgs.user,
5834
- group: consumeArgs.group,
5835
- amount: consumeArgs.amount,
5836
- blockNumber: Number(rawLog.blockNumber)
5837
- });
5838
- continue;
5839
- }
5840
- if (rawLog.eventName === takeEvent.name) {
5841
- const takeArgs = rawLog.args;
5842
- if (takeArgs.maker === void 0 || takeArgs.group === void 0 || takeArgs.consumed === void 0) {
5843
- logger.debug({
5844
- collector,
5845
- chainId: client.chain.id,
5846
- msg: "Skipping Take log because it is missing required args"
5847
- });
5848
- continue;
5849
- }
5850
- recordLog({
5851
- kind: "take",
5852
- id: `${rawLog.blockNumber.toString()}-${rawLog.logIndex.toString()}-${client.chain.id}-${rawLog.transactionHash}`,
5853
- chainId: client.chain.id,
5854
- maker: takeArgs.maker,
5855
- group: takeArgs.group,
5856
- consumed: takeArgs.consumed,
5857
- blockNumber: Number(rawLog.blockNumber)
5858
- });
5859
- }
5860
- }
6409
+ const consumedEvents = processConsumedLogs({
6410
+ logs: parsedLogs,
6411
+ chainId: client.chain.id
6412
+ });
6413
+ const debtTransfers = processDebtTransfers({
6414
+ logs: parsedLogs,
6415
+ chainId: client.chain.id
6416
+ });
6417
+ const collateralTransfers = processCollateralTransfers({
6418
+ logs: parsedLogs,
6419
+ chainId: client.chain.id
6420
+ });
6421
+ const seizureEvents = processCollateralSeizures({
6422
+ logs: parsedLogs,
6423
+ chainId: client.chain.id
6424
+ });
5861
6425
  await db.transaction(async (dbTx) => {
5862
- const existingEventIds = /* @__PURE__ */ new Set();
5863
- if (eventIds.size > 0) {
5864
- const ids = Array.from(eventIds);
5865
- for (let index = 0; index < ids.length; index += 500) {
5866
- const slice = ids.slice(index, index + 500);
5867
- const { rows } = await dbTx.execute(sql`
5868
- SELECT event_id
5869
- FROM ${consumedEvents}
5870
- WHERE event_id IN (${sql.join(slice.map((id) => sql`${id}`), sql`,`)});
5871
- `);
5872
- for (const row of rows) existingEventIds.add(row.event_id);
5873
- }
5874
- }
5875
- const consumedByGroup = /* @__PURE__ */ new Map();
5876
- if (groups$3.size > 0) {
5877
- const groupList = Array.from(groups$3.values());
5878
- for (let index = 0; index < groupList.length; index += 500) {
5879
- const slice = groupList.slice(index, index + 500);
5880
- const { rows } = await dbTx.execute(sql`
5881
- WITH targets(chain_id, maker, "group") AS (
5882
- VALUES ${sql.join(slice.map((group) => sql`(${group.chainId}::bigint, ${group.maker.toLowerCase()}::varchar(42), ${group.group.toLowerCase()}::varchar(66))`), sql`,`)}
5883
- )
5884
- SELECT
5885
- targets.chain_id,
5886
- targets.maker,
5887
- targets."group",
5888
- COALESCE(g.consumed, 0)::numeric AS consumed
5889
- FROM targets
5890
- LEFT JOIN ${groups} g
5891
- ON g.chain_id = targets.chain_id
5892
- AND g.maker = targets.maker
5893
- AND g."group" = targets."group";
5894
- `);
5895
- for (const row of rows) {
5896
- const groupKey = buildGroupKey({
5897
- chainId: Number(row.chain_id),
5898
- maker: row.maker,
5899
- group: row.group
5900
- });
5901
- consumedByGroup.set(groupKey, BigInt(row.consumed ?? "0"));
5902
- }
5903
- }
5904
- }
5905
- const events = [];
5906
- for (const log of normalizedLogs) {
5907
- if (existingEventIds.has(log.id)) continue;
5908
- const groupKey = buildGroupKey({
5909
- chainId: log.chainId,
5910
- maker: log.maker,
5911
- group: log.group
5912
- });
5913
- const previousConsumed = consumedByGroup.get(groupKey) ?? 0n;
5914
- if (log.kind === "consume") {
5915
- events.push({
5916
- id: log.id,
5917
- chainId: log.chainId,
5918
- maker: log.maker,
5919
- group: log.group,
5920
- amount: log.amount,
5921
- blockNumber: log.blockNumber
5922
- });
5923
- consumedByGroup.set(groupKey, previousConsumed + log.amount);
5924
- continue;
5925
- }
5926
- const delta = log.consumed - previousConsumed;
5927
- if (delta <= 0n) {
5928
- logger.debug({
5929
- collector,
5930
- chainId: client.chain.id,
5931
- msg: "Skipping Take log because consumed did not increase",
5932
- previous_consumed: previousConsumed.toString(),
5933
- consumed: log.consumed.toString()
5934
- });
5935
- continue;
5936
- }
5937
- events.push({
5938
- id: log.id,
5939
- chainId: log.chainId,
5940
- maker: log.maker,
5941
- group: log.group,
5942
- amount: delta,
5943
- blockNumber: log.blockNumber
5944
- });
5945
- consumedByGroup.set(groupKey, log.consumed);
5946
- }
5947
- try {
5948
- await dbTx.consumed.create(events);
5949
- if (events.length > 0) logger.info({
5950
- msg: `Events indexed`,
5951
- collector,
5952
- count: events.length,
5953
- chain_id: client.chain.id,
5954
- block_range: [startBlock, lastStreamBlockNumber]
5955
- });
5956
- } catch (err) {
5957
- logger.error({
5958
- err,
5959
- msg: "Failed to process consumed events"
5960
- });
6426
+ const [resolvedSeizureTransfers, resolvedConsumedEvents] = await Promise.all([resolveSeizureTransfers({
6427
+ dbTx,
6428
+ seizureEvents
6429
+ }), resolveConsumedEvents({
6430
+ dbTx,
6431
+ consumedEvents,
6432
+ chainId: client.chain.id,
6433
+ collector,
6434
+ logger
6435
+ })]);
6436
+ if (debtTransfers.length > 0) {
6437
+ await dbTx.positions.upsert(transfersToPositions(debtTransfers));
6438
+ await dbTx.transfers.create(debtTransfers);
5961
6439
  }
6440
+ const allCollateralTransfers = [...resolvedSeizureTransfers, ...collateralTransfers];
6441
+ if (allCollateralTransfers.length > 0) {
6442
+ await dbTx.positions.upsert(transfersToPositions(allCollateralTransfers));
6443
+ await dbTx.transfers.create(allCollateralTransfers);
6444
+ }
6445
+ await dbTx.consumed.create(resolvedConsumedEvents);
6446
+ if (resolvedConsumedEvents.length > 0) logger.info({
6447
+ msg: "Events indexed",
6448
+ collector,
6449
+ consumed_count: resolvedConsumedEvents.length,
6450
+ debt_transfer_count: debtTransfers.length,
6451
+ collateral_transfer_count: allCollateralTransfers.length,
6452
+ chain_id: client.chain.id,
6453
+ block_range: [startBlock, lastStreamBlockNumber]
6454
+ });
5962
6455
  blockNumber = lastStreamBlockNumber;
5963
6456
  try {
5964
6457
  await dbTx.blocks.advanceCollector({
@@ -5974,15 +6467,27 @@ async function* collectConsumedEvents(parameters) {
5974
6467
  chainId: client.chain.id
5975
6468
  });
5976
6469
  blockNumber = ancestor.blockNumber;
5977
- const deleted = await dbTx.consumed.delete({
6470
+ const deletedConsumed = await dbTx.consumed.delete({
5978
6471
  chainId: client.chain.id,
5979
6472
  blockNumberGte: blockNumber + 1
5980
6473
  });
6474
+ const deletedDebtTransfers = await dbTx.transfers.delete({
6475
+ chainId: client.chain.id,
6476
+ blockNumberGte: blockNumber + 1,
6477
+ positionTypeId: positionTypeId(Type.DEBT_OF)
6478
+ });
6479
+ const deletedCollateralTransfers = await dbTx.transfers.delete({
6480
+ chainId: client.chain.id,
6481
+ blockNumberGte: blockNumber + 1,
6482
+ positionTypeId: positionTypeId(Type.COLLATERAL_OF)
6483
+ });
5981
6484
  logger.info({
5982
6485
  collector,
5983
6486
  chain_id: client.chain.id,
5984
- msg: `Reorg detected, events deleted`,
5985
- count: deleted,
6487
+ msg: "Reorg detected, events deleted",
6488
+ consumed_deleted: deletedConsumed,
6489
+ debt_transfers_deleted: deletedDebtTransfers,
6490
+ collateral_transfers_deleted: deletedCollateralTransfers,
5986
6491
  block_number: blockNumber
5987
6492
  });
5988
6493
  await dbTx.blocks.advanceCollector({
@@ -5993,7 +6498,7 @@ async function* collectConsumedEvents(parameters) {
5993
6498
  });
5994
6499
  reorgDetected = true;
5995
6500
  } catch (err) {
5996
- const msg = "Failed to delete consumed events when handling reorg.";
6501
+ const msg = "Failed to delete events when handling reorg.";
5997
6502
  logger.error({
5998
6503
  collector,
5999
6504
  chainId: client.chain.id,
@@ -6008,10 +6513,210 @@ async function* collectConsumedEvents(parameters) {
6008
6513
  yield blockNumber;
6009
6514
  startBlock = blockNumber;
6010
6515
  }
6516
+ if (!reorgDetected) await db.transaction(async (dbTx) => {
6517
+ const collectorState = await dbTx.blocks.getCollector({
6518
+ collectorName: collector,
6519
+ chainId: client.chain.id
6520
+ });
6521
+ const deletedConsumed = await dbTx.consumed.delete({
6522
+ chainId: client.chain.id,
6523
+ blockNumberGte: collectorState.blockNumber + 1
6524
+ });
6525
+ const deletedDebtTransfers = await dbTx.transfers.delete({
6526
+ chainId: client.chain.id,
6527
+ blockNumberGte: collectorState.blockNumber + 1,
6528
+ positionTypeId: positionTypeId(Type.DEBT_OF)
6529
+ });
6530
+ const deletedCollateralTransfers = await dbTx.transfers.delete({
6531
+ chainId: client.chain.id,
6532
+ blockNumberGte: collectorState.blockNumber + 1,
6533
+ positionTypeId: positionTypeId(Type.COLLATERAL_OF)
6534
+ });
6535
+ if (deletedConsumed > 0 || deletedDebtTransfers > 0 || deletedCollateralTransfers > 0) logger.info({
6536
+ collector,
6537
+ chain_id: client.chain.id,
6538
+ msg: "Reorg detected, events deleted",
6539
+ consumed_deleted: deletedConsumed,
6540
+ debt_transfers_deleted: deletedDebtTransfers,
6541
+ collateral_transfers_deleted: deletedCollateralTransfers,
6542
+ block_number: collectorState.blockNumber
6543
+ });
6544
+ });
6545
+ }
6546
+ async function resolveSeizureTransfers(parameters) {
6547
+ const { dbTx, seizureEvents } = parameters;
6548
+ if (seizureEvents.length === 0) return [];
6549
+ const uniqueObligationIds = [...new Set(seizureEvents.map((event) => event.obligationId.toLowerCase()))];
6550
+ const rows = await dbTx.select({
6551
+ obligationId: obligationIdKeys.obligationId,
6552
+ collateralIndex: obligationCollateralsV2.collateralIndex,
6553
+ asset: obligationCollateralsV2.asset
6554
+ }).from(obligationIdKeys).innerJoin(obligationCollateralsV2, eq(obligationIdKeys.obligationKey, obligationCollateralsV2.obligationKey)).where(inArray(obligationIdKeys.obligationId, uniqueObligationIds));
6555
+ const resolutionMap = /* @__PURE__ */ new Map();
6556
+ for (const row of rows) resolutionMap.set(`${row.obligationId.toLowerCase()}-${row.collateralIndex}`, row.asset);
6557
+ const resolvedSeizureTransfers = [];
6558
+ for (const seizure of seizureEvents) {
6559
+ const key = `${seizure.obligationId.toLowerCase()}-${seizure.collateralIndex}`;
6560
+ const asset = resolutionMap.get(key);
6561
+ if (asset === void 0) throw new Error(`Unresolvable Liquidate seizure: obligationId=${seizure.obligationId}, collateralIndex=${seizure.collateralIndex}, chainId=${seizure.chainId}, blockNumber=${seizure.blockNumber}`);
6562
+ resolvedSeizureTransfers.push(from$8({
6563
+ id: seizure.id,
6564
+ chainId: seizure.chainId,
6565
+ contract: seizure.obligationId,
6566
+ from: seizure.user,
6567
+ to: zeroAddress,
6568
+ value: seizure.amount,
6569
+ type: Type.COLLATERAL_OF,
6570
+ asset,
6571
+ blockNumber: seizure.blockNumber
6572
+ }));
6573
+ }
6574
+ return resolvedSeizureTransfers;
6575
+ }
6576
+ async function resolveConsumedEvents(parameters) {
6577
+ const { dbTx, consumedEvents, chainId, collector, logger } = parameters;
6578
+ if (consumedEvents.length === 0) return [];
6579
+ const [existingConsumedIds, consumedByGroup] = await Promise.all([getExistingConsumedIds({
6580
+ dbTx,
6581
+ consumedEvents
6582
+ }), getConsumedByGroup({
6583
+ dbTx,
6584
+ consumedEvents
6585
+ })]);
6586
+ const resolvedEvents = [];
6587
+ for (const log of consumedEvents) {
6588
+ if (existingConsumedIds.has(log.id)) continue;
6589
+ const groupKey = buildGroupKey({
6590
+ chainId: log.chainId,
6591
+ maker: log.maker,
6592
+ group: log.group
6593
+ });
6594
+ const previousConsumed = consumedByGroup.get(groupKey) ?? 0n;
6595
+ if (log.kind === "consume") {
6596
+ resolvedEvents.push({
6597
+ id: log.id,
6598
+ chainId: log.chainId,
6599
+ maker: log.maker,
6600
+ group: log.group,
6601
+ amount: log.amount,
6602
+ blockNumber: log.blockNumber
6603
+ });
6604
+ consumedByGroup.set(groupKey, previousConsumed + log.amount);
6605
+ continue;
6606
+ }
6607
+ const delta = log.consumed - previousConsumed;
6608
+ if (delta <= 0n) {
6609
+ logger.debug({
6610
+ collector,
6611
+ chainId,
6612
+ msg: "Skipping Take log because consumed did not increase",
6613
+ previous_consumed: previousConsumed.toString(),
6614
+ consumed: log.consumed.toString()
6615
+ });
6616
+ continue;
6617
+ }
6618
+ resolvedEvents.push({
6619
+ id: log.id,
6620
+ chainId: log.chainId,
6621
+ maker: log.maker,
6622
+ group: log.group,
6623
+ amount: delta,
6624
+ blockNumber: log.blockNumber
6625
+ });
6626
+ consumedByGroup.set(groupKey, log.consumed);
6627
+ }
6628
+ return resolvedEvents;
6629
+ }
6630
+ async function getExistingConsumedIds(parameters) {
6631
+ const { dbTx, consumedEvents: consumedEvents$1 } = parameters;
6632
+ const existingConsumedIds = /* @__PURE__ */ new Set();
6633
+ const ids = Array.from(new Set(consumedEvents$1.map((event) => event.id)));
6634
+ for (let index = 0; index < ids.length; index += 500) {
6635
+ const slice = ids.slice(index, index + 500);
6636
+ const { rows } = await dbTx.execute(sql`
6637
+ SELECT event_id
6638
+ FROM ${consumedEvents}
6639
+ WHERE event_id IN (${sql.join(slice.map((id) => sql`${id}`), sql`,`)});
6640
+ `);
6641
+ for (const row of rows) existingConsumedIds.add(row.event_id);
6642
+ }
6643
+ return existingConsumedIds;
6644
+ }
6645
+ async function getConsumedByGroup(parameters) {
6646
+ const { dbTx, consumedEvents } = parameters;
6647
+ const groups$3 = /* @__PURE__ */ new Map();
6648
+ for (const event of consumedEvents) {
6649
+ const key = buildGroupKey({
6650
+ chainId: event.chainId,
6651
+ maker: event.maker,
6652
+ group: event.group
6653
+ });
6654
+ if (!groups$3.has(key)) groups$3.set(key, {
6655
+ chainId: event.chainId,
6656
+ maker: event.maker,
6657
+ group: event.group
6658
+ });
6659
+ }
6660
+ const consumedByGroup = /* @__PURE__ */ new Map();
6661
+ const groupList = Array.from(groups$3.values());
6662
+ for (let index = 0; index < groupList.length; index += 500) {
6663
+ const slice = groupList.slice(index, index + 500);
6664
+ const { rows } = await dbTx.execute(sql`
6665
+ WITH targets(chain_id, maker, "group") AS (
6666
+ VALUES ${sql.join(slice.map((group) => sql`(${group.chainId}::bigint, ${group.maker.toLowerCase()}::varchar(42), ${group.group.toLowerCase()}::varchar(66))`), sql`,`)}
6667
+ )
6668
+ SELECT
6669
+ targets.chain_id,
6670
+ targets.maker,
6671
+ targets."group",
6672
+ COALESCE(g.consumed, 0)::numeric AS consumed
6673
+ FROM targets
6674
+ LEFT JOIN ${groups} g
6675
+ ON g.chain_id = targets.chain_id
6676
+ AND g.maker = targets.maker
6677
+ AND g."group" = targets."group";
6678
+ `);
6679
+ for (const row of rows) {
6680
+ const groupKey = buildGroupKey({
6681
+ chainId: Number(row.chain_id),
6682
+ maker: row.maker,
6683
+ group: row.group
6684
+ });
6685
+ consumedByGroup.set(groupKey, BigInt(row.consumed ?? "0"));
6686
+ }
6687
+ }
6688
+ return consumedByGroup;
6689
+ }
6690
+ function transfersToPositions(transfers) {
6691
+ if (transfers.length === 0) return [];
6692
+ const transferType = transfers[0].type;
6693
+ if (transfers.some((transfer) => transfer.type !== transferType)) throw new Error("Cannot map transfers with mixed position types to positions.");
6694
+ const positionKeys = /* @__PURE__ */ new Map();
6695
+ for (const transfer of transfers) for (const user of [transfer.from, transfer.to]) {
6696
+ const key = `${transfer.chainId}-${transfer.contract}-${user}-${transfer.asset}`.toLowerCase();
6697
+ const existing = positionKeys.get(key);
6698
+ if (!existing || transfer.blockNumber < existing.blockNumber) positionKeys.set(key, {
6699
+ chainId: transfer.chainId,
6700
+ contract: transfer.contract,
6701
+ user,
6702
+ asset: transfer.asset,
6703
+ blockNumber: transfer.blockNumber
6704
+ });
6705
+ }
6706
+ return Array.from(positionKeys.values()).map((position) => from$10({
6707
+ chainId: position.chainId,
6708
+ contract: position.contract,
6709
+ user: position.user,
6710
+ type: transferType,
6711
+ balance: 0n,
6712
+ asset: position.asset,
6713
+ blockNumber: position.blockNumber
6714
+ }));
6011
6715
  }
6012
6716
 
6013
6717
  //#endregion
6014
6718
  //#region src/indexer/collectors/CollectFunctions/collectOffers.ts
6719
+ const ERC20_TYPE_ID = Object.values(Type).indexOf(Type.ERC20) + 1;
6015
6720
  async function* collectOffersV2(parameters) {
6016
6721
  let { db, collector, client, lastBlockNumber: blockNumber, gatekeeper, options: { maxBatchSize = 1e3, blockWindow } = {} } = parameters;
6017
6722
  const logger = getLogger();
@@ -6025,9 +6730,10 @@ async function* collectOffersV2(parameters) {
6025
6730
  });
6026
6731
  throw new Error(msg);
6027
6732
  }
6733
+ const morphoV2 = client.chain.custom.morpho.address;
6028
6734
  const signatureDomain = {
6029
6735
  chainId: client.chain.id,
6030
- verifyingContract: client.chain.custom.morpho.address
6736
+ verifyingContract: morphoV2
6031
6737
  };
6032
6738
  const { blockNumber: latestBlockNumberChain } = await db.blocks.getChain(client.chain.id);
6033
6739
  const stream = streamLogs({
@@ -6090,10 +6796,14 @@ async function* collectOffersV2(parameters) {
6090
6796
  await db.transaction(async (dbTx) => {
6091
6797
  const { epoch, blockNumber: latestBlockNumber } = await dbTx.blocks.getChain(client.chain.id);
6092
6798
  const treesToInsert = [];
6799
+ const pathsToInsert = [];
6093
6800
  let totalValidOffers = 0;
6094
6801
  const offersWithBlock = [];
6095
6802
  for (const { tree, signature, blockNumber: treeBlockNumber } of decodedTrees) try {
6096
- const allowedResults = await gatekeeper.isAllowed(tree.offers);
6803
+ const allowedResults = await gatekeeper.isAllowed({
6804
+ offers: tree.offers,
6805
+ chainId: client.chain.id
6806
+ });
6097
6807
  const hasBlockWindowViolation = treeBlockNumber > latestBlockNumber;
6098
6808
  if (!(allowedResults.issues.length === 0 && allowedResults.valid.length === tree.offers.length) || hasBlockWindowViolation) {
6099
6809
  if (allowedResults.issues.length > 0) {
@@ -6112,14 +6822,35 @@ async function* collectOffersV2(parameters) {
6112
6822
  continue;
6113
6823
  }
6114
6824
  treesToInsert.push({
6115
- tree,
6825
+ root: tree.root,
6116
6826
  signature
6117
6827
  });
6118
6828
  totalValidOffers += tree.offers.length;
6119
- offersWithBlock.push(...tree.offers.map((offer) => ({
6120
- offer,
6121
- blockNumber: treeBlockNumber
6122
- })));
6829
+ const obligationIdsByOfferHash = /* @__PURE__ */ new Map();
6830
+ for (const offer of tree.offers) {
6831
+ const offerHash = hash(offer).toLowerCase();
6832
+ const obligationId$3 = obligationId(offer, {
6833
+ chainId: client.chain.id,
6834
+ morphoV2
6835
+ }).toLowerCase();
6836
+ offersWithBlock.push({
6837
+ offer,
6838
+ blockNumber: treeBlockNumber,
6839
+ obligationId: obligationId$3
6840
+ });
6841
+ obligationIdsByOfferHash.set(offerHash, obligationId$3);
6842
+ }
6843
+ for (const proof of proofs(tree)) {
6844
+ const offerHash = hash(proof.offer).toLowerCase();
6845
+ const obligationId = obligationIdsByOfferHash.get(offerHash);
6846
+ if (obligationId === void 0) continue;
6847
+ pathsToInsert.push({
6848
+ offerHash,
6849
+ obligationId,
6850
+ treeRoot: tree.root,
6851
+ proof: proof.path
6852
+ });
6853
+ }
6123
6854
  } catch (err) {
6124
6855
  const error = err instanceof Error ? err : new Error(String(err));
6125
6856
  logger.error({
@@ -6129,19 +6860,23 @@ async function* collectOffersV2(parameters) {
6129
6860
  });
6130
6861
  throw new Error("Gatekeeper validation failed", { cause: error });
6131
6862
  }
6132
- const dependencies = buildOfferDependencies$1(offersWithBlock);
6863
+ const dependencies = buildOfferDependencies$1({
6864
+ offers: offersWithBlock,
6865
+ chainId: client.chain.id,
6866
+ morphoV2
6867
+ });
6133
6868
  await dbTx.oracles.upsert(dependencies.oracles);
6134
6869
  await dbTx.obligations.create(dependencies.obligations);
6135
6870
  await dbTx.groups.create(dependencies.groups);
6136
- const insertedHashes = await dbTx.offers.create(dependencies.offerBatches);
6137
- if (treesToInsert.length > 0) await dbTx.trees.create(treesToInsert);
6138
- const insertedOffers = filterInsertedOffers({
6139
- offers: offersWithBlock,
6140
- hashes: insertedHashes
6141
- });
6871
+ const insertedReferences = await dbTx.offers.create(dependencies.offerBatches);
6872
+ if (treesToInsert.length > 0) await dbTx.trees.upsert(treesToInsert);
6873
+ if (pathsToInsert.length > 0) await dbTx.trees.upsertPaths(pathsToInsert);
6142
6874
  const { callbacks, positions, lots } = decodeCallbacks({
6143
- chainId: client.chain.id,
6144
- offers: insertedOffers
6875
+ offers: filterInsertedOffers({
6876
+ offers: offersWithBlock,
6877
+ references: insertedReferences
6878
+ }),
6879
+ chainId: client.chain.id
6145
6880
  });
6146
6881
  if (positions.length > 0) await dbTx.positions.upsert(positions);
6147
6882
  if (callbacks.length > 0) await dbTx.callbacks.upsert(callbacks);
@@ -6204,7 +6939,7 @@ async function* collectOffersV2(parameters) {
6204
6939
  }
6205
6940
  }
6206
6941
  function decodeCallbacks(parameters) {
6207
- const { offers } = parameters;
6942
+ const { offers, chainId } = parameters;
6208
6943
  if (offers.length === 0) return {
6209
6944
  callbacks: [],
6210
6945
  positions: [],
@@ -6213,12 +6948,12 @@ function decodeCallbacks(parameters) {
6213
6948
  const callbacks = [];
6214
6949
  const positions = [];
6215
6950
  const lots = [];
6216
- for (const { offer, blockNumber: offerBlockNumber } of offers) {
6951
+ for (const { offer, blockNumber: offerBlockNumber, obligationId } of offers) {
6217
6952
  if (!offer.buy) continue;
6218
6953
  if (!isEmptyCallback(offer)) continue;
6219
6954
  const loanToken = offer.loanToken.toLowerCase();
6220
6955
  positions.push(from$10({
6221
- chainId: offer.chainId,
6956
+ chainId,
6222
6957
  contract: loanToken,
6223
6958
  user: offer.maker,
6224
6959
  type: Type.ERC20,
@@ -6226,20 +6961,23 @@ function decodeCallbacks(parameters) {
6226
6961
  blockNumber: offerBlockNumber
6227
6962
  }));
6228
6963
  lots.push({
6229
- positionChainId: offer.chainId,
6964
+ positionChainId: chainId,
6230
6965
  positionContract: loanToken,
6231
6966
  positionUser: offer.maker,
6967
+ positionTypeId: ERC20_TYPE_ID,
6232
6968
  group: offer.group,
6233
- obligationId: obligationId(offer),
6969
+ obligationId,
6234
6970
  size: offer.assets
6235
6971
  });
6236
6972
  callbacks.push({
6237
6973
  offerHash: hash(offer),
6974
+ obligationId,
6238
6975
  callbacks: [{
6239
- chainId: offer.chainId,
6976
+ chainId,
6240
6977
  contract: loanToken,
6241
6978
  user: offer.maker,
6242
- amount: offer.assets
6979
+ amount: offer.assets,
6980
+ positionTypeId: ERC20_TYPE_ID
6243
6981
  }]
6244
6982
  });
6245
6983
  }
@@ -6249,34 +6987,42 @@ function decodeCallbacks(parameters) {
6249
6987
  lots
6250
6988
  };
6251
6989
  }
6252
- function buildOfferDependencies$1(offers) {
6990
+ function buildOfferDependencies$1(parameters) {
6991
+ const { offers, chainId, morphoV2 } = parameters;
6253
6992
  const obligationsById = /* @__PURE__ */ new Map();
6254
6993
  const oraclesByKey = /* @__PURE__ */ new Map();
6255
6994
  const groupsByKey = /* @__PURE__ */ new Map();
6256
6995
  const offersByBlock = /* @__PURE__ */ new Map();
6257
- for (const { offer, blockNumber } of offers) {
6996
+ for (const { offer, blockNumber, obligationId } of offers) {
6258
6997
  const list = offersByBlock.get(blockNumber) ?? [];
6259
- list.push(offer);
6998
+ list.push({
6999
+ offer,
7000
+ obligationId,
7001
+ chainId
7002
+ });
6260
7003
  offersByBlock.set(blockNumber, list);
6261
- const obligationId$2 = obligationId(offer);
6262
- if (!obligationsById.get(obligationId$2)) obligationsById.set(obligationId$2, from$13({
6263
- chainId: offer.chainId,
6264
- loanToken: offer.loanToken,
6265
- maturity: offer.maturity,
6266
- collaterals: offer.collaterals
6267
- }));
7004
+ if (!obligationsById.get(obligationId)) obligationsById.set(obligationId, {
7005
+ obligationId,
7006
+ chainId,
7007
+ morphoV2,
7008
+ obligation: from$13({
7009
+ loanToken: offer.loanToken,
7010
+ maturity: offer.maturity,
7011
+ collaterals: offer.collaterals
7012
+ })
7013
+ });
6268
7014
  for (const collateral of offer.collaterals) {
6269
- const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
7015
+ const oracleKey = `${chainId}-${collateral.oracle}`.toLowerCase();
6270
7016
  if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
6271
- chainId: offer.chainId,
7017
+ chainId,
6272
7018
  address: collateral.oracle,
6273
7019
  price: null,
6274
7020
  blockNumber
6275
7021
  }));
6276
7022
  }
6277
- const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
7023
+ const groupKey = `${chainId}-${offer.maker}-${offer.group}`.toLowerCase();
6278
7024
  if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
6279
- chainId: offer.chainId,
7025
+ chainId,
6280
7026
  maker: offer.maker,
6281
7027
  group: offer.group,
6282
7028
  blockNumber
@@ -6293,15 +7039,22 @@ function buildOfferDependencies$1(offers) {
6293
7039
  };
6294
7040
  }
6295
7041
  function filterInsertedOffers(parameters) {
6296
- if (parameters.hashes.length === 0) return [];
6297
- const inserted = new Set(parameters.hashes.map((hash) => hash.toLowerCase()));
7042
+ if (parameters.references.length === 0) return [];
7043
+ const keyOf = (input) => `${input.hash.toLowerCase()}:${input.obligationId.toLowerCase()}`;
7044
+ const inserted = new Set(parameters.references.map((offer) => keyOf({
7045
+ hash: offer.hash,
7046
+ obligationId: offer.obligationId
7047
+ })));
6298
7048
  const seen = /* @__PURE__ */ new Set();
6299
7049
  const filtered = [];
6300
7050
  for (const entry of parameters.offers) {
6301
- const hash$2 = hash(entry.offer).toLowerCase();
6302
- if (!inserted.has(hash$2)) continue;
6303
- if (seen.has(hash$2)) continue;
6304
- seen.add(hash$2);
7051
+ const key = keyOf({
7052
+ hash: hash(entry.offer),
7053
+ obligationId: entry.obligationId
7054
+ });
7055
+ if (!inserted.has(key)) continue;
7056
+ if (seen.has(key)) continue;
7057
+ seen.add(key);
6305
7058
  filtered.push(entry);
6306
7059
  }
6307
7060
  return filtered;
@@ -6416,7 +7169,7 @@ async function snapshotVaultPositions(parameters) {
6416
7169
  convertToAssets: (shares) => {
6417
7170
  const contract = contracts.get(position.contract.toLowerCase());
6418
7171
  if (!contract) return;
6419
- if (contract.decimalsOffset === void 0 || contract.totalAssets === void 0 || contract.totalSupply === void 0) return;
7172
+ if (contract.decimalsOffset === void 0 || contract.totalAssets === void 0 || contract.totalSupply === void 0 || contract.asset === void 0) return;
6420
7173
  try {
6421
7174
  position.balance = convertToAssets({
6422
7175
  shares,
@@ -6571,12 +7324,15 @@ async function* collectPositions(parameters) {
6571
7324
  from: log.args.from,
6572
7325
  to: log.args.to,
6573
7326
  value: log.args.value,
7327
+ type: Type.ERC20,
7328
+ asset: log.address,
6574
7329
  blockNumber: Number(log.blockNumber)
6575
7330
  }));
6576
7331
  }
6577
7332
  const { positions } = await db.positions.get({
6578
7333
  chainId: client.chain.id,
6579
- filled: false
7334
+ filled: false,
7335
+ type: Type.ERC20
6580
7336
  });
6581
7337
  const newPositions = [];
6582
7338
  try {
@@ -6699,7 +7455,8 @@ async function* collectPositions(parameters) {
6699
7455
  blockNumber = ancestor.blockNumber;
6700
7456
  const emptied = await dbTx.positions.setEmptyAfter({
6701
7457
  chainId: client.chain.id,
6702
- blockNumber: blockNumber + 1
7458
+ blockNumber: blockNumber + 1,
7459
+ type: Type.ERC20
6703
7460
  });
6704
7461
  logger.info({
6705
7462
  msg: "Reorg detected, positions set to empty",
@@ -6926,10 +7683,10 @@ function createBuilder(parameters) {
6926
7683
  }
6927
7684
  }));
6928
7685
  },
6929
- buildConsumedEventsCollector: ({ options: { maxBatchSize = 1e3 } = {} } = {}) => {
6930
- return createCollector("consumed_events", (p) => collectConsumedEvents({
7686
+ buildMorphoV2Collector: ({ options: { maxBatchSize = 1e3 } = {} } = {}) => {
7687
+ return createCollector("morpho_v2", (p) => collectMorphoV2({
6931
7688
  ...p,
6932
- collector: "consumed_events",
7689
+ collector: "morpho_v2",
6933
7690
  options: {
6934
7691
  maxBatchSize,
6935
7692
  blockWindow
@@ -6978,7 +7735,7 @@ const from$2 = (parameters) => {
6978
7735
  });
6979
7736
  return {
6980
7737
  offersCollector: collectorBuilder.buildOffersCollector({ options: { maxBatchSize } }),
6981
- consumedEventsCollector: collectorBuilder.buildConsumedEventsCollector({ options: { maxBatchSize } }),
7738
+ morphoV2Collector: collectorBuilder.buildMorphoV2Collector({ options: { maxBatchSize } }),
6982
7739
  pricesCollector: collectorBuilder.buildPricesCollector({ options: {
6983
7740
  maxBatchSize,
6984
7741
  retryAttempts,
@@ -6996,7 +7753,7 @@ const from$2 = (parameters) => {
6996
7753
  //#region src/indexer/Indexer.ts
6997
7754
  function from$1(config) {
6998
7755
  const { client, gatekeeper, db, interval = 1e4, maxBatchSize = 1e3, maxBlockNumber, blockWindow, retryAttempts, retryDelayMs } = config;
6999
- const { offersCollector, consumedEventsCollector, positionsCollector, pricesCollector } = from$2({
7756
+ const { offersCollector, morphoV2Collector, positionsCollector, pricesCollector } = from$2({
7000
7757
  client,
7001
7758
  db,
7002
7759
  gatekeeper,
@@ -7011,7 +7768,7 @@ function from$1(config) {
7011
7768
  client,
7012
7769
  collectors: [
7013
7770
  offersCollector,
7014
- consumedEventsCollector,
7771
+ morphoV2Collector,
7015
7772
  positionsCollector,
7016
7773
  pricesCollector
7017
7774
  ]
@@ -7282,7 +8039,7 @@ const reconcile = async (parameters) => {
7282
8039
  //#region src/indexer/collectors/Collector.ts
7283
8040
  const names = [
7284
8041
  "offers",
7285
- "consumed_events",
8042
+ "morpho_v2",
7286
8043
  "positions",
7287
8044
  "prices"
7288
8045
  ];
@@ -7681,18 +8438,17 @@ async function _getOffers(db, params) {
7681
8438
  const { obligationId, side, now, priceSortDirection, cursor, limit } = params;
7682
8439
  const raw = await db.execute(sql`
7683
8440
  WITH collats AS MATERIALIZED (
7684
- SELECT oc.obligation_id,
8441
+ SELECT oia.obligation_id,
7685
8442
  COALESCE(jsonb_agg(jsonb_build_object(
7686
8443
  'asset', oc.asset,
7687
- 'oracle', oracle.address,
8444
+ 'oracle', oc.oracle_address,
7688
8445
  'lltv', oc.lltv
7689
8446
  ) ORDER BY oc.asset), '[]'::jsonb) AS collaterals
7690
- FROM ${obligationCollateralsV2} oc
7691
- JOIN ${oracles} oracle
7692
- ON oracle.chain_id = oc.oracle_chain_id
7693
- AND oracle.address = oc.oracle_address
7694
- WHERE oc.obligation_id = ${obligationId}
7695
- GROUP BY oc.obligation_id
8447
+ FROM ${obligationIdKeys} oia
8448
+ JOIN ${obligationCollateralsV2} oc
8449
+ ON oc.obligation_key = oia.obligation_key
8450
+ WHERE oia.obligation_id = ${obligationId}
8451
+ GROUP BY oia.obligation_id
7696
8452
  ),
7697
8453
  winners AS (
7698
8454
  SELECT DISTINCT ON (o.group_chain_id, o.group_maker, o."group_group")
@@ -7700,6 +8456,7 @@ async function _getOffers(db, params) {
7700
8456
  FROM ${offers} o
7701
8457
  LEFT JOIN ${validations} v
7702
8458
  ON v.offer_hash = o.hash
8459
+ AND v.obligation_id = o.obligation_id
7703
8460
  LEFT JOIN ${status} s
7704
8461
  ON s.id = v.status_id
7705
8462
  WHERE o.obligation_id = ${obligationId}
@@ -7726,8 +8483,10 @@ async function _getOffers(db, params) {
7726
8483
  ON g.chain_id = w.group_chain_id
7727
8484
  AND g.maker = w.group_maker
7728
8485
  AND g."group" = w."group_group"
8486
+ JOIN ${obligationIdKeys} oia
8487
+ ON oia.obligation_id = w.obligation_id
7729
8488
  JOIN ${obligations} obl
7730
- ON obl.obligation_id = w.obligation_id
8489
+ ON obl.obligation_key = oia.obligation_key
7731
8490
  ),
7732
8491
  paged AS (
7733
8492
  SELECT e.*
@@ -7826,7 +8585,9 @@ async function _getOffers(db, params) {
7826
8585
  END
7827
8586
  )) AS lot_balance
7828
8587
  FROM paged p
7829
- LEFT JOIN ${offersCallbacks} oc ON oc.offer_hash = p.hash
8588
+ LEFT JOIN ${offersCallbacks} oc
8589
+ ON oc.offer_hash = p.hash
8590
+ AND oc.obligation_id = p.obligation_id
7830
8591
  LEFT JOIN ${callbacks} c ON c.id = oc.callback_id
7831
8592
  LEFT JOIN ${lots} l
7832
8593
  ON l.chain_id = c.position_chain_id
@@ -7911,11 +8672,13 @@ async function _getOffers(db, params) {
7911
8672
  AND NOT EXISTS (
7912
8673
  SELECT 1 FROM ${offersCallbacks} oc2
7913
8674
  WHERE oc2.offer_hash = p.hash
8675
+ AND oc2.obligation_id = p.obligation_id
7914
8676
  )
7915
8677
  )
7916
8678
  -- Final SELECT with inline takeable computation
7917
8679
  SELECT
7918
8680
  oc.hash,
8681
+ oc.obligation_id,
7919
8682
  oc.group_maker,
7920
8683
  oc.assets,
7921
8684
  oc.obligation_units,
@@ -7964,6 +8727,7 @@ async function _getOffers(db, params) {
7964
8727
  const receiverIfMakerIsSeller = (row.receiver_if_maker_is_seller ?? row.group_maker).toLowerCase();
7965
8728
  return {
7966
8729
  hash: row.hash,
8730
+ obligationId: row.obligation_id,
7967
8731
  maker: row.group_maker,
7968
8732
  assets: BigInt(row.assets),
7969
8733
  obligationUnits: BigInt(row.obligation_units ?? 0),
@@ -8092,18 +8856,21 @@ function create$14(db) {
8092
8856
  idCache.set(preimage, id);
8093
8857
  return id;
8094
8858
  };
8095
- for (const { offerHash, callbacks } of inputs) {
8859
+ for (const { offerHash, obligationId, callbacks } of inputs) {
8096
8860
  const normalizedOfferHash = offerHash.toLowerCase();
8861
+ const normalizedObligationId = obligationId.toLowerCase();
8097
8862
  for (const callback of callbacks) {
8098
8863
  const normalized = {
8099
8864
  chainId: callback.chainId,
8100
8865
  contract: callback.contract.toLowerCase(),
8101
8866
  user: callback.user.toLowerCase(),
8102
- amount: callback.amount
8867
+ amount: callback.amount,
8868
+ positionTypeId: callback.positionTypeId
8103
8869
  };
8104
8870
  const id = callbackId(normalized);
8105
8871
  offersCallbacksRows.push({
8106
8872
  offerHash: normalizedOfferHash,
8873
+ obligationId: normalizedObligationId,
8107
8874
  callbackId: id
8108
8875
  });
8109
8876
  if (seenCallbackIds.has(id)) continue;
@@ -8113,6 +8880,7 @@ function create$14(db) {
8113
8880
  positionChainId: normalized.chainId,
8114
8881
  positionContract: normalized.contract,
8115
8882
  positionUser: normalized.user,
8883
+ positionTypeId: normalized.positionTypeId,
8116
8884
  amount: normalized.amount.toString()
8117
8885
  });
8118
8886
  }
@@ -8225,6 +8993,17 @@ function create$11(db) {
8225
8993
  const existing = lotsByKey.get(key);
8226
8994
  if (!existing || offer.size > existing.size) lotsByKey.set(key, offer);
8227
8995
  }
8996
+ const positionsByKey = /* @__PURE__ */ new Map();
8997
+ for (const offer of lotsByKey.values()) {
8998
+ const posKey = `${offer.positionChainId}-${offer.positionContract}-${offer.positionUser}`.toLowerCase();
8999
+ if (!positionsByKey.has(posKey)) positionsByKey.set(posKey, {
9000
+ chainId: offer.positionChainId,
9001
+ contract: offer.positionContract.toLowerCase(),
9002
+ user: offer.positionUser.toLowerCase(),
9003
+ positionTypeId: offer.positionTypeId
9004
+ });
9005
+ }
9006
+ for (const row of positionsByKey.values()) await db.insert(lotsPositions).values(row).onConflictDoNothing();
8228
9007
  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) {
8229
9008
  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())));
8230
9009
  const newLower = BigInt(maxUpperResult[0]?.maxUpper ?? "0");
@@ -8256,52 +9035,66 @@ function create$10(db) {
8256
9035
  const chainIds = parameters?.chainId;
8257
9036
  const now$3 = now();
8258
9037
  return (await db.select({
8259
- chainId: obligations.chainId,
9038
+ obligationId: obligationIdKeys.obligationId,
9039
+ chainId: obligationIdKeys.chainId,
9040
+ morphoV2: obligationIdKeys.morphoV2,
8260
9041
  loanToken: obligations.loanToken,
8261
- collaterals: sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${oracles.address}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
9042
+ collaterals: sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${obligationCollateralsV2.oracleAddress}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
8262
9043
  maturity: obligations.maturity
8263
- }).from(obligations).innerJoin(obligationCollateralsV2, eq(obligations.obligationId, obligationCollateralsV2.obligationId)).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
8264
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).groupBy(obligations.obligationId).where(and(chainIds !== void 0 && chainIds.length > 0 ? inArray(obligations.chainId, chainIds) : void 0, gte(obligations.maturity, now$3))).orderBy(asc(obligations.obligationId))).map((row) => from$13({
9044
+ }).from(obligationIdKeys).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoin(obligationCollateralsV2, eq(obligations.obligationKey, obligationCollateralsV2.obligationKey)).groupBy(obligationIdKeys.obligationId, obligationIdKeys.chainId, obligationIdKeys.morphoV2, obligations.loanToken, obligations.maturity).where(and(chainIds !== void 0 && chainIds.length > 0 ? inArray(obligationIdKeys.chainId, chainIds) : void 0, gte(obligations.maturity, now$3))).orderBy(asc(obligationIdKeys.obligationId))).map((row) => ({
9045
+ obligationId: row.obligationId,
8265
9046
  chainId: row.chainId,
8266
- loanToken: row.loanToken,
8267
- collaterals: row.collaterals.sort((left, right) => left.asset.localeCompare(right.asset)).map((collateral) => from$14({
8268
- asset: collateral.asset,
8269
- oracle: collateral.oracle,
8270
- lltv: from$15(BigInt(collateral.lltv))
8271
- })),
8272
- maturity: row.maturity
9047
+ morphoV2: row.morphoV2,
9048
+ obligation: from$13({
9049
+ loanToken: row.loanToken,
9050
+ collaterals: row.collaterals.sort((left, right) => left.asset.localeCompare(right.asset)).map((collateral) => from$14({
9051
+ asset: collateral.asset,
9052
+ oracle: collateral.oracle,
9053
+ lltv: from$15(BigInt(collateral.lltv))
9054
+ })),
9055
+ maturity: row.maturity
9056
+ })
8273
9057
  }));
8274
9058
  },
8275
9059
  create: async (obligations$1) => {
8276
9060
  if (obligations$1.length === 0) return;
8277
- const obligationsById = /* @__PURE__ */ new Map();
8278
- for (const obligation of obligations$1) {
8279
- const id$1 = id(obligation).toLowerCase();
8280
- if (!obligationsById.get(id$1)) obligationsById.set(id$1, obligation);
9061
+ const obligationsByKey = /* @__PURE__ */ new Map();
9062
+ const obligationIdKeysById = /* @__PURE__ */ new Map();
9063
+ for (const input of obligations$1) {
9064
+ const obligationKey = key(input.obligation).toLowerCase();
9065
+ if (!obligationsByKey.has(obligationKey)) obligationsByKey.set(obligationKey, input.obligation);
9066
+ const obligationId = input.obligationId.toLowerCase();
9067
+ if (!obligationIdKeysById.has(obligationId)) obligationIdKeysById.set(obligationId, input);
8281
9068
  }
8282
9069
  try {
8283
9070
  await db.transaction(async (dbTx) => {
8284
- const obligationRows = obligations$1.map((obligation) => ({
8285
- obligationId: id(obligation),
8286
- chainId: obligation.chainId,
8287
- loanToken: obligation.loanToken.toLowerCase(),
8288
- maturity: obligation.maturity
9071
+ const obligationRows = Array.from(obligationsByKey.entries()).map(([obligationKey, item]) => ({
9072
+ obligationKey,
9073
+ loanToken: item.loanToken.toLowerCase(),
9074
+ maturity: item.maturity
8289
9075
  }));
8290
9076
  for (const batch of batch$1(obligationRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligations).values(batch).onConflictDoNothing();
8291
- const collateralRows = obligations$1.flatMap((obligation) => {
8292
- return obligation.collaterals.map((collateral) => ({
8293
- obligationId: id(obligation),
9077
+ const obligationIdKeyRows = Array.from(obligationIdKeysById.entries()).map(([obligationId, input]) => ({
9078
+ obligationId,
9079
+ obligationKey: key(input.obligation).toLowerCase(),
9080
+ chainId: input.chainId,
9081
+ morphoV2: input.morphoV2.toLowerCase()
9082
+ }));
9083
+ for (const batch of batch$1(obligationIdKeyRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationIdKeys).values(batch).onConflictDoNothing();
9084
+ const collateralRows = Array.from(obligationsByKey.entries()).flatMap(([obligationKey, item]) => {
9085
+ return [...item.collaterals].sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())).map((collateral, collateralIndex) => ({
9086
+ obligationKey,
8294
9087
  asset: collateral.asset.toLowerCase(),
8295
- oracleChainId: obligation.chainId,
8296
9088
  oracleAddress: collateral.oracle.toLowerCase(),
8297
- lltv: collateral.lltv
9089
+ lltv: collateral.lltv,
9090
+ collateralIndex
8298
9091
  }));
8299
9092
  });
8300
9093
  for (const batch of batch$1(collateralRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(obligationCollateralsV2).values(batch).onConflictDoNothing();
8301
9094
  });
8302
9095
  } catch (err) {
8303
9096
  const error = err instanceof Error ? err : new Error(String(err));
8304
- throw new Error("Obligations.create failed. Ensure oracles exist before inserting obligations.", { cause: error });
9097
+ throw new Error("Obligations.create failed. Ensure obligation id keys are valid before inserting offers.", { cause: error });
8305
9098
  }
8306
9099
  }
8307
9100
  };
@@ -8315,10 +9108,10 @@ function create$9(config) {
8315
9108
  return {
8316
9109
  create: async (batches) => {
8317
9110
  if (batches.length === 0) return [];
8318
- const offersRows = batches.flatMap(({ blockNumber, offers }) => offers.map((offer) => ({
9111
+ const offersRows = batches.flatMap(({ blockNumber, offers }) => offers.map(({ offer, obligationId, chainId }) => ({
8319
9112
  ...serialize(offer),
8320
- obligationId: obligationId(offer),
8321
- groupChainId: offer.chainId,
9113
+ obligationId: obligationId.toLowerCase(),
9114
+ groupChainId: chainId,
8322
9115
  groupMaker: offer.maker.toLowerCase(),
8323
9116
  callbackAddress: offer.callback.address.toLowerCase(),
8324
9117
  callbackData: offer.callback.data,
@@ -8328,48 +9121,67 @@ function create$9(config) {
8328
9121
  if (offersRows.length === 0) return [];
8329
9122
  try {
8330
9123
  return await db.transaction(async (dbTx) => {
8331
- const selectExisting = async (hashes) => {
8332
- if (hashes.length === 0) return /* @__PURE__ */ new Set();
9124
+ const keyOf = (input) => `${input.hash.toLowerCase()}:${input.obligationId.toLowerCase()}`;
9125
+ const selectExisting = async (offers$1) => {
9126
+ if (offers$1.length === 0) return /* @__PURE__ */ new Set();
9127
+ const expected = new Set(offers$1.map((offer) => keyOf({
9128
+ hash: offer.hash,
9129
+ obligationId: offer.obligationId
9130
+ })));
8333
9131
  const existing = /* @__PURE__ */ new Set();
9132
+ const hashes = [...new Set(offers$1.map((offer) => offer.hash.toLowerCase()))];
8334
9133
  for (const batch of batch$1(hashes, DEFAULT_BATCH_SIZE)) {
8335
- const rows = await dbTx.select({ hash: offers.hash }).from(offers).where(inArray(offers.hash, batch));
8336
- for (const row of rows) existing.add(String(row.hash).toLowerCase());
9134
+ const rows = await dbTx.select({
9135
+ hash: offers.hash,
9136
+ obligationId: offers.obligationId
9137
+ }).from(offers).where(inArray(offers.hash, batch));
9138
+ for (const row of rows) {
9139
+ const key = keyOf({
9140
+ hash: String(row.hash),
9141
+ obligationId: String(row.obligationId)
9142
+ });
9143
+ if (expected.has(key)) existing.add(key);
9144
+ }
8337
9145
  }
8338
9146
  return existing;
8339
9147
  };
8340
9148
  const inserted = [];
8341
9149
  for (const batch of batch$1(offersRows, DEFAULT_BATCH_SIZE)) {
8342
9150
  const rows = await dbTx.insert(offers).values(batch).onConflictDoNothing().returning();
8343
- inserted.push(...rows.map((row) => row.hash));
9151
+ inserted.push(...rows.map((row) => ({
9152
+ hash: row.hash,
9153
+ obligationId: row.obligationId
9154
+ })));
8344
9155
  }
8345
9156
  const existing = await selectExisting(inserted);
8346
- return inserted.filter((hash) => existing.has(hash));
9157
+ return inserted.filter((offer) => existing.has(keyOf({
9158
+ hash: offer.hash,
9159
+ obligationId: offer.obligationId
9160
+ })));
8347
9161
  });
8348
9162
  } catch (err) {
8349
9163
  const error = err instanceof Error ? err : new Error(String(err));
8350
- throw new Error("Offers.create failed. Ensure obligations and groups exist before inserting offers.", { cause: error });
9164
+ throw new Error("Offers.create failed. Ensure obligation id keys and groups exist before inserting offers.", { cause: error });
8351
9165
  }
8352
9166
  },
8353
9167
  get: async (parameters) => {
8354
9168
  const limit = parameters?.limit ?? DEFAULT_LIMIT$3;
8355
- const cursor = parameters?.cursor;
9169
+ const rawCursor = parameters?.cursor;
8356
9170
  const maker = parameters?.maker;
8357
- if (cursor !== null && cursor !== void 0) {
8358
- if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
8359
- }
9171
+ const cursor = rawCursor !== null && rawCursor !== void 0 ? parseCursor$2(rawCursor) : void 0;
8360
9172
  const collateralsLateral = db.select({ collaterals: sql`COALESCE(
8361
9173
  jsonb_agg(
8362
9174
  jsonb_build_object(
8363
9175
  'asset', ${obligationCollateralsV2.asset},
8364
- 'oracle', ${oracles.address},
9176
+ 'oracle', ${obligationCollateralsV2.oracleAddress},
8365
9177
  'lltv', ${obligationCollateralsV2.lltv}
8366
9178
  )
8367
9179
  ),
8368
9180
  '[]'::jsonb
8369
- )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
8370
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
9181
+ )`.as("collaterals") }).from(obligationCollateralsV2).where(eq(obligationCollateralsV2.obligationKey, obligationIdKeys.obligationKey)).as("collaterals_lateral");
8371
9182
  const rows = (await db.select({
8372
9183
  hash: offers.hash,
9184
+ obligationId: offers.obligationId,
8373
9185
  maker: offers.groupMaker,
8374
9186
  assets: offers.assets,
8375
9187
  obligationUnits: offers.obligationUnits,
@@ -8381,17 +9193,18 @@ function create$9(config) {
8381
9193
  group: offers.group,
8382
9194
  session: offers.session,
8383
9195
  buy: offers.buy,
8384
- chainId: obligations.chainId,
9196
+ chainId: obligationIdKeys.chainId,
8385
9197
  loanToken: obligations.loanToken,
8386
9198
  callbackAddress: offers.callbackAddress,
8387
9199
  callbackData: offers.callbackData,
8388
9200
  receiverIfMakerIsSeller: offers.receiverIfMakerIsSeller,
8389
9201
  collaterals: collateralsLateral.collaterals,
8390
9202
  blockNumber: offers.blockNumber
8391
- }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
9203
+ }).from(offers).innerJoin(obligationIdKeys, eq(offers.obligationId, obligationIdKeys.obligationId)).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== void 0 ? or(gt(offers.hash, cursor.hash), and(eq(offers.hash, cursor.hash), gt(offers.obligationId, cursor.obligationId))) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0)).orderBy(asc(offers.hash), asc(offers.obligationId)).limit(limit)).map((row) => {
8392
9204
  const receiverIfMakerIsSeller = (row.receiverIfMakerIsSeller ?? row.maker).toLowerCase();
8393
9205
  return {
8394
9206
  hash: row.hash,
9207
+ obligationId: row.obligationId,
8395
9208
  maker: row.maker,
8396
9209
  assets: BigInt(row.assets),
8397
9210
  obligationUnits: BigInt(row.obligationUnits),
@@ -8423,7 +9236,10 @@ function create$9(config) {
8423
9236
  });
8424
9237
  return {
8425
9238
  rows,
8426
- nextCursor: rows.length === limit ? rows[rows.length - 1].hash : null
9239
+ nextCursor: rows.length === limit ? formatCursor$2({
9240
+ hash: rows[rows.length - 1].hash,
9241
+ obligationId: rows[rows.length - 1].obligationId
9242
+ }) : null
8427
9243
  };
8428
9244
  },
8429
9245
  delete: async (parameters) => {
@@ -8446,7 +9262,7 @@ function create$9(config) {
8446
9262
  const query = ({ side }) => db.selectDistinctOn([offers.obligationId], {
8447
9263
  obligationId: offers.obligationId,
8448
9264
  tick: offers.tick
8449
- }).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`);
9265
+ }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, and(eq(offers.hash, validations.offerHash), eq(offers.obligationId, validations.obligationId))).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`);
8450
9266
  const [bestBuys, bestSells] = await Promise.all([query({ side: "buy" }), query({ side: "sell" })]);
8451
9267
  const quotes = /* @__PURE__ */ new Map();
8452
9268
  for (const row of bestSells) quotes.set(row.obligationId, {
@@ -8476,6 +9292,18 @@ function create$9(config) {
8476
9292
  }
8477
9293
  };
8478
9294
  }
9295
+ const HEX_32$2 = /^0x[a-fA-F0-9]{64}$/;
9296
+ function parseCursor$2(cursor) {
9297
+ const [hash, obligationId] = cursor.split(":");
9298
+ if (!hash || !obligationId || !HEX_32$2.test(hash) || !HEX_32$2.test(obligationId)) throw new Error("Invalid cursor format");
9299
+ return {
9300
+ hash: hash.toLowerCase(),
9301
+ obligationId: obligationId.toLowerCase()
9302
+ };
9303
+ }
9304
+ function formatCursor$2(input) {
9305
+ return `${input.hash.toLowerCase()}:${input.obligationId.toLowerCase()}`;
9306
+ }
8479
9307
 
8480
9308
  //#endregion
8481
9309
  //#region src/database/domains/Offsets.ts
@@ -8549,14 +9377,15 @@ const create$6 = (db) => {
8549
9377
  upsert: async (positions$1) => {
8550
9378
  const positionsMap = /* @__PURE__ */ new Map();
8551
9379
  for (const p of positions$1) {
8552
- const key = `${p.chainId}-${p.contract}-${p.user}`.toLowerCase();
8553
- const zeroKey = `${p.chainId}-${p.contract}-${zeroAddress}`.toLowerCase();
9380
+ const key = `${p.chainId}-${p.contract}-${p.user}-${p.asset}`.toLowerCase();
9381
+ const zeroKey = `${p.chainId}-${p.contract}-${zeroAddress}-${p.asset}`.toLowerCase();
8554
9382
  if (!positionsMap.has(zeroKey)) positionsMap.set(zeroKey, {
8555
9383
  chainId: p.chainId,
8556
9384
  contract: p.contract,
8557
9385
  user: zeroAddress,
8558
9386
  balance: 0n,
8559
9387
  type: p.type,
9388
+ asset: p.asset,
8560
9389
  blockNumber: p.blockNumber
8561
9390
  });
8562
9391
  if (!positionsMap.has(key)) {
@@ -8567,14 +9396,14 @@ const create$6 = (db) => {
8567
9396
  }
8568
9397
  if (positionsMap.size === 0) return 0;
8569
9398
  const rows = Array.from(positionsMap.values()).map((p) => {
8570
- const positionTypeId = Object.values(Type).indexOf(p.type) + 1;
9399
+ const positionTypeId$1 = positionTypeId(p.type);
8571
9400
  return {
8572
9401
  chainId: p.chainId,
8573
9402
  contract: p.contract.toLowerCase(),
8574
9403
  user: p.user.toLowerCase(),
8575
- positionTypeId,
9404
+ positionTypeId: positionTypeId$1,
8576
9405
  ...p.balance !== void 0 ? { balance: p.balance.toString() } : {},
8577
- ...p.asset !== void 0 ? { asset: p.asset.toLowerCase() } : {},
9406
+ asset: p.asset.toLowerCase(),
8578
9407
  blockNumber: p.blockNumber
8579
9408
  };
8580
9409
  });
@@ -8584,11 +9413,12 @@ const create$6 = (db) => {
8584
9413
  target: [
8585
9414
  positions.chainId,
8586
9415
  positions.contract,
8587
- positions.user
9416
+ positions.user,
9417
+ positions.positionTypeId,
9418
+ positions.asset
8588
9419
  ],
8589
9420
  set: {
8590
9421
  balance: sql`EXCLUDED.balance`,
8591
- asset: sql`COALESCE(EXCLUDED.asset, ${positions.asset})`,
8592
9422
  blockNumber: sql`EXCLUDED.block_number`,
8593
9423
  updatedAt: sql`NOW()`
8594
9424
  },
@@ -8607,20 +9437,23 @@ const create$6 = (db) => {
8607
9437
  cursor = {
8608
9438
  chainId: parsed.chainId,
8609
9439
  contract: parsed.contract,
8610
- user: parsed.user
9440
+ user: parsed.user,
9441
+ positionTypeId: parsed.positionTypeId ?? 0,
9442
+ asset: parsed.asset ?? ""
8611
9443
  };
8612
9444
  }
8613
- const positions$2 = await db.select().from(positions).where(and(ne(positions.user, zeroAddress), filled === void 0 ? sql`true` : sql`${positions.balance} IS ${filled === true ? sql`NOT` : sql``} NULL`, type !== void 0 ? eq(positions.positionTypeId, Object.values(Type).indexOf(type) + 1) : sql`true`, chainId !== void 0 ? eq(positions.chainId, chainId) : sql`true`, (() => {
9445
+ const positions$2 = await db.select().from(positions).where(and(ne(positions.user, zeroAddress), filled === void 0 ? sql`true` : sql`${positions.balance} IS ${filled === true ? sql`NOT` : sql``} NULL`, type !== void 0 ? eq(positions.positionTypeId, positionTypeId(type)) : sql`true`, chainId !== void 0 ? eq(positions.chainId, chainId) : sql`true`, (() => {
8614
9446
  if (cursor === null || cursor === void 0) return sql`true`;
8615
9447
  return sql`
8616
- (${chainId === void 0 ? sql`"chain_id", ` : sql``}"contract", "user") > (${chainId === void 0 ? sql`${cursor.chainId}, ` : sql``}${cursor.contract}, ${cursor.user})
9448
+ (${chainId === void 0 ? sql`"chain_id", ` : sql``}"contract", "user", "position_type_id", "asset") > (${chainId === void 0 ? sql`${cursor.chainId}, ` : sql``}${cursor.contract}, ${cursor.user}, ${cursor.positionTypeId}, ${cursor.asset})
8617
9449
  `;
8618
- })())).orderBy(asc(positions.chainId), asc(positions.contract), asc(positions.user), asc(positions.blockNumber)).limit(limit);
9450
+ })())).orderBy(asc(positions.chainId), asc(positions.contract), asc(positions.user), asc(positions.positionTypeId), asc(positions.asset)).limit(limit);
8619
9451
  const nextCursor = positions$2.length === limit ? Buffer.from(JSON.stringify({
8620
9452
  chainId: positions$2[positions$2.length - 1].chainId.toString(),
8621
9453
  contract: positions$2[positions$2.length - 1].contract,
8622
9454
  user: positions$2[positions$2.length - 1].user,
8623
- blockNumber: positions$2[positions$2.length - 1].blockNumber.toString()
9455
+ positionTypeId: positions$2[positions$2.length - 1].positionTypeId,
9456
+ asset: positions$2[positions$2.length - 1].asset
8624
9457
  })).toString("base64url") : null;
8625
9458
  return {
8626
9459
  positions: positions$2.map((p) => ({
@@ -8629,14 +9462,14 @@ const create$6 = (db) => {
8629
9462
  user: p.user,
8630
9463
  type: Object.values(Type)[p.positionTypeId - 1],
8631
9464
  balance: p.balance !== null ? BigInt(p.balance) : void 0,
8632
- ...p.asset !== null ? { asset: p.asset } : {},
9465
+ asset: p.asset,
8633
9466
  blockNumber: p.blockNumber
8634
9467
  })),
8635
9468
  nextCursor
8636
9469
  };
8637
9470
  },
8638
9471
  getByUser: async (parameters) => {
8639
- const { user, limit = DEFAULT_LIMIT$2, cursor: encodedCursor } = parameters;
9472
+ const { user, type, limit = DEFAULT_LIMIT$2, cursor: encodedCursor } = parameters;
8640
9473
  let cursor = null;
8641
9474
  if (encodedCursor !== null && encodedCursor !== void 0) {
8642
9475
  const parsed = JSON.parse(Buffer.from(encodedCursor, "base64url").toString("utf8"));
@@ -8644,6 +9477,8 @@ const create$6 = (db) => {
8644
9477
  cursor = {
8645
9478
  chainId: parsed.chainId,
8646
9479
  contract: parsed.contract,
9480
+ positionTypeId: parsed.positionTypeId ?? 0,
9481
+ asset: parsed.asset ?? "",
8647
9482
  obligationId: parsed.obligationId ?? null
8648
9483
  };
8649
9484
  }
@@ -8731,6 +9566,8 @@ const create$6 = (db) => {
8731
9566
  p.contract,
8732
9567
  p."user",
8733
9568
  p.block_number,
9569
+ p.position_type_id,
9570
+ p.asset,
8734
9571
  po.obligation_id,
8735
9572
  COALESCE(po.reserved_balance, '0') AS reserved_balance
8736
9573
  FROM ${positions} p
@@ -8740,14 +9577,18 @@ const create$6 = (db) => {
8740
9577
  AND LOWER(po."user") = LOWER(p."user")
8741
9578
  WHERE LOWER(p."user") = LOWER(${user})
8742
9579
  AND p."user" != ${zeroAddress}
8743
- ${cursor !== null ? sql`AND (p.chain_id, p.contract, COALESCE(po.obligation_id, '')) > (${cursor.chainId}, ${cursor.contract}, ${cursor.obligationId ?? ""})` : sql``}
8744
- ORDER BY p.chain_id ASC, p.contract ASC, po.obligation_id ASC NULLS FIRST
9580
+ ${type !== void 0 ? sql`AND p.position_type_id = ${positionTypeId(type)}` : sql``}
9581
+ ${cursor !== null ? sql`AND (p.chain_id, p.contract, p.position_type_id, p.asset, COALESCE(po.obligation_id, '')) > (${cursor.chainId}, ${cursor.contract}, ${cursor.positionTypeId}, ${cursor.asset}, ${cursor.obligationId ?? ""})` : sql``}
9582
+ ORDER BY p.chain_id ASC, p.contract ASC, p.position_type_id ASC, p.asset ASC, po.obligation_id ASC NULLS FIRST
8745
9583
  LIMIT ${limit}
8746
9584
  `);
8747
- const nextCursor = raw.rows.length === limit ? Buffer.from(JSON.stringify({
8748
- chainId: raw.rows[raw.rows.length - 1].chain_id.toString(),
8749
- contract: raw.rows[raw.rows.length - 1].contract,
8750
- obligationId: raw.rows[raw.rows.length - 1].obligation_id
9585
+ const lastRow = raw.rows[raw.rows.length - 1];
9586
+ const nextCursor = raw.rows.length === limit && lastRow !== void 0 ? Buffer.from(JSON.stringify({
9587
+ chainId: lastRow.chain_id.toString(),
9588
+ contract: lastRow.contract,
9589
+ positionTypeId: lastRow.position_type_id,
9590
+ asset: lastRow.asset,
9591
+ obligationId: lastRow.obligation_id
8751
9592
  })).toString("base64url") : null;
8752
9593
  return {
8753
9594
  positions: raw.rows.map((row) => ({
@@ -8762,21 +9603,23 @@ const create$6 = (db) => {
8762
9603
  };
8763
9604
  },
8764
9605
  setEmptyAfter: async (parameters) => {
8765
- const { chainId, blockNumber } = parameters;
9606
+ const { chainId, blockNumber, type } = parameters;
9607
+ const typeId = positionTypeId(type);
8766
9608
  return await db.transaction(async (tx) => {
8767
9609
  const updatedPositions = await tx.update(positions).set({
8768
9610
  balance: null,
8769
9611
  blockNumber,
8770
9612
  updatedAt: sql`NOW()`
8771
- }).where(and(eq(positions.chainId, chainId), sql`${positions.blockNumber} >= ${blockNumber}`)).returning();
9613
+ }).where(and(eq(positions.chainId, chainId), sql`${positions.blockNumber} >= ${blockNumber}`, eq(positions.positionTypeId, typeId))).returning();
8772
9614
  if (updatedPositions.length === 0) return 0;
8773
9615
  await tx.execute(sql`
8774
9616
  DELETE FROM ${transfers} t
8775
- USING (VALUES ${sql.join(updatedPositions.map((u) => sql`(${u.chainId}::bigint, ${u.contract}::varchar(42), ${u.user}::varchar(42))`), sql`,`)}) AS s(chain_id, "contract", "user")
9617
+ USING (VALUES ${sql.join(updatedPositions.map((u) => sql`(${u.chainId}::bigint, ${u.contract}::varchar(66), ${u.user}::varchar(42))`), sql`,`)}) AS s(chain_id, "contract", "user")
8776
9618
  WHERE
8777
9619
  t.chain_id = s.chain_id
8778
9620
  AND t."contract" = s."contract"
8779
9621
  AND (t."from" = s."user" OR t."to" = s."user")
9622
+ AND t.position_type_id = ${typeId}
8780
9623
  `);
8781
9624
  return updatedPositions.length;
8782
9625
  });
@@ -8786,33 +9629,35 @@ const create$6 = (db) => {
8786
9629
 
8787
9630
  //#endregion
8788
9631
  //#region src/database/domains/Transfers.ts
8789
- const create$5 = (db) => ({ create: async (transfers$1) => {
8790
- if (transfers$1.length === 0) return 0;
8791
- return await db.transaction(async (dbTx) => {
8792
- let totalInserted = 0;
8793
- for (const transfersBatch of batch$1(transfers$1, DEFAULT_BATCH_SIZE)) {
8794
- const { rows: inserted } = await dbTx.execute(sql`
8795
- INSERT INTO ${transfers} (event_id, chain_id, "contract", "from", "to", "value", block_number)
8796
- SELECT * FROM (VALUES ${sql.join(transfersBatch.map((transfer) => sql`(${transfer.id}::varchar(128), ${transfer.chainId}::bigint, ${transfer.contract.toLowerCase()}::varchar(42), ${transfer.from.toLowerCase()}::varchar(42), ${transfer.to.toLowerCase()}::varchar(42), ${transfer.value.toString()}::numeric(78, 0), ${transfer.blockNumber}::bigint)`), sql`,`)}) AS v(event_id, chain_id, "contract", "from", "to", "value", block_number)
9632
+ const create$5 = (db) => ({
9633
+ create: async (transfers$1) => {
9634
+ if (transfers$1.length === 0) return 0;
9635
+ const typeId = positionTypeId(transfers$1[0].type);
9636
+ return await db.transaction(async (dbTx) => {
9637
+ let totalInserted = 0;
9638
+ for (const transfersBatch of batch$1(transfers$1, DEFAULT_BATCH_SIZE)) {
9639
+ const { rows: inserted } = await dbTx.execute(sql`
9640
+ INSERT INTO ${transfers} (event_id, chain_id, "contract", "from", "to", "value", position_type_id, asset, block_number)
9641
+ SELECT * FROM (VALUES ${sql.join(transfersBatch.map((transfer) => sql`(${transfer.id}::varchar(128), ${transfer.chainId}::bigint, ${transfer.contract.toLowerCase()}::varchar(66), ${transfer.from.toLowerCase()}::varchar(42), ${transfer.to.toLowerCase()}::varchar(42), ${transfer.value.toString()}::numeric(78, 0), ${typeId}::integer, ${transfer.asset.toLowerCase()}::varchar(42), ${transfer.blockNumber}::bigint)`), sql`,`)}) AS v(event_id, chain_id, "contract", "from", "to", "value", position_type_id, asset, block_number)
8797
9642
  WHERE
8798
9643
  EXISTS (
8799
9644
  SELECT 1 FROM ${positions} p
8800
- WHERE p.chain_id = v.chain_id AND p."contract" = v."contract" AND p."user" = v."from" AND p.balance IS NOT NULL
9645
+ WHERE p.chain_id = v.chain_id AND p."contract" = v."contract" AND p."user" = v."from" AND p.position_type_id = v.position_type_id AND p.asset = v.asset AND p.balance IS NOT NULL
8801
9646
  )
8802
9647
  AND
8803
9648
  EXISTS (
8804
9649
  SELECT 1 FROM ${positions} p
8805
- WHERE p.chain_id = v.chain_id AND p."contract" = v."contract" AND p."user" = v."to" AND p.balance IS NOT NULL
9650
+ WHERE p.chain_id = v.chain_id AND p."contract" = v."contract" AND p."user" = v."to" AND p.position_type_id = v.position_type_id AND p.asset = v.asset AND p.balance IS NOT NULL
8806
9651
  )
8807
9652
  ON CONFLICT DO NOTHING
8808
9653
  RETURNING *;
8809
9654
  `);
8810
- if (inserted.length === 0) continue;
8811
- await dbTx.execute(sql`
9655
+ if (inserted.length === 0) continue;
9656
+ await dbTx.execute(sql`
8812
9657
  WITH inserted AS (
8813
9658
  VALUES ${sql.join(inserted.map((t) => {
8814
- return sql`(${t.chain_id}::bigint, ${t.contract}::varchar(42), ${t.from}::varchar(42), ${t.to}::varchar(42), ${t.value}::numeric(78, 0), ${t.block_number}::bigint)`;
8815
- }), sql`,`)}
9659
+ return sql`(${t.chain_id}::bigint, ${t.contract}::varchar(66), ${t.from}::varchar(42), ${t.to}::varchar(42), ${t.value}::numeric(78, 0), ${t.asset}::varchar(42), ${t.block_number}::bigint)`;
9660
+ }), sql`,`)}
8816
9661
  ),
8817
9662
  new_transfers AS (
8818
9663
  SELECT
@@ -8821,7 +9666,8 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8821
9666
  column3 AS "from",
8822
9667
  column4 AS "to",
8823
9668
  column5 AS "value",
8824
- column6 AS block_number
9669
+ column6 AS asset,
9670
+ column7 AS block_number
8825
9671
  FROM inserted
8826
9672
  ),
8827
9673
  diffs AS (
@@ -8830,6 +9676,7 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8830
9676
  nt."contract",
8831
9677
  nt."from" AS "user",
8832
9678
  -nt."value" AS delta,
9679
+ nt.asset,
8833
9680
  nt.block_number
8834
9681
  FROM new_transfers nt
8835
9682
  UNION ALL
@@ -8838,6 +9685,7 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8838
9685
  nt."contract",
8839
9686
  nt."to" AS "user",
8840
9687
  nt."value" AS delta,
9688
+ nt.asset,
8841
9689
  nt.block_number
8842
9690
  FROM new_transfers nt
8843
9691
  ),
@@ -8847,12 +9695,15 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8847
9695
  d."contract",
8848
9696
  d."user",
8849
9697
  d.delta,
9698
+ d.asset,
8850
9699
  d.block_number
8851
9700
  FROM diffs d
8852
9701
  JOIN ${positions} p
8853
9702
  ON p.chain_id = d.chain_id
8854
9703
  AND p."contract" = d."contract"
8855
9704
  AND p."user" = d."user"
9705
+ AND p.position_type_id = ${typeId}
9706
+ AND p.asset = d.asset
8856
9707
  -- Only keep per-event diffs that are at or after the current position block
8857
9708
  WHERE d.block_number >= p.block_number
8858
9709
  ),
@@ -8861,10 +9712,11 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8861
9712
  chain_id,
8862
9713
  "contract",
8863
9714
  "user",
9715
+ asset,
8864
9716
  SUM(delta) AS sum_delta,
8865
9717
  MAX(block_number) AS max_block
8866
9718
  FROM valid_diffs
8867
- GROUP BY 1,2,3
9719
+ GROUP BY 1,2,3,4
8868
9720
  )
8869
9721
  UPDATE ${positions} AS p
8870
9722
  SET
@@ -8876,12 +9728,109 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8876
9728
  p.chain_id = a.chain_id
8877
9729
  AND p."contract" = a."contract"
8878
9730
  AND p."user" = a."user"
9731
+ AND p.asset = a.asset
9732
+ AND p.position_type_id = ${typeId}
8879
9733
  `);
8880
- totalInserted += inserted.length;
8881
- }
8882
- return totalInserted;
8883
- });
8884
- } });
9734
+ totalInserted += inserted.length;
9735
+ }
9736
+ return totalInserted;
9737
+ });
9738
+ },
9739
+ delete: async (parameters) => {
9740
+ const { chainId, blockNumberGte, positionTypeId } = parameters;
9741
+ return await db.transaction(async (dbTx) => {
9742
+ const { rows: toDelete } = await dbTx.execute(sql`
9743
+ SELECT event_id, chain_id, "contract", "from", "to", "value"::text, asset, block_number
9744
+ FROM ${transfers}
9745
+ WHERE chain_id = ${chainId}
9746
+ AND position_type_id = ${positionTypeId}
9747
+ AND block_number >= ${blockNumberGte}
9748
+ `);
9749
+ if (toDelete.length === 0) return 0;
9750
+ await dbTx.execute(sql`
9751
+ WITH to_delete AS (
9752
+ VALUES ${sql.join(toDelete.map((t) => sql`(${t.chain_id}::bigint, ${t.contract}::varchar(66), ${t.from}::varchar(42), ${t.to}::varchar(42), ${t.value}::numeric(78, 0), ${t.asset}::varchar(42))`), sql`,`)}
9753
+ ),
9754
+ reverse_diffs AS (
9755
+ SELECT column1 AS chain_id, column2 AS "contract", column3 AS "user", column5 AS delta, column6 AS asset
9756
+ FROM to_delete
9757
+ UNION ALL
9758
+ SELECT column1 AS chain_id, column2 AS "contract", column4 AS "user", -column5 AS delta, column6 AS asset
9759
+ FROM to_delete
9760
+ ),
9761
+ aggregated AS (
9762
+ SELECT chain_id, "contract", "user", asset, SUM(delta) AS sum_delta
9763
+ FROM reverse_diffs
9764
+ GROUP BY 1,2,3,4
9765
+ ),
9766
+ remaining_max AS (
9767
+ SELECT t.chain_id, t."contract",
9768
+ CASE WHEN t."from" = a."user" THEN t."from" ELSE t."to" END AS "user",
9769
+ t.asset,
9770
+ MAX(t.block_number) AS max_block
9771
+ FROM ${transfers} t
9772
+ JOIN aggregated a
9773
+ ON t.chain_id = a.chain_id AND t."contract" = a."contract"
9774
+ AND t.asset = a.asset
9775
+ AND (t."from" = a."user" OR t."to" = a."user")
9776
+ WHERE t.position_type_id = ${positionTypeId}
9777
+ AND t.block_number < ${blockNumberGte}
9778
+ GROUP BY t.chain_id, t."contract",
9779
+ CASE WHEN t."from" = a."user" THEN t."from" ELSE t."to" END,
9780
+ t.asset
9781
+ )
9782
+ UPDATE ${positions} AS p
9783
+ SET
9784
+ balance = COALESCE(p.balance, 0) + a.sum_delta,
9785
+ block_number = COALESCE(rm.max_block, p.block_number),
9786
+ updated_at = NOW()
9787
+ FROM aggregated a
9788
+ LEFT JOIN remaining_max rm
9789
+ ON rm.chain_id = a.chain_id AND rm."contract" = a."contract" AND rm."user" = a."user" AND rm.asset = a.asset
9790
+ WHERE p.chain_id = a.chain_id
9791
+ AND p."contract" = a."contract"
9792
+ AND p."user" = a."user"
9793
+ AND p.asset = a.asset
9794
+ AND p.position_type_id = ${positionTypeId}
9795
+ `);
9796
+ await dbTx.execute(sql`
9797
+ DELETE FROM ${transfers}
9798
+ WHERE chain_id = ${chainId}
9799
+ AND position_type_id = ${positionTypeId}
9800
+ AND block_number >= ${blockNumberGte}
9801
+ `);
9802
+ await dbTx.execute(sql`
9803
+ DELETE FROM ${positions} p
9804
+ USING (
9805
+ SELECT DISTINCT chain_id, "contract", "user", asset
9806
+ FROM (
9807
+ SELECT chain_id, "contract", "from" AS "user", asset FROM (
9808
+ VALUES ${sql.join(toDelete.map((t) => sql`(${t.chain_id}::bigint, ${t.contract}::varchar(66), ${t.from}::varchar(42), ${t.asset}::varchar(42))`), sql`,`)}
9809
+ ) AS d(chain_id, "contract", "from", asset)
9810
+ UNION
9811
+ SELECT chain_id, "contract", "to" AS "user", asset FROM (
9812
+ VALUES ${sql.join(toDelete.map((t) => sql`(${t.chain_id}::bigint, ${t.contract}::varchar(66), ${t.to}::varchar(42), ${t.asset}::varchar(42))`), sql`,`)}
9813
+ ) AS d(chain_id, "contract", "to", asset)
9814
+ ) AS combined
9815
+ ) AS affected
9816
+ WHERE p.chain_id = affected.chain_id
9817
+ AND p."contract" = affected."contract"
9818
+ AND p."user" = affected."user"
9819
+ AND p.asset = affected.asset
9820
+ AND p.position_type_id = ${positionTypeId}
9821
+ AND NOT EXISTS (
9822
+ SELECT 1 FROM ${transfers} t
9823
+ WHERE t.chain_id = p.chain_id
9824
+ AND t."contract" = p."contract"
9825
+ AND t.asset = p.asset
9826
+ AND t.position_type_id = ${positionTypeId}
9827
+ AND (t."from" = p."user" OR t."to" = p."user")
9828
+ )
9829
+ `);
9830
+ return toDelete.length;
9831
+ });
9832
+ }
9833
+ });
8885
9834
 
8886
9835
  //#endregion
8887
9836
  //#region src/database/domains/Trees.ts
@@ -8894,34 +9843,22 @@ const create$5 = (db) => ({ create: async (transfers$1) => {
8894
9843
  function create$4(config) {
8895
9844
  const db = config.db;
8896
9845
  return {
8897
- create: async (trees$1) => {
9846
+ upsert: async (trees$1) => {
8898
9847
  if (trees$1.length === 0) return [];
8899
9848
  try {
8900
9849
  return await db.transaction(async (dbTx) => {
8901
9850
  const roots = [];
8902
- for (const { tree, signature } of trees$1) {
8903
- const root = tree.root.toLowerCase();
8904
- roots.push(root);
9851
+ for (const { root, signature } of trees$1) {
9852
+ const normalizedRoot = root.toLowerCase();
9853
+ const normalizedSignature = signature.toLowerCase();
9854
+ roots.push(normalizedRoot);
8905
9855
  await dbTx.insert(trees).values({
8906
- root,
8907
- rootSignature: signature.toLowerCase()
9856
+ root: normalizedRoot,
9857
+ rootSignature: normalizedSignature
8908
9858
  }).onConflictDoUpdate({
8909
9859
  target: [trees.root],
8910
9860
  set: {
8911
- rootSignature: signature.toLowerCase(),
8912
- createdAt: sql`NOW()`
8913
- }
8914
- });
8915
- const pathRows = proofs(tree).map((proof) => ({
8916
- offerHash: hash(proof.offer).toLowerCase(),
8917
- treeRoot: root,
8918
- proofNodes: concatenateProofs(proof.path)
8919
- }));
8920
- for (const batch of batch$1(pathRows, DEFAULT_BATCH_SIZE)) await dbTx.insert(merklePaths).values(batch).onConflictDoUpdate({
8921
- target: [merklePaths.offerHash],
8922
- set: {
8923
- treeRoot: sql`excluded.tree_root`,
8924
- proofNodes: sql`excluded.proof_nodes`,
9861
+ rootSignature: normalizedSignature,
8925
9862
  createdAt: sql`NOW()`
8926
9863
  }
8927
9864
  });
@@ -8930,24 +9867,60 @@ function create$4(config) {
8930
9867
  });
8931
9868
  } catch (err) {
8932
9869
  const error = err instanceof Error ? err : new Error(String(err));
8933
- throw new Error("Trees.create failed. Ensure offers exist before inserting merkle paths.", { cause: error });
9870
+ throw new Error("Trees.upsert failed. Ensure obligations and offers exist before upserting roots.", { cause: error });
9871
+ }
9872
+ },
9873
+ upsertPaths: async (paths) => {
9874
+ if (paths.length === 0) return;
9875
+ const rows = paths.map((path) => ({
9876
+ offerHash: path.offerHash.toLowerCase(),
9877
+ obligationId: path.obligationId.toLowerCase(),
9878
+ treeRoot: path.treeRoot.toLowerCase(),
9879
+ proofNodes: concatenateProofs(path.proof)
9880
+ }));
9881
+ try {
9882
+ for (const batch of batch$1(rows, DEFAULT_BATCH_SIZE)) await db.insert(merklePaths).values(batch).onConflictDoUpdate({
9883
+ target: [merklePaths.offerHash, merklePaths.obligationId],
9884
+ set: {
9885
+ treeRoot: sql`excluded.tree_root`,
9886
+ proofNodes: sql`excluded.proof_nodes`,
9887
+ createdAt: sql`NOW()`
9888
+ }
9889
+ });
9890
+ } catch (err) {
9891
+ const error = err instanceof Error ? err : new Error(String(err));
9892
+ throw new Error("Trees.upsertPaths failed. Ensure offers and roots exist before inserting merkle paths.", { cause: error });
8934
9893
  }
8935
9894
  },
8936
- getAttestations: async (hashes) => {
8937
- if (hashes.length === 0) return /* @__PURE__ */ new Map();
8938
- const normalizedHashes = hashes.map((h) => h.toLowerCase());
9895
+ getAttestations: async (references) => {
9896
+ if (references.length === 0) return /* @__PURE__ */ new Map();
9897
+ const normalizedReferences = references.map((reference) => ({
9898
+ offerHash: reference.offerHash.toLowerCase(),
9899
+ obligationId: reference.obligationId.toLowerCase()
9900
+ }));
9901
+ const hashes = [...new Set(normalizedReferences.map((reference) => reference.offerHash))];
9902
+ const obligationIds = [...new Set(normalizedReferences.map((reference) => reference.obligationId))];
8939
9903
  const results = await db.select({
8940
9904
  offerHash: merklePaths.offerHash,
9905
+ obligationId: merklePaths.obligationId,
8941
9906
  treeRoot: merklePaths.treeRoot,
8942
9907
  proofNodes: merklePaths.proofNodes,
8943
9908
  rootSignature: trees.rootSignature
8944
- }).from(merklePaths).innerJoin(trees, eq(merklePaths.treeRoot, trees.root)).where(inArray(merklePaths.offerHash, normalizedHashes));
9909
+ }).from(merklePaths).innerJoin(trees, eq(merklePaths.treeRoot, trees.root)).where(sql`${inArray(merklePaths.offerHash, hashes)} AND ${inArray(merklePaths.obligationId, obligationIds)}`);
9910
+ const expectedKeys = new Set(normalizedReferences.map(toAttestationKey));
8945
9911
  const attestationMap = /* @__PURE__ */ new Map();
8946
- for (const row of results) attestationMap.set(row.offerHash, {
8947
- root: row.treeRoot,
8948
- signature: row.rootSignature,
8949
- proof: splitProofs(row.proofNodes)
8950
- });
9912
+ for (const row of results) {
9913
+ const key = toAttestationKey({
9914
+ offerHash: row.offerHash,
9915
+ obligationId: row.obligationId
9916
+ });
9917
+ if (!expectedKeys.has(key)) continue;
9918
+ attestationMap.set(key, {
9919
+ root: row.treeRoot.toLowerCase(),
9920
+ signature: row.rootSignature.toLowerCase(),
9921
+ proof: splitProofs(row.proofNodes)
9922
+ });
9923
+ }
8951
9924
  return attestationMap;
8952
9925
  }
8953
9926
  };
@@ -8958,7 +9931,7 @@ function create$4(config) {
8958
9931
  */
8959
9932
  function concatenateProofs(proofs) {
8960
9933
  if (proofs.length === 0) return "0x";
8961
- return `0x${proofs.map((p) => p.slice(2)).join("")}`;
9934
+ return `0x${proofs.map((proof) => proof.toLowerCase().slice(2)).join("")}`;
8962
9935
  }
8963
9936
  /**
8964
9937
  * Splits a concatenated hex string back into an array of 32-byte hex hashes.
@@ -8968,9 +9941,12 @@ function splitProofs(concatenated) {
8968
9941
  if (!concatenated || concatenated === "0x" || concatenated.length <= 2) return [];
8969
9942
  const hex = concatenated.slice(2);
8970
9943
  const proofs = [];
8971
- for (let i = 0; i < hex.length; i += 64) proofs.push(`0x${hex.slice(i, i + 64)}`);
9944
+ for (let i = 0; i < hex.length; i += 64) proofs.push(`0x${hex.slice(i, i + 64).toLowerCase()}`);
8972
9945
  return proofs;
8973
9946
  }
9947
+ function toAttestationKey(offer) {
9948
+ return `${offer.offerHash.toLowerCase()}:${offer.obligationId.toLowerCase()}`;
9949
+ }
8974
9950
 
8975
9951
  //#endregion
8976
9952
  //#region src/database/domains/Validations.ts
@@ -8979,26 +9955,26 @@ function create$3(db) {
8979
9955
  return {
8980
9956
  get: async (params) => {
8981
9957
  const { status: status$2, cursor, limit = DEFAULT_LIMIT$1 } = params ?? {};
8982
- if (cursor !== null && cursor !== void 0) {
8983
- if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
8984
- }
9958
+ const parsedCursor = cursor !== null && cursor !== void 0 ? parseCursor$1(cursor) : void 0;
8985
9959
  let query = db.select({
8986
9960
  offerHash: validations.offerHash,
9961
+ obligationId: validations.obligationId,
8987
9962
  code: status.code
8988
9963
  }).from(validations).innerJoin(status, eq(validations.statusId, status.id));
8989
9964
  if (status$2 !== void 0) query = query.where(eq(status.code, status$2));
8990
- if (cursor !== null && cursor !== void 0) {
8991
- const cursorCondition = gt(validations.offerHash, cursor.toLowerCase());
9965
+ if (parsedCursor !== void 0) {
9966
+ const cursorCondition = or(gt(validations.offerHash, parsedCursor.offerHash), and(eq(validations.offerHash, parsedCursor.offerHash), gt(validations.obligationId, parsedCursor.obligationId)));
8992
9967
  if (status$2 !== void 0) query = query.where(and(eq(status.code, status$2), cursorCondition));
8993
9968
  else query = query.where(cursorCondition);
8994
9969
  }
8995
- const mapped = (await query.orderBy(asc(validations.offerHash)).limit(limit)).map((row) => ({
9970
+ const mapped = (await query.orderBy(asc(validations.offerHash), asc(validations.obligationId)).limit(limit)).map((row) => ({
8996
9971
  offerHash: row.offerHash,
9972
+ obligationId: row.obligationId,
8997
9973
  status: row.code
8998
9974
  }));
8999
9975
  return {
9000
9976
  validations: mapped,
9001
- nextCursor: mapped.length === limit ? mapped[mapped.length - 1].offerHash : null
9977
+ nextCursor: mapped.length === limit ? formatCursor$1(mapped[mapped.length - 1]) : null
9002
9978
  };
9003
9979
  },
9004
9980
  upsert: async (results) => {
@@ -9008,6 +9984,7 @@ function create$3(db) {
9008
9984
  if (invalidStatuses.length > 0) throw new Error(`Unknown validation status: ${invalidStatuses.join(", ")}`);
9009
9985
  const normalized = results.map((r) => ({
9010
9986
  offerHash: r.offerHash.toLowerCase(),
9987
+ obligationId: r.obligationId.toLowerCase(),
9011
9988
  status: r.status
9012
9989
  }));
9013
9990
  const uniqueStatuses = Array.from(new Set(normalized.map((r) => r.status)));
@@ -9019,10 +9996,11 @@ function create$3(db) {
9019
9996
  for (const status of uniqueStatuses) if (!statusMap.has(status)) throw new Error(`Unknown validation status: ${status}`);
9020
9997
  const values = normalized.map((row) => ({
9021
9998
  offerHash: row.offerHash,
9999
+ obligationId: row.obligationId,
9022
10000
  statusId: statusMap.get(row.status)
9023
10001
  }));
9024
10002
  for (const batch of batch$1(values, DEFAULT_BATCH_SIZE)) await db.insert(validations).values(batch).onConflictDoUpdate({
9025
- target: [validations.offerHash],
10003
+ target: [validations.offerHash, validations.obligationId],
9026
10004
  set: {
9027
10005
  statusId: sql`excluded.status_id`,
9028
10006
  updatedAt: sql`NOW()`
@@ -9031,6 +10009,18 @@ function create$3(db) {
9031
10009
  }
9032
10010
  };
9033
10011
  }
10012
+ const HEX_32$1 = /^0x[a-fA-F0-9]{64}$/;
10013
+ function parseCursor$1(cursor) {
10014
+ const [offerHash, obligationId] = cursor.split(":");
10015
+ if (!offerHash || !obligationId || !HEX_32$1.test(offerHash) || !HEX_32$1.test(obligationId)) throw new Error("Invalid cursor format");
10016
+ return {
10017
+ offerHash: offerHash.toLowerCase(),
10018
+ obligationId: obligationId.toLowerCase()
10019
+ };
10020
+ }
10021
+ function formatCursor$1(input) {
10022
+ return `${input.offerHash.toLowerCase()}:${input.obligationId.toLowerCase()}`;
10023
+ }
9034
10024
 
9035
10025
  //#endregion
9036
10026
  //#region src/database/readers/ObligationsListing.ts
@@ -9073,23 +10063,22 @@ function create$2(parameters) {
9073
10063
  const loanTokenFilter = loanTokens !== void 0 && loanTokens.length > 0 ? sql`(${sql.join(loanTokens.map((token) => sql`LOWER(${obligations.loanToken}) = ${token.toLowerCase()}`), sql` OR `)})` : void 0;
9074
10064
  const collateralFilter = collateralTokens !== void 0 && collateralTokens.length > 0 ? sql`EXISTS (
9075
10065
  SELECT 1 FROM ${obligationCollateralsV2} oc
9076
- WHERE oc.obligation_id = ${obligations.obligationId}
10066
+ WHERE oc.obligation_key = ${obligations.obligationKey}
9077
10067
  AND (${sql.join(collateralTokens.map((token) => sql`LOWER(oc.asset) = ${token.toLowerCase()}`), sql` OR `)})
9078
10068
  )` : void 0;
9079
- const bestAskTick = db.select({ askTick: offers.tick }).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(eq(offers.obligationId, obligations.obligationId), eq(offers.buy, false), gte(offers.expiry, now$1), gte(offers.maturity, now$1), lte(offers.start, now$1), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(desc(offers.tick)).limit(1).as("best_ask_tick");
9080
- const bestBidTick = db.select({ bidTick: offers.tick }).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(eq(offers.obligationId, obligations.obligationId), eq(offers.buy, true), gte(offers.expiry, now$1), gte(offers.maturity, now$1), lte(offers.start, now$1), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(asc(offers.tick)).limit(1).as("best_bid_tick");
10069
+ const bestAskTick = db.select({ askTick: offers.tick }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, and(eq(offers.hash, validations.offerHash), eq(offers.obligationId, validations.obligationId))).leftJoin(status, eq(validations.statusId, status.id)).where(and(eq(offers.obligationId, obligationIdKeys.obligationId), eq(offers.buy, false), gte(offers.expiry, now$1), gte(offers.maturity, now$1), lte(offers.start, now$1), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(desc(offers.tick)).limit(1).as("best_ask_tick");
10070
+ const bestBidTick = db.select({ bidTick: offers.tick }).from(offers).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).leftJoin(validations, and(eq(offers.hash, validations.offerHash), eq(offers.obligationId, validations.obligationId))).leftJoin(status, eq(validations.statusId, status.id)).where(and(eq(offers.obligationId, obligationIdKeys.obligationId), eq(offers.buy, true), gte(offers.expiry, now$1), gte(offers.maturity, now$1), lte(offers.start, now$1), sql`(${status.code} IS NULL OR ${status.code} = ${Status.VALID})`)).orderBy(asc(offers.tick)).limit(1).as("best_bid_tick");
9081
10071
  const obligationsWithQuotes = db.select({
9082
- obligationId: obligations.obligationId,
9083
- chainId: obligations.chainId,
10072
+ obligationId: obligationIdKeys.obligationId,
10073
+ chainId: obligationIdKeys.chainId,
9084
10074
  loanToken: obligations.loanToken,
9085
- collaterals: sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${oracles.address}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
10075
+ collaterals: sql`ARRAY_AGG(jsonb_build_object('asset', ${obligationCollateralsV2.asset}, 'oracle', ${obligationCollateralsV2.oracleAddress}, 'lltv', ${obligationCollateralsV2.lltv}))`.as("collaterals"),
9086
10076
  maturity: obligations.maturity,
9087
10077
  askTick: sql`MAX(${bestAskTick.askTick})`.as("ask_tick"),
9088
10078
  bidTick: sql`MAX(${bestBidTick.bidTick})`.as("bid_tick"),
9089
10079
  ask: sql`COALESCE(MAX(${bestAskTick.askTick}) + 1, 0)`.as("ask"),
9090
10080
  bid: sql`COALESCE(MAX(${bestBidTick.bidTick}) + 1, 0)`.as("bid")
9091
- }).from(obligations).innerJoin(obligationCollateralsV2, eq(obligations.obligationId, obligationCollateralsV2.obligationId)).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
9092
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).leftJoinLateral(bestAskTick, sql`true`).leftJoinLateral(bestBidTick, sql`true`).groupBy(obligations.obligationId).where(and(ids !== void 0 && ids.length > 0 ? inArray(obligations.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? inArray(obligations.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? inArray(obligations.maturity, maturities) : gte(obligations.maturity, now$1), collateralFilter)).as("obligations_with_quotes");
10081
+ }).from(obligationIdKeys).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoin(obligationCollateralsV2, eq(obligations.obligationKey, obligationCollateralsV2.obligationKey)).leftJoinLateral(bestAskTick, sql`true`).leftJoinLateral(bestBidTick, sql`true`).groupBy(obligationIdKeys.obligationId, obligationIdKeys.chainId, obligations.loanToken, obligations.maturity).where(and(ids !== void 0 && ids.length > 0 ? inArray(obligationIdKeys.obligationId, ids) : void 0, chainIds !== void 0 && chainIds.length > 0 ? inArray(obligationIdKeys.chainId, chainIds) : void 0, loanTokenFilter, maturities !== void 0 && maturities.length > 0 ? inArray(obligations.maturity, maturities) : gte(obligations.maturity, now$1), collateralFilter)).as("obligations_with_quotes");
9093
10082
  const sortColumns = {
9094
10083
  id: obligationsWithQuotes.obligationId,
9095
10084
  ask: obligationsWithQuotes.ask,
@@ -9109,22 +10098,25 @@ function create$2(parameters) {
9109
10098
  }).from(obligationsWithQuotes).where(buildCursorFilter(sortColumns, sort, cursorValues)).orderBy(...buildOrderBy(sortColumns, sort)).limit(limit + 1);
9110
10099
  const hasMore = rows.length > limit;
9111
10100
  const listedRows = (hasMore ? rows.slice(0, limit) : rows).map((row) => {
10101
+ const obligation = from$13({
10102
+ loanToken: row.loanToken,
10103
+ collaterals: row.collaterals.sort((left, right) => left.asset.localeCompare(right.asset)).map((collateral) => from$14({
10104
+ asset: collateral.asset,
10105
+ oracle: collateral.oracle,
10106
+ lltv: from$15(BigInt(collateral.lltv))
10107
+ })),
10108
+ maturity: row.maturity
10109
+ });
10110
+ const quote = from$9({
10111
+ obligationId: row.obligationId,
10112
+ ask: { tick: row.askTick },
10113
+ bid: { tick: row.bidTick }
10114
+ });
9112
10115
  return {
9113
- obligation: from$13({
9114
- chainId: row.chainId,
9115
- loanToken: row.loanToken,
9116
- collaterals: row.collaterals.sort((left, right) => left.asset.localeCompare(right.asset)).map((collateral) => from$14({
9117
- asset: collateral.asset,
9118
- oracle: collateral.oracle,
9119
- lltv: from$15(BigInt(collateral.lltv))
9120
- })),
9121
- maturity: row.maturity
9122
- }),
9123
- quote: from$9({
9124
- obligationId: row.obligationId,
9125
- ask: { tick: row.askTick },
9126
- bid: { tick: row.bidTick }
9127
- }),
10116
+ obligationId: row.obligationId,
10117
+ chainId: row.chainId,
10118
+ obligation,
10119
+ quote,
9128
10120
  cursorValues: {
9129
10121
  id: row.obligationId,
9130
10122
  ask: toBigInt(row.ask),
@@ -9142,6 +10134,8 @@ function create$2(parameters) {
9142
10134
  }) : null;
9143
10135
  return {
9144
10136
  obligations: listedRows.map((row) => ({
10137
+ obligationId: row.obligationId,
10138
+ chainId: row.chainId,
9145
10139
  obligation: row.obligation,
9146
10140
  quote: row.quote
9147
10141
  })),
@@ -9445,7 +10439,7 @@ async function postMigrate(driver) {
9445
10439
  const tracer = getTracer("db.postMigrate");
9446
10440
  await startActiveSpan(tracer, "db.postMigrate", async () => {
9447
10441
  await driver.execute(`INSERT INTO "${VERSION}"."status" ("code") VALUES ('${Status.VALID}'), ('${Status.SIMULATION_ERROR}') ON CONFLICT DO NOTHING;`);
9448
- await driver.execute(`INSERT INTO "${VERSION}"."position_types" ("type") VALUES ('${Type.ERC20}'), ('${Type.VAULT_V1}') ON CONFLICT DO NOTHING;`);
10442
+ await driver.execute(`INSERT INTO "${VERSION}"."position_types" ("type") VALUES ('${Type.ERC20}'), ('${Type.VAULT_V1}'), ('${Type.DEBT_OF}'), ('${Type.COLLATERAL_OF}') ON CONFLICT DO NOTHING;`);
9449
10443
  await driver.execute(`
9450
10444
  CREATE OR REPLACE FUNCTION apply_group_consumed_stmt_ins()
9451
10445
  RETURNS trigger
@@ -9599,19 +10593,23 @@ async function postMigrate(driver) {
9599
10593
  BEGIN
9600
10594
  DELETE FROM "${VERSION}"."positions" p
9601
10595
  USING (
9602
- SELECT DISTINCT c.position_chain_id, c.position_contract, c.position_user
10596
+ SELECT DISTINCT c.position_chain_id, c.position_contract, c.position_user, c.position_type_id
9603
10597
  FROM deleted_rows d
9604
10598
  JOIN "${VERSION}"."callbacks" c ON c.id = d.callback_id
9605
10599
  ) AS affected
10600
+ JOIN "${VERSION}"."position_types" pt ON pt.id = affected.position_type_id
9606
10601
  WHERE p.chain_id = affected.position_chain_id
9607
10602
  AND p.contract = affected.position_contract
9608
10603
  AND p."user" = affected.position_user
10604
+ AND p.position_type_id = affected.position_type_id
10605
+ AND pt.type = 'erc20'
9609
10606
  AND NOT EXISTS (
9610
10607
  SELECT 1 FROM "${VERSION}"."callbacks" c2
9611
10608
  JOIN "${VERSION}"."offers_callbacks" oc ON oc.callback_id = c2.id
9612
10609
  WHERE c2.position_chain_id = p.chain_id
9613
10610
  AND c2.position_contract = p.contract
9614
10611
  AND c2.position_user = p."user"
10612
+ AND c2.position_type_id = p.position_type_id
9615
10613
  );
9616
10614
  RETURN NULL;
9617
10615
  END;
@@ -9658,7 +10656,7 @@ async function postMigrate(driver) {
9658
10656
  RETURNS TRIGGER AS $$
9659
10657
  DECLARE
9660
10658
  orphan_obligation_ids TEXT[];
9661
- candidate_oracles RECORD;
10659
+ orphan_obligation_keys TEXT[];
9662
10660
  BEGIN
9663
10661
  -- 1. Find orphan obligation IDs
9664
10662
  SELECT ARRAY_AGG(DISTINCT obligation_id) INTO orphan_obligation_ids
@@ -9666,33 +10664,39 @@ async function postMigrate(driver) {
9666
10664
  WHERE NOT EXISTS (
9667
10665
  SELECT 1 FROM "${VERSION}"."offers" ov
9668
10666
  WHERE ov.obligation_id = d.obligation_id
10667
+ )
10668
+ AND NOT EXISTS (
10669
+ SELECT 1 FROM "${VERSION}"."positions" p
10670
+ JOIN "${VERSION}"."position_types" pt ON pt.id = p.position_type_id
10671
+ WHERE p."contract" = d.obligation_id
10672
+ AND pt.type IN ('debtOf', 'collateralOf')
9669
10673
  );
9670
10674
 
9671
- -- 2. If no orphan obligations, exit early
10675
+ -- 2. If no orphan obligation IDs, exit early
9672
10676
  IF orphan_obligation_ids IS NULL OR array_length(orphan_obligation_ids, 1) IS NULL THEN
9673
10677
  RETURN NULL;
9674
10678
  END IF;
9675
10679
 
9676
- -- 3. Capture candidate oracles (from collaterals of orphan obligations)
9677
- CREATE TEMP TABLE _candidate_oracles ON COMMIT DROP AS
9678
- SELECT DISTINCT oc.oracle_chain_id, oc.oracle_address
9679
- FROM "${VERSION}"."obligation_collaterals_v2" oc
9680
- WHERE oc.obligation_id = ANY(orphan_obligation_ids);
10680
+ -- 3. Delete orphan obligation id keys
10681
+ DELETE FROM "${VERSION}"."obligation_id_keys" oia
10682
+ WHERE oia.obligation_id = ANY(orphan_obligation_ids);
9681
10683
 
9682
- -- 4. Delete orphan obligations (cascades to collaterals)
9683
- DELETE FROM "${VERSION}"."obligations" ob
9684
- WHERE ob.obligation_id = ANY(orphan_obligation_ids);
10684
+ -- 4. Find canonical obligation keys with no remaining obligation id keys
10685
+ SELECT ARRAY_AGG(ob.obligation_key) INTO orphan_obligation_keys
10686
+ FROM "${VERSION}"."obligations" ob
10687
+ WHERE NOT EXISTS (
10688
+ SELECT 1 FROM "${VERSION}"."obligation_id_keys" oia
10689
+ WHERE oia.obligation_key = ob.obligation_key
10690
+ );
9685
10691
 
9686
- -- 5. Delete oracles that are now orphaned (no remaining collateral references)
9687
- DELETE FROM "${VERSION}"."oracles" o
9688
- USING _candidate_oracles co
9689
- WHERE o.chain_id = co.oracle_chain_id
9690
- AND o.address = co.oracle_address
9691
- AND NOT EXISTS (
9692
- SELECT 1 FROM "${VERSION}"."obligation_collaterals_v2" oc
9693
- WHERE oc.oracle_chain_id = o.chain_id
9694
- AND oc.oracle_address = o.address
9695
- );
10692
+ -- 5. If no orphan canonical obligations, exit early
10693
+ IF orphan_obligation_keys IS NULL OR array_length(orphan_obligation_keys, 1) IS NULL THEN
10694
+ RETURN NULL;
10695
+ END IF;
10696
+
10697
+ -- 6. Delete orphan canonical obligations (cascades to collaterals)
10698
+ DELETE FROM "${VERSION}"."obligations" ob
10699
+ WHERE ob.obligation_key = ANY(orphan_obligation_keys);
9696
10700
 
9697
10701
  RETURN NULL;
9698
10702
  END;
@@ -9710,16 +10714,21 @@ async function postMigrate(driver) {
9710
10714
  RETURNS trigger
9711
10715
  LANGUAGE plpgsql AS $$
9712
10716
  BEGIN
9713
- INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", obligation_id, value)
9714
- VALUES (
9715
- OLD.chain_id,
9716
- OLD."user",
9717
- OLD.contract,
9718
- OLD."group",
9719
- OLD.obligation_id,
9720
- OLD.upper::numeric - OLD.lower::numeric
9721
- )
9722
- ON CONFLICT (chain_id, "user", contract, "group", obligation_id) DO NOTHING;
10717
+ BEGIN
10718
+ INSERT INTO "${VERSION}"."offsets" (chain_id, "user", contract, "group", obligation_id, value)
10719
+ VALUES (
10720
+ OLD.chain_id,
10721
+ OLD."user",
10722
+ OLD.contract,
10723
+ OLD."group",
10724
+ OLD.obligation_id,
10725
+ OLD.upper::numeric - OLD.lower::numeric
10726
+ )
10727
+ ON CONFLICT (chain_id, "user", contract, "group", obligation_id) DO NOTHING;
10728
+ EXCEPTION WHEN foreign_key_violation THEN
10729
+ -- lots_positions may be gone during cascaded deletes
10730
+ NULL;
10731
+ END;
9723
10732
  RETURN OLD;
9724
10733
  END;
9725
10734
  $$;
@@ -9742,11 +10751,26 @@ async function postMigrate(driver) {
9742
10751
  AND l.contract = OLD.contract
9743
10752
  AND l."user" = OLD."user"
9744
10753
  ) THEN
9745
- -- No lots remain, delete the position (cascades to offsets)
10754
+ -- No lots remain, delete erc20 positions and their lots_positions bridge rows.
10755
+ -- debtOf/collateralOf positions live independently of offers and must not be deleted here.
10756
+ -- First delete the position (for ERC20, asset = contract).
9746
10757
  DELETE FROM "${VERSION}"."positions" p
9747
- WHERE p.chain_id = OLD.chain_id
9748
- AND p.contract = OLD.contract
9749
- AND p."user" = OLD."user";
10758
+ USING "${VERSION}"."lots_positions" lp
10759
+ JOIN "${VERSION}"."position_types" pt ON pt.id = lp.position_type_id
10760
+ WHERE lp.chain_id = OLD.chain_id
10761
+ AND lp.contract = OLD.contract
10762
+ AND lp."user" = OLD."user"
10763
+ AND p.chain_id = lp.chain_id
10764
+ AND p.contract = lp.contract
10765
+ AND p."user" = lp."user"
10766
+ AND p.position_type_id = lp.position_type_id
10767
+ AND pt.type = 'erc20';
10768
+
10769
+ -- Delete the lots_positions bridge row (cascades to offsets via offsets_lots_positions_fk).
10770
+ DELETE FROM "${VERSION}"."lots_positions" lp
10771
+ WHERE lp.chain_id = OLD.chain_id
10772
+ AND lp.contract = OLD.contract
10773
+ AND lp."user" = OLD."user";
9750
10774
  END IF;
9751
10775
  RETURN NULL;
9752
10776
  END;
@@ -9846,7 +10870,7 @@ function resolveRouterConfig(config, env) {
9846
10870
  const override = config.chains[name];
9847
10871
  if (!override) throw new Error(`Chain ${name} is missing configuration.`);
9848
10872
  const rpcUrl = resolveRpcUrl(name, override, env, indexerChains.includes(name));
9849
- const chainWithContracts = applyChainOverride(chains$2[name], override);
10873
+ const chainWithContracts = applyChainOverride(chains$1[name], override);
9850
10874
  acc[name] = {
9851
10875
  chain: rpcUrl ? applyRpcUrlOverride(chainWithContracts, rpcUrl) : chainWithContracts,
9852
10876
  rpcUrl
@@ -9955,7 +10979,7 @@ function resolveRpcUrls(baseChain, rpcUrl) {
9955
10979
  function createDefaultConfig() {
9956
10980
  const chainName = "ethereum-virtual-testnet";
9957
10981
  const rpcUrl = "https://virtual.mainnet.eu.rpc.tenderly.co/49383bba-e2c5-423e-bc94-8e78c91f1130";
9958
- const baseChain = chains$2[chainName];
10982
+ const baseChain = chains$1[chainName];
9959
10983
  const chainWithRpc = applyRpcUrlOverride(baseChain, rpcUrl);
9960
10984
  const chain = {
9961
10985
  ...chainWithRpc,
@@ -10067,7 +11091,10 @@ const gatekeeperCmd = new RouterCmd("gatekeeper");
10067
11091
  gatekeeperCmd.description("Start Gatekeeper validation service.").action(async (opts) => {
10068
11092
  const { gatekeeper: gatekeeperConfig, chainRegistry, logger } = opts;
10069
11093
  await runWithLogger(logger, async () => {
10070
- const gatekeeperCore = create$21({ rules: morphoRules(chainRegistry.list()) });
11094
+ const gatekeeperCore = create$21({ rules: (chainId) => morphoRules({
11095
+ chains: chainRegistry.list(),
11096
+ chainId
11097
+ }) });
10071
11098
  const handle = await start$1({
10072
11099
  gatekeeper: gatekeeperCore,
10073
11100
  chainRegistry,
@@ -10551,7 +11578,7 @@ async function getObligation(params, db) {
10551
11578
  if (listing.obligations.length === 0) return failure(new NotFoundError("Obligation not found"));
10552
11579
  const obligation = listing.obligations[0];
10553
11580
  return success({
10554
- data: from$5(obligation.obligation, obligation.quote),
11581
+ data: from$5(obligation.obligation, obligation.quote, obligation.chainId),
10555
11582
  cursor: null
10556
11583
  });
10557
11584
  } catch (err) {
@@ -10592,7 +11619,7 @@ async function getObligations(queryParameters, db) {
10592
11619
  limit: query.limit
10593
11620
  });
10594
11621
  return success({
10595
- data: listing.obligations.map((item) => from$5(item.obligation, item.quote)),
11622
+ data: listing.obligations.map((item) => from$5(item.obligation, item.quote, item.chainId)),
10596
11623
  cursor: listing.nextCursor
10597
11624
  });
10598
11625
  } catch (err) {
@@ -10617,23 +11644,20 @@ async function getObligations(queryParameters, db) {
10617
11644
  */
10618
11645
  async function getOffersQuery(db, parameters) {
10619
11646
  const limit = parameters?.limit ?? DEFAULT_LIMIT$3;
10620
- const cursor = parameters?.cursor;
11647
+ const rawCursor = parameters?.cursor;
10621
11648
  const maker = parameters?.maker;
10622
- if (cursor !== null && cursor !== void 0) {
10623
- if (!cursor.startsWith("0x") || cursor.length !== 66) throw new Error("Invalid cursor format");
10624
- }
11649
+ const cursor = maker && rawCursor !== void 0 && rawCursor !== null ? parseMakerCursor(rawCursor) : void 0;
10625
11650
  const now = Math.floor((Date.now() - 1) / 1e3);
10626
11651
  const collateralsLateral = db.select({ collaterals: sql`COALESCE(
10627
11652
  jsonb_agg(
10628
11653
  jsonb_build_object(
10629
11654
  'asset', ${obligationCollateralsV2.asset},
10630
- 'oracle', ${oracles.address},
11655
+ 'oracle', ${obligationCollateralsV2.oracleAddress},
10631
11656
  'lltv', ${obligationCollateralsV2.lltv}
10632
11657
  )
10633
11658
  ),
10634
11659
  '[]'::jsonb
10635
- )`.as("collaterals") }).from(obligationCollateralsV2).innerJoin(oracles, sql`${obligationCollateralsV2.oracleChainId} = ${oracles.chainId}
10636
- AND ${obligationCollateralsV2.oracleAddress} = ${oracles.address}`).where(eq(obligationCollateralsV2.obligationId, offers.obligationId)).as("collaterals_lateral");
11660
+ )`.as("collaterals") }).from(obligationCollateralsV2).where(eq(obligationCollateralsV2.obligationKey, obligationIdKeys.obligationKey)).as("collaterals_lateral");
10637
11661
  const lotBalanceExpr = sql`GREATEST(0, LEAST(
10638
11662
  COALESCE(${positions.balance}, 0)::numeric
10639
11663
  + COALESCE((
@@ -10678,6 +11702,7 @@ async function getOffersQuery(db, parameters) {
10678
11702
  AND LOWER(${lots.user}) = LOWER(${callbacks.positionUser})
10679
11703
  AND LOWER(${lots.group}) = LOWER(${offers.group})
10680
11704
  WHERE ${offersCallbacks.offerHash} = ${offers.hash}
11705
+ AND ${offersCallbacks.obligationId} = ${offers.obligationId}
10681
11706
  ORDER BY
10682
11707
  ${callbacks.positionChainId},
10683
11708
  LOWER(${callbacks.positionContract}),
@@ -10687,6 +11712,7 @@ async function getOffersQuery(db, parameters) {
10687
11712
  ), 0)`;
10688
11713
  const rows = (await db.select({
10689
11714
  hash: offers.hash,
11715
+ obligationId: offers.obligationId,
10690
11716
  maker: offers.groupMaker,
10691
11717
  assets: offers.assets,
10692
11718
  obligationUnits: offers.obligationUnits,
@@ -10699,7 +11725,7 @@ async function getOffersQuery(db, parameters) {
10699
11725
  group: offers.group,
10700
11726
  session: offers.session,
10701
11727
  buy: offers.buy,
10702
- chainId: obligations.chainId,
11728
+ chainId: obligationIdKeys.chainId,
10703
11729
  loanToken: obligations.loanToken,
10704
11730
  callbackAddress: offers.callbackAddress,
10705
11731
  callbackData: offers.callbackData,
@@ -10716,7 +11742,7 @@ async function getOffersQuery(db, parameters) {
10716
11742
  )
10717
11743
  END
10718
11744
  ))`.as("takeable")
10719
- }).from(offers).innerJoin(obligations, eq(offers.obligationId, obligations.obligationId)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== null && cursor !== void 0 ? gt(offers.hash, cursor) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, gte(offers.expiry, now), gte(offers.maturity, now), maker === void 0 ? sql`GREATEST(0,
11745
+ }).from(offers).innerJoin(obligationIdKeys, eq(offers.obligationId, obligationIdKeys.obligationId)).innerJoin(obligations, eq(obligationIdKeys.obligationKey, obligations.obligationKey)).innerJoin(groups, and(eq(offers.groupChainId, groups.chainId), eq(offers.groupMaker, groups.maker), eq(offers.group, groups.group))).innerJoinLateral(collateralsLateral, sql`true`).where(and(cursor !== void 0 ? cursor.obligationId === void 0 ? gt(offers.hash, cursor.hash) : or(gt(offers.hash, cursor.hash), and(eq(offers.hash, cursor.hash), gt(offers.obligationId, cursor.obligationId))) : void 0, maker !== void 0 ? eq(offers.groupMaker, maker.toLowerCase()) : void 0, gte(offers.expiry, now), gte(offers.maturity, now), maker === void 0 ? sql`GREATEST(0,
10720
11746
  CASE WHEN ${offers.buy} = false
10721
11747
  THEN ${offers.assets}::numeric - ${groups.consumed}::numeric
10722
11748
  ELSE LEAST(
@@ -10724,10 +11750,11 @@ async function getOffersQuery(db, parameters) {
10724
11750
  ${availableExpr}::numeric
10725
11751
  )
10726
11752
  END
10727
- ) > 0` : void 0)).orderBy(asc(offers.hash)).limit(limit)).map((row) => {
11753
+ ) > 0` : void 0)).orderBy(asc(offers.hash), asc(offers.obligationId)).limit(limit)).map((row) => {
10728
11754
  const receiverIfMakerIsSeller = (row.receiverIfMakerIsSeller ?? row.maker).toLowerCase();
10729
11755
  return {
10730
11756
  hash: row.hash,
11757
+ obligationId: row.obligationId,
10731
11758
  maker: row.maker,
10732
11759
  assets: BigInt(row.assets),
10733
11760
  obligationUnits: BigInt(row.obligationUnits),
@@ -10759,7 +11786,7 @@ async function getOffersQuery(db, parameters) {
10759
11786
  });
10760
11787
  return {
10761
11788
  rows,
10762
- nextCursor: rows.length === limit ? rows[rows.length - 1].hash : null
11789
+ nextCursor: rows.length === limit ? maker === void 0 ? rows[rows.length - 1].hash : formatOfferCursor(rows[rows.length - 1]) : null
10763
11790
  };
10764
11791
  }
10765
11792
  async function getOffers(queryParameters, db) {
@@ -10778,12 +11805,18 @@ async function getOffers(queryParameters, db) {
10778
11805
  cursor: query.cursor,
10779
11806
  limit: query.limit
10780
11807
  });
10781
- const hashes = rows.map((row) => row.hash);
10782
- const attestationMap = await db.trees.getAttestations(hashes);
11808
+ const offers = rows.map((row) => ({
11809
+ offerHash: row.hash,
11810
+ obligationId: row.obligationId
11811
+ }));
11812
+ const attestationMap = await db.trees.getAttestations(offers);
10783
11813
  return success({
10784
11814
  data: rows.map((row) => {
10785
- const hash$1 = hash(row);
10786
- const attestation = attestationMap.get(hash$1);
11815
+ const key = toAttestationKey({
11816
+ offerHash: row.hash,
11817
+ obligationId: row.obligationId
11818
+ });
11819
+ const attestation = attestationMap.get(key);
10787
11820
  return from$4({
10788
11821
  ...row,
10789
11822
  ...attestation
@@ -10801,6 +11834,19 @@ async function getOffers(queryParameters, db) {
10801
11834
  return failure(err);
10802
11835
  }
10803
11836
  }
11837
+ function parseMakerCursor(cursor) {
11838
+ const [rawHash, rawObligationId, ...tail] = cursor.split(":");
11839
+ if (tail.length > 0) throw new Error("Invalid cursor format");
11840
+ if (!rawHash || !rawObligationId || !HEX_32.test(rawHash) || !HEX_32.test(rawObligationId)) throw new Error("Invalid cursor format");
11841
+ return {
11842
+ hash: rawHash.toLowerCase(),
11843
+ obligationId: rawObligationId.toLowerCase()
11844
+ };
11845
+ }
11846
+ function formatOfferCursor(row) {
11847
+ return `${row.hash.toLowerCase()}:${row.obligationId.toLowerCase()}`;
11848
+ }
11849
+ const HEX_32 = /^0x[a-fA-F0-9]{64}$/;
10804
11850
 
10805
11851
  //#endregion
10806
11852
  //#region src/api/Controllers/getUserPositions.ts
@@ -11005,8 +12051,12 @@ function createHttpClient(config) {
11005
12051
  body: json
11006
12052
  };
11007
12053
  };
11008
- const isAllowed = async (offers) => {
11009
- const { statusCode, body } = await validate({ offers: offers.map((offer) => toSnakeCase(offer)) });
12054
+ const isAllowed = async (parameters) => {
12055
+ const { offers, chainId } = parameters;
12056
+ const { statusCode, body } = await validate({
12057
+ chain_id: chainId,
12058
+ offers: offers.map((offer) => toSnakeCase(offer))
12059
+ });
11010
12060
  if (statusCode !== 200) {
11011
12061
  const errorMessage = extractErrorMessage(body);
11012
12062
  throw new Error(`Gatekeeper validation failed: ${errorMessage ?? `status ${statusCode}`}`);
@@ -11064,7 +12114,10 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
11064
12114
  let gatekeeperUrl = gatekeeperConfig?.url;
11065
12115
  let gatekeeperHandle = null;
11066
12116
  if (!gatekeeperUrl) {
11067
- const gatekeeperCore = create$21({ rules: morphoRules(chainRegistry.list()) });
12117
+ const gatekeeperCore = create$21({ rules: (chainId) => morphoRules({
12118
+ chains: chainRegistry.list(),
12119
+ chainId
12120
+ }) });
11068
12121
  gatekeeperHandle = await start$1({
11069
12122
  gatekeeper: gatekeeperCore,
11070
12123
  chainRegistry,
@@ -11096,17 +12149,23 @@ startCmd.description("Start Router services.").addOption(new Option("--mock <n>"
11096
12149
  }
11097
12150
  if (file) {
11098
12151
  const offers = await getOffersFromFile(file);
12152
+ const offersOnChain = withSingleChainContext({
12153
+ offers,
12154
+ chainRegistry
12155
+ });
11099
12156
  const offerBlockNumber = 1;
11100
12157
  const positionBlockNumber = offerBlockNumber + 1;
11101
12158
  await seedOffers({
11102
12159
  db,
11103
- offers,
12160
+ chainRegistry,
12161
+ offers: offersOnChain,
11104
12162
  blockNumber: offerBlockNumber
11105
12163
  });
11106
12164
  await seedOfferAssociations({
11107
12165
  db,
12166
+ chainRegistry,
11108
12167
  gatekeeper,
11109
- offers,
12168
+ offers: offersOnChain,
11110
12169
  blockNumber: positionBlockNumber
11111
12170
  });
11112
12171
  logger.info({
@@ -11203,7 +12262,7 @@ async function seedMockOffers(parameters) {
11203
12262
  const expiry = now + 3600;
11204
12263
  const maturity = from$16("end_of_next_month");
11205
12264
  const oraclePrice = 10n ** 36n;
11206
- const offers = createMockOffers({
12265
+ const offersOnChain = createMockOffers({
11207
12266
  count,
11208
12267
  chains: configuredChains,
11209
12268
  assetsByChainId,
@@ -11216,23 +12275,35 @@ async function seedMockOffers(parameters) {
11216
12275
  });
11217
12276
  await seedOffers({
11218
12277
  db,
11219
- offers,
12278
+ chainRegistry,
12279
+ offers: offersOnChain,
11220
12280
  blockNumber: offerBlockNumber
11221
12281
  });
11222
- await db.oracles.upsert(seedMockOracles(offers, oraclePrice, positionBlockNumber));
12282
+ await db.oracles.upsert(seedMockOracles(offersOnChain, oraclePrice, positionBlockNumber));
11223
12283
  await seedOfferAssociations({
11224
12284
  db,
12285
+ chainRegistry,
11225
12286
  gatekeeper,
11226
- offers,
12287
+ offers: offersOnChain,
11227
12288
  blockNumber: positionBlockNumber
11228
12289
  });
11229
- return offers;
12290
+ return offersOnChain.map((entry) => entry.offer);
11230
12291
  }
11231
12292
  async function getOffersFromFile(filePath) {
11232
12293
  const content = await readFile(filePath, "utf-8");
11233
12294
  const data = JSON.parse(content);
11234
12295
  return Array.isArray(data) ? data.map(from$12) : [from$12(data)];
11235
12296
  }
12297
+ function withSingleChainContext(parameters) {
12298
+ const { offers, chainRegistry } = parameters;
12299
+ const chains = chainRegistry.list();
12300
+ if (chains.length !== 1) throw new Error(`Offer files require exactly one configured chain because offers are chain-agnostic. Found ${chains.length}.`);
12301
+ const chainId = chains[0].id;
12302
+ return offers.map((offer) => ({
12303
+ offer,
12304
+ chainId
12305
+ }));
12306
+ }
11236
12307
  function createMockOffers(parameters) {
11237
12308
  const { count, chains, assetsByChainId, oraclesByChainId, assetsDecimals, start, expiry, maturity, chainRegistry } = parameters;
11238
12309
  if (chains.length === 0) throw new Error("No chains configured for mock offers");
@@ -11243,7 +12314,6 @@ function createMockOffers(parameters) {
11243
12314
  if (allowedAssets.length === 0) throw new Error(`No allowed assets configured for chain ${chain.id}`);
11244
12315
  const loanToken = allowedAssets[Math.floor(Math.random() * allowedAssets.length)];
11245
12316
  const offer = random({
11246
- chains: [chain],
11247
12317
  loanTokens: [loanToken],
11248
12318
  assetsDecimals,
11249
12319
  start,
@@ -11270,25 +12340,28 @@ function createMockOffers(parameters) {
11270
12340
  oracle: allowedOracles[Math.floor(Math.random() * allowedOracles.length)],
11271
12341
  lltv
11272
12342
  }));
11273
- if (!chainRegistry.getById(offer.chainId)) throw new Error(`Missing chain config for id ${offer.chainId}`);
11274
- return from$12({
11275
- ...offer,
11276
- loanToken,
11277
- collaterals,
11278
- callback: {
11279
- address: zeroAddress,
11280
- data: "0x"
11281
- }
11282
- });
12343
+ if (!chainRegistry.getById(chain.id)) throw new Error(`Missing chain config for id ${chain.id}`);
12344
+ return {
12345
+ chainId: chain.id,
12346
+ offer: from$12({
12347
+ ...offer,
12348
+ loanToken,
12349
+ collaterals,
12350
+ callback: {
12351
+ address: zeroAddress,
12352
+ data: "0x"
12353
+ }
12354
+ })
12355
+ };
11283
12356
  });
11284
12357
  }
11285
12358
  function seedMockOracles(offers, price, blockNumber) {
11286
12359
  const oracleMap = /* @__PURE__ */ new Map();
11287
- for (const offer of offers) for (const collateral of offer.collaterals) {
11288
- const key = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
12360
+ for (const { offer, chainId } of offers) for (const collateral of offer.collaterals) {
12361
+ const key = `${chainId}-${collateral.oracle}`.toLowerCase();
11289
12362
  if (oracleMap.has(key)) continue;
11290
12363
  oracleMap.set(key, from$11({
11291
- chainId: offer.chainId,
12364
+ chainId,
11292
12365
  address: collateral.oracle,
11293
12366
  price: price.toString(),
11294
12367
  blockNumber
@@ -11297,56 +12370,75 @@ function seedMockOracles(offers, price, blockNumber) {
11297
12370
  return Array.from(oracleMap.values());
11298
12371
  }
11299
12372
  async function seedOffers(parameters) {
11300
- const { db, offers, blockNumber } = parameters;
12373
+ const { db, chainRegistry, offers, blockNumber } = parameters;
11301
12374
  if (offers.length === 0) return;
11302
12375
  const dependencies = buildOfferDependencies({
11303
12376
  offers,
11304
- blockNumber
12377
+ blockNumber,
12378
+ chainRegistry
11305
12379
  });
11306
12380
  await db.oracles.upsert(dependencies.oracles);
11307
12381
  await db.obligations.create(dependencies.obligations);
11308
12382
  await db.groups.create(dependencies.groups);
11309
12383
  await db.offers.create([{
11310
12384
  blockNumber,
11311
- offers
12385
+ offers: dependencies.offers
11312
12386
  }]);
11313
12387
  }
11314
12388
  async function seedOfferAssociations(parameters) {
11315
- const { db, offers, blockNumber } = parameters;
12389
+ const { db, chainRegistry, offers, blockNumber } = parameters;
11316
12390
  if (offers.length === 0) return;
11317
12391
  const { callbacks, positions, lots } = buildOfferAssociationsFromOffers({
11318
12392
  offers,
11319
- blockNumber
12393
+ blockNumber,
12394
+ chainRegistry
11320
12395
  });
11321
12396
  if (positions.length > 0) await db.positions.upsert(positions);
11322
12397
  if (callbacks.length > 0) await db.callbacks.upsert(callbacks);
11323
12398
  if (lots.length > 0) await db.lots.create(lots);
11324
12399
  }
11325
12400
  function buildOfferDependencies(parameters) {
11326
- const { offers, blockNumber } = parameters;
12401
+ const { offers, blockNumber, chainRegistry } = parameters;
11327
12402
  const obligationsById = /* @__PURE__ */ new Map();
12403
+ const offersWithIds = [];
11328
12404
  const oraclesByKey = /* @__PURE__ */ new Map();
11329
12405
  const groupsByKey = /* @__PURE__ */ new Map();
11330
- for (const offer of offers) {
11331
- const obligationId$1 = obligationId(offer);
11332
- if (!obligationsById.get(obligationId$1)) obligationsById.set(obligationId$1, from$13({
11333
- chainId: offer.chainId,
11334
- loanToken: offer.loanToken,
11335
- maturity: offer.maturity,
11336
- collaterals: offer.collaterals
11337
- }));
12406
+ for (const { offer, chainId } of offers) {
12407
+ const morphoV2 = resolveMorphoV2Address({
12408
+ chainRegistry,
12409
+ chainId
12410
+ });
12411
+ const obligationId$2 = obligationId(offer, {
12412
+ chainId,
12413
+ morphoV2
12414
+ });
12415
+ offersWithIds.push({
12416
+ offer,
12417
+ obligationId: obligationId$2,
12418
+ chainId
12419
+ });
12420
+ if (!obligationsById.get(obligationId$2)) obligationsById.set(obligationId$2, {
12421
+ obligationId: obligationId$2,
12422
+ chainId,
12423
+ morphoV2,
12424
+ obligation: from$13({
12425
+ loanToken: offer.loanToken,
12426
+ maturity: offer.maturity,
12427
+ collaterals: offer.collaterals
12428
+ })
12429
+ });
11338
12430
  for (const collateral of offer.collaterals) {
11339
- const oracleKey = `${offer.chainId}-${collateral.oracle}`.toLowerCase();
12431
+ const oracleKey = `${chainId}-${collateral.oracle}`.toLowerCase();
11340
12432
  if (!oraclesByKey.has(oracleKey)) oraclesByKey.set(oracleKey, from$11({
11341
- chainId: offer.chainId,
12433
+ chainId,
11342
12434
  address: collateral.oracle,
11343
12435
  price: null,
11344
12436
  blockNumber
11345
12437
  }));
11346
12438
  }
11347
- const groupKey = `${offer.chainId}-${offer.maker}-${offer.group}`.toLowerCase();
12439
+ const groupKey = `${chainId}-${offer.maker}-${offer.group}`.toLowerCase();
11348
12440
  if (!groupsByKey.has(groupKey)) groupsByKey.set(groupKey, {
11349
- chainId: offer.chainId,
12441
+ chainId,
11350
12442
  maker: offer.maker,
11351
12443
  group: offer.group,
11352
12444
  blockNumber
@@ -11354,21 +12446,31 @@ function buildOfferDependencies(parameters) {
11354
12446
  }
11355
12447
  return {
11356
12448
  obligations: Array.from(obligationsById.values()),
12449
+ offers: offersWithIds,
11357
12450
  oracles: Array.from(oraclesByKey.values()),
11358
12451
  groups: Array.from(groupsByKey.values())
11359
12452
  };
11360
12453
  }
11361
12454
  function buildOfferAssociationsFromOffers(parameters) {
11362
- const { offers, blockNumber } = parameters;
12455
+ const { offers, blockNumber, chainRegistry } = parameters;
12456
+ const erc20TypeId = Object.values(Type).indexOf(Type.ERC20) + 1;
11363
12457
  const callbacks = [];
11364
12458
  const positions = [];
11365
12459
  const lots = [];
11366
- for (const offer of offers) {
12460
+ for (const { offer, chainId } of offers) {
11367
12461
  if (!offer.buy) continue;
12462
+ const morphoV2 = resolveMorphoV2Address({
12463
+ chainRegistry,
12464
+ chainId
12465
+ });
11368
12466
  const loanToken = offer.loanToken.toLowerCase();
11369
12467
  const offerHash = hash(offer);
12468
+ const obligationId$1 = obligationId(offer, {
12469
+ chainId,
12470
+ morphoV2
12471
+ });
11370
12472
  positions.push(from$10({
11371
- chainId: offer.chainId,
12473
+ chainId,
11372
12474
  contract: loanToken,
11373
12475
  user: offer.maker,
11374
12476
  type: Type.ERC20,
@@ -11378,19 +12480,22 @@ function buildOfferAssociationsFromOffers(parameters) {
11378
12480
  }));
11379
12481
  callbacks.push({
11380
12482
  offerHash,
12483
+ obligationId: obligationId$1,
11381
12484
  callbacks: [{
11382
- chainId: offer.chainId,
12485
+ chainId,
11383
12486
  contract: loanToken,
11384
12487
  user: offer.maker,
11385
- amount: offer.assets
12488
+ amount: offer.assets,
12489
+ positionTypeId: erc20TypeId
11386
12490
  }]
11387
12491
  });
11388
12492
  lots.push({
11389
- positionChainId: offer.chainId,
12493
+ positionChainId: chainId,
11390
12494
  positionContract: loanToken,
11391
12495
  positionUser: offer.maker,
12496
+ positionTypeId: erc20TypeId,
11392
12497
  group: offer.group,
11393
- obligationId: obligationId(offer),
12498
+ obligationId: obligationId$1,
11394
12499
  size: offer.assets
11395
12500
  });
11396
12501
  }
@@ -11400,6 +12505,13 @@ function buildOfferAssociationsFromOffers(parameters) {
11400
12505
  lots
11401
12506
  };
11402
12507
  }
12508
+ function resolveMorphoV2Address(parameters) {
12509
+ const chain = parameters.chainRegistry.getById(parameters.chainId);
12510
+ if (!chain) throw new Error(`Missing chain configuration for chain ${parameters.chainId}.`);
12511
+ const morphoV2 = chain.custom.morpho.address.toLowerCase();
12512
+ if (morphoV2 === zeroAddress) throw new Error(`Morpho V2 address is zero for chain ${parameters.chainId}.`);
12513
+ return morphoV2;
12514
+ }
11403
12515
 
11404
12516
  //#endregion
11405
12517
  //#region src/cli/cli.ts