@morpho-dev/router 0.1.5 → 0.1.7

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.
@@ -1,13 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  var mempool = require('@morpho-dev/mempool');
4
- var jsBase64 = require('js-base64');
4
+ var viem = require('viem');
5
+ var actions = require('viem/actions');
6
+ var async_hooks = require('async_hooks');
5
7
  var v4 = require('zod/v4');
8
+ var drizzleOrm = require('drizzle-orm');
9
+ var pgCore = require('drizzle-orm/pg-core');
10
+ var jsBase64 = require('js-base64');
6
11
  var zodOpenapi = require('zod-openapi');
7
- var viem = require('viem');
8
12
  var nodeServer = require('@hono/node-server');
9
13
  var hono = require('hono');
10
- var async_hooks = require('async_hooks');
14
+ var path = require('path');
15
+ var pglite = require('@electric-sql/pglite');
16
+ var nodePostgres = require('drizzle-orm/node-postgres');
17
+ var migrator = require('drizzle-orm/node-postgres/migrator');
18
+ var pglite$1 = require('drizzle-orm/pglite');
19
+ var migrator$1 = require('drizzle-orm/pglite/migrator');
20
+ var pg = require('pg');
21
+
22
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
23
+
24
+ var path__default = /*#__PURE__*/_interopDefault(path);
11
25
 
12
26
  var __defProp = Object.defineProperty;
13
27
  var __export = (target, all) => {
@@ -27,7 +41,7 @@ var CallbackType = /* @__PURE__ */ ((CallbackType2) => {
27
41
  return CallbackType2;
28
42
  })(CallbackType || {});
29
43
  function buildLiquidity(parameters) {
30
- const { type, user, contract, chainId, amount, index = 0, updatedAt = /* @__PURE__ */ new Date() } = parameters;
44
+ const { type, user, contract, chainId, amount, index: index2 = 0, updatedAt = /* @__PURE__ */ new Date() } = parameters;
31
45
  if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
32
46
  throw new Error(`CallbackType not implemented: ${type}`);
33
47
  const amountStr = amount.toString();
@@ -46,7 +60,7 @@ function buildLiquidity(parameters) {
46
60
  queue: {
47
61
  queueId: id,
48
62
  availableLiquidityPoolId: id,
49
- index,
63
+ index: index2,
50
64
  updatedAt
51
65
  },
52
66
  pool: {
@@ -68,101 +82,436 @@ function getCallbackIdForOffer(offer) {
68
82
  return null;
69
83
  }
70
84
 
71
- // src/Cursor.ts
72
- var Cursor_exports = {};
73
- __export(Cursor_exports, {
74
- decode: () => decode,
75
- encode: () => encode,
76
- validate: () => validate
85
+ // src/Collector/index.ts
86
+ var Collector_exports = {};
87
+ __export(Collector_exports, {
88
+ createBuyWithEmptyCallbackLiquidityCollector: () => createBuyWithEmptyCallbackLiquidityCollector,
89
+ createChainReorgsCollector: () => createChainReorgsCollector,
90
+ createConsumedEventsCollector: () => createConsumedEventsCollector,
91
+ createMempoolCollector: () => createMempoolCollector,
92
+ start: () => start
77
93
  });
78
- function validate(cursor) {
79
- if (!cursor || typeof cursor !== "object") {
80
- throw new Error("Cursor must be an object");
81
- }
82
- const c = cursor;
83
- if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
84
- throw new Error(
85
- `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
86
- );
87
- }
88
- if (!["asc", "desc"].includes(c.dir)) {
89
- throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
94
+
95
+ // src/Liquidity.ts
96
+ var Liquidity_exports = {};
97
+ __export(Liquidity_exports, {
98
+ fetch: () => fetch2,
99
+ fetchBalancesAndAllowances: () => fetchBalancesAndAllowances,
100
+ serialize: () => serialize
101
+ });
102
+ async function fetchBalancesAndAllowances(parameters) {
103
+ const { client, spender, pairs, options } = parameters;
104
+ if (pairs.length === 0) return /* @__PURE__ */ new Map();
105
+ const batchSize = Math.max(1, options?.batchSize ?? 5e3);
106
+ const retryAttempts = Math.max(1, options?.retryAttempts ?? 3);
107
+ const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 50);
108
+ const blockNumber = options?.blockNumber ? BigInt(options.blockNumber) : void 0;
109
+ const out = /* @__PURE__ */ new Map();
110
+ for (const pairsBatch of mempool.Utils.batch(pairs, batchSize)) {
111
+ const balanceContracts = [];
112
+ const allowanceContracts = [];
113
+ for (const { user, token } of pairsBatch) {
114
+ balanceContracts.push({
115
+ address: token,
116
+ abi: viem.erc20Abi,
117
+ functionName: "balanceOf",
118
+ args: [user]
119
+ });
120
+ allowanceContracts.push({
121
+ address: token,
122
+ abi: viem.erc20Abi,
123
+ functionName: "allowance",
124
+ args: [user, spender]
125
+ });
126
+ }
127
+ const [balances, allowances] = await Promise.all([
128
+ mempool.Utils.retry(
129
+ () => client.multicall({
130
+ allowFailure: false,
131
+ contracts: balanceContracts,
132
+ ...blockNumber ? { blockNumber } : {}
133
+ }),
134
+ retryAttempts,
135
+ retryDelayMs
136
+ ),
137
+ mempool.Utils.retry(
138
+ () => client.multicall({
139
+ allowFailure: false,
140
+ contracts: allowanceContracts,
141
+ ...blockNumber ? { blockNumber } : {}
142
+ }),
143
+ retryAttempts,
144
+ retryDelayMs
145
+ )
146
+ ]);
147
+ for (let i = 0; i < pairsBatch.length; i++) {
148
+ const { user, token } = pairsBatch[i];
149
+ const balance = balances[i];
150
+ const allowance = allowances[i];
151
+ let perUser = out.get(user);
152
+ if (!perUser) {
153
+ perUser = /* @__PURE__ */ new Map();
154
+ out.set(user, perUser);
155
+ }
156
+ perUser.set(token, { balance, allowance });
157
+ }
90
158
  }
91
- if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
92
- throw new Error(
93
- `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
94
- );
159
+ return out;
160
+ }
161
+ async function fetch2(parameters) {
162
+ const { client, chainId, spender, type, pairs, options } = parameters;
163
+ if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
164
+ throw new Error(`CallbackType not implemented: ${type}`);
165
+ const map = await fetchBalancesAndAllowances({
166
+ client,
167
+ spender,
168
+ pairs: pairs.map(({ user, contract }) => ({ user, token: contract })),
169
+ options
170
+ });
171
+ const out = [];
172
+ for (const [user, perContract] of map) {
173
+ for (const [contract, { balance, allowance }] of perContract) {
174
+ const amount = balance < allowance ? balance : allowance;
175
+ out.push(
176
+ buildLiquidity({
177
+ type,
178
+ user,
179
+ contract,
180
+ chainId,
181
+ amount: amount.toString(),
182
+ index: 0
183
+ })
184
+ );
185
+ }
95
186
  }
96
- const validations = {
97
- rate: {
98
- field: "rate",
99
- type: "string",
100
- pattern: /^\d+$/,
101
- error: "numeric string"
102
- },
103
- amount: {
104
- field: "assets",
105
- type: "string",
106
- pattern: /^\d+$/,
107
- error: "numeric string"
108
- },
109
- maturity: {
110
- field: "maturity",
111
- type: "number",
112
- validator: (val) => val > 0,
113
- error: "positive number"
187
+ return out;
188
+ }
189
+ function serialize(liquidity) {
190
+ const normalized = {
191
+ userPosition: {
192
+ id: liquidity.userPosition.id,
193
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
194
+ user: liquidity.userPosition.user,
195
+ chainId: String(liquidity.userPosition.chainId),
196
+ amount: String(liquidity.userPosition.amount)
114
197
  },
115
- expiry: {
116
- field: "expiry",
117
- type: "number",
118
- validator: (val) => val > 0,
119
- error: "positive number"
120
- }
198
+ queues: liquidity.queues.map((queueWithPool) => ({
199
+ queue: {
200
+ queueId: queueWithPool.queue.queueId,
201
+ availableLiquidityPoolId: queueWithPool.queue.availableLiquidityPoolId,
202
+ index: queueWithPool.queue.index
203
+ },
204
+ pool: {
205
+ id: queueWithPool.pool.id,
206
+ amount: String(queueWithPool.pool.amount)
207
+ }
208
+ })).sort(
209
+ (left, right) => {
210
+ const leftQueueId = left.queue.queueId || "";
211
+ const rightQueueId = right.queue.queueId || "";
212
+ if (leftQueueId < rightQueueId) return -1;
213
+ if (leftQueueId > rightQueueId) return 1;
214
+ const leftPoolId = left.pool.id;
215
+ const rightPoolId = right.pool.id;
216
+ if (leftPoolId < rightPoolId) return -1;
217
+ if (leftPoolId > rightPoolId) return 1;
218
+ const leftIndex = left.queue.index;
219
+ const rightIndex = right.queue.index;
220
+ if (leftIndex < rightIndex) return -1;
221
+ if (leftIndex > rightIndex) return 1;
222
+ return 0;
223
+ }
224
+ )
121
225
  };
122
- const validation = validations[c.sort];
123
- if (!validation) {
124
- throw new Error(`Invalid sort field: ${c.sort}`);
125
- }
126
- const fieldValue = c[validation.field];
127
- if (!fieldValue) {
128
- throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
129
- }
130
- if (typeof fieldValue !== validation.type) {
131
- throw new Error(
132
- `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
133
- );
134
- }
135
- if (validation.pattern && !validation.pattern.test(fieldValue)) {
136
- throw new Error(
137
- `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
226
+ return JSON.stringify(normalized);
227
+ }
228
+
229
+ // src/Collector/BuyWithEmptyCallbackLiquidityCollector.ts
230
+ function createBuyWithEmptyCallbackLiquidityCollector(params) {
231
+ const {
232
+ client,
233
+ collectorBlockStore,
234
+ liquidityStore,
235
+ offerStore,
236
+ chain,
237
+ options: { maxBatchSize, interval } = {}
238
+ } = params;
239
+ const collectorName = "buy_with_empty_callback_liquidity";
240
+ async function getUniqueUserTokenPairs() {
241
+ const unique = /* @__PURE__ */ new Set();
242
+ const pairs = [];
243
+ let cursor;
244
+ do {
245
+ const { offers: offers2, nextCursor } = await offerStore.getAll({
246
+ query: {
247
+ side: "buy",
248
+ chains: [Number(chain.id)],
249
+ cursor,
250
+ limit: 1e3
251
+ }
252
+ });
253
+ cursor = nextCursor ?? void 0;
254
+ for (const offer of offers2) {
255
+ const creator = offer.offering;
256
+ const loanToken = offer.loanToken;
257
+ const key = `${offer.offering}-${offer.loanToken}`.toLowerCase();
258
+ if (unique.has(key)) continue;
259
+ unique.add(key);
260
+ pairs.push({ user: creator, token: loanToken });
261
+ }
262
+ } while (cursor);
263
+ return pairs;
264
+ }
265
+ const collect = mempool.Utils.lazy((emit) => {
266
+ return mempool.Utils.poll(
267
+ async () => {
268
+ const publicClient = client.extend(viem.publicActions);
269
+ const pairs = await getUniqueUserTokenPairs();
270
+ const latestBlockNumber = await actions.getBlockNumber(publicClient);
271
+ const liquidityEntries = await fetch2({
272
+ client: publicClient,
273
+ chainId: chain.id,
274
+ spender: chain.morpho,
275
+ type: "buy_with_empty_callback" /* BuyWithEmptyCallback */,
276
+ pairs: pairs.map(({ user, token }) => ({ user, contract: token })),
277
+ options: {
278
+ blockNumber: Number(latestBlockNumber),
279
+ batchSize: maxBatchSize
280
+ }
281
+ });
282
+ const existingLiquidityEntries = await liquidityStore.getAll();
283
+ const existingById = /* @__PURE__ */ new Map();
284
+ for (const liquidityEntry of existingLiquidityEntries) {
285
+ const liquidityId = liquidityEntry.userPosition.id;
286
+ const serializedEntry = serialize(liquidityEntry);
287
+ existingById.set(liquidityId, serializedEntry);
288
+ }
289
+ for (const liquidity of liquidityEntries) {
290
+ const serialized = serialize(liquidity);
291
+ const prevSerialized = existingById.get(liquidity.userPosition.id);
292
+ if (prevSerialized === serialized) continue;
293
+ await liquidityStore.save({ liquidity });
294
+ }
295
+ await collectorBlockStore.saveBlockNumber({
296
+ collectorName,
297
+ chainId: chain.id,
298
+ blockNumber: Number(latestBlockNumber)
299
+ });
300
+ emit(Number(latestBlockNumber));
301
+ },
302
+ { interval: interval ?? 3e4 }
138
303
  );
139
- }
140
- if (validation.validator && !validation.validator(fieldValue)) {
141
- throw new Error(
142
- `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
304
+ });
305
+ const onReorg = (_lastFinalizedBlockNumber) => {
306
+ };
307
+ return {
308
+ name: collectorName,
309
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName, chainId: chain.id }),
310
+ collect,
311
+ onReorg
312
+ };
313
+ }
314
+ function createChainReorgsCollector(parameters) {
315
+ const collectorName = "chain_reorgs";
316
+ const {
317
+ client,
318
+ subscribers,
319
+ collectorStore,
320
+ chain,
321
+ options: { maxBatchSize = 25, interval } = {}
322
+ } = parameters;
323
+ let finalizedBlock = null;
324
+ let unfinalizedBlocks = [];
325
+ const commonAncestor = (block) => {
326
+ const parent = unfinalizedBlocks.find((b) => b.hash === block.parentHash);
327
+ if (parent) return parent;
328
+ if (finalizedBlock == null) throw new Error("Failed to get common ancestor");
329
+ return finalizedBlock;
330
+ };
331
+ const reconcile = async (block) => {
332
+ if (block.hash === null || block.number === null || block.parentHash === null)
333
+ throw new Error("Failed to get block");
334
+ const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
335
+ if (latestBlock === void 0) {
336
+ unfinalizedBlocks.push({
337
+ hash: block.hash,
338
+ number: block.number,
339
+ parentHash: block.parentHash
340
+ });
341
+ return;
342
+ }
343
+ if (latestBlock.hash === block.hash) return;
344
+ if (latestBlock.number >= block.number) {
345
+ const ancestor = commonAncestor(block);
346
+ console.log(
347
+ `reorg detected, latestBlock.number: ${latestBlock.number} > block.number: ${block.number} on chain ${chain.id}. Ancestor: ${ancestor.number}`
348
+ );
349
+ subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
350
+ unfinalizedBlocks = [];
351
+ return;
352
+ }
353
+ if (latestBlock.number + 1n < block.number) {
354
+ console.log(
355
+ `missing blocks between block ${latestBlock.number} and block ${block.number} on chain ${chain.id}`
356
+ );
357
+ const missingBlockNumbers = (() => {
358
+ const missingBlockNumbers2 = [];
359
+ let start2 = latestBlock.number + 1n;
360
+ const threshold = latestBlock.number + BigInt(maxBatchSize) > block.number ? block.number : latestBlock.number + BigInt(maxBatchSize);
361
+ while (start2 < threshold) {
362
+ missingBlockNumbers2.push(start2);
363
+ start2 = start2 + 1n;
364
+ }
365
+ return missingBlockNumbers2;
366
+ })();
367
+ const missingBlocks = await Promise.all(
368
+ missingBlockNumbers.map(
369
+ (blockNumber) => client.getBlock({ blockNumber, includeTransactions: false })
370
+ )
371
+ );
372
+ for (const missingBlock of missingBlocks) await reconcile(missingBlock);
373
+ await reconcile(block);
374
+ return;
375
+ }
376
+ if (block.parentHash !== latestBlock.hash) {
377
+ const ancestor = commonAncestor(block);
378
+ console.log(
379
+ `reorg detected, block.parentHash: ${block.parentHash} !== latestBlock.hash: ${latestBlock.hash} on chain ${chain.id}. Ancestor: ${ancestor.number}`
380
+ );
381
+ subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
382
+ unfinalizedBlocks = [];
383
+ return;
384
+ }
385
+ unfinalizedBlocks.push({
386
+ hash: block.hash,
387
+ number: block.number,
388
+ parentHash: block.parentHash
389
+ });
390
+ };
391
+ const collect = mempool.Utils.lazy((emit) => {
392
+ let tick = 0;
393
+ const fetchFinalizedBlock = async () => {
394
+ if (tick % 20 === 0 || finalizedBlock === null) {
395
+ finalizedBlock = await client.getBlock({
396
+ blockTag: "finalized",
397
+ includeTransactions: false
398
+ });
399
+ if (finalizedBlock === null) throw new Error("Failed to get finalized block");
400
+ }
401
+ tick++;
402
+ };
403
+ return mempool.Utils.poll(
404
+ async () => {
405
+ await fetchFinalizedBlock();
406
+ const latestBlock = await client.getBlock({
407
+ blockTag: "latest",
408
+ includeTransactions: false
409
+ });
410
+ await reconcile(latestBlock);
411
+ const lastBlockNumber = Number(latestBlock.number);
412
+ await collectorStore.saveBlockNumber({
413
+ collectorName: "chain_reorgs",
414
+ chainId: chain.id,
415
+ blockNumber: lastBlockNumber
416
+ });
417
+ emit(lastBlockNumber);
418
+ },
419
+ { interval: interval ?? 3e4 }
143
420
  );
144
- }
145
- return true;
421
+ });
422
+ return {
423
+ name: collectorName,
424
+ lastSyncedBlock: async () => await collectorStore.getBlockNumber({ collectorName, chainId: chain.id }),
425
+ collect,
426
+ onReorg: (_) => {
427
+ }
428
+ };
146
429
  }
147
- function encode(c) {
148
- return jsBase64.Base64.encodeURL(JSON.stringify(c));
149
- }
150
- function decode(token) {
151
- if (!token) return null;
152
- const decoded = JSON.parse(jsBase64.Base64.decode(token));
153
- validate(decoded);
154
- return decoded;
430
+
431
+ // src/Collector/Collector.ts
432
+ function start(collector) {
433
+ let stopped = false;
434
+ const it = collector.collect();
435
+ (async () => {
436
+ while (!stopped) await it.next();
437
+ await it.return();
438
+ })();
439
+ return () => {
440
+ stopped = true;
441
+ };
155
442
  }
156
443
 
157
- // src/core/apiSchema/index.ts
158
- var apiSchema_exports = {};
159
- __export(apiSchema_exports, {
160
- OpenApi: () => OpenApi,
161
- fromResponse: () => fromResponse,
162
- parse: () => parse,
163
- safeParse: () => safeParse,
164
- toResponse: () => toResponse
444
+ // src/Logger.ts
445
+ var Logger_exports = {};
446
+ __export(Logger_exports, {
447
+ LogLevelValues: () => LogLevelValues,
448
+ defaultLogger: () => defaultLogger,
449
+ getLogger: () => getLogger,
450
+ runWithLogger: () => runWithLogger,
451
+ silentLogger: () => silentLogger
165
452
  });
453
+ var LogLevelValues = [
454
+ "silent",
455
+ "trace",
456
+ "debug",
457
+ "info",
458
+ "warn",
459
+ "error",
460
+ "fatal"
461
+ ];
462
+ function defaultLogger(minLevel) {
463
+ const threshold = minLevel ?? "trace";
464
+ const levelIndexByName = LogLevelValues.reduce(
465
+ (acc, lvl, idx) => {
466
+ acc[lvl] = idx;
467
+ return acc;
468
+ },
469
+ {}
470
+ );
471
+ const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
472
+ return {
473
+ // biome-ignore lint/suspicious/noConsole: console is used for logging
474
+ trace: isEnabled("trace") ? console.trace.bind(console) : () => {
475
+ },
476
+ // biome-ignore lint/suspicious/noConsole: console is used for logging
477
+ debug: isEnabled("debug") ? console.debug.bind(console) : () => {
478
+ },
479
+ // biome-ignore lint/suspicious/noConsole: console is used for logging
480
+ info: isEnabled("info") ? console.info.bind(console) : () => {
481
+ },
482
+ // biome-ignore lint/suspicious/noConsole: console is used for logging
483
+ warn: isEnabled("warn") ? console.warn.bind(console) : () => {
484
+ },
485
+ // biome-ignore lint/suspicious/noConsole: console is used for logging
486
+ error: isEnabled("error") ? console.error.bind(console) : () => {
487
+ },
488
+ fatal: isEnabled("fatal") ? (...args) => console.error("[fatal]", ...args) : () => {
489
+ }
490
+ };
491
+ }
492
+ function silentLogger() {
493
+ return {
494
+ trace: () => {
495
+ },
496
+ debug: () => {
497
+ },
498
+ info: () => {
499
+ },
500
+ warn: () => {
501
+ },
502
+ error: () => {
503
+ },
504
+ fatal: () => {
505
+ }
506
+ };
507
+ }
508
+ var loggerContext = new async_hooks.AsyncLocalStorage();
509
+ function runWithLogger(logger, fn) {
510
+ return loggerContext.run(logger, fn);
511
+ }
512
+ function getLogger() {
513
+ return loggerContext.getStore() ?? defaultLogger();
514
+ }
166
515
 
167
516
  // src/RouterOffer.ts
168
517
  var RouterOffer_exports = {};
@@ -244,154 +593,711 @@ var InvalidRouterOfferError = class extends mempool.Errors.BaseError {
244
593
  }
245
594
  };
246
595
 
247
- // src/core/apiSchema/requests.ts
248
- var MAX_LIMIT = 100;
249
- var DEFAULT_LIMIT = 20;
250
- var MAX_LLTV = 100;
251
- var MIN_LLTV = 0;
252
- var GetOffersQueryParams = v4.z.object({
253
- // Core filtering parameters
254
- creators: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
255
- message: "Creators must be comma-separated Ethereum addresses"
256
- }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
257
- description: "Filter by multiple creator addresses (comma-separated)",
258
- example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
259
- }),
260
- side: v4.z.enum(["buy", "sell"]).optional().meta({
261
- description: "Filter by offer type: buy offers or sell offers",
262
- example: "buy"
263
- }),
264
- chains: v4.z.string().regex(/^\d+(,\d+)*$/, {
265
- message: "Chains must be comma-separated chain IDs"
266
- }).transform((val) => val.split(",").map(Number)).optional().meta({
267
- description: "Filter by multiple blockchain networks (comma-separated chain IDs)",
268
- example: "1,137,10"
269
- }),
270
- loan_tokens: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
271
- message: "Loan assets must be comma-separated Ethereum addresses"
272
- }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
273
- description: "Filter by multiple loan assets (comma-separated)",
274
- example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
275
- }),
276
- status: v4.z.string().regex(/^[a-zA-Z_]+(,[a-zA-Z_]+)*$/, {
277
- message: "Status must be comma-separated status values"
278
- }).transform((val) => val.split(",")).refine((statuses) => statuses.every((status) => OfferStatusValues.includes(status)), {
279
- message: `Invalid status value. Must be one of: ${OfferStatusValues.join(", ")}`
280
- }).optional().meta({
281
- description: `Filter by multiple statuses (comma-separated). Valid values: ${OfferStatusValues.join(", ")}. By default, only offers with 'valid' status are returned.`,
282
- example: "valid,callback_error"
283
- }),
284
- callback_addresses: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
285
- message: "Callback addresses must be comma-separated Ethereum addresses"
286
- }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
287
- description: "Filter by multiple callback addresses (comma-separated)",
288
- example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
289
- }),
290
- // Asset range
291
- min_amount: v4.z.bigint({ coerce: true }).positive({
292
- message: "Min amount must be a positive number"
293
- }).optional().meta({
294
- description: "Minimum amount of assets in the offer",
295
- example: "1000"
296
- }),
297
- max_amount: v4.z.bigint({ coerce: true }).positive({
298
- message: "Max amount must be a positive number"
299
- }).optional().meta({
300
- description: "Maximum amount of assets in the offer",
301
- example: "10000"
302
- }),
303
- // Rate range
304
- min_rate: v4.z.bigint({ coerce: true }).positive({
305
- message: "Min rate must be a positive number"
306
- }).optional().meta({
307
- description: "Minimum rate per asset (in wei)",
308
- example: "500000000000000000"
309
- }),
310
- max_rate: v4.z.bigint({ coerce: true }).positive({
311
- message: "Max rate must be a positive number"
312
- }).optional().meta({
313
- description: "Maximum rate per asset (in wei)",
314
- example: "1500000000000000000"
315
- }),
316
- // Time range
317
- min_maturity: v4.z.coerce.number().int().min(0).optional().meta({
318
- description: "Minimum maturity timestamp (Unix timestamp in seconds)",
319
- example: "1700000000"
320
- }),
321
- max_maturity: v4.z.coerce.number().int().min(0).optional().meta({
322
- description: "Maximum maturity timestamp (Unix timestamp in seconds)",
323
- example: "1800000000"
324
- }),
325
- min_expiry: v4.z.coerce.number().int().optional().meta({
326
- description: "Minimum expiry timestamp (Unix timestamp in seconds)",
327
- example: "1700000000"
328
- }),
329
- max_expiry: v4.z.coerce.number().int().optional().meta({
330
- description: "Maximum expiry timestamp (Unix timestamp in seconds)",
331
- example: "1800000000"
332
- }),
333
- // Collateral filtering
334
- collateral_assets: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
335
- message: "Collateral assets must be comma-separated Ethereum addresses"
336
- }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
337
- description: "Filter by multiple collateral assets (comma-separated)",
338
- example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
339
- }),
340
- collateral_oracles: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
341
- message: "Collateral oracles must be comma-separated Ethereum addresses"
342
- }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
343
- description: "Filter by multiple rate oracles (comma-separated)",
344
- example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
345
- }),
346
- collateral_tuple: v4.z.string().transform((val, ctx) => {
347
- const pattern = /^(0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)(#0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)*$/;
348
- if (!pattern.test(val)) {
349
- ctx.addIssue({
350
- code: "custom",
351
- message: "collateral_tuple has an invalid format",
352
- input: val
353
- });
354
- }
355
- return val.split("#").map((tuple) => {
356
- const parts = tuple.split(":");
357
- if (parts.length === 0 || !parts[0]) {
358
- ctx.addIssue({
359
- code: "custom",
360
- message: "Asset address is required for each collateral tuple",
361
- path: ["asset"],
362
- input: val
596
+ // src/Collector/ConsumedEventsCollector.ts
597
+ function createConsumedEventsCollector(parameters) {
598
+ const {
599
+ client,
600
+ contractAddress,
601
+ offerStore,
602
+ collectorBlockStore,
603
+ options: { maxBatchSize, interval } = {},
604
+ chainId
605
+ } = parameters;
606
+ const logger = getLogger();
607
+ const collectorName = "consumed_events";
608
+ const collect = mempool.Utils.lazy((emit) => {
609
+ return mempool.Utils.poll(
610
+ async () => {
611
+ const lastSyncedBlock = await collectorBlockStore.getBlockNumber({
612
+ collectorName,
613
+ chainId
363
614
  });
364
- return v4.z.NEVER;
365
- }
366
- const asset = parts[0]?.toLowerCase();
367
- const oracle = parts[1]?.toLowerCase();
368
- const lltv = parts[2] ? parseFloat(parts[2]) : void 0;
369
- if (lltv !== void 0 && (lltv < MIN_LLTV || lltv > MAX_LLTV)) {
370
- ctx.addIssue({
371
- code: "custom",
372
- message: `LLTV must be between ${MIN_LLTV} and ${MAX_LLTV} (0-100%)`,
373
- path: ["lltv"],
374
- input: val
615
+ const stream = mempool.Chain.streamLogs({
616
+ client,
617
+ contractAddress,
618
+ event: consumedEvent,
619
+ blockNumberGte: lastSyncedBlock,
620
+ order: "asc",
621
+ options: { maxBatchSize }
375
622
  });
376
- return v4.z.NEVER;
377
- }
378
- let lltvValue;
379
- if (lltv !== void 0) {
380
- try {
381
- lltvValue = mempool.LLTV.from(viem.parseUnits(lltv.toString(), 16));
382
- } catch (e) {
383
- ctx.issues.push({
384
- code: "custom",
385
- message: e instanceof mempool.LLTV.InvalidLLTVError || e instanceof mempool.LLTV.InvalidOptionError ? e.message : "Invalid LLTV.",
386
- input: lltv,
387
- path: ["lltv"]
388
- });
389
- return v4.z.NEVER;
623
+ for await (const logs of stream) {
624
+ if (logs.length === 0) break;
625
+ const parsedLogs = viem.parseEventLogs({ abi: [consumedEvent], logs });
626
+ const offersConsumed = [];
627
+ for (const log of parsedLogs) {
628
+ offersConsumed.push(
629
+ fromConsumedLog({
630
+ blockNumber: log.blockNumber,
631
+ logIndex: log.logIndex,
632
+ chainId: Number(chainId),
633
+ transactionHash: log.transactionHash,
634
+ user: log.args.user,
635
+ nonce: log.args.nonce,
636
+ amount: log.args.amount
637
+ })
638
+ );
639
+ }
640
+ try {
641
+ for (const offerConsumed of offersConsumed) {
642
+ logger.debug?.("updating consumed amount", { offerConsumed });
643
+ await offerStore.updateConsumedAmount({
644
+ id: offerConsumed.id,
645
+ chainId: offerConsumed.chainId,
646
+ offering: offerConsumed.offering,
647
+ nonce: offerConsumed.nonce,
648
+ consumed: offerConsumed.amount
649
+ });
650
+ }
651
+ const lastBlockNumber = Number(logs[logs.length - 1]?.blockNumber);
652
+ await collectorBlockStore.saveBlockNumber({
653
+ collectorName: "consumed_events",
654
+ chainId,
655
+ blockNumber: lastBlockNumber
656
+ });
657
+ emit(lastBlockNumber);
658
+ } catch (err) {
659
+ logger.error({ err }, "Failed to process offer_consumed events");
660
+ }
661
+ }
662
+ },
663
+ { interval: interval ?? 3e4 }
664
+ );
665
+ });
666
+ const onReorg = (_lastFinalizedBlockNumber) => {
667
+ };
668
+ return {
669
+ name: collectorName,
670
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName, chainId }),
671
+ onReorg,
672
+ collect
673
+ };
674
+ }
675
+
676
+ // src/Validation.ts
677
+ var Validation_exports = {};
678
+ __export(Validation_exports, {
679
+ run: () => run
680
+ });
681
+ async function run(parameters) {
682
+ const { items, rules, ctx = {}, chunkSize } = parameters;
683
+ const issues = [];
684
+ let validItems = items.slice();
685
+ for (const rule of rules) {
686
+ if (validItems.length === 0) return { valid: [], issues };
687
+ const indicesToRemove = /* @__PURE__ */ new Set();
688
+ if (rule.kind === "single") {
689
+ for (let i = 0; i < validItems.length; i++) {
690
+ const item = validItems[i];
691
+ const issue = await rule.run(item, ctx);
692
+ if (issue) {
693
+ issues.push({ ...issue, ruleName: rule.name, item });
694
+ indicesToRemove.add(i);
390
695
  }
391
696
  }
392
- return {
393
- asset,
394
- oracle,
697
+ } else if (rule.kind === "batch") {
698
+ const exec = async (slice, offset) => {
699
+ const map = await rule.run(slice, ctx);
700
+ for (let i = 0; i < slice.length; i++) {
701
+ const issue = map.get(i);
702
+ if (issue !== void 0) {
703
+ issues.push({ ...issue, ruleName: rule.name, item: slice[i] });
704
+ indicesToRemove.add(offset + i);
705
+ }
706
+ }
707
+ };
708
+ if (!chunkSize) await exec(validItems, 0);
709
+ else {
710
+ for (let i = 0; i < validItems.length; i += chunkSize) {
711
+ await exec(validItems.slice(i, i + chunkSize), i);
712
+ }
713
+ }
714
+ }
715
+ validItems = validItems.filter((_, i) => !indicesToRemove.has(i));
716
+ }
717
+ return {
718
+ valid: validItems,
719
+ issues
720
+ };
721
+ }
722
+
723
+ // src/ValidationRule.ts
724
+ var ValidationRule_exports = {};
725
+ __export(ValidationRule_exports, {
726
+ batch: () => batch,
727
+ morpho: () => morpho,
728
+ single: () => single
729
+ });
730
+ function single(name, run2) {
731
+ return { kind: "single", name, run: run2 };
732
+ }
733
+ function batch(name, run2) {
734
+ return { kind: "batch", name, run: run2 };
735
+ }
736
+ function morpho() {
737
+ const chainId = single("chain_id", (offer, { chain }) => {
738
+ if (chain.id !== offer.chainId) {
739
+ return {
740
+ message: `Chain ID ${offer.chainId} is not the same as the chain ID in the context (${chain.id})`
741
+ };
742
+ }
743
+ });
744
+ const loanToken = single("loan_token", (offer, { chain }) => {
745
+ const tokens = new Set(
746
+ Array.from(chain.whitelistedAssets.values()).map((a) => a.toLowerCase())
747
+ );
748
+ if (!tokens.has(offer.loanToken.toLowerCase())) {
749
+ return {
750
+ message: `Loan token ${offer.loanToken} is not whitelisted on chain ${offer.chainId}`
751
+ };
752
+ }
753
+ });
754
+ const expiry = single("expiry", (offer, _) => {
755
+ if (offer.expiry < Math.floor(Date.now() / 1e3)) {
756
+ return { message: "Expiry mismatch" };
757
+ }
758
+ });
759
+ const callback = single("empty_callback", (offer, _) => {
760
+ if (!offer.buy || offer.callback.data !== "0x") {
761
+ return { message: "Callback not supported yet." };
762
+ }
763
+ });
764
+ return [
765
+ chainId,
766
+ loanToken,
767
+ expiry,
768
+ // note: callback rule should be the last one, since it does not mean that the offer is forever invalid
769
+ // integrators should be able to choose if they want to keep the offer or not
770
+ callback
771
+ ];
772
+ }
773
+
774
+ // src/Collector/MempoolCollector.ts
775
+ function createMempoolCollector(parameters) {
776
+ const logger = getLogger();
777
+ const {
778
+ mempool: mempool$1,
779
+ offerStore,
780
+ collectorBlockStore,
781
+ chain,
782
+ options: { maxBatchSize, interval } = {}
783
+ } = parameters;
784
+ const collectorName = "mempool_offers";
785
+ const collect = mempool.Utils.lazy((emit) => {
786
+ return mempool$1.watch({
787
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName, chainId: chain.id }),
788
+ polling: { maxBatchSize, interval },
789
+ onOffers: async (offers2, blockNumber) => {
790
+ try {
791
+ const { valid: validOffers, issues } = await run({
792
+ items: offers2,
793
+ rules: morpho(),
794
+ ctx: { chain }
795
+ });
796
+ const invalidOffersToSave = [];
797
+ const issueToStatus = {
798
+ empty_callback: "callback_not_supported",
799
+ sell_offers_empty_callback: "callback_not_supported",
800
+ buy_offers_empty_callback: "callback_error"
801
+ };
802
+ for (const issue of issues) {
803
+ const status = issueToStatus[issue.ruleName];
804
+ if (status) {
805
+ invalidOffersToSave.push({
806
+ offer: issue.item,
807
+ status,
808
+ metadata: { issue: issue.ruleName }
809
+ });
810
+ }
811
+ }
812
+ await offerStore.createMany([
813
+ ...validOffers.map((offer) => ({
814
+ offer,
815
+ status: "valid"
816
+ })),
817
+ ...invalidOffersToSave
818
+ ]);
819
+ } catch (err) {
820
+ logger.error(
821
+ { err },
822
+ "Failed to process offer_created events, falling back to unverified status"
823
+ );
824
+ await offerStore.createMany(
825
+ offers2.map((offer) => ({
826
+ offer,
827
+ status: "unverified",
828
+ metadata: { issue: "Offer processing failed" }
829
+ }))
830
+ );
831
+ }
832
+ await collectorBlockStore.saveBlockNumber({
833
+ collectorName,
834
+ chainId: chain.id,
835
+ blockNumber
836
+ });
837
+ emit(blockNumber);
838
+ }
839
+ });
840
+ });
841
+ const onReorg = (_lastFinalizedBlockNumber) => {
842
+ };
843
+ return {
844
+ name: collectorName,
845
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName, chainId: chain.id }),
846
+ collect,
847
+ onReorg
848
+ };
849
+ }
850
+
851
+ // src/CollectorBlockStore.ts
852
+ var CollectorBlockStore_exports = {};
853
+ __export(CollectorBlockStore_exports, {
854
+ create: () => create,
855
+ memory: () => memory
856
+ });
857
+
858
+ // src/OfferStore/drizzle/schema.ts
859
+ var schema_exports = {};
860
+ __export(schema_exports, {
861
+ VERSION: () => VERSION,
862
+ availableLiquidityPools: () => availableLiquidityPools,
863
+ availableLiquidityQueues: () => availableLiquidityQueues,
864
+ collectorBlockNumbers: () => collectorBlockNumbers,
865
+ consumed: () => consumed,
866
+ offerCollaterals: () => offerCollaterals,
867
+ offerStatus: () => offerStatus,
868
+ offers: () => offers,
869
+ userPositions: () => userPositions
870
+ });
871
+ var VERSION = "offers_v1.1";
872
+ var s = pgCore.pgSchema(VERSION);
873
+ var offers = s.table(
874
+ "offers",
875
+ {
876
+ hash: pgCore.varchar("hash", { length: 66 }).primaryKey(),
877
+ offering: pgCore.varchar("offering", { length: 42 }).notNull(),
878
+ assets: pgCore.numeric("assets", { precision: 78, scale: 0 }).notNull(),
879
+ rate: pgCore.bigint("rate", { mode: "bigint" }).notNull(),
880
+ maturity: pgCore.integer("maturity").notNull(),
881
+ expiry: pgCore.integer("expiry").notNull(),
882
+ start: pgCore.integer("start").notNull(),
883
+ nonce: pgCore.bigint("nonce", { mode: "bigint" }).notNull(),
884
+ buy: pgCore.boolean("buy").notNull(),
885
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
886
+ loanToken: pgCore.varchar("loan_token", { length: 42 }).notNull(),
887
+ callbackAddress: pgCore.varchar("callback_address", { length: 42 }).notNull(),
888
+ callbackData: pgCore.text("callback_data").notNull(),
889
+ callbackGasLimit: pgCore.bigint("callback_gas_limit", { mode: "bigint" }).notNull(),
890
+ signature: pgCore.varchar("signature", { length: 132 }),
891
+ callbackId: pgCore.varchar("callback_id", { length: 256 }),
892
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
893
+ },
894
+ (table) => [
895
+ pgCore.index("offers_offering_idx").on(table.offering),
896
+ pgCore.index("offers_buy_idx").on(table.buy),
897
+ pgCore.index("offers_chain_id_idx").on(table.chainId),
898
+ pgCore.index("offers_loan_token_idx").on(table.loanToken),
899
+ pgCore.index("offers_maturity_idx").on(table.maturity),
900
+ pgCore.index("offers_expiry_idx").on(table.expiry),
901
+ pgCore.index("offers_rate_idx").on(table.rate),
902
+ pgCore.index("offers_assets_idx").on(table.assets),
903
+ // Compound indices for cursor pagination with hash
904
+ pgCore.index("offers_rate_hash_idx").on(table.rate, table.hash),
905
+ pgCore.index("offers_maturity_hash_idx").on(table.maturity, table.hash),
906
+ pgCore.index("offers_expiry_hash_idx").on(table.expiry, table.hash),
907
+ pgCore.index("offers_assets_hash_idx").on(table.assets, table.hash)
908
+ ]
909
+ );
910
+ var offerCollaterals = s.table(
911
+ "offer_collaterals",
912
+ {
913
+ id: pgCore.serial("id").primaryKey(),
914
+ offerHash: pgCore.varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
915
+ asset: pgCore.varchar("asset", { length: 42 }).notNull(),
916
+ oracle: pgCore.varchar("oracle", { length: 42 }).notNull(),
917
+ lltv: pgCore.bigint("lltv", { mode: "bigint" }).notNull()
918
+ },
919
+ (table) => [
920
+ pgCore.index("offer_collaterals_offer_hash_idx").on(table.offerHash),
921
+ pgCore.index("offer_collaterals_asset_idx").on(table.asset),
922
+ pgCore.index("offer_collaterals_oracle_idx").on(table.oracle),
923
+ // Composite index
924
+ pgCore.index("offer_collaterals_tuple_idx").on(table.asset, table.oracle, table.lltv)
925
+ ]
926
+ );
927
+ var offerStatus = s.table(
928
+ "offer_status",
929
+ {
930
+ id: pgCore.serial("id").primaryKey(),
931
+ offerHash: pgCore.varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
932
+ status: pgCore.text("status").$type().notNull(),
933
+ metadata: pgCore.jsonb("metadata"),
934
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
935
+ },
936
+ (table) => [
937
+ pgCore.index("offer_status_offer_hash_created_at_idx").on(table.offerHash, drizzleOrm.desc(table.createdAt)),
938
+ pgCore.index("offer_status_status_idx").on(table.status)
939
+ ]
940
+ );
941
+ var consumed = s.table(
942
+ "consumed_per_user_and_nonce",
943
+ {
944
+ id: pgCore.varchar("id", { length: 255 }).primaryKey(),
945
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
946
+ offering: pgCore.varchar("offering", { length: 42 }).notNull(),
947
+ nonce: pgCore.bigint("nonce", { mode: "bigint" }).notNull(),
948
+ consumed: pgCore.numeric("consumed", { precision: 78, scale: 0 }).notNull(),
949
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
950
+ },
951
+ (table) => [
952
+ pgCore.index("consumed_per_user_and_nonce_chain_id_offering_nonce_created_at_idx").on(
953
+ table.chainId,
954
+ table.offering,
955
+ table.nonce,
956
+ drizzleOrm.desc(table.createdAt)
957
+ )
958
+ ]
959
+ );
960
+ var collectorBlockNumbers = s.table(
961
+ "collector_block_numbers",
962
+ {
963
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
964
+ name: pgCore.text("name").notNull(),
965
+ blockNumber: pgCore.bigint("block_number", { mode: "number" }).notNull(),
966
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
967
+ },
968
+ (table) => [pgCore.uniqueIndex("collector_block_numbers_chain_name_idx").on(table.chainId, table.name)]
969
+ );
970
+ var availableLiquidityPools = s.table("available_liquidity_pools", {
971
+ id: pgCore.varchar("id", { length: 255 }).primaryKey(),
972
+ amount: pgCore.numeric("amount", { precision: 78, scale: 0 }).notNull(),
973
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
974
+ });
975
+ var availableLiquidityQueues = s.table(
976
+ "available_liquidity_queues",
977
+ {
978
+ queueId: pgCore.varchar("queue_id", { length: 255 }).notNull(),
979
+ availableLiquidityPoolId: pgCore.varchar("available_liquidity_pool_id", { length: 255 }).notNull().references(() => availableLiquidityPools.id, { onDelete: "cascade" }),
980
+ index: pgCore.integer("index").notNull(),
981
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
982
+ },
983
+ (table) => [
984
+ pgCore.primaryKey({
985
+ columns: [table.queueId, table.availableLiquidityPoolId],
986
+ name: "available_liquidity_queues_pk"
987
+ }),
988
+ pgCore.index("available_liquidity_queues_queue_index_idx").on(table.queueId, table.index)
989
+ ]
990
+ );
991
+ var userPositions = s.table(
992
+ "user_positions",
993
+ {
994
+ id: pgCore.varchar("id", { length: 255 }).primaryKey(),
995
+ availableLiquidityQueueId: pgCore.varchar("available_liquidity_queue_id", { length: 255 }).notNull(),
996
+ user: pgCore.varchar("user", { length: 255 }).notNull(),
997
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
998
+ amount: pgCore.numeric("amount", { precision: 78, scale: 0 }).notNull(),
999
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
1000
+ },
1001
+ (table) => [
1002
+ pgCore.index("user_positions_available_liquidity_queue_id_idx").on(table.availableLiquidityQueueId)
1003
+ ]
1004
+ );
1005
+
1006
+ // src/CollectorBlockStore.ts
1007
+ var create = (config) => {
1008
+ const db = config.db;
1009
+ return {
1010
+ getBlockNumber: async (parameters) => {
1011
+ const name = parameters.collectorName.toLowerCase();
1012
+ const result = await db.select({ blockNumber: collectorBlockNumbers.blockNumber }).from(collectorBlockNumbers).where(
1013
+ drizzleOrm.and(
1014
+ drizzleOrm.eq(collectorBlockNumbers.name, name),
1015
+ drizzleOrm.eq(collectorBlockNumbers.chainId, parameters.chainId)
1016
+ )
1017
+ ).limit(1);
1018
+ if (result.length === 0)
1019
+ return mempool.Chain.getChain(parameters.chainId)?.mempool.deploymentBlock || 0;
1020
+ return Number(result[0].blockNumber);
1021
+ },
1022
+ saveBlockNumber: async (parameters) => {
1023
+ const name = parameters.collectorName.toLowerCase();
1024
+ await db.insert(collectorBlockNumbers).values({
1025
+ chainId: parameters.chainId,
1026
+ name,
1027
+ blockNumber: parameters.blockNumber,
1028
+ updatedAt: /* @__PURE__ */ new Date()
1029
+ }).onConflictDoUpdate({
1030
+ target: [collectorBlockNumbers.chainId, collectorBlockNumbers.name],
1031
+ set: {
1032
+ blockNumber: parameters.blockNumber,
1033
+ updatedAt: /* @__PURE__ */ new Date()
1034
+ }
1035
+ });
1036
+ }
1037
+ };
1038
+ };
1039
+ function memory() {
1040
+ const blockNumbers = /* @__PURE__ */ new Map();
1041
+ return {
1042
+ getBlockNumber: async (parameters) => {
1043
+ const name = parameters.collectorName.toLowerCase();
1044
+ return blockNumbers.get(name)?.get(parameters.chainId) || mempool.Chain.getChain(parameters.chainId)?.mempool.deploymentBlock || 0;
1045
+ },
1046
+ saveBlockNumber: async (parameters) => {
1047
+ const name = parameters.collectorName.toLowerCase();
1048
+ if (!blockNumbers.has(name)) {
1049
+ blockNumbers.set(name, /* @__PURE__ */ new Map());
1050
+ }
1051
+ if (!blockNumbers.get(name)?.has(parameters.chainId)) {
1052
+ blockNumbers.get(name).set(parameters.chainId, parameters.blockNumber);
1053
+ }
1054
+ blockNumbers.get(name).set(parameters.chainId, parameters.blockNumber);
1055
+ }
1056
+ };
1057
+ }
1058
+
1059
+ // src/Cursor.ts
1060
+ var Cursor_exports = {};
1061
+ __export(Cursor_exports, {
1062
+ decode: () => decode,
1063
+ encode: () => encode,
1064
+ validate: () => validate
1065
+ });
1066
+ function validate(cursor) {
1067
+ if (!cursor || typeof cursor !== "object") {
1068
+ throw new Error("Cursor must be an object");
1069
+ }
1070
+ const c = cursor;
1071
+ if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
1072
+ throw new Error(
1073
+ `Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
1074
+ );
1075
+ }
1076
+ if (!["asc", "desc"].includes(c.dir)) {
1077
+ throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
1078
+ }
1079
+ if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
1080
+ throw new Error(
1081
+ `Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
1082
+ );
1083
+ }
1084
+ const validations = {
1085
+ rate: {
1086
+ field: "rate",
1087
+ type: "string",
1088
+ pattern: /^\d+$/,
1089
+ error: "numeric string"
1090
+ },
1091
+ amount: {
1092
+ field: "assets",
1093
+ type: "string",
1094
+ pattern: /^\d+$/,
1095
+ error: "numeric string"
1096
+ },
1097
+ maturity: {
1098
+ field: "maturity",
1099
+ type: "number",
1100
+ validator: (val) => val > 0,
1101
+ error: "positive number"
1102
+ },
1103
+ expiry: {
1104
+ field: "expiry",
1105
+ type: "number",
1106
+ validator: (val) => val > 0,
1107
+ error: "positive number"
1108
+ }
1109
+ };
1110
+ const validation = validations[c.sort];
1111
+ if (!validation) {
1112
+ throw new Error(`Invalid sort field: ${c.sort}`);
1113
+ }
1114
+ const fieldValue = c[validation.field];
1115
+ if (!fieldValue) {
1116
+ throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
1117
+ }
1118
+ if (typeof fieldValue !== validation.type) {
1119
+ throw new Error(
1120
+ `${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
1121
+ );
1122
+ }
1123
+ if (validation.pattern && !validation.pattern.test(fieldValue)) {
1124
+ throw new Error(
1125
+ `Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
1126
+ );
1127
+ }
1128
+ if (validation.validator && !validation.validator(fieldValue)) {
1129
+ throw new Error(
1130
+ `Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
1131
+ );
1132
+ }
1133
+ return true;
1134
+ }
1135
+ function encode(c) {
1136
+ return jsBase64.Base64.encodeURL(JSON.stringify(c));
1137
+ }
1138
+ function decode(token) {
1139
+ if (!token) return null;
1140
+ const decoded = JSON.parse(jsBase64.Base64.decode(token));
1141
+ validate(decoded);
1142
+ return decoded;
1143
+ }
1144
+
1145
+ // src/core/apiSchema/index.ts
1146
+ var apiSchema_exports = {};
1147
+ __export(apiSchema_exports, {
1148
+ OpenApi: () => OpenApi,
1149
+ fromResponse: () => fromResponse,
1150
+ parse: () => parse,
1151
+ safeParse: () => safeParse,
1152
+ toResponse: () => toResponse
1153
+ });
1154
+ var MAX_LIMIT = 100;
1155
+ var DEFAULT_LIMIT = 20;
1156
+ var MAX_LLTV = 100;
1157
+ var MIN_LLTV = 0;
1158
+ var GetOffersQueryParams = v4.z.object({
1159
+ // Core filtering parameters
1160
+ creators: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
1161
+ message: "Creators must be comma-separated Ethereum addresses"
1162
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
1163
+ description: "Filter by multiple creator addresses (comma-separated)",
1164
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
1165
+ }),
1166
+ side: v4.z.enum(["buy", "sell"]).optional().meta({
1167
+ description: "Filter by offer type: buy offers or sell offers",
1168
+ example: "buy"
1169
+ }),
1170
+ chains: v4.z.string().regex(/^\d+(,\d+)*$/, {
1171
+ message: "Chains must be comma-separated chain IDs"
1172
+ }).transform((val) => val.split(",").map(Number)).optional().meta({
1173
+ description: "Filter by multiple blockchain networks (comma-separated chain IDs)",
1174
+ example: "1,137,10"
1175
+ }),
1176
+ loan_tokens: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
1177
+ message: "Loan assets must be comma-separated Ethereum addresses"
1178
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
1179
+ description: "Filter by multiple loan assets (comma-separated)",
1180
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
1181
+ }),
1182
+ status: v4.z.string().regex(/^[a-zA-Z_]+(,[a-zA-Z_]+)*$/, {
1183
+ message: "Status must be comma-separated status values"
1184
+ }).transform((val) => val.split(",")).refine((statuses) => statuses.every((status) => OfferStatusValues.includes(status)), {
1185
+ message: `Invalid status value. Must be one of: ${OfferStatusValues.join(", ")}`
1186
+ }).optional().meta({
1187
+ description: `Filter by multiple statuses (comma-separated). Valid values: ${OfferStatusValues.join(", ")}. By default, only offers with 'valid' status are returned.`,
1188
+ example: "valid,callback_error"
1189
+ }),
1190
+ callback_addresses: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
1191
+ message: "Callback addresses must be comma-separated Ethereum addresses"
1192
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
1193
+ description: "Filter by multiple callback addresses (comma-separated)",
1194
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
1195
+ }),
1196
+ // Asset range
1197
+ min_amount: v4.z.bigint({ coerce: true }).positive({
1198
+ message: "Min amount must be a positive number"
1199
+ }).optional().meta({
1200
+ description: "Minimum amount of assets in the offer",
1201
+ example: "1000"
1202
+ }),
1203
+ max_amount: v4.z.bigint({ coerce: true }).positive({
1204
+ message: "Max amount must be a positive number"
1205
+ }).optional().meta({
1206
+ description: "Maximum amount of assets in the offer",
1207
+ example: "10000"
1208
+ }),
1209
+ // Rate range
1210
+ min_rate: v4.z.bigint({ coerce: true }).positive({
1211
+ message: "Min rate must be a positive number"
1212
+ }).optional().meta({
1213
+ description: "Minimum rate per asset (in wei)",
1214
+ example: "500000000000000000"
1215
+ }),
1216
+ max_rate: v4.z.bigint({ coerce: true }).positive({
1217
+ message: "Max rate must be a positive number"
1218
+ }).optional().meta({
1219
+ description: "Maximum rate per asset (in wei)",
1220
+ example: "1500000000000000000"
1221
+ }),
1222
+ // Time range
1223
+ min_maturity: v4.z.coerce.number().int().min(0).optional().meta({
1224
+ description: "Minimum maturity timestamp (Unix timestamp in seconds)",
1225
+ example: "1700000000"
1226
+ }),
1227
+ max_maturity: v4.z.coerce.number().int().min(0).optional().meta({
1228
+ description: "Maximum maturity timestamp (Unix timestamp in seconds)",
1229
+ example: "1800000000"
1230
+ }),
1231
+ min_expiry: v4.z.coerce.number().int().optional().meta({
1232
+ description: "Minimum expiry timestamp (Unix timestamp in seconds)",
1233
+ example: "1700000000"
1234
+ }),
1235
+ max_expiry: v4.z.coerce.number().int().optional().meta({
1236
+ description: "Maximum expiry timestamp (Unix timestamp in seconds)",
1237
+ example: "1800000000"
1238
+ }),
1239
+ // Collateral filtering
1240
+ collateral_assets: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
1241
+ message: "Collateral assets must be comma-separated Ethereum addresses"
1242
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
1243
+ description: "Filter by multiple collateral assets (comma-separated)",
1244
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
1245
+ }),
1246
+ collateral_oracles: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
1247
+ message: "Collateral oracles must be comma-separated Ethereum addresses"
1248
+ }).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
1249
+ description: "Filter by multiple rate oracles (comma-separated)",
1250
+ example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
1251
+ }),
1252
+ collateral_tuple: v4.z.string().transform((val, ctx) => {
1253
+ const pattern = /^(0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)(#0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)*$/;
1254
+ if (!pattern.test(val)) {
1255
+ ctx.addIssue({
1256
+ code: "custom",
1257
+ message: "collateral_tuple has an invalid format",
1258
+ input: val
1259
+ });
1260
+ }
1261
+ return val.split("#").map((tuple) => {
1262
+ const parts = tuple.split(":");
1263
+ if (parts.length === 0 || !parts[0]) {
1264
+ ctx.addIssue({
1265
+ code: "custom",
1266
+ message: "Asset address is required for each collateral tuple",
1267
+ path: ["asset"],
1268
+ input: val
1269
+ });
1270
+ return v4.z.NEVER;
1271
+ }
1272
+ const asset = parts[0]?.toLowerCase();
1273
+ const oracle = parts[1]?.toLowerCase();
1274
+ const lltv = parts[2] ? parseFloat(parts[2]) : void 0;
1275
+ if (lltv !== void 0 && (lltv < MIN_LLTV || lltv > MAX_LLTV)) {
1276
+ ctx.addIssue({
1277
+ code: "custom",
1278
+ message: `LLTV must be between ${MIN_LLTV} and ${MAX_LLTV} (0-100%)`,
1279
+ path: ["lltv"],
1280
+ input: val
1281
+ });
1282
+ return v4.z.NEVER;
1283
+ }
1284
+ let lltvValue;
1285
+ if (lltv !== void 0) {
1286
+ try {
1287
+ lltvValue = mempool.LLTV.from(viem.parseUnits(lltv.toString(), 16));
1288
+ } catch (e) {
1289
+ ctx.issues.push({
1290
+ code: "custom",
1291
+ message: e instanceof mempool.LLTV.InvalidLLTVError || e instanceof mempool.LLTV.InvalidOptionError ? e.message : "Invalid LLTV.",
1292
+ input: lltv,
1293
+ path: ["lltv"]
1294
+ });
1295
+ return v4.z.NEVER;
1296
+ }
1297
+ }
1298
+ return {
1299
+ asset,
1300
+ oracle,
395
1301
  lltv: lltvValue
396
1302
  };
397
1303
  });
@@ -741,19 +1647,19 @@ var OpenApi = zodOpenapi.createDocument({
741
1647
 
742
1648
  // src/core/apiSchema/utils.ts
743
1649
  function toResponse(routerOffer) {
744
- const { consumed, status, metadata, ...offer } = routerOffer;
1650
+ const { consumed: consumed2, status, metadata, ...offer } = routerOffer;
745
1651
  return {
746
1652
  offer,
747
- consumed,
1653
+ consumed: consumed2,
748
1654
  status,
749
1655
  metadata
750
1656
  };
751
1657
  }
752
1658
  function fromResponse(offerResponse) {
753
- const { offer, consumed, status, metadata } = offerResponse;
1659
+ const { offer, consumed: consumed2, status, metadata } = offerResponse;
754
1660
  return {
755
1661
  ...offer,
756
- consumed,
1662
+ consumed: consumed2,
757
1663
  status,
758
1664
  metadata
759
1665
  };
@@ -879,8 +1785,8 @@ async function get(config, parameters) {
879
1785
  if (parameters.limit !== void 0) {
880
1786
  url.searchParams.set("limit", parameters.limit.toString());
881
1787
  }
882
- const { cursor: returnedCursor, data: offers } = await getApi(config, url);
883
- const routerOffers = offers.map(mempool.Format.fromSnakeCase).map(fromResponse);
1788
+ const { cursor: returnedCursor, data: offers2 } = await getApi(config, url);
1789
+ const routerOffers = offers2.map(mempool.Format.fromSnakeCase).map(fromResponse);
884
1790
  return {
885
1791
  cursor: returnedCursor,
886
1792
  offers: routerOffers.map(from).map(toResponse)
@@ -921,8 +1827,8 @@ async function match(config, parameters) {
921
1827
  if (parameters.limit !== void 0) {
922
1828
  url.searchParams.set("limit", parameters.limit.toString());
923
1829
  }
924
- const { cursor: returnedCursor, data: offers } = await getApi(config, url);
925
- const routerOffers = offers.map(mempool.Format.fromSnakeCase).map(fromResponse);
1830
+ const { cursor: returnedCursor, data: offers2 } = await getApi(config, url);
1831
+ const routerOffers = offers2.map(mempool.Format.fromSnakeCase).map(fromResponse);
926
1832
  return {
927
1833
  cursor: returnedCursor,
928
1834
  offers: routerOffers.map(from).map(toResponse)
@@ -934,501 +1840,87 @@ async function getApi(config, url) {
934
1840
  switch (true) {
935
1841
  case pathname.includes("/v1/offers/match"):
936
1842
  action = "match_offers";
937
- break;
938
- case pathname.includes("/v1/offers"):
939
- action = "get_offers";
940
- break;
941
- default:
942
- throw new HttpGetOffersFailedError("Unknown endpoint", {
943
- details: `Unsupported path: ${pathname}`
944
- });
945
- }
946
- const schemaParseResult = safeParse(action, Object.fromEntries(url.searchParams));
947
- if (!schemaParseResult.success) {
948
- throw new HttpGetOffersFailedError(`Invalid URL parameters`, {
949
- details: schemaParseResult.error.issues[0]?.message
950
- });
951
- }
952
- const response = await fetch(url.toString(), {
953
- method: "GET",
954
- headers: config.headers
955
- });
956
- if (!response.ok) {
957
- switch (response.status) {
958
- case 401:
959
- throw new HttpUnauthorizedError();
960
- case 403:
961
- throw new HttpForbiddenError();
962
- case 429:
963
- throw new HttpRateLimitError();
964
- }
965
- throw new HttpGetOffersFailedError(`GET request returned ${response.status}`, {
966
- details: await response.text()
967
- });
968
- }
969
- return response.json();
970
- }
971
- var InvalidUrlError = class extends mempool.Errors.BaseError {
972
- name = "Router.InvalidUrlError";
973
- constructor(url) {
974
- super(`URL "${url}" is not http/https.`);
975
- }
976
- };
977
- var HttpUnauthorizedError = class extends mempool.Errors.BaseError {
978
- name = "Router.HttpUnauthorizedError";
979
- constructor() {
980
- super("Unauthorized.", {
981
- metaMessages: ["Ensure that an API key is provided."]
982
- });
983
- }
984
- };
985
- var HttpForbiddenError = class extends mempool.Errors.BaseError {
986
- name = "Router.HttpForbiddenError";
987
- constructor() {
988
- super("Forbidden.", {
989
- metaMessages: ["Ensure that the API key is valid."]
990
- });
991
- }
992
- };
993
- var HttpRateLimitError = class extends mempool.Errors.BaseError {
994
- name = "Router.HttpRateLimitError";
995
- constructor() {
996
- super("Rate limit exceeded.", {
997
- metaMessages: [
998
- "The number of allowed requests has been exceeded. You must wait for the rate limit to reset."
999
- ]
1000
- });
1001
- }
1002
- };
1003
- var HttpGetOffersFailedError = class extends mempool.Errors.BaseError {
1004
- name = "Router.HttpGetOffersFailedError";
1005
- constructor(message, { details } = {}) {
1006
- super(message, {
1007
- metaMessages: [details]
1008
- });
1009
- }
1010
- };
1011
-
1012
- // src/OfferStore/index.ts
1013
- var OfferStore_exports = {};
1014
- __export(OfferStore_exports, {
1015
- memory: () => memory
1016
- });
1017
- function memory(parameters) {
1018
- const map = parameters.offers;
1019
- const filled = parameters.filled;
1020
- const consumedIds = /* @__PURE__ */ new Set();
1021
- const create = async (parameters2) => {
1022
- if (map.has(parameters2.offer.hash.toLowerCase())) return parameters2.offer.hash;
1023
- const callbackId = getCallbackIdForOffer(parameters2.offer);
1024
- map.set(parameters2.offer.hash.toLowerCase(), {
1025
- ...parameters2.offer,
1026
- ...callbackId ? { callbackId } : {},
1027
- status: parameters2.status,
1028
- metadata: parameters2.metadata
1029
- });
1030
- const chainId = parameters2.offer.chainId;
1031
- const address = parameters2.offer.offering.toLowerCase();
1032
- const nonce = parameters2.offer.nonce;
1033
- const filledForChain = filled.get(chainId) || /* @__PURE__ */ new Map();
1034
- const filledForOffering = filledForChain.get(address) || /* @__PURE__ */ new Map();
1035
- if (!filledForOffering.has(nonce)) filledForOffering.set(nonce, 0n);
1036
- filledForChain.set(address, filledForOffering);
1037
- filled.set(chainId, filledForChain);
1038
- return Promise.resolve(parameters2.offer.hash);
1039
- };
1040
- const sort = (sortBy, sortOrder, a, b) => {
1041
- sortBy = sortBy || "expiry";
1042
- sortOrder = sortOrder || "desc";
1043
- const sortKey = sortBy === "amount" ? "assets" : sortBy;
1044
- if (a[sortKey] === b[sortKey]) {
1045
- if (a.hash === b.hash) return 0;
1046
- return sortOrder === "asc" ? a.hash > b.hash ? 1 : -1 : b.hash > a.hash ? 1 : -1;
1047
- }
1048
- switch (sortBy) {
1049
- case "rate":
1050
- if (a.rate === b.rate) {
1051
- if (a.hash === b.hash) return 0;
1052
- return sortOrder === "asc" ? a.hash > b.hash ? 1 : -1 : a.hash > b.hash ? -1 : 1;
1053
- }
1054
- return sortOrder === "asc" ? a.rate > b.rate ? 1 : -1 : b.rate > a.rate ? 1 : -1;
1055
- case "maturity":
1056
- if (a.maturity === b.maturity) {
1057
- if (a.hash === b.hash) return 0;
1058
- return sortOrder === "asc" ? a.hash > b.hash ? 1 : -1 : a.hash > b.hash ? -1 : 1;
1059
- }
1060
- return sortOrder === "asc" ? a.maturity > b.maturity ? 1 : -1 : b.maturity > a.maturity ? 1 : -1;
1061
- case "expiry":
1062
- if (a.expiry === b.expiry) {
1063
- if (a.hash === b.hash) return 0;
1064
- return sortOrder === "asc" ? a.hash > b.hash ? 1 : -1 : a.hash > b.hash ? -1 : 1;
1065
- }
1066
- return sortOrder === "asc" ? a.expiry > b.expiry ? 1 : -1 : b.expiry > a.expiry ? 1 : -1;
1067
- case "amount":
1068
- if (a.assets === b.assets) {
1069
- if (a.hash === b.hash) return 0;
1070
- return sortOrder === "asc" ? a.hash > b.hash ? 1 : -1 : a.hash > b.hash ? -1 : 1;
1071
- }
1072
- return sortOrder === "asc" ? a.assets > b.assets ? 1 : -1 : b.assets > a.assets ? 1 : -1;
1073
- default:
1074
- if (a.expiry === b.expiry) {
1075
- if (a.hash === b.hash) return 0;
1076
- return sortOrder === "asc" ? a.hash > b.hash ? 1 : -1 : a.hash > b.hash ? -1 : 1;
1077
- }
1078
- return sortOrder === "asc" ? a.expiry > b.expiry ? 1 : -1 : b.expiry > a.expiry ? 1 : -1;
1079
- }
1080
- };
1081
- return {
1082
- create,
1083
- createMany: async (parameters2) => {
1084
- return Promise.all(
1085
- parameters2.map((p) => create({ offer: p.offer, status: p.status, metadata: p.metadata }))
1086
- );
1087
- },
1088
- getAll: async (params) => {
1089
- const { query } = params || {};
1090
- let {
1091
- creators,
1092
- side,
1093
- chains,
1094
- loanTokens,
1095
- status = ["valid"],
1096
- callbackAddresses,
1097
- minAmount,
1098
- maxAmount,
1099
- minRate,
1100
- maxRate,
1101
- minMaturity,
1102
- maxMaturity,
1103
- minExpiry,
1104
- maxExpiry,
1105
- collateralAssets,
1106
- collateralOracles,
1107
- collateralTuple,
1108
- minLltv,
1109
- maxLltv,
1110
- sortBy = "expiry",
1111
- sortOrder = "desc",
1112
- cursor: queryCursor,
1113
- limit = 20
1114
- } = query || {};
1115
- const now = mempool.Time.now();
1116
- const buy = side === "buy";
1117
- let offers = Array.from(map.values()).map((o) => ({
1118
- ...o,
1119
- consumed: filled.get(o.chainId)?.get(o.offering.toLowerCase())?.get(o.nonce) || 0n
1120
- })).filter((o) => o.consumed < o.assets);
1121
- const cursor = decode(queryCursor);
1122
- if (cursor) {
1123
- if (cursor.sort !== sortBy || cursor.dir !== sortOrder) {
1124
- throw new Error("Cursor does not match the current sort parameters");
1125
- }
1126
- switch (cursor.sort) {
1127
- case "rate":
1128
- offers = offers.filter(
1129
- (o) => (sortOrder === "asc" ? o.rate >= BigInt(cursor.rate) : o.rate <= BigInt(cursor.rate)) && (o.rate !== BigInt(cursor.rate) || (sortOrder === "asc" ? o.hash > cursor.hash : o.hash < cursor.hash))
1130
- );
1131
- break;
1132
- case "maturity":
1133
- offers = offers.filter(
1134
- (o) => (sortOrder === "asc" ? o.maturity >= BigInt(cursor.maturity) : o.maturity <= BigInt(cursor.maturity)) && (o.maturity !== mempool.Maturity.from(cursor.maturity) || (sortOrder === "asc" ? o.hash > cursor.hash : o.hash < cursor.hash))
1135
- );
1136
- break;
1137
- case "expiry":
1138
- offers = offers.filter(
1139
- (o) => (sortOrder === "asc" ? o.expiry >= cursor.expiry : o.expiry <= cursor.expiry) && (o.expiry !== cursor.expiry || (sortOrder === "asc" ? o.hash > cursor.hash : o.hash < cursor.hash))
1140
- );
1141
- break;
1142
- case "amount":
1143
- offers = offers.filter(
1144
- (o) => (sortOrder === "asc" ? o.assets >= BigInt(cursor.assets) : o.assets <= BigInt(cursor.assets)) && (o.assets !== BigInt(cursor.assets) || (sortOrder === "asc" ? o.hash > cursor.hash : o.hash < cursor.hash))
1145
- );
1146
- break;
1147
- default:
1148
- throw new Error("Invalid sort parameter");
1149
- }
1150
- offers = offers.filter((o) => o.hash !== cursor.hash);
1151
- }
1152
- creators && (creators = creators.map((c) => c.toLowerCase()));
1153
- loanTokens && (loanTokens = loanTokens.map((lt) => lt.toLowerCase()));
1154
- callbackAddresses && (callbackAddresses = callbackAddresses.map((ca) => ca.toLowerCase()));
1155
- collateralAssets && (collateralAssets = collateralAssets.map((ca) => ca.toLowerCase()));
1156
- collateralOracles && (collateralOracles = collateralOracles.map((co) => co.toLowerCase()));
1157
- collateralTuple && (collateralTuple = collateralTuple.map((ct) => ({
1158
- asset: ct.asset.toLowerCase(),
1159
- oracle: ct.oracle?.toLowerCase()
1160
- })));
1161
- offers = offers.filter((o) => o.expiry >= now);
1162
- creators && (offers = offers.filter((o) => creators.includes(o.offering.toLowerCase())));
1163
- side && (offers = offers.filter((o) => o.buy === buy));
1164
- chains && (offers = offers.filter((o) => chains.includes(Number(o.chainId))));
1165
- loanTokens && (offers = offers.filter((o) => loanTokens.includes(o.loanToken.toLowerCase())));
1166
- status && (offers = offers.filter((o) => status.includes(o.status)));
1167
- callbackAddresses && (offers = offers.filter(
1168
- (o) => callbackAddresses.includes(o.callback.address.toLowerCase())
1169
- ));
1170
- minAmount && (offers = offers.filter((o) => o.assets >= minAmount));
1171
- maxAmount && (offers = offers.filter((o) => o.assets <= maxAmount));
1172
- minRate && (offers = offers.filter((o) => o.rate >= minRate));
1173
- maxRate && (offers = offers.filter((o) => o.rate <= maxRate));
1174
- minMaturity && (offers = offers.filter((o) => o.maturity >= minMaturity));
1175
- maxMaturity && (offers = offers.filter((o) => o.maturity <= maxMaturity));
1176
- minExpiry && (offers = offers.filter((o) => o.expiry >= minExpiry));
1177
- maxExpiry && (offers = offers.filter((o) => o.expiry <= maxExpiry));
1178
- collateralAssets && (offers = offers.filter(
1179
- (o) => o.collaterals.some((c) => collateralAssets.includes(c.asset.toLowerCase()))
1180
- ));
1181
- collateralOracles && (offers = offers.filter(
1182
- (o) => o.collaterals.some((c) => collateralOracles.includes(c.oracle.toLowerCase()))
1183
- ));
1184
- collateralTuple && (offers = offers.filter(
1185
- (o) => o.collaterals.some(
1186
- (c) => collateralTuple.some(
1187
- (ct) => c.asset.toLowerCase() === ct.asset.toLowerCase() && (ct.oracle ? c.oracle.toLowerCase() === ct.oracle.toLowerCase() : true) && (ct.lltv ? c.lltv === mempool.LLTV.from(BigInt(ct.lltv)) : true)
1188
- )
1189
- )
1190
- ));
1191
- minLltv && (offers = offers.filter(
1192
- (o) => o.collaterals.every((c) => c.lltv >= viem.parseUnits(minLltv.toString(), 16))
1193
- ));
1194
- maxLltv && (offers = offers.filter(
1195
- (o) => o.collaterals.every((c) => c.lltv <= viem.parseUnits(maxLltv.toString(), 16))
1196
- ));
1197
- offers = offers.sort((a, b) => sort(sortBy, sortOrder, a, b));
1198
- let nextCursor = null;
1199
- if (offers.length > limit) {
1200
- const last = offers[limit - 1];
1201
- const base = {
1202
- sort: sortBy,
1203
- dir: sortOrder,
1204
- hash: last.hash
1205
- };
1206
- switch (sortBy) {
1207
- case "rate":
1208
- base.rate = last.rate.toString();
1209
- break;
1210
- case "amount":
1211
- base.assets = last.assets.toString();
1212
- break;
1213
- case "maturity":
1214
- base.maturity = last.maturity;
1215
- break;
1216
- default:
1217
- base.expiry = last.expiry;
1218
- }
1219
- nextCursor = encode(base);
1220
- }
1221
- offers = offers.slice(0, limit);
1222
- const data = offers.map((o) => ({
1223
- ...mempool.Offer.from({
1224
- offering: o.offering,
1225
- assets: o.assets,
1226
- rate: o.rate,
1227
- maturity: mempool.Maturity.from(o.maturity),
1228
- expiry: o.expiry,
1229
- start: o.start,
1230
- nonce: o.nonce,
1231
- buy: o.buy,
1232
- chainId: o.chainId,
1233
- loanToken: o.loanToken,
1234
- collaterals: o.collaterals.map((c) => ({ asset: c.asset, oracle: c.oracle, lltv: c.lltv })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
1235
- callback: {
1236
- address: o.callback.address,
1237
- data: o.callback.data,
1238
- gasLimit: o.callback.gasLimit
1239
- },
1240
- ...o.signature !== null && o.signature !== void 0 ? { signature: o.signature } : {}
1241
- }),
1242
- consumed: o.consumed,
1243
- status: o.status,
1244
- ...o.metadata ? { metadata: o.metadata } : {}
1245
- }));
1246
- return {
1247
- offers: data,
1248
- nextCursor
1249
- };
1250
- },
1251
- findMatchingOffers: async (params) => {
1252
- const {
1253
- side,
1254
- chainId,
1255
- rate,
1256
- collaterals = [],
1257
- maturity,
1258
- minMaturity,
1259
- maxMaturity,
1260
- loanToken,
1261
- creator,
1262
- cursor: queryCursor,
1263
- limit = 20
1264
- } = params;
1265
- const now = mempool.Time.now();
1266
- const isBuying = side === "buy";
1267
- const sortOrder = isBuying ? "desc" : "asc";
1268
- let offers = Array.from(map.values()).map((o) => ({
1269
- ...o,
1270
- consumed: filled.get(o.chainId)?.get(o.offering.toLowerCase())?.get(o.nonce) || 0n
1271
- })).filter((o) => o.consumed < o.assets);
1272
- const cursor = decode(queryCursor);
1273
- if (cursor) {
1274
- if (cursor.sort !== "rate" || cursor.dir !== sortOrder) {
1275
- throw new Error("Cursor does not match the current sort parameters");
1276
- }
1277
- offers = offers.filter(
1278
- (o) => sortOrder === "asc" ? o.rate >= BigInt(cursor.rate) : o.rate <= BigInt(cursor.rate)
1279
- );
1280
- }
1281
- offers = offers.filter((o) => o.buy === !isBuying);
1282
- offers = offers.filter((o) => o.chainId === BigInt(chainId));
1283
- offers = offers.filter((o) => o.expiry >= now);
1284
- rate && (offers = offers.filter((o) => isBuying ? o.rate >= rate : o.rate <= rate));
1285
- collaterals.length > 0 && (offers = offers.filter(
1286
- (o) => isBuying ? (
1287
- // when wanting to buy, sell offer collaterals ⊆ user buy collaterals
1288
- o.collaterals.every((oc) => {
1289
- return collaterals.some(
1290
- (c) => oc.asset.toLowerCase() === c.asset.toLowerCase() && oc.oracle.toLowerCase() === c.oracle.toLowerCase() && oc.lltv === c.lltv
1291
- );
1292
- })
1293
- ) : (
1294
- // when wanting to sell, user sell collaterals ⊆ buy offer collaterals
1295
- collaterals.every((c) => {
1296
- return o.collaterals.some(
1297
- (oc) => oc.asset.toLowerCase() === c.asset.toLowerCase() && oc.oracle.toLowerCase() === c.oracle.toLowerCase() && oc.lltv === c.lltv
1298
- );
1299
- })
1300
- )
1301
- ));
1302
- maturity && (offers = offers.filter((o) => o.maturity === maturity));
1303
- minMaturity && (offers = offers.filter((o) => o.maturity >= minMaturity));
1304
- maxMaturity && (offers = offers.filter((o) => o.maturity <= maxMaturity));
1305
- loanToken && (offers = offers.filter((o) => o.loanToken.toLowerCase() === loanToken.toLowerCase()));
1306
- creator && (offers = offers.filter((o) => o.offering.toLowerCase() === creator.toLowerCase()));
1307
- offers = offers.filter((o) => ["valid"].includes(o.status));
1308
- const byGroup = /* @__PURE__ */ new Map();
1309
- for (const offer of offers) {
1310
- const groupKey = `${offer.chainId}-${offer.offering.toLowerCase()}-${offer.nonce}-${offer.buy}`;
1311
- const current = byGroup.get(groupKey);
1312
- if (!current) {
1313
- byGroup.set(groupKey, offer);
1314
- continue;
1315
- }
1316
- const remainingCandidate = offer.assets - offer.consumed;
1317
- const remainingCurrent = current.assets - current.consumed;
1318
- let candidateIsBetter = false;
1319
- if (offer.buy) {
1320
- if (offer.rate !== current.rate) candidateIsBetter = offer.rate < current.rate;
1321
- else if (remainingCandidate !== remainingCurrent)
1322
- candidateIsBetter = remainingCandidate > remainingCurrent;
1323
- else if (offer.maturity !== current.maturity)
1324
- candidateIsBetter = offer.maturity > current.maturity;
1325
- else candidateIsBetter = offer.hash < current.hash;
1326
- } else {
1327
- if (offer.rate !== current.rate) candidateIsBetter = offer.rate > current.rate;
1328
- else if (remainingCandidate !== remainingCurrent)
1329
- candidateIsBetter = remainingCandidate > remainingCurrent;
1330
- else if (offer.maturity !== current.maturity)
1331
- candidateIsBetter = offer.maturity > current.maturity;
1332
- else candidateIsBetter = offer.hash < current.hash;
1333
- }
1334
- if (candidateIsBetter) byGroup.set(groupKey, offer);
1335
- }
1336
- offers = Array.from(byGroup.values());
1337
- offers = offers.sort((a, b) => sort("rate", sortOrder, a, b));
1338
- cursor && (offers = offers.filter((o) => o.hash !== cursor.hash));
1339
- let nextCursor = null;
1340
- if (offers.length > limit) {
1341
- const last = offers[limit - 1];
1342
- nextCursor = encode({
1343
- sort: "rate",
1344
- dir: sortOrder,
1345
- hash: last.hash,
1346
- rate: last.rate.toString()
1347
- });
1348
- }
1349
- offers = offers.slice(0, limit);
1350
- const data = offers.map((o) => ({
1351
- ...mempool.Offer.from({
1352
- offering: o.offering,
1353
- assets: o.assets,
1354
- rate: o.rate,
1355
- maturity: mempool.Maturity.from(o.maturity),
1356
- expiry: o.expiry,
1357
- start: o.start,
1358
- nonce: o.nonce,
1359
- buy: o.buy,
1360
- chainId: o.chainId,
1361
- loanToken: o.loanToken,
1362
- collaterals: o.collaterals.map((c) => ({ asset: c.asset, oracle: c.oracle, lltv: c.lltv })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
1363
- callback: {
1364
- address: o.callback.address,
1365
- data: o.callback.data,
1366
- gasLimit: o.callback.gasLimit
1367
- },
1368
- ...o.signature !== null && o.signature !== void 0 ? { signature: o.signature } : {}
1369
- }),
1370
- consumed: o.consumed,
1371
- status: o.status,
1372
- ...o.metadata ? { metadata: o.metadata } : {}
1373
- }));
1374
- return {
1375
- offers: data,
1376
- nextCursor
1377
- };
1378
- },
1379
- delete: async (hash) => {
1380
- if (!map.has(hash.toLowerCase())) return false;
1381
- map.delete(hash.toLowerCase());
1382
- return true;
1383
- },
1384
- deleteMany: async (hashes) => {
1385
- let deleted = 0;
1386
- for (const hash of hashes) {
1387
- if (map.has(hash.toLowerCase())) {
1388
- map.delete(hash.toLowerCase());
1389
- deleted++;
1390
- }
1391
- }
1392
- return deleted;
1393
- },
1394
- updateStatus: async (parameters2) => {
1395
- const key = parameters2.offerHash.toLowerCase();
1396
- const existing = map.get(key);
1397
- if (!existing) return;
1398
- if (existing.status === parameters2.status) return;
1399
- map.set(key, {
1400
- ...existing,
1401
- status: parameters2.status,
1402
- metadata: parameters2.metadata
1843
+ break;
1844
+ case pathname.includes("/v1/offers"):
1845
+ action = "get_offers";
1846
+ break;
1847
+ default:
1848
+ throw new HttpGetOffersFailedError("Unknown endpoint", {
1849
+ details: `Unsupported path: ${pathname}`
1403
1850
  });
1404
- },
1405
- updateConsumedAmount: async (parameters2) => {
1406
- if (consumedIds.has(parameters2.id)) return;
1407
- consumedIds.add(parameters2.id);
1408
- const chainId = parameters2.chainId;
1409
- const address = parameters2.offering.toLowerCase();
1410
- const nonce = parameters2.nonce;
1411
- const filledForChain = filled.get(chainId) || /* @__PURE__ */ new Map();
1412
- const filledForOffering = filledForChain.get(address) || /* @__PURE__ */ new Map();
1413
- const current = filledForOffering.get(nonce) || 0n;
1414
- filledForOffering.set(nonce, current + parameters2.consumed);
1415
- filledForChain.set(address, filledForOffering);
1416
- filled.set(chainId, filledForChain);
1851
+ }
1852
+ const schemaParseResult = safeParse(action, Object.fromEntries(url.searchParams));
1853
+ if (!schemaParseResult.success) {
1854
+ throw new HttpGetOffersFailedError(`Invalid URL parameters`, {
1855
+ details: schemaParseResult.error.issues[0]?.message
1856
+ });
1857
+ }
1858
+ const response = await fetch(url.toString(), {
1859
+ method: "GET",
1860
+ headers: config.headers
1861
+ });
1862
+ if (!response.ok) {
1863
+ switch (response.status) {
1864
+ case 401:
1865
+ throw new HttpUnauthorizedError();
1866
+ case 403:
1867
+ throw new HttpForbiddenError();
1868
+ case 429:
1869
+ throw new HttpRateLimitError();
1417
1870
  }
1418
- };
1871
+ throw new HttpGetOffersFailedError(`GET request returned ${response.status}`, {
1872
+ details: await response.text()
1873
+ });
1874
+ }
1875
+ return response.json();
1419
1876
  }
1420
-
1421
- // src/core/router/Server.ts
1877
+ var InvalidUrlError = class extends mempool.Errors.BaseError {
1878
+ name = "Router.InvalidUrlError";
1879
+ constructor(url) {
1880
+ super(`URL "${url}" is not http/https.`);
1881
+ }
1882
+ };
1883
+ var HttpUnauthorizedError = class extends mempool.Errors.BaseError {
1884
+ name = "Router.HttpUnauthorizedError";
1885
+ constructor() {
1886
+ super("Unauthorized.", {
1887
+ metaMessages: ["Ensure that an API key is provided."]
1888
+ });
1889
+ }
1890
+ };
1891
+ var HttpForbiddenError = class extends mempool.Errors.BaseError {
1892
+ name = "Router.HttpForbiddenError";
1893
+ constructor() {
1894
+ super("Forbidden.", {
1895
+ metaMessages: ["Ensure that the API key is valid."]
1896
+ });
1897
+ }
1898
+ };
1899
+ var HttpRateLimitError = class extends mempool.Errors.BaseError {
1900
+ name = "Router.HttpRateLimitError";
1901
+ constructor() {
1902
+ super("Rate limit exceeded.", {
1903
+ metaMessages: [
1904
+ "The number of allowed requests has been exceeded. You must wait for the rate limit to reset."
1905
+ ]
1906
+ });
1907
+ }
1908
+ };
1909
+ var HttpGetOffersFailedError = class extends mempool.Errors.BaseError {
1910
+ name = "Router.HttpGetOffersFailedError";
1911
+ constructor(message, { details } = {}) {
1912
+ super(message, {
1913
+ metaMessages: [details]
1914
+ });
1915
+ }
1916
+ };
1422
1917
  async function serve(parameters) {
1423
1918
  const app = new hono.Hono();
1424
- const store = parameters.store ? parameters.store : memory({
1425
- offers: /* @__PURE__ */ new Map(),
1426
- filled: /* @__PURE__ */ new Map()
1427
- });
1919
+ const store = parameters.store;
1428
1920
  app.get("/v1/offers", async (c) => {
1429
1921
  try {
1430
1922
  const params = parse("get_offers", c.req.query());
1431
- const offers = await store.getAll({
1923
+ const offers2 = await store.getAll({
1432
1924
  query: {
1433
1925
  creators: params.creators,
1434
1926
  side: params.side,
@@ -1456,10 +1948,10 @@ async function serve(parameters) {
1456
1948
  }
1457
1949
  });
1458
1950
  return success(c, {
1459
- data: offers.offers.map(
1951
+ data: offers2.offers.map(
1460
1952
  (offer) => mempool.Format.stringifyBigint(mempool.Format.toSnakeCase(toResponse(offer)))
1461
1953
  ),
1462
- cursor: offers.nextCursor ?? null
1954
+ cursor: offers2.nextCursor ?? null
1463
1955
  });
1464
1956
  } catch (err) {
1465
1957
  console.error(err);
@@ -1469,7 +1961,7 @@ async function serve(parameters) {
1469
1961
  app.get("/v1/offers/match", async (c) => {
1470
1962
  try {
1471
1963
  const params = parse("match_offers", c.req.query());
1472
- const offers = await store.findMatchingOffers({
1964
+ const offers2 = await store.findMatchingOffers({
1473
1965
  side: params.side,
1474
1966
  chainId: params.chain_id,
1475
1967
  rate: params.rate,
@@ -1483,10 +1975,10 @@ async function serve(parameters) {
1483
1975
  limit: params.limit
1484
1976
  });
1485
1977
  return success(c, {
1486
- data: offers.offers.map(
1978
+ data: offers2.offers.map(
1487
1979
  (offer) => mempool.Format.stringifyBigint(mempool.Format.toSnakeCase(toResponse(offer)))
1488
1980
  ),
1489
- cursor: offers.nextCursor ?? null
1981
+ cursor: offers2.nextCursor ?? null
1490
1982
  });
1491
1983
  } catch (err) {
1492
1984
  console.error(err);
@@ -1594,318 +2086,1032 @@ function handleAPIError(error2, c) {
1594
2086
  });
1595
2087
  }
1596
2088
 
1597
- // src/Liquidity.ts
1598
- var Liquidity_exports = {};
1599
- __export(Liquidity_exports, {
1600
- fetch: () => fetch2,
1601
- fetchBalancesAndAllowances: () => fetchBalancesAndAllowances,
1602
- serialize: () => serialize
1603
- });
1604
- async function fetchBalancesAndAllowances(parameters) {
1605
- const { client, spender, pairs, options } = parameters;
1606
- if (pairs.length === 0) return /* @__PURE__ */ new Map();
1607
- const batchSize = Math.max(1, options?.batchSize ?? 5e3);
1608
- const retryAttempts = Math.max(1, options?.retryAttempts ?? 3);
1609
- const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 50);
1610
- const blockNumber = options?.blockNumber ? BigInt(options.blockNumber) : void 0;
1611
- const out = /* @__PURE__ */ new Map();
1612
- for (const pairsBatch of mempool.Utils.batch(pairs, batchSize)) {
1613
- const balanceContracts = [];
1614
- const allowanceContracts = [];
1615
- for (const { user, token } of pairsBatch) {
1616
- balanceContracts.push({
1617
- address: token,
1618
- abi: viem.erc20Abi,
1619
- functionName: "balanceOf",
1620
- args: [user]
1621
- });
1622
- allowanceContracts.push({
1623
- address: token,
1624
- abi: viem.erc20Abi,
1625
- functionName: "allowance",
1626
- args: [user, spender]
1627
- });
1628
- }
1629
- const [balances, allowances] = await Promise.all([
1630
- mempool.Utils.retry(
1631
- () => client.multicall({
1632
- allowFailure: false,
1633
- contracts: balanceContracts,
1634
- ...blockNumber ? { blockNumber } : {}
1635
- }),
1636
- retryAttempts,
1637
- retryDelayMs
1638
- ),
1639
- mempool.Utils.retry(
1640
- () => client.multicall({
1641
- allowFailure: false,
1642
- contracts: allowanceContracts,
1643
- ...blockNumber ? { blockNumber } : {}
1644
- }),
1645
- retryAttempts,
1646
- retryDelayMs
1647
- )
1648
- ]);
1649
- for (let i = 0; i < pairsBatch.length; i++) {
1650
- const { user, token } = pairsBatch[i];
1651
- const balance = balances[i];
1652
- const allowance = allowances[i];
1653
- let perUser = out.get(user);
1654
- if (!perUser) {
1655
- perUser = /* @__PURE__ */ new Map();
1656
- out.set(user, perUser);
1657
- }
1658
- perUser.set(token, { balance, allowance });
1659
- }
1660
- }
1661
- return out;
1662
- }
1663
- async function fetch2(parameters) {
1664
- const { client, chainId, spender, type, pairs, options } = parameters;
1665
- if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
1666
- throw new Error(`CallbackType not implemented: ${type}`);
1667
- const map = await fetchBalancesAndAllowances({
1668
- client,
1669
- spender,
1670
- pairs: pairs.map(({ user, contract }) => ({ user, token: contract })),
1671
- options
1672
- });
1673
- const out = [];
1674
- for (const [user, perContract] of map) {
1675
- for (const [contract, { balance, allowance }] of perContract) {
1676
- const amount = balance < allowance ? balance : allowance;
1677
- out.push(
1678
- buildLiquidity({
1679
- type,
1680
- user,
1681
- contract,
1682
- chainId,
1683
- amount: amount.toString(),
1684
- index: 0
1685
- })
1686
- );
1687
- }
1688
- }
1689
- return out;
1690
- }
1691
- function serialize(liquidity) {
1692
- const normalized = {
1693
- userPosition: {
1694
- id: liquidity.userPosition.id,
1695
- availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
1696
- user: liquidity.userPosition.user,
1697
- chainId: String(liquidity.userPosition.chainId),
1698
- amount: String(liquidity.userPosition.amount)
1699
- },
1700
- queues: liquidity.queues.map((queueWithPool) => ({
1701
- queue: {
1702
- queueId: queueWithPool.queue.queueId,
1703
- availableLiquidityPoolId: queueWithPool.queue.availableLiquidityPoolId,
1704
- index: queueWithPool.queue.index
1705
- },
1706
- pool: {
1707
- id: queueWithPool.pool.id,
1708
- amount: String(queueWithPool.pool.amount)
1709
- }
1710
- })).sort(
1711
- (left, right) => {
1712
- const leftQueueId = left.queue.queueId || "";
1713
- const rightQueueId = right.queue.queueId || "";
1714
- if (leftQueueId < rightQueueId) return -1;
1715
- if (leftQueueId > rightQueueId) return 1;
1716
- const leftPoolId = left.pool.id;
1717
- const rightPoolId = right.pool.id;
1718
- if (leftPoolId < rightPoolId) return -1;
1719
- if (leftPoolId > rightPoolId) return 1;
1720
- const leftIndex = left.queue.index;
1721
- const rightIndex = right.queue.index;
1722
- if (leftIndex < rightIndex) return -1;
1723
- if (leftIndex > rightIndex) return 1;
1724
- return 0;
1725
- }
1726
- )
1727
- };
1728
- return JSON.stringify(normalized);
1729
- }
1730
-
1731
- // src/Logger.ts
1732
- var Logger_exports = {};
1733
- __export(Logger_exports, {
1734
- LogLevelValues: () => LogLevelValues,
1735
- defaultLogger: () => defaultLogger,
1736
- getLogger: () => getLogger,
1737
- runWithLogger: () => runWithLogger,
1738
- silentLogger: () => silentLogger
2089
+ // src/LiquidityStore.ts
2090
+ var LiquidityStore_exports = {};
2091
+ __export(LiquidityStore_exports, {
2092
+ create: () => create2,
2093
+ memory: () => memory2
1739
2094
  });
1740
- var LogLevelValues = [
1741
- "silent",
1742
- "trace",
1743
- "debug",
1744
- "info",
1745
- "warn",
1746
- "error",
1747
- "fatal"
1748
- ];
1749
- function defaultLogger(minLevel) {
1750
- const threshold = minLevel ?? "trace";
1751
- const levelIndexByName = LogLevelValues.reduce(
1752
- (acc, lvl, idx) => {
1753
- acc[lvl] = idx;
1754
- return acc;
1755
- },
1756
- {}
1757
- );
1758
- const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
2095
+ var create2 = (config) => {
2096
+ const db = config.db;
1759
2097
  return {
1760
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1761
- trace: isEnabled("trace") ? console.trace.bind(console) : () => {
1762
- },
1763
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1764
- debug: isEnabled("debug") ? console.debug.bind(console) : () => {
1765
- },
1766
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1767
- info: isEnabled("info") ? console.info.bind(console) : () => {
1768
- },
1769
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1770
- warn: isEnabled("warn") ? console.warn.bind(console) : () => {
2098
+ getByUserPositionId: async (parameters) => {
2099
+ const up = await db.select().from(userPositions).where(drizzleOrm.eq(userPositions.id, parameters.userPositionId)).limit(1);
2100
+ if (up.length === 0) return null;
2101
+ const userPositionRow = up[0];
2102
+ const rows = await db.select({ queue: availableLiquidityQueues, pool: availableLiquidityPools }).from(availableLiquidityQueues).innerJoin(
2103
+ availableLiquidityPools,
2104
+ drizzleOrm.eq(availableLiquidityPools.id, availableLiquidityQueues.availableLiquidityPoolId)
2105
+ ).where(drizzleOrm.eq(availableLiquidityQueues.queueId, userPositionRow.availableLiquidityQueueId));
2106
+ const queues = rows.map((row) => ({
2107
+ queue: row.queue,
2108
+ pool: row.pool
2109
+ }));
2110
+ return { userPosition: userPositionRow, queues };
1771
2111
  },
1772
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1773
- error: isEnabled("error") ? console.error.bind(console) : () => {
2112
+ getAll: async () => {
2113
+ const rows = await db.select({
2114
+ userPosition: userPositions,
2115
+ queue: availableLiquidityQueues,
2116
+ pool: availableLiquidityPools
2117
+ }).from(userPositions).innerJoin(
2118
+ availableLiquidityQueues,
2119
+ drizzleOrm.eq(availableLiquidityQueues.queueId, userPositions.availableLiquidityQueueId)
2120
+ ).innerJoin(
2121
+ availableLiquidityPools,
2122
+ drizzleOrm.eq(availableLiquidityPools.id, availableLiquidityQueues.availableLiquidityPoolId)
2123
+ );
2124
+ const byUserPosition = /* @__PURE__ */ new Map();
2125
+ for (const row of rows) {
2126
+ const id = row.userPosition.id;
2127
+ if (!byUserPosition.has(id)) {
2128
+ byUserPosition.set(id, { userPosition: row.userPosition, queues: [] });
2129
+ }
2130
+ byUserPosition.get(id).queues.push({ queue: row.queue, pool: row.pool });
2131
+ }
2132
+ return Array.from(byUserPosition.values());
1774
2133
  },
1775
- fatal: isEnabled("fatal") ? (...args) => console.error("[fatal]", ...args) : () => {
2134
+ save: async (parameters) => {
2135
+ const { liquidity } = parameters;
2136
+ await db.transaction(async (tx) => {
2137
+ for (const qp of liquidity.queues) {
2138
+ await tx.insert(availableLiquidityPools).values({
2139
+ id: qp.pool.id,
2140
+ amount: qp.pool.amount,
2141
+ updatedAt: /* @__PURE__ */ new Date()
2142
+ }).onConflictDoUpdate({
2143
+ target: availableLiquidityPools.id,
2144
+ set: {
2145
+ amount: qp.pool.amount,
2146
+ updatedAt: /* @__PURE__ */ new Date()
2147
+ }
2148
+ });
2149
+ }
2150
+ for (const qp of liquidity.queues) {
2151
+ await tx.insert(availableLiquidityQueues).values({
2152
+ queueId: qp.queue.queueId,
2153
+ availableLiquidityPoolId: qp.pool.id,
2154
+ index: qp.queue.index,
2155
+ updatedAt: /* @__PURE__ */ new Date()
2156
+ }).onConflictDoUpdate({
2157
+ target: [
2158
+ availableLiquidityQueues.queueId,
2159
+ availableLiquidityQueues.availableLiquidityPoolId
2160
+ ],
2161
+ set: {
2162
+ index: qp.queue.index,
2163
+ updatedAt: /* @__PURE__ */ new Date()
2164
+ }
2165
+ });
2166
+ }
2167
+ await tx.insert(userPositions).values({
2168
+ id: liquidity.userPosition.id,
2169
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
2170
+ user: liquidity.userPosition.user,
2171
+ chainId: liquidity.userPosition.chainId,
2172
+ amount: liquidity.userPosition.amount,
2173
+ updatedAt: /* @__PURE__ */ new Date()
2174
+ }).onConflictDoUpdate({
2175
+ target: userPositions.id,
2176
+ set: {
2177
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
2178
+ user: liquidity.userPosition.user,
2179
+ chainId: liquidity.userPosition.chainId,
2180
+ amount: liquidity.userPosition.amount,
2181
+ updatedAt: /* @__PURE__ */ new Date()
2182
+ }
2183
+ });
2184
+ });
1776
2185
  }
1777
2186
  };
1778
- }
1779
- function silentLogger() {
2187
+ };
2188
+ function memory2() {
2189
+ const poolsById = /* @__PURE__ */ new Map();
2190
+ const queuesByComposite = /* @__PURE__ */ new Map();
2191
+ const queueIndexByQueueId = /* @__PURE__ */ new Map();
2192
+ const userPositionsById = /* @__PURE__ */ new Map();
1780
2193
  return {
1781
- trace: () => {
1782
- },
1783
- debug: () => {
1784
- },
1785
- info: () => {
1786
- },
1787
- warn: () => {
2194
+ getByUserPositionId: async (parameters) => {
2195
+ const up = userPositionsById.get(parameters.userPositionId);
2196
+ if (!up) return null;
2197
+ const compositeKeys = queueIndexByQueueId.get(up.availableLiquidityQueueId) || /* @__PURE__ */ new Set();
2198
+ const queues = [];
2199
+ for (const key of compositeKeys) {
2200
+ const q = queuesByComposite.get(key);
2201
+ if (!q) continue;
2202
+ const p = poolsById.get(q.availableLiquidityPoolId);
2203
+ if (!p) continue;
2204
+ queues.push({ queue: q, pool: p });
2205
+ }
2206
+ return { userPosition: up, queues };
1788
2207
  },
1789
- error: () => {
2208
+ getAll: async () => {
2209
+ const results = [];
2210
+ for (const up of userPositionsById.values()) {
2211
+ const compositeKeys = queueIndexByQueueId.get(up.availableLiquidityQueueId) || /* @__PURE__ */ new Set();
2212
+ const queues = [];
2213
+ for (const key of compositeKeys) {
2214
+ const q = queuesByComposite.get(key);
2215
+ if (!q) continue;
2216
+ const p = poolsById.get(q.availableLiquidityPoolId);
2217
+ if (!p) continue;
2218
+ queues.push({ queue: q, pool: p });
2219
+ }
2220
+ results.push({ userPosition: up, queues });
2221
+ }
2222
+ return results;
1790
2223
  },
1791
- fatal: () => {
2224
+ save: async (parameters) => {
2225
+ const { liquidity } = parameters;
2226
+ for (const qp of liquidity.queues) {
2227
+ poolsById.set(qp.pool.id, {
2228
+ id: qp.pool.id,
2229
+ amount: qp.pool.amount,
2230
+ updatedAt: /* @__PURE__ */ new Date()
2231
+ });
2232
+ }
2233
+ for (const qp of liquidity.queues) {
2234
+ const qid = qp.queue.queueId;
2235
+ if (!qid) continue;
2236
+ const composite = `${qid}::${qp.pool.id}`;
2237
+ queuesByComposite.set(composite, {
2238
+ queueId: qid,
2239
+ availableLiquidityPoolId: qp.pool.id,
2240
+ index: qp.queue.index,
2241
+ updatedAt: /* @__PURE__ */ new Date()
2242
+ });
2243
+ if (!queueIndexByQueueId.has(qid)) queueIndexByQueueId.set(qid, /* @__PURE__ */ new Set());
2244
+ queueIndexByQueueId.get(qid).add(composite);
2245
+ }
2246
+ userPositionsById.set(liquidity.userPosition.id, {
2247
+ id: liquidity.userPosition.id,
2248
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
2249
+ user: liquidity.userPosition.user,
2250
+ chainId: liquidity.userPosition.chainId,
2251
+ amount: liquidity.userPosition.amount,
2252
+ updatedAt: /* @__PURE__ */ new Date()
2253
+ });
1792
2254
  }
1793
2255
  };
1794
2256
  }
1795
- var loggerContext = new async_hooks.AsyncLocalStorage();
1796
- function runWithLogger(logger, fn) {
1797
- return loggerContext.run(logger, fn);
1798
- }
1799
- function getLogger() {
1800
- return loggerContext.getStore() ?? defaultLogger();
1801
- }
1802
2257
 
1803
- // src/Validation.ts
1804
- var Validation_exports = {};
1805
- __export(Validation_exports, {
1806
- run: () => run
2258
+ // src/OfferStore/index.ts
2259
+ var OfferStore_exports = {};
2260
+ __export(OfferStore_exports, {
2261
+ create: () => create3
1807
2262
  });
1808
- async function run(parameters) {
1809
- const { items, rules, ctx = {}, chunkSize } = parameters;
1810
- const issues = [];
1811
- let validItems = items.slice();
1812
- for (const rule of rules) {
1813
- if (validItems.length === 0) return { valid: [], issues };
1814
- const indicesToRemove = /* @__PURE__ */ new Set();
1815
- if (rule.kind === "single") {
1816
- for (let i = 0; i < validItems.length; i++) {
1817
- const item = validItems[i];
1818
- const issue = await rule.run(item, ctx);
1819
- if (issue) {
1820
- issues.push({ ...issue, ruleName: rule.name, item });
1821
- indicesToRemove.add(i);
2263
+ function create3(config) {
2264
+ const db = config.db;
2265
+ return {
2266
+ create: async (parameters) => {
2267
+ const { offer, status, metadata } = parameters;
2268
+ return await db.transaction(async (tx) => {
2269
+ const callbackId = getCallbackIdForOffer(offer);
2270
+ const result = await tx.insert(offers).values({
2271
+ hash: offer.hash.toLowerCase(),
2272
+ offering: offer.offering.toLowerCase(),
2273
+ assets: offer.assets.toString(),
2274
+ rate: offer.rate,
2275
+ maturity: offer.maturity,
2276
+ expiry: offer.expiry,
2277
+ start: offer.start,
2278
+ nonce: offer.nonce,
2279
+ buy: offer.buy,
2280
+ chainId: offer.chainId,
2281
+ loanToken: offer.loanToken.toLowerCase(),
2282
+ callbackAddress: offer.callback.address.toLowerCase(),
2283
+ callbackData: offer.callback.data,
2284
+ callbackGasLimit: offer.callback.gasLimit,
2285
+ signature: offer.signature,
2286
+ callbackId: callbackId ?? null
2287
+ }).onConflictDoNothing().returning();
2288
+ if (result.length === 0) {
2289
+ return offer.hash;
2290
+ }
2291
+ for (const collateral of offer.collaterals) {
2292
+ await tx.insert(offerCollaterals).values({
2293
+ offerHash: offer.hash.toLowerCase(),
2294
+ asset: collateral.asset.toLowerCase(),
2295
+ oracle: collateral.oracle.toLowerCase(),
2296
+ lltv: collateral.lltv
2297
+ });
2298
+ }
2299
+ await tx.insert(offerStatus).values({
2300
+ offerHash: offer.hash.toLowerCase(),
2301
+ status,
2302
+ metadata
2303
+ });
2304
+ return offer.hash;
2305
+ });
2306
+ },
2307
+ createMany: async (parameters) => {
2308
+ return await db.transaction(async (tx) => {
2309
+ const hashes = [];
2310
+ for (const { offer, status, metadata } of parameters) {
2311
+ const callbackId = getCallbackIdForOffer(offer);
2312
+ const result = await tx.insert(offers).values({
2313
+ hash: offer.hash.toLowerCase(),
2314
+ offering: offer.offering.toLowerCase(),
2315
+ assets: offer.assets.toString(),
2316
+ rate: offer.rate,
2317
+ maturity: offer.maturity,
2318
+ expiry: offer.expiry,
2319
+ start: offer.start,
2320
+ nonce: offer.nonce,
2321
+ buy: offer.buy,
2322
+ chainId: offer.chainId,
2323
+ loanToken: offer.loanToken.toLowerCase(),
2324
+ callbackAddress: offer.callback.address.toLowerCase(),
2325
+ callbackData: offer.callback.data,
2326
+ callbackGasLimit: offer.callback.gasLimit,
2327
+ signature: offer.signature,
2328
+ callbackId: callbackId ?? null
2329
+ }).onConflictDoNothing().returning();
2330
+ if (result.length === 0) continue;
2331
+ for (const collateral of offer.collaterals) {
2332
+ await tx.insert(offerCollaterals).values({
2333
+ offerHash: offer.hash.toLowerCase(),
2334
+ asset: collateral.asset.toLowerCase(),
2335
+ oracle: collateral.oracle.toLowerCase(),
2336
+ lltv: collateral.lltv
2337
+ });
2338
+ }
2339
+ await tx.insert(offerStatus).values({
2340
+ offerHash: offer.hash.toLowerCase(),
2341
+ status,
2342
+ metadata
2343
+ });
2344
+ hashes.push(offer.hash);
1822
2345
  }
2346
+ return hashes;
2347
+ });
2348
+ },
2349
+ getAll: async (params) => {
2350
+ const { query } = params ?? {};
2351
+ const conditions = [];
2352
+ const now = mempool.Time.now();
2353
+ if (query?.creators && query.creators.length > 0) {
2354
+ conditions.push(
2355
+ drizzleOrm.inArray(
2356
+ offers.offering,
2357
+ query.creators.map((c) => c.toLowerCase())
2358
+ )
2359
+ );
1823
2360
  }
1824
- } else if (rule.kind === "batch") {
1825
- const exec = async (slice, offset) => {
1826
- const map = await rule.run(slice, ctx);
1827
- for (let i = 0; i < slice.length; i++) {
1828
- const issue = map.get(i);
1829
- if (issue !== void 0) {
1830
- issues.push({ ...issue, ruleName: rule.name, item: slice[i] });
1831
- indicesToRemove.add(offset + i);
2361
+ if (query?.side) {
2362
+ conditions.push(drizzleOrm.eq(offers.buy, query.side === "buy"));
2363
+ }
2364
+ if (query?.chains && query.chains.length > 0) {
2365
+ conditions.push(
2366
+ drizzleOrm.inArray(
2367
+ offers.chainId,
2368
+ query.chains.map((chain) => BigInt(chain))
2369
+ )
2370
+ );
2371
+ }
2372
+ if (query?.loanTokens && query.loanTokens.length > 0) {
2373
+ conditions.push(
2374
+ drizzleOrm.inArray(
2375
+ offers.loanToken,
2376
+ query.loanTokens.map((t) => t.toLowerCase())
2377
+ )
2378
+ );
2379
+ }
2380
+ if (query?.callbackAddresses && query.callbackAddresses.length > 0) {
2381
+ conditions.push(
2382
+ drizzleOrm.inArray(
2383
+ offers.callbackAddress,
2384
+ query.callbackAddresses.map((c) => c.toLowerCase())
2385
+ )
2386
+ );
2387
+ }
2388
+ conditions.push(drizzleOrm.gte(offers.expiry, now));
2389
+ if (query?.minAmount !== void 0) {
2390
+ conditions.push(drizzleOrm.gte(offers.assets, query.minAmount.toString()));
2391
+ }
2392
+ if (query?.maxAmount !== void 0) {
2393
+ conditions.push(drizzleOrm.lte(offers.assets, query.maxAmount.toString()));
2394
+ }
2395
+ if (query?.minRate !== void 0) {
2396
+ conditions.push(drizzleOrm.gte(offers.rate, query.minRate));
2397
+ }
2398
+ if (query?.maxRate !== void 0) {
2399
+ conditions.push(drizzleOrm.lte(offers.rate, query.maxRate));
2400
+ }
2401
+ if (query?.minMaturity !== void 0) {
2402
+ conditions.push(drizzleOrm.gte(offers.maturity, query.minMaturity));
2403
+ }
2404
+ if (query?.maxMaturity !== void 0) {
2405
+ conditions.push(drizzleOrm.lte(offers.maturity, query.maxMaturity));
2406
+ }
2407
+ if (query?.minExpiry !== void 0) {
2408
+ conditions.push(drizzleOrm.gte(offers.expiry, query.minExpiry));
2409
+ }
2410
+ if (query?.maxExpiry !== void 0) {
2411
+ conditions.push(drizzleOrm.lte(offers.expiry, query.maxExpiry));
2412
+ }
2413
+ if (query?.collateralAssets && query.collateralAssets.length > 0) {
2414
+ conditions.push(
2415
+ drizzleOrm.inArray(
2416
+ offerCollaterals.asset,
2417
+ query.collateralAssets.map((a) => a.toLowerCase())
2418
+ )
2419
+ );
2420
+ }
2421
+ if (query?.collateralOracles && query.collateralOracles.length > 0) {
2422
+ conditions.push(
2423
+ drizzleOrm.inArray(
2424
+ offerCollaterals.oracle,
2425
+ query.collateralOracles.map((o) => o.toLowerCase())
2426
+ )
2427
+ );
2428
+ }
2429
+ if (query?.collateralTuple && query.collateralTuple.length > 0) {
2430
+ const tupleClauses = query.collateralTuple.map((tuple) => {
2431
+ const parts = [
2432
+ drizzleOrm.sql`${offerCollaterals.asset} = ${tuple.asset.toLowerCase()}`
2433
+ ];
2434
+ if (tuple.oracle) {
2435
+ parts.push(drizzleOrm.sql`${offerCollaterals.oracle} = ${tuple.oracle.toLowerCase()}`);
2436
+ }
2437
+ if (tuple.lltv !== void 0) {
2438
+ parts.push(drizzleOrm.sql`${offerCollaterals.lltv} = ${tuple.lltv}`);
2439
+ }
2440
+ let clause = parts[0];
2441
+ for (let i = 1; i < parts.length; i++) {
2442
+ clause = drizzleOrm.sql`${clause} AND ${parts[i]}`;
1832
2443
  }
2444
+ return drizzleOrm.sql`(${clause})`;
2445
+ }).filter((c) => c !== void 0);
2446
+ if (tupleClauses.length > 0) {
2447
+ let combined = tupleClauses[0];
2448
+ for (let i = 1; i < tupleClauses.length; i++) {
2449
+ combined = drizzleOrm.sql`${combined} OR ${tupleClauses[i]}`;
2450
+ }
2451
+ conditions.push(drizzleOrm.sql`(${combined})`);
1833
2452
  }
1834
- };
1835
- if (!chunkSize) await exec(validItems, 0);
1836
- else {
1837
- for (let i = 0; i < validItems.length; i += chunkSize) {
1838
- await exec(validItems.slice(i, i + chunkSize), i);
2453
+ }
2454
+ if (query?.minLltv !== void 0) {
2455
+ conditions.push(drizzleOrm.gte(offerCollaterals.lltv, viem.parseUnits(query.minLltv.toString(), 16)));
2456
+ }
2457
+ if (query?.maxLltv !== void 0) {
2458
+ conditions.push(drizzleOrm.lte(offerCollaterals.lltv, viem.parseUnits(query.maxLltv.toString(), 16)));
2459
+ }
2460
+ const sortBy = query?.sortBy ?? "expiry";
2461
+ const sortOrder = query?.sortOrder ?? "desc";
2462
+ const sortColumn = (() => {
2463
+ switch (sortBy) {
2464
+ case "rate":
2465
+ return offers.rate;
2466
+ case "maturity":
2467
+ return offers.maturity;
2468
+ case "expiry":
2469
+ return offers.expiry;
2470
+ case "amount":
2471
+ return offers.assets;
2472
+ default:
2473
+ return offers.expiry;
2474
+ }
2475
+ })();
2476
+ const cursor = decode(query?.cursor);
2477
+ if (cursor) {
2478
+ if (cursor.sort !== sortBy || cursor.dir !== sortOrder) {
2479
+ throw new Error("Cursor does not match the current sort parameters");
2480
+ }
2481
+ const op = sortOrder === "asc" ? drizzleOrm.sql`>` : drizzleOrm.sql`<`;
2482
+ const cursorVal = (() => {
2483
+ switch (sortBy) {
2484
+ case "rate":
2485
+ return BigInt(cursor.rate);
2486
+ case "amount":
2487
+ return BigInt(cursor.assets);
2488
+ case "maturity":
2489
+ return cursor.maturity;
2490
+ case "expiry":
2491
+ return cursor.expiry;
2492
+ default:
2493
+ return cursor.expiry;
2494
+ }
2495
+ })();
2496
+ conditions.push(drizzleOrm.sql`(${sortColumn}, ${offers.hash}) ${op} (${cursorVal}, ${cursor.hash})`);
2497
+ }
2498
+ const limit = query?.limit ?? 20;
2499
+ const latestStatus = db.select({
2500
+ status: offerStatus.status,
2501
+ metadata: offerStatus.metadata
2502
+ }).from(offerStatus).where(drizzleOrm.eq(offerStatus.offerHash, offers.hash)).orderBy(drizzleOrm.desc(offerStatus.createdAt)).limit(1).as("latest_status");
2503
+ const sumConsumed = db.select({
2504
+ consumed: drizzleOrm.sql`COALESCE(SUM(${consumed.consumed}), 0)`.as("consumed")
2505
+ }).from(consumed).where(
2506
+ drizzleOrm.and(
2507
+ drizzleOrm.eq(consumed.offering, offers.offering),
2508
+ drizzleOrm.eq(consumed.nonce, offers.nonce),
2509
+ drizzleOrm.eq(consumed.chainId, offers.chainId)
2510
+ )
2511
+ ).as("sum_consumed");
2512
+ const results = await db.select({
2513
+ hash: offers.hash,
2514
+ offering: offers.offering,
2515
+ assets: offers.assets,
2516
+ consumed: sumConsumed.consumed,
2517
+ rate: offers.rate,
2518
+ maturity: offers.maturity,
2519
+ expiry: offers.expiry,
2520
+ start: offers.start,
2521
+ nonce: offers.nonce,
2522
+ buy: offers.buy,
2523
+ chainId: offers.chainId,
2524
+ loanToken: offers.loanToken,
2525
+ callbackAddress: offers.callbackAddress,
2526
+ callbackData: offers.callbackData,
2527
+ callbackGasLimit: offers.callbackGasLimit,
2528
+ signature: offers.signature,
2529
+ createdAt: offers.createdAt,
2530
+ collateralAsset: offerCollaterals.asset,
2531
+ collateralOracle: offerCollaterals.oracle,
2532
+ collateralLltv: offerCollaterals.lltv,
2533
+ status: latestStatus.status,
2534
+ metadata: latestStatus.metadata
2535
+ }).from(offers).leftJoin(offerCollaterals, drizzleOrm.eq(offers.hash, offerCollaterals.offerHash)).leftJoinLateral(latestStatus, drizzleOrm.sql`true`).leftJoinLateral(sumConsumed, drizzleOrm.sql`true`).where(
2536
+ drizzleOrm.and(
2537
+ conditions.length > 0 ? drizzleOrm.and(...conditions) : drizzleOrm.sql`true`,
2538
+ query?.status && query.status.length > 0 ? drizzleOrm.inArray(latestStatus.status, query.status) : drizzleOrm.eq(latestStatus.status, "valid"),
2539
+ drizzleOrm.sql`( ${offers.assets} - COALESCE(${sumConsumed.consumed}, 0) ) > 0`
2540
+ )
2541
+ ).orderBy(
2542
+ ...sortOrder === "asc" ? [drizzleOrm.asc(sortColumn), drizzleOrm.asc(offers.hash)] : [drizzleOrm.desc(sortColumn), drizzleOrm.desc(offers.hash)]
2543
+ ).limit(limit);
2544
+ const offerMap = /* @__PURE__ */ new Map();
2545
+ for (const row of results) {
2546
+ const offer = offerMap.get(row.hash) || {
2547
+ hash: row.hash,
2548
+ offering: row.offering,
2549
+ assets: BigInt(row.assets),
2550
+ consumed: BigInt(row.consumed),
2551
+ rate: row.rate,
2552
+ maturity: row.maturity,
2553
+ expiry: row.expiry,
2554
+ start: row.start,
2555
+ nonce: row.nonce,
2556
+ buy: row.buy,
2557
+ chainId: row.chainId,
2558
+ loanToken: row.loanToken,
2559
+ callbackAddress: row.callbackAddress,
2560
+ callbackData: row.callbackData,
2561
+ callbackGasLimit: row.callbackGasLimit,
2562
+ signature: row.signature,
2563
+ createdAt: row.createdAt,
2564
+ collaterals: [],
2565
+ status: row.status,
2566
+ metadata: row.metadata
2567
+ };
2568
+ if (row.collateralAsset && row.collateralOracle && row.collateralLltv) {
2569
+ offer.collaterals.push({
2570
+ asset: row.collateralAsset,
2571
+ oracle: row.collateralOracle,
2572
+ lltv: mempool.LLTV.from(row.collateralLltv)
2573
+ });
2574
+ }
2575
+ offerMap.set(row.hash, offer);
2576
+ }
2577
+ let nextCursor = null;
2578
+ if (results.length === limit && results.length > 0) {
2579
+ const lastRow = results[results.length - 1];
2580
+ const base = {
2581
+ sort: sortBy,
2582
+ dir: sortOrder,
2583
+ hash: lastRow.hash
2584
+ };
2585
+ switch (sortBy) {
2586
+ case "rate":
2587
+ base.rate = lastRow.rate.toString();
2588
+ break;
2589
+ case "amount":
2590
+ base.assets = lastRow.assets.toString();
2591
+ break;
2592
+ case "maturity":
2593
+ base.maturity = lastRow.maturity;
2594
+ break;
2595
+ default:
2596
+ base.expiry = lastRow.expiry;
2597
+ }
2598
+ nextCursor = encode(base);
2599
+ }
2600
+ const transformedResults = [];
2601
+ for (const offerData of offerMap.values()) {
2602
+ const offer = mempool.Offer.from({
2603
+ offering: offerData.offering,
2604
+ assets: offerData.assets,
2605
+ rate: offerData.rate,
2606
+ maturity: mempool.Maturity.from(offerData.maturity),
2607
+ expiry: offerData.expiry,
2608
+ start: offerData.start,
2609
+ nonce: offerData.nonce,
2610
+ buy: offerData.buy,
2611
+ chainId: offerData.chainId,
2612
+ loanToken: offerData.loanToken,
2613
+ collaterals: offerData.collaterals.map((c) => ({
2614
+ asset: c.asset,
2615
+ oracle: c.oracle,
2616
+ lltv: mempool.LLTV.from(c.lltv)
2617
+ })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
2618
+ callback: {
2619
+ address: offerData.callbackAddress,
2620
+ data: offerData.callbackData,
2621
+ gasLimit: offerData.callbackGasLimit
2622
+ },
2623
+ ...offerData.signature !== null ? { signature: offerData.signature } : void 0
2624
+ });
2625
+ transformedResults.push({
2626
+ ...offer,
2627
+ consumed: offerData.consumed,
2628
+ status: offerData.status,
2629
+ ...offerData.metadata !== null ? { metadata: offerData.metadata } : void 0
2630
+ });
2631
+ }
2632
+ return { offers: transformedResults, nextCursor };
2633
+ },
2634
+ /**
2635
+ * Returns offers that match specified offer based on the provided parameters.
2636
+ *
2637
+ * Rules for filtering:
2638
+ * - If offer expiry is in the past compared to now, it should not be included
2639
+ * - If offer maturity is in the past compared to now, it should not be included
2640
+ * - It should return if buyOffer.rate >= sellOffer.rate
2641
+ * - It should return if buyOffer.userAddress != sellOffer.userAddress
2642
+ * - It should return if maturity matches
2643
+ * - It should return if loanToken matches
2644
+ * - If the incoming intent is a buy / lend offer, only consider existing sell / borrow offers.
2645
+ * - If the incoming intent is a sell / borrow offer, only consider existing buy / lend offers.
2646
+ * - When the intent is lend: require offer_collaterals ⊆ intent_collaterals
2647
+ * - When the intent is borrow: require intent_collaterals ⊆ offer_collaterals
2648
+ * - It should sort by increasing rate if the input offer is a buy/lend offer
2649
+ * - It should sort by decreasing rate if the input offer is a sell/borrow offer
2650
+ */
2651
+ findMatchingOffers: async (params) => {
2652
+ const {
2653
+ side,
2654
+ chainId,
2655
+ rate,
2656
+ collaterals = [],
2657
+ maturity,
2658
+ minMaturity,
2659
+ maxMaturity,
2660
+ loanToken,
2661
+ creator,
2662
+ cursor,
2663
+ limit = 20
2664
+ } = params;
2665
+ const isIncomingBuy = side === "buy";
2666
+ const nowEpochSeconds = Math.floor(Date.now() / 1e3);
2667
+ const rateSortDirection = isIncomingBuy ? "desc" : "asc";
2668
+ const baseWhereClauses = [];
2669
+ baseWhereClauses.push(drizzleOrm.eq(offers.buy, !isIncomingBuy));
2670
+ baseWhereClauses.push(drizzleOrm.eq(offers.chainId, BigInt(chainId)));
2671
+ baseWhereClauses.push(drizzleOrm.gte(offers.expiry, nowEpochSeconds));
2672
+ baseWhereClauses.push(drizzleOrm.gte(offers.maturity, nowEpochSeconds));
2673
+ if (maturity) baseWhereClauses.push(drizzleOrm.eq(offers.maturity, maturity));
2674
+ if (minMaturity) baseWhereClauses.push(drizzleOrm.gte(offers.maturity, minMaturity));
2675
+ if (maxMaturity) baseWhereClauses.push(drizzleOrm.lte(offers.maturity, maxMaturity));
2676
+ if (loanToken) baseWhereClauses.push(drizzleOrm.eq(offers.loanToken, loanToken.toLowerCase()));
2677
+ if (creator) baseWhereClauses.push(drizzleOrm.eq(offers.offering, creator.toLowerCase()));
2678
+ if (rate)
2679
+ baseWhereClauses.push(isIncomingBuy ? drizzleOrm.gte(offers.rate, rate) : drizzleOrm.lte(offers.rate, rate));
2680
+ const parsedCursor = decode(cursor);
2681
+ if (parsedCursor) {
2682
+ if (parsedCursor.sort !== "rate" || parsedCursor.dir !== rateSortDirection) {
2683
+ throw new Error("Cursor does not match the current sort parameters");
2684
+ }
2685
+ }
2686
+ if (collaterals.length > 0) {
2687
+ const collateralValues = collaterals.map(
2688
+ (c) => `('${c.asset.toLowerCase()}', '${c.oracle.toLowerCase()}', ${c.lltv.toString()})`
2689
+ ).join(", ");
2690
+ const userCollaterals = drizzleOrm.sql`(VALUES ${drizzleOrm.sql.raw(collateralValues)}) AS user_collaterals(asset, oracle, lltv)`;
2691
+ const sellPredicate = drizzleOrm.sql`
2692
+ NOT EXISTS (
2693
+ SELECT 1 FROM ${userCollaterals}
2694
+ WHERE NOT EXISTS (
2695
+ SELECT 1 FROM ${offerCollaterals} offer_collaterals
2696
+ WHERE offer_collaterals.offer_hash = ${offers.hash}
2697
+ AND offer_collaterals.asset = user_collaterals.asset
2698
+ AND offer_collaterals.oracle = user_collaterals.oracle
2699
+ AND offer_collaterals.lltv = user_collaterals.lltv
2700
+ )
2701
+ )
2702
+ `;
2703
+ const buyPredicate = drizzleOrm.sql`
2704
+ NOT EXISTS (
2705
+ SELECT 1 FROM ${offerCollaterals} offer_collaterals
2706
+ WHERE offer_collaterals.offer_hash = ${offers.hash}
2707
+ AND NOT EXISTS (
2708
+ SELECT 1 FROM ${userCollaterals}
2709
+ WHERE user_collaterals.asset = offer_collaterals.asset
2710
+ AND user_collaterals.oracle = offer_collaterals.oracle
2711
+ AND user_collaterals.lltv = offer_collaterals.lltv
2712
+ )
2713
+ )
2714
+ `;
2715
+ baseWhereClauses.push(isIncomingBuy ? buyPredicate : sellPredicate);
2716
+ }
2717
+ const latestStatus = db.select({
2718
+ status: offerStatus.status,
2719
+ metadata: offerStatus.metadata
2720
+ }).from(offerStatus).where(drizzleOrm.eq(offerStatus.offerHash, offers.hash)).orderBy(drizzleOrm.desc(offerStatus.createdAt)).limit(1).as("latest_status");
2721
+ const sumConsumed = db.select({
2722
+ consumed: drizzleOrm.sql`COALESCE(SUM(${consumed.consumed}), 0)`.as("consumed")
2723
+ }).from(consumed).where(
2724
+ drizzleOrm.and(
2725
+ drizzleOrm.eq(consumed.offering, offers.offering),
2726
+ drizzleOrm.eq(consumed.nonce, offers.nonce),
2727
+ drizzleOrm.eq(consumed.chainId, offers.chainId)
2728
+ )
2729
+ ).as("sum_consumed");
2730
+ const statusCondition = drizzleOrm.eq(latestStatus.status, "valid");
2731
+ const bestOffers = db.selectDistinctOn(
2732
+ // group key
2733
+ [offers.offering, offers.nonce, offers.buy],
2734
+ {
2735
+ hash: offers.hash,
2736
+ offering: offers.offering,
2737
+ assets: offers.assets,
2738
+ consumed: sumConsumed.consumed,
2739
+ remaining: drizzleOrm.sql`${offers.assets} - COALESCE(${sumConsumed.consumed}, 0)`.as("remaining"),
2740
+ rate: offers.rate,
2741
+ maturity: offers.maturity,
2742
+ expiry: offers.expiry,
2743
+ start: offers.start,
2744
+ nonce: offers.nonce,
2745
+ buy: offers.buy,
2746
+ chainId: offers.chainId,
2747
+ loanToken: offers.loanToken,
2748
+ callbackAddress: offers.callbackAddress,
2749
+ callbackData: offers.callbackData,
2750
+ callbackGasLimit: offers.callbackGasLimit,
2751
+ signature: offers.signature,
2752
+ callbackId: offers.callbackId,
2753
+ status: latestStatus.status,
2754
+ metadata: latestStatus.metadata
2755
+ }
2756
+ ).from(offers).leftJoinLateral(latestStatus, drizzleOrm.sql`true`).leftJoinLateral(sumConsumed, drizzleOrm.sql`true`).where(
2757
+ drizzleOrm.and(
2758
+ drizzleOrm.and(...baseWhereClauses),
2759
+ statusCondition,
2760
+ drizzleOrm.sql`( ${offers.assets} - COALESCE(${sumConsumed.consumed}, 0) ) > 0`
2761
+ )
2762
+ ).orderBy(
2763
+ offers.offering,
2764
+ offers.nonce,
2765
+ offers.buy,
2766
+ // 1 price (direction depends on side)
2767
+ drizzleOrm.sql`CASE WHEN ${offers.buy} THEN ${offers.rate} ELSE -${offers.rate} END`,
2768
+ // 2 size (remaining)
2769
+ drizzleOrm.sql`( ${offers.assets} - COALESCE(${sumConsumed.consumed}, 0) ) DESC`,
2770
+ // 3 term (longer maturity)
2771
+ drizzleOrm.desc(offers.maturity)
2772
+ ).as("best_offers");
2773
+ const queueLiquidity = db.select({
2774
+ callbackId: userPositions.id,
2775
+ userAmount: userPositions.amount,
2776
+ // user's per-callback cap
2777
+ queueLiquidity: drizzleOrm.sql`COALESCE(SUM(${availableLiquidityPools.amount}), 0)`.as(
2778
+ "queue_liquidity"
2779
+ )
2780
+ }).from(userPositions).leftJoin(
2781
+ availableLiquidityQueues,
2782
+ drizzleOrm.eq(userPositions.availableLiquidityQueueId, availableLiquidityQueues.queueId)
2783
+ ).leftJoin(
2784
+ availableLiquidityPools,
2785
+ drizzleOrm.eq(availableLiquidityQueues.availableLiquidityPoolId, availableLiquidityPools.id)
2786
+ ).groupBy(userPositions.id).as("queue_liquidity");
2787
+ const sortExpr = drizzleOrm.sql`CASE WHEN ${bestOffers.buy} THEN ${bestOffers.rate} ELSE -${bestOffers.rate} END`;
2788
+ const offersWithEligibility = db.select({
2789
+ hash: bestOffers.hash,
2790
+ offering: bestOffers.offering,
2791
+ assets: bestOffers.assets,
2792
+ consumed: bestOffers.consumed,
2793
+ remaining: bestOffers.remaining,
2794
+ rate: bestOffers.rate,
2795
+ maturity: bestOffers.maturity,
2796
+ expiry: bestOffers.expiry,
2797
+ start: bestOffers.start,
2798
+ nonce: bestOffers.nonce,
2799
+ buy: bestOffers.buy,
2800
+ chainId: bestOffers.chainId,
2801
+ loanToken: bestOffers.loanToken,
2802
+ callbackAddress: bestOffers.callbackAddress,
2803
+ callbackData: bestOffers.callbackData,
2804
+ callbackGasLimit: bestOffers.callbackGasLimit,
2805
+ signature: bestOffers.signature,
2806
+ callbackId: bestOffers.callbackId,
2807
+ status: bestOffers.status,
2808
+ metadata: bestOffers.metadata,
2809
+ // liquidity caps
2810
+ userAmount: drizzleOrm.sql`COALESCE(${queueLiquidity.userAmount}, 0)`.as("user_amount"),
2811
+ queueLiquidity: drizzleOrm.sql`COALESCE(${queueLiquidity.queueLiquidity}, 0)`.as(
2812
+ "queue_liquidity"
2813
+ ),
2814
+ // running total of remaining per callback, ordered by the *same* priority as selection
2815
+ cumulativeRemaining: drizzleOrm.sql`
2816
+ SUM(${bestOffers.remaining}) OVER (
2817
+ PARTITION BY ${bestOffers.callbackId}
2818
+ ORDER BY ${sortExpr}, ${drizzleOrm.asc(bestOffers.hash)}
2819
+ ROWS UNBOUNDED PRECEDING
2820
+ )
2821
+ `.as("cumulative_remaining"),
2822
+ eligible: drizzleOrm.sql`
2823
+ CASE
2824
+ WHEN ${bestOffers.buy} = false THEN true
2825
+ WHEN ${bestOffers.remaining} <= 0 THEN false
2826
+ ELSE ( SUM(${bestOffers.remaining}) OVER (
2827
+ PARTITION BY ${bestOffers.callbackId}
2828
+ ORDER BY ${sortExpr}, ${drizzleOrm.asc(bestOffers.hash)}
2829
+ ROWS UNBOUNDED PRECEDING
2830
+ )
2831
+ <= LEAST(
2832
+ COALESCE(${queueLiquidity.userAmount}, 0),
2833
+ COALESCE(${queueLiquidity.queueLiquidity}, 0)
2834
+ )
2835
+ )
2836
+ END
2837
+ `.as("eligible")
2838
+ }).from(bestOffers).leftJoin(queueLiquidity, drizzleOrm.eq(bestOffers.callbackId, queueLiquidity.callbackId)).as("offers_with_eligibility");
2839
+ const hardCap = limit * 5 + 1 + (parsedCursor ? 1 : 0);
2840
+ const validOffers = db.select({
2841
+ // pass-through all fields + a global row_number for hard cap
2842
+ hash: offersWithEligibility.hash,
2843
+ offering: offersWithEligibility.offering,
2844
+ assets: offersWithEligibility.assets,
2845
+ consumed: offersWithEligibility.consumed,
2846
+ remaining: offersWithEligibility.remaining,
2847
+ rate: offersWithEligibility.rate,
2848
+ maturity: offersWithEligibility.maturity,
2849
+ expiry: offersWithEligibility.expiry,
2850
+ start: offersWithEligibility.start,
2851
+ nonce: offersWithEligibility.nonce,
2852
+ buy: offersWithEligibility.buy,
2853
+ chainId: offersWithEligibility.chainId,
2854
+ loanToken: offersWithEligibility.loanToken,
2855
+ callbackAddress: offersWithEligibility.callbackAddress,
2856
+ callbackData: offersWithEligibility.callbackData,
2857
+ callbackGasLimit: offersWithEligibility.callbackGasLimit,
2858
+ signature: offersWithEligibility.signature,
2859
+ callbackId: offersWithEligibility.callbackId,
2860
+ status: offersWithEligibility.status,
2861
+ metadata: offersWithEligibility.metadata,
2862
+ userAmount: offersWithEligibility.userAmount,
2863
+ queueLiquidity: offersWithEligibility.queueLiquidity,
2864
+ cumulativeRemaining: offersWithEligibility.cumulativeRemaining,
2865
+ eligible: offersWithEligibility.eligible,
2866
+ // sort expression is the same again as in offersWithEligibility
2867
+ rowNumber: drizzleOrm.sql`
2868
+ ROW_NUMBER() OVER (
2869
+ ORDER BY
2870
+ CASE WHEN ${offersWithEligibility.buy} THEN ${offersWithEligibility.rate} ELSE -${offersWithEligibility.rate} END,
2871
+ ${drizzleOrm.asc(offersWithEligibility.hash)}
2872
+ )
2873
+ `.as("row_number")
2874
+ }).from(offersWithEligibility).where(drizzleOrm.sql`${offersWithEligibility.eligible} = true`).limit(hardCap).as("valid_offers");
2875
+ const cursorTuple = parsedCursor ? {
2876
+ rate: BigInt(parsedCursor.rate),
2877
+ hash: parsedCursor.hash
2878
+ } : null;
2879
+ const withCollats = await db.select({
2880
+ // base fields
2881
+ hash: drizzleOrm.sql`${validOffers.hash}`,
2882
+ offering: drizzleOrm.sql`${validOffers.offering}`,
2883
+ assets: validOffers.assets,
2884
+ consumed: validOffers.consumed,
2885
+ rate: validOffers.rate,
2886
+ maturity: validOffers.maturity,
2887
+ expiry: validOffers.expiry,
2888
+ start: validOffers.start,
2889
+ nonce: validOffers.nonce,
2890
+ buy: validOffers.buy,
2891
+ chainId: validOffers.chainId,
2892
+ loanToken: drizzleOrm.sql`${validOffers.loanToken}`,
2893
+ callbackAddress: drizzleOrm.sql`${validOffers.callbackAddress}`,
2894
+ callbackData: drizzleOrm.sql`${validOffers.callbackData}`,
2895
+ callbackGasLimit: validOffers.callbackGasLimit,
2896
+ signature: drizzleOrm.sql`${validOffers.signature}`,
2897
+ callbackId: validOffers.callbackId,
2898
+ status: drizzleOrm.sql`${validOffers.status}`,
2899
+ metadata: validOffers.metadata,
2900
+ // collateral fields
2901
+ collateralAsset: drizzleOrm.sql`${offerCollaterals.asset}`,
2902
+ collateralOracle: drizzleOrm.sql`${offerCollaterals.oracle}`,
2903
+ collateralLltv: offerCollaterals.lltv
2904
+ }).from(validOffers).leftJoin(offerCollaterals, drizzleOrm.eq(validOffers.hash, offerCollaterals.offerHash)).where(
2905
+ drizzleOrm.and(
2906
+ ...cursorTuple ? [
2907
+ drizzleOrm.sql`(${validOffers.rate}, ${validOffers.hash}) ${rateSortDirection === "asc" ? drizzleOrm.sql`>` : drizzleOrm.sql`<`} (${cursorTuple.rate}, ${cursorTuple.hash})`
2908
+ ] : []
2909
+ )
2910
+ ).orderBy(
2911
+ rateSortDirection === "asc" ? drizzleOrm.asc(validOffers.rate) : drizzleOrm.desc(validOffers.rate),
2912
+ drizzleOrm.asc(validOffers.hash)
2913
+ );
2914
+ const buildOffersMap = (rows, skipHash) => {
2915
+ const map = /* @__PURE__ */ new Map();
2916
+ for (const row of rows) {
2917
+ const entry = map.get(row.hash) ?? { base: row, collaterals: [] };
2918
+ if (row.collateralAsset && row.collateralOracle && row.collateralLltv) {
2919
+ entry.collaterals.push({
2920
+ asset: row.collateralAsset,
2921
+ oracle: row.collateralOracle,
2922
+ lltv: mempool.LLTV.from(row.collateralLltv)
2923
+ });
2924
+ }
2925
+ map.set(row.hash, entry);
1839
2926
  }
2927
+ return map;
2928
+ };
2929
+ const allEntries = Array.from(buildOffersMap(withCollats).values());
2930
+ const pageEntries = allEntries.slice(0, limit);
2931
+ let nextCursor = null;
2932
+ if (allEntries.length > limit) {
2933
+ const last = pageEntries[pageEntries.length - 1].base;
2934
+ nextCursor = encode({
2935
+ sort: "rate",
2936
+ dir: rateSortDirection,
2937
+ hash: last.hash,
2938
+ rate: last.rate.toString()
2939
+ });
2940
+ }
2941
+ const data = pageEntries.map(({ base, collaterals: cols }) => ({
2942
+ ...mempool.Offer.from({
2943
+ offering: base.offering,
2944
+ assets: BigInt(base.assets),
2945
+ rate: base.rate,
2946
+ maturity: mempool.Maturity.from(base.maturity),
2947
+ expiry: base.expiry,
2948
+ start: base.start,
2949
+ nonce: base.nonce,
2950
+ buy: base.buy,
2951
+ chainId: base.chainId,
2952
+ loanToken: base.loanToken,
2953
+ collaterals: cols.sort(
2954
+ (a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())
2955
+ ),
2956
+ callback: {
2957
+ address: base.callbackAddress,
2958
+ data: base.callbackData,
2959
+ gasLimit: base.callbackGasLimit
2960
+ },
2961
+ ...base.signature !== null ? { signature: base.signature } : void 0
2962
+ }),
2963
+ consumed: BigInt(base.consumed),
2964
+ status: base.status,
2965
+ ...base.metadata ? { metadata: base.metadata } : {}
2966
+ }));
2967
+ return { offers: data, nextCursor };
2968
+ },
2969
+ delete: async (hash) => {
2970
+ const result = await db.delete(offers).where(drizzleOrm.eq(offers.hash, hash.toLowerCase()));
2971
+ return result.affectedRows > 0;
2972
+ },
2973
+ deleteMany: async (hashes) => {
2974
+ if (hashes.length === 0) {
2975
+ return 0;
1840
2976
  }
2977
+ return await db.transaction(async (tx) => {
2978
+ const normalizedHashes = hashes.map((hash) => hash.toLowerCase());
2979
+ const result = await tx.delete(offers).where(
2980
+ drizzleOrm.sql`${offers.hash} = ANY(${drizzleOrm.sql.raw(`ARRAY[${normalizedHashes.map((h) => `'${h}'`).join(", ")}]`)})`
2981
+ );
2982
+ return result.affectedRows;
2983
+ });
2984
+ },
2985
+ updateStatus: async (parameters) => {
2986
+ const latest = await db.select({ status: offerStatus.status }).from(offerStatus).where(drizzleOrm.eq(offerStatus.offerHash, parameters.offerHash.toLowerCase())).orderBy(drizzleOrm.desc(offerStatus.createdAt)).limit(1);
2987
+ if (latest.length > 0 && latest[0].status === parameters.status) return;
2988
+ await db.insert(offerStatus).values({
2989
+ offerHash: parameters.offerHash.toLowerCase(),
2990
+ status: parameters.status,
2991
+ metadata: parameters.metadata
2992
+ });
2993
+ },
2994
+ updateConsumedAmount: async (parameters) => {
2995
+ await db.insert(consumed).values({
2996
+ id: parameters.id,
2997
+ chainId: parameters.chainId,
2998
+ offering: parameters.offering.toLowerCase(),
2999
+ nonce: parameters.nonce,
3000
+ consumed: parameters.consumed.toString()
3001
+ }).onConflictDoNothing();
1841
3002
  }
1842
- validItems = validItems.filter((_, i) => !indicesToRemove.has(i));
1843
- }
1844
- return {
1845
- valid: validItems,
1846
- issues
1847
3003
  };
1848
3004
  }
1849
3005
 
1850
- // src/ValidationRule.ts
1851
- var ValidationRule_exports = {};
1852
- __export(ValidationRule_exports, {
1853
- batch: () => batch,
1854
- morpho: () => morpho,
1855
- single: () => single
3006
+ // src/OfferStore/PG.ts
3007
+ var PG_exports = {};
3008
+ __export(PG_exports, {
3009
+ applyMigrations: () => applyMigrations,
3010
+ clean: () => clean,
3011
+ connect: () => connect2
1856
3012
  });
1857
- function single(name, run2) {
1858
- return { kind: "single", name, run: run2 };
3013
+ function connect2(parameters) {
3014
+ if (parameters.type === "pg") {
3015
+ const pool2 = new pg.Pool({ connectionString: parameters.endpoint });
3016
+ const client2 = nodePostgres.drizzle(pool2, { schema: schema_exports });
3017
+ return Object.assign(client2, { name: "pg", pool: pool2 });
3018
+ }
3019
+ const pool = new pglite.PGlite();
3020
+ const client = pglite$1.drizzle(pool, { schema: schema_exports });
3021
+ return Object.assign(client, { name: "pglite", pool });
1859
3022
  }
1860
- function batch(name, run2) {
1861
- return { kind: "batch", name, run: run2 };
3023
+ async function applyMigrations(pg) {
3024
+ const migrationsFolder = process.env.AWS_LAMBDA_FUNCTION_NAME ? path__default.default.join(process.cwd(), "drizzle", VERSION) : path__default.default.join(__dirname, "drizzle", VERSION);
3025
+ await pg.execute(`create schema if not exists "${VERSION}"`);
3026
+ if (pg.name === "pg") {
3027
+ await migrator.migrate(pg, { migrationsFolder });
3028
+ return;
3029
+ }
3030
+ await migrator$1.migrate(pg, { migrationsFolder });
1862
3031
  }
1863
- function morpho() {
1864
- const chainId = single("chain_id", (offer, { chain }) => {
1865
- if (chain.id !== offer.chainId) {
1866
- return {
1867
- message: `Chain ID ${offer.chainId} is not the same as the chain ID in the context (${chain.id})`
1868
- };
1869
- }
3032
+ async function clean(pg) {
3033
+ await pg.execute(`drop schema if exists "${VERSION}" cascade`);
3034
+ await pg.execute(`create schema "${VERSION}"`);
3035
+ await pg.execute("drop schema if exists drizzle cascade");
3036
+ }
3037
+
3038
+ // src/Services.ts
3039
+ var Services_exports = {};
3040
+ __export(Services_exports, {
3041
+ from: () => from2
3042
+ });
3043
+ var from2 = async (config) => {
3044
+ const { chain, rpcUrl, dbConfig } = config;
3045
+ const walletClient = viem.createWalletClient({
3046
+ chain,
3047
+ transport: viem.http(rpcUrl)
3048
+ }).extend(viem.publicActions);
3049
+ if (dbConfig.type === "pg" && !dbConfig.endpoint) {
3050
+ throw new Error("dbOffer.endpoint is required when dbOffer.type is pg");
3051
+ }
3052
+ const DB = dbConfig.type === "pg" ? connect2({ type: "pg", endpoint: dbConfig.endpoint }) : connect2({ type: "pglite" });
3053
+ const offerStore = create3({ db: DB });
3054
+ const collectorBlockStore = create({
3055
+ db: DB
1870
3056
  });
1871
- const loanToken = single("loan_token", (offer, { chain }) => {
1872
- const tokens = new Set(
1873
- Array.from(chain.whitelistedAssets.values()).map((a) => a.toLowerCase())
1874
- );
1875
- if (!tokens.has(offer.loanToken.toLowerCase())) {
1876
- return {
1877
- message: `Loan token ${offer.loanToken} is not whitelisted on chain ${offer.chainId}`
1878
- };
3057
+ const liquidityStore = create2({ db: DB });
3058
+ const mempoolOffersCollector = createMempoolCollector({
3059
+ mempool: mempool.Mempool.connect({
3060
+ client: walletClient,
3061
+ mempoolAddress: chain.mempool.address
3062
+ }),
3063
+ offerStore,
3064
+ collectorBlockStore,
3065
+ chain,
3066
+ options: {
3067
+ interval: 1e4
1879
3068
  }
1880
3069
  });
1881
- const expiry = single("expiry", (offer, _) => {
1882
- if (offer.expiry < Math.floor(Date.now() / 1e3)) {
1883
- return { message: "Expiry mismatch" };
3070
+ const consumedEventsCollector = createConsumedEventsCollector({
3071
+ client: walletClient,
3072
+ contractAddress: chain.morpho,
3073
+ offerStore,
3074
+ collectorBlockStore,
3075
+ chainId: chain.id,
3076
+ options: {
3077
+ interval: 1e4
1884
3078
  }
1885
3079
  });
1886
- const callback = single("empty_callback", (offer, _) => {
1887
- if (!offer.buy || offer.callback.data !== "0x") {
1888
- return { message: "Callback not supported yet." };
3080
+ const buyWithEmptyCallbackLiquidityCollector = createBuyWithEmptyCallbackLiquidityCollector({
3081
+ client: walletClient,
3082
+ offerStore,
3083
+ collectorBlockStore,
3084
+ liquidityStore,
3085
+ chain,
3086
+ options: {
3087
+ interval: 1e4
1889
3088
  }
1890
3089
  });
1891
- return [
1892
- chainId,
1893
- loanToken,
1894
- expiry,
1895
- // note: callback rule should be the last one, since it does not mean that the offer is forever invalid
1896
- // integrators should be able to choose if they want to keep the offer or not
1897
- callback
1898
- ];
1899
- }
3090
+ return {
3091
+ offerStore,
3092
+ collectorBlockStore,
3093
+ liquidityStore,
3094
+ mempoolOffersCollector,
3095
+ consumedEventsCollector,
3096
+ buyWithEmptyCallbackLiquidityCollector,
3097
+ DB
3098
+ };
3099
+ };
1900
3100
 
1901
3101
  exports.ApiSchema = apiSchema_exports;
1902
3102
  exports.Callback = Callback_exports;
3103
+ exports.Collector = Collector_exports;
3104
+ exports.CollectorBlockStore = CollectorBlockStore_exports;
1903
3105
  exports.Cursor = Cursor_exports;
1904
3106
  exports.Liquidity = Liquidity_exports;
3107
+ exports.LiquidityStore = LiquidityStore_exports;
1905
3108
  exports.Logger = Logger_exports;
1906
3109
  exports.OfferStore = OfferStore_exports;
3110
+ exports.OffersSchema = schema_exports;
3111
+ exports.PG = PG_exports;
1907
3112
  exports.Router = router_exports;
1908
3113
  exports.RouterOffer = RouterOffer_exports;
3114
+ exports.Services = Services_exports;
1909
3115
  exports.Validation = Validation_exports;
1910
3116
  exports.ValidationRule = ValidationRule_exports;
1911
3117
  Object.keys(mempool).forEach(function (k) {