@morpho-dev/router 0.1.6 → 0.1.8

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');
5
4
  var v4 = require('zod/v4');
6
5
  var zodOpenapi = require('zod-openapi');
7
6
  var viem = require('viem');
7
+ var jsBase64 = require('js-base64');
8
+ var actions = require('viem/actions');
9
+ var async_hooks = require('async_hooks');
10
+ var drizzleOrm = require('drizzle-orm');
11
+ var pgCore = require('drizzle-orm/pg-core');
12
+ var path = require('path');
13
+ var pglite = require('@electric-sql/pglite');
14
+ var nodePostgres = require('drizzle-orm/node-postgres');
15
+ var migrator = require('drizzle-orm/node-postgres/migrator');
16
+ var pglite$1 = require('drizzle-orm/pglite');
17
+ var migrator$1 = require('drizzle-orm/pglite/migrator');
18
+ var pg = require('pg');
8
19
  var nodeServer = require('@hono/node-server');
9
20
  var hono = require('hono');
10
- var async_hooks = require('async_hooks');
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) => {
@@ -15,60 +29,17 @@ var __export = (target, all) => {
15
29
  __defProp(target, name, { get: all[name], enumerable: true });
16
30
  };
17
31
 
18
- // src/Callback.ts
19
- var Callback_exports = {};
20
- __export(Callback_exports, {
21
- CallbackType: () => CallbackType,
22
- buildLiquidity: () => buildLiquidity,
23
- getCallbackIdForOffer: () => getCallbackIdForOffer
32
+ // src/core/apiSchema/index.ts
33
+ var apiSchema_exports = {};
34
+ __export(apiSchema_exports, {
35
+ OpenApi: () => OpenApi,
36
+ fromResponse: () => fromResponse,
37
+ parse: () => parse,
38
+ safeParse: () => safeParse,
39
+ toResponse: () => toResponse
24
40
  });
25
- var CallbackType = /* @__PURE__ */ ((CallbackType2) => {
26
- CallbackType2["BuyWithEmptyCallback"] = "buy_with_empty_callback";
27
- return CallbackType2;
28
- })(CallbackType || {});
29
- function buildLiquidity(parameters) {
30
- const { type, user, contract, chainId, amount, index = 0, updatedAt = /* @__PURE__ */ new Date() } = parameters;
31
- if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
32
- throw new Error(`CallbackType not implemented: ${type}`);
33
- const amountStr = amount.toString();
34
- const id = `${user}-${chainId.toString()}-${type}-${contract}`.toLowerCase();
35
- return {
36
- userPosition: {
37
- id,
38
- availableLiquidityQueueId: id,
39
- user: user.toLowerCase(),
40
- chainId,
41
- amount: amountStr,
42
- updatedAt
43
- },
44
- queues: [
45
- {
46
- queue: {
47
- queueId: id,
48
- availableLiquidityPoolId: id,
49
- index,
50
- updatedAt
51
- },
52
- pool: {
53
- id,
54
- amount: amountStr,
55
- updatedAt
56
- }
57
- }
58
- ]
59
- };
60
- }
61
- function getCallbackIdForOffer(offer) {
62
- if (offer.buy && offer.callback.data === "0x") {
63
- const type = "buy_with_empty_callback" /* BuyWithEmptyCallback */;
64
- const user = offer.offering;
65
- const loanToken = offer.loanToken;
66
- return `${user}-${offer.chainId.toString()}-${type}-${loanToken}`.toLowerCase();
67
- }
68
- return null;
69
- }
70
41
 
71
- // src/Cursor.ts
42
+ // src/core/Cursor.ts
72
43
  var Cursor_exports = {};
73
44
  __export(Cursor_exports, {
74
45
  decode: () => decode,
@@ -154,17 +125,7 @@ function decode(token) {
154
125
  return decoded;
155
126
  }
156
127
 
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
165
- });
166
-
167
- // src/RouterOffer.ts
128
+ // src/core/RouterOffer.ts
168
129
  var RouterOffer_exports = {};
169
130
  __export(RouterOffer_exports, {
170
131
  InvalidRouterOfferError: () => InvalidRouterOfferError,
@@ -741,1171 +702,2477 @@ var OpenApi = zodOpenapi.createDocument({
741
702
 
742
703
  // src/core/apiSchema/utils.ts
743
704
  function toResponse(routerOffer) {
744
- const { consumed, status, metadata, ...offer } = routerOffer;
705
+ const { consumed: consumed2, status, metadata, ...offer } = routerOffer;
745
706
  return {
746
707
  offer,
747
- consumed,
708
+ consumed: consumed2,
748
709
  status,
749
710
  metadata
750
711
  };
751
712
  }
752
713
  function fromResponse(offerResponse) {
753
- const { offer, consumed, status, metadata } = offerResponse;
714
+ const { offer, consumed: consumed2, status, metadata } = offerResponse;
754
715
  return {
755
716
  ...offer,
756
- consumed,
717
+ consumed: consumed2,
757
718
  status,
758
719
  metadata
759
720
  };
760
721
  }
761
722
 
762
- // src/core/router/index.ts
763
- var router_exports = {};
764
- __export(router_exports, {
765
- APIError: () => APIError,
766
- BadRequestError: () => BadRequestError,
767
- HttpForbiddenError: () => HttpForbiddenError,
768
- HttpGetOffersFailedError: () => HttpGetOffersFailedError,
769
- HttpRateLimitError: () => HttpRateLimitError,
770
- HttpUnauthorizedError: () => HttpUnauthorizedError,
771
- InternalServerError: () => InternalServerError,
772
- InvalidUrlError: () => InvalidUrlError,
773
- NotFoundError: () => NotFoundError,
774
- ValidationError: () => ValidationError,
775
- connect: () => connect,
776
- error: () => error,
777
- get: () => get,
778
- handleZodError: () => handleZodError,
779
- match: () => match,
780
- serve: () => serve,
781
- success: () => success
723
+ // src/core/Callback.ts
724
+ var Callback_exports = {};
725
+ __export(Callback_exports, {
726
+ CallbackType: () => CallbackType,
727
+ buildLiquidity: () => buildLiquidity,
728
+ getCallbackIdForOffer: () => getCallbackIdForOffer
782
729
  });
783
- function connect(opts) {
784
- const u = new URL(opts?.url || "https://router.morpho.dev");
785
- if (u.protocol !== "http:" && u.protocol !== "https:") {
786
- throw new InvalidUrlError(u.toString());
787
- }
788
- const headers = opts?.headers ?? new Headers();
789
- headers.set("Content-Type", "application/json");
790
- opts?.apiKey !== void 0 ? headers.set("X-API-Key", opts.apiKey) : null;
791
- const config = {
792
- url: u,
793
- headers
794
- };
730
+ var CallbackType = /* @__PURE__ */ ((CallbackType2) => {
731
+ CallbackType2["BuyWithEmptyCallback"] = "buy_with_empty_callback";
732
+ return CallbackType2;
733
+ })(CallbackType || {});
734
+ function buildLiquidity(parameters) {
735
+ const { type, user, contract, chainId, amount, index: index2 = 0, updatedAt = /* @__PURE__ */ new Date() } = parameters;
736
+ if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
737
+ throw new Error(`CallbackType not implemented: ${type}`);
738
+ const amountStr = amount.toString();
739
+ const id = `${user}-${chainId.toString()}-${type}-${contract}`.toLowerCase();
795
740
  return {
796
- ...config,
797
- get: (parameters) => get(config, parameters),
798
- match: (parameters) => match(config, parameters)
741
+ userPosition: {
742
+ id,
743
+ availableLiquidityQueueId: id,
744
+ user: user.toLowerCase(),
745
+ chainId,
746
+ amount: amountStr,
747
+ updatedAt
748
+ },
749
+ queues: [
750
+ {
751
+ queue: {
752
+ queueId: id,
753
+ availableLiquidityPoolId: id,
754
+ index: index2,
755
+ updatedAt
756
+ },
757
+ pool: {
758
+ id,
759
+ amount: amountStr,
760
+ updatedAt
761
+ }
762
+ }
763
+ ]
799
764
  };
800
765
  }
801
- async function get(config, parameters) {
802
- const url = new URL(`${config.url.toString()}v1/offers`);
803
- if (parameters.creators?.length) {
804
- url.searchParams.set("creators", parameters.creators.join(","));
805
- }
806
- if (parameters.side) {
807
- url.searchParams.set("side", parameters.side);
808
- }
809
- if (parameters.chains?.length) {
810
- url.searchParams.set("chains", parameters.chains.join(","));
766
+ function getCallbackIdForOffer(offer) {
767
+ if (offer.buy && offer.callback.data === "0x") {
768
+ const type = "buy_with_empty_callback" /* BuyWithEmptyCallback */;
769
+ const user = offer.offering;
770
+ const loanToken = offer.loanToken;
771
+ return `${user}-${offer.chainId.toString()}-${type}-${loanToken}`.toLowerCase();
811
772
  }
812
- if (parameters.loanTokens?.length) {
813
- url.searchParams.set("loan_tokens", parameters.loanTokens.join(","));
773
+ return null;
774
+ }
775
+
776
+ // src/core/Collector/index.ts
777
+ var Collector_exports = {};
778
+ __export(Collector_exports, {
779
+ createBuyWithEmptyCallbackLiquidityCollector: () => createBuyWithEmptyCallbackLiquidityCollector,
780
+ createChainReorgsCollector: () => createChainReorgsCollector,
781
+ createConsumedEventsCollector: () => createConsumedEventsCollector,
782
+ createMempoolCollector: () => createMempoolCollector,
783
+ start: () => start
784
+ });
785
+
786
+ // src/core/Liquidity.ts
787
+ var Liquidity_exports = {};
788
+ __export(Liquidity_exports, {
789
+ fetch: () => fetch2,
790
+ fetchBalancesAndAllowances: () => fetchBalancesAndAllowances,
791
+ serialize: () => serialize
792
+ });
793
+ async function fetchBalancesAndAllowances(parameters) {
794
+ const { client, spender, pairs, options } = parameters;
795
+ if (pairs.length === 0) return /* @__PURE__ */ new Map();
796
+ const batchSize = Math.max(1, options?.batchSize ?? 5e3);
797
+ const retryAttempts = Math.max(1, options?.retryAttempts ?? 3);
798
+ const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 50);
799
+ const blockNumber = options?.blockNumber ? BigInt(options.blockNumber) : void 0;
800
+ const out = /* @__PURE__ */ new Map();
801
+ for (const pairsBatch of mempool.Utils.batch(pairs, batchSize)) {
802
+ const balanceContracts = [];
803
+ const allowanceContracts = [];
804
+ for (const { user, token } of pairsBatch) {
805
+ balanceContracts.push({
806
+ address: token,
807
+ abi: viem.erc20Abi,
808
+ functionName: "balanceOf",
809
+ args: [user]
810
+ });
811
+ allowanceContracts.push({
812
+ address: token,
813
+ abi: viem.erc20Abi,
814
+ functionName: "allowance",
815
+ args: [user, spender]
816
+ });
817
+ }
818
+ const [balances, allowances] = await Promise.all([
819
+ mempool.Utils.retry(
820
+ () => client.multicall({
821
+ allowFailure: false,
822
+ contracts: balanceContracts,
823
+ ...blockNumber ? { blockNumber } : {}
824
+ }),
825
+ retryAttempts,
826
+ retryDelayMs
827
+ ),
828
+ mempool.Utils.retry(
829
+ () => client.multicall({
830
+ allowFailure: false,
831
+ contracts: allowanceContracts,
832
+ ...blockNumber ? { blockNumber } : {}
833
+ }),
834
+ retryAttempts,
835
+ retryDelayMs
836
+ )
837
+ ]);
838
+ for (let i = 0; i < pairsBatch.length; i++) {
839
+ const { user, token } = pairsBatch[i];
840
+ const balance = balances[i];
841
+ const allowance = allowances[i];
842
+ let perUser = out.get(user);
843
+ if (!perUser) {
844
+ perUser = /* @__PURE__ */ new Map();
845
+ out.set(user, perUser);
846
+ }
847
+ perUser.set(token, { balance, allowance });
848
+ }
814
849
  }
815
- if (parameters.status?.length) {
816
- url.searchParams.set("status", parameters.status.join(","));
850
+ return out;
851
+ }
852
+ async function fetch2(parameters) {
853
+ const { client, chainId, spender, type, pairs, options } = parameters;
854
+ if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
855
+ throw new Error(`CallbackType not implemented: ${type}`);
856
+ const map = await fetchBalancesAndAllowances({
857
+ client,
858
+ spender,
859
+ pairs: pairs.map(({ user, contract }) => ({ user, token: contract })),
860
+ options
861
+ });
862
+ const out = [];
863
+ for (const [user, perContract] of map) {
864
+ for (const [contract, { balance, allowance }] of perContract) {
865
+ const amount = balance < allowance ? balance : allowance;
866
+ out.push(
867
+ buildLiquidity({
868
+ type,
869
+ user,
870
+ contract,
871
+ chainId,
872
+ amount: amount.toString(),
873
+ index: 0
874
+ })
875
+ );
876
+ }
817
877
  }
818
- if (parameters.callbackAddresses?.length) {
819
- url.searchParams.set("callback_addresses", parameters.callbackAddresses.join(","));
820
- }
821
- if (parameters.minAmount !== void 0) {
822
- url.searchParams.set("min_amount", parameters.minAmount.toString());
823
- }
824
- if (parameters.maxAmount !== void 0) {
825
- url.searchParams.set("max_amount", parameters.maxAmount.toString());
826
- }
827
- if (parameters.minRate !== void 0) {
828
- url.searchParams.set("min_rate", parameters.minRate.toString());
829
- }
830
- if (parameters.maxRate !== void 0) {
831
- url.searchParams.set("max_rate", parameters.maxRate.toString());
832
- }
833
- if (parameters.minMaturity !== void 0) {
834
- url.searchParams.set("min_maturity", parameters.minMaturity.toString());
835
- }
836
- if (parameters.maxMaturity !== void 0) {
837
- url.searchParams.set("max_maturity", parameters.maxMaturity.toString());
838
- }
839
- if (parameters.minExpiry !== void 0) {
840
- url.searchParams.set("min_expiry", parameters.minExpiry.toString());
841
- }
842
- if (parameters.maxExpiry !== void 0) {
843
- url.searchParams.set("max_expiry", parameters.maxExpiry.toString());
844
- }
845
- if (parameters.collateralAssets?.length) {
846
- url.searchParams.set("collateral_assets", parameters.collateralAssets.join(","));
847
- }
848
- if (parameters.collateralOracles?.length) {
849
- url.searchParams.set("collateral_oracles", parameters.collateralOracles.join(","));
850
- }
851
- if (parameters.collateralTuple?.length) {
852
- const tupleStr = parameters.collateralTuple.map(({ asset, oracle, lltv }) => {
853
- let result = asset;
854
- if (oracle) {
855
- result += `:${oracle}`;
856
- } else if (lltv !== void 0) {
857
- result += `:`;
878
+ return out;
879
+ }
880
+ function serialize(liquidity) {
881
+ const normalized = {
882
+ userPosition: {
883
+ id: liquidity.userPosition.id,
884
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
885
+ user: liquidity.userPosition.user,
886
+ chainId: String(liquidity.userPosition.chainId),
887
+ amount: String(liquidity.userPosition.amount)
888
+ },
889
+ queues: liquidity.queues.map((queueWithPool) => ({
890
+ queue: {
891
+ queueId: queueWithPool.queue.queueId,
892
+ availableLiquidityPoolId: queueWithPool.queue.availableLiquidityPoolId,
893
+ index: queueWithPool.queue.index
894
+ },
895
+ pool: {
896
+ id: queueWithPool.pool.id,
897
+ amount: String(queueWithPool.pool.amount)
858
898
  }
859
- if (lltv !== void 0) result += `:${viem.formatUnits(lltv, 16)}`;
860
- return result;
861
- }).join("#");
862
- url.searchParams.set("collateral_tuple", tupleStr);
863
- }
864
- if (parameters.minLltv !== void 0) {
865
- url.searchParams.set("min_lltv", viem.formatUnits(parameters.minLltv, 16));
866
- }
867
- if (parameters.maxLltv !== void 0) {
868
- url.searchParams.set("max_lltv", viem.formatUnits(parameters.maxLltv, 16));
869
- }
870
- if (parameters.sortBy) {
871
- url.searchParams.set("sort_by", parameters.sortBy);
872
- }
873
- if (parameters.sortOrder) {
874
- url.searchParams.set("sort_order", parameters.sortOrder);
875
- }
876
- if (parameters.cursor) {
877
- url.searchParams.set("cursor", parameters.cursor);
878
- }
879
- if (parameters.limit !== void 0) {
880
- url.searchParams.set("limit", parameters.limit.toString());
881
- }
882
- const { cursor: returnedCursor, data: offers } = await getApi(config, url);
883
- const routerOffers = offers.map(mempool.Format.fromSnakeCase).map(fromResponse);
884
- return {
885
- cursor: returnedCursor,
886
- offers: routerOffers.map(from).map(toResponse)
899
+ })).sort(
900
+ (left, right) => {
901
+ const leftQueueId = left.queue.queueId || "";
902
+ const rightQueueId = right.queue.queueId || "";
903
+ if (leftQueueId < rightQueueId) return -1;
904
+ if (leftQueueId > rightQueueId) return 1;
905
+ const leftPoolId = left.pool.id;
906
+ const rightPoolId = right.pool.id;
907
+ if (leftPoolId < rightPoolId) return -1;
908
+ if (leftPoolId > rightPoolId) return 1;
909
+ const leftIndex = left.queue.index;
910
+ const rightIndex = right.queue.index;
911
+ if (leftIndex < rightIndex) return -1;
912
+ if (leftIndex > rightIndex) return 1;
913
+ return 0;
914
+ }
915
+ )
887
916
  };
917
+ return JSON.stringify(normalized);
888
918
  }
889
- async function match(config, parameters) {
890
- const url = new URL(`${config.url.toString()}v1/offers/match`);
891
- url.searchParams.set("side", parameters.side);
892
- url.searchParams.set("chain_id", parameters.chainId.toString());
893
- if (parameters.rate !== void 0) {
894
- url.searchParams.set("rate", parameters.rate.toString());
895
- }
896
- if (parameters.collaterals?.length) {
897
- const collateralsStr = parameters.collaterals.map(({ asset, oracle, lltv }) => `${asset}:${oracle}:${viem.formatUnits(lltv, 16)}`).join("#");
898
- url.searchParams.set("collaterals", collateralsStr);
899
- }
900
- if (parameters.maturity !== void 0) {
901
- url.searchParams.set("maturity", parameters.maturity.toString());
902
- }
903
- if (parameters.minMaturity !== void 0) {
904
- url.searchParams.set("min_maturity", parameters.minMaturity.toString());
905
- }
906
- if (parameters.maxMaturity !== void 0) {
907
- url.searchParams.set("max_maturity", parameters.maxMaturity.toString());
908
- }
909
- if (parameters.loanToken) {
910
- url.searchParams.set("loan_token", parameters.loanToken);
911
- }
912
- if (parameters.creator) {
913
- url.searchParams.set("creator", parameters.creator);
914
- }
915
- if (parameters.status?.length) {
916
- url.searchParams.set("status", parameters.status.join(","));
917
- }
918
- if (parameters.cursor) {
919
- url.searchParams.set("cursor", parameters.cursor);
920
- }
921
- if (parameters.limit !== void 0) {
922
- url.searchParams.set("limit", parameters.limit.toString());
923
- }
924
- const { cursor: returnedCursor, data: offers } = await getApi(config, url);
925
- const routerOffers = offers.map(mempool.Format.fromSnakeCase).map(fromResponse);
919
+
920
+ // src/core/Logger.ts
921
+ var Logger_exports = {};
922
+ __export(Logger_exports, {
923
+ LogLevelValues: () => LogLevelValues,
924
+ defaultLogger: () => defaultLogger,
925
+ getLogger: () => getLogger,
926
+ runWithLogger: () => runWithLogger,
927
+ silentLogger: () => silentLogger
928
+ });
929
+ var LogLevelValues = [
930
+ "silent",
931
+ "trace",
932
+ "debug",
933
+ "info",
934
+ "warn",
935
+ "error",
936
+ "fatal"
937
+ ];
938
+ function defaultLogger(minLevel) {
939
+ const threshold = minLevel ?? "info";
940
+ const levelIndexByName = LogLevelValues.reduce(
941
+ (acc, lvl, idx) => {
942
+ acc[lvl] = idx;
943
+ return acc;
944
+ },
945
+ {}
946
+ );
947
+ const isEnabled = (methodLevel) => levelIndexByName[methodLevel] >= levelIndexByName[threshold];
948
+ const wrap = (consoleMethod, methodLevel) => isEnabled(methodLevel) ? (entry) => {
949
+ console[consoleMethod](viem.stringify({ level: methodLevel, ...entry }));
950
+ } : () => {
951
+ };
926
952
  return {
927
- cursor: returnedCursor,
928
- offers: routerOffers.map(from).map(toResponse)
953
+ trace: wrap("trace", "trace"),
954
+ debug: wrap("debug", "debug"),
955
+ info: wrap("info", "info"),
956
+ warn: wrap("warn", "warn"),
957
+ error: wrap("error", "error"),
958
+ fatal: wrap("error", "fatal")
929
959
  };
930
960
  }
931
- async function getApi(config, url) {
932
- const pathname = url.pathname;
933
- let action;
934
- switch (true) {
935
- case pathname.includes("/v1/offers/match"):
936
- 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}`
961
+ function silentLogger() {
962
+ const noop = () => {
963
+ };
964
+ return { trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop };
965
+ }
966
+ var loggerContext = new async_hooks.AsyncLocalStorage();
967
+ function runWithLogger(logger, fn) {
968
+ return loggerContext.run(logger, fn);
969
+ }
970
+ function getLogger() {
971
+ return loggerContext.getStore() ?? defaultLogger();
972
+ }
973
+
974
+ // src/core/Collector/BuyWithEmptyCallbackLiquidityCollector.ts
975
+ function createBuyWithEmptyCallbackLiquidityCollector(params) {
976
+ const {
977
+ client,
978
+ collectorBlockStore,
979
+ liquidityStore,
980
+ offerStore,
981
+ chain,
982
+ options: { maxBatchSize = 1e3, interval = 3e4 } = {}
983
+ } = params;
984
+ const collector = "buy_with_empty_callback_liquidity";
985
+ async function getUniqueUserTokenPairs() {
986
+ const unique = /* @__PURE__ */ new Set();
987
+ const pairs = [];
988
+ let cursor;
989
+ do {
990
+ const { offers: offers2, nextCursor } = await offerStore.getAll({
991
+ query: {
992
+ side: "buy",
993
+ chains: [Number(chain.id)],
994
+ cursor,
995
+ limit: 1e3
996
+ }
944
997
  });
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
998
+ cursor = nextCursor ?? void 0;
999
+ for (const offer of offers2) {
1000
+ const creator = offer.offering;
1001
+ const loanToken = offer.loanToken;
1002
+ const key = `${offer.offering}-${offer.loanToken}`.toLowerCase();
1003
+ if (unique.has(key)) continue;
1004
+ unique.add(key);
1005
+ pairs.push({ user: creator, token: loanToken });
1006
+ }
1007
+ } while (cursor);
1008
+ return pairs;
1009
+ }
1010
+ const collect = mempool.Utils.lazy((emit) => {
1011
+ const logger = getLogger();
1012
+ logger.info({
1013
+ collector,
1014
+ chainId: chain.id,
1015
+ msg: "start",
1016
+ interval,
1017
+ maxBatchSize
950
1018
  });
951
- }
952
- const response = await fetch(url.toString(), {
953
- method: "GET",
954
- headers: config.headers
1019
+ return mempool.Utils.poll(
1020
+ async () => {
1021
+ const publicClient = client.extend(viem.publicActions);
1022
+ const pairs = await getUniqueUserTokenPairs();
1023
+ const latestBlockNumber = Number(await actions.getBlockNumber(publicClient));
1024
+ logger.debug({
1025
+ collector,
1026
+ chainId: chain.id,
1027
+ msg: "snapshot started",
1028
+ blockNumber: latestBlockNumber
1029
+ });
1030
+ const liquidityEntries = await fetch2({
1031
+ client: publicClient,
1032
+ chainId: chain.id,
1033
+ spender: chain.morpho,
1034
+ type: "buy_with_empty_callback" /* BuyWithEmptyCallback */,
1035
+ pairs: pairs.map(({ user, token }) => ({ user, contract: token })),
1036
+ options: {
1037
+ blockNumber: latestBlockNumber,
1038
+ batchSize: maxBatchSize
1039
+ }
1040
+ });
1041
+ logger.debug({
1042
+ collector,
1043
+ chainId: chain.id,
1044
+ msg: `found ${liquidityEntries.length} liquidity entries`,
1045
+ blockNumber: latestBlockNumber
1046
+ });
1047
+ const existingLiquidityEntries = await liquidityStore.getAll();
1048
+ const existingById = /* @__PURE__ */ new Map();
1049
+ for (const liquidityEntry of existingLiquidityEntries) {
1050
+ const liquidityId = liquidityEntry.userPosition.id;
1051
+ const serializedEntry = serialize(liquidityEntry);
1052
+ existingById.set(liquidityId, serializedEntry);
1053
+ }
1054
+ for (const liquidity of liquidityEntries) {
1055
+ const serialized = serialize(liquidity);
1056
+ const prevSerialized = existingById.get(liquidity.userPosition.id);
1057
+ if (prevSerialized === serialized) continue;
1058
+ await liquidityStore.save({ liquidity });
1059
+ }
1060
+ await collectorBlockStore.saveBlockNumber({
1061
+ collectorName: collector,
1062
+ chainId: chain.id,
1063
+ blockNumber: latestBlockNumber
1064
+ });
1065
+ logger.info({
1066
+ collector,
1067
+ chainId: chain.id,
1068
+ msg: "snapshot done",
1069
+ blockNumber: latestBlockNumber
1070
+ });
1071
+ emit(latestBlockNumber);
1072
+ },
1073
+ { interval }
1074
+ );
955
1075
  });
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();
1076
+ const onReorg = (_lastFinalizedBlockNumber) => {
1077
+ };
1078
+ return {
1079
+ name: collector,
1080
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName: collector, chainId: chain.id }),
1081
+ collect,
1082
+ onReorg
1083
+ };
1084
+ }
1085
+ function createChainReorgsCollector(parameters) {
1086
+ const collectorName = "chain_reorgs";
1087
+ const {
1088
+ client,
1089
+ subscribers,
1090
+ collectorStore,
1091
+ chain,
1092
+ options: { maxBatchSize = 25, interval } = {}
1093
+ } = parameters;
1094
+ let finalizedBlock = null;
1095
+ let unfinalizedBlocks = [];
1096
+ const commonAncestor = (block) => {
1097
+ const parent = unfinalizedBlocks.find((b) => b.hash === block.parentHash);
1098
+ if (parent) return parent;
1099
+ if (finalizedBlock == null) throw new Error("Failed to get common ancestor");
1100
+ return finalizedBlock;
1101
+ };
1102
+ const reconcile = async (block) => {
1103
+ if (block.hash === null || block.number === null || block.parentHash === null)
1104
+ throw new Error("Failed to get block");
1105
+ const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
1106
+ if (latestBlock === void 0) {
1107
+ unfinalizedBlocks.push({
1108
+ hash: block.hash,
1109
+ number: block.number,
1110
+ parentHash: block.parentHash
1111
+ });
1112
+ return;
964
1113
  }
965
- throw new HttpGetOffersFailedError(`GET request returned ${response.status}`, {
966
- details: await response.text()
1114
+ if (latestBlock.hash === block.hash) return;
1115
+ if (latestBlock.number >= block.number) {
1116
+ const ancestor = commonAncestor(block);
1117
+ console.log(
1118
+ `reorg detected, latestBlock.number: ${latestBlock.number} > block.number: ${block.number} on chain ${chain.id}. Ancestor: ${ancestor.number}`
1119
+ );
1120
+ subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
1121
+ unfinalizedBlocks = [];
1122
+ return;
1123
+ }
1124
+ if (latestBlock.number + 1n < block.number) {
1125
+ console.log(
1126
+ `missing blocks between block ${latestBlock.number} and block ${block.number} on chain ${chain.id}`
1127
+ );
1128
+ const missingBlockNumbers = (() => {
1129
+ const missingBlockNumbers2 = [];
1130
+ let start2 = latestBlock.number + 1n;
1131
+ const threshold = latestBlock.number + BigInt(maxBatchSize) > block.number ? block.number : latestBlock.number + BigInt(maxBatchSize);
1132
+ while (start2 < threshold) {
1133
+ missingBlockNumbers2.push(start2);
1134
+ start2 = start2 + 1n;
1135
+ }
1136
+ return missingBlockNumbers2;
1137
+ })();
1138
+ const missingBlocks = await Promise.all(
1139
+ missingBlockNumbers.map(
1140
+ (blockNumber) => client.getBlock({ blockNumber, includeTransactions: false })
1141
+ )
1142
+ );
1143
+ for (const missingBlock of missingBlocks) await reconcile(missingBlock);
1144
+ await reconcile(block);
1145
+ return;
1146
+ }
1147
+ if (block.parentHash !== latestBlock.hash) {
1148
+ const ancestor = commonAncestor(block);
1149
+ console.log(
1150
+ `reorg detected, block.parentHash: ${block.parentHash} !== latestBlock.hash: ${latestBlock.hash} on chain ${chain.id}. Ancestor: ${ancestor.number}`
1151
+ );
1152
+ subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
1153
+ unfinalizedBlocks = [];
1154
+ return;
1155
+ }
1156
+ unfinalizedBlocks.push({
1157
+ hash: block.hash,
1158
+ number: block.number,
1159
+ parentHash: block.parentHash
967
1160
  });
968
- }
969
- return response.json();
1161
+ };
1162
+ const collect = mempool.Utils.lazy((emit) => {
1163
+ let tick = 0;
1164
+ const fetchFinalizedBlock = async () => {
1165
+ if (tick % 20 === 0 || finalizedBlock === null) {
1166
+ finalizedBlock = await client.getBlock({
1167
+ blockTag: "finalized",
1168
+ includeTransactions: false
1169
+ });
1170
+ if (finalizedBlock === null) throw new Error("Failed to get finalized block");
1171
+ }
1172
+ tick++;
1173
+ };
1174
+ return mempool.Utils.poll(
1175
+ async () => {
1176
+ await fetchFinalizedBlock();
1177
+ const latestBlock = await client.getBlock({
1178
+ blockTag: "latest",
1179
+ includeTransactions: false
1180
+ });
1181
+ await reconcile(latestBlock);
1182
+ const lastBlockNumber = Number(latestBlock.number);
1183
+ await collectorStore.saveBlockNumber({
1184
+ collectorName: "chain_reorgs",
1185
+ chainId: chain.id,
1186
+ blockNumber: lastBlockNumber
1187
+ });
1188
+ emit(lastBlockNumber);
1189
+ },
1190
+ { interval: interval ?? 3e4 }
1191
+ );
1192
+ });
1193
+ return {
1194
+ name: collectorName,
1195
+ lastSyncedBlock: async () => await collectorStore.getBlockNumber({ collectorName, chainId: chain.id }),
1196
+ collect,
1197
+ onReorg: (_) => {
1198
+ }
1199
+ };
970
1200
  }
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
1201
 
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);
1202
+ // src/core/Collector/Collector.ts
1203
+ function start(collector) {
1204
+ let stopped = false;
1205
+ const it = collector.collect();
1206
+ (async () => {
1207
+ while (!stopped) await it.next();
1208
+ await it.return();
1209
+ })();
1210
+ return () => {
1211
+ stopped = true;
1039
1212
  };
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;
1213
+ }
1214
+ function createConsumedEventsCollector(parameters) {
1215
+ const {
1216
+ client,
1217
+ contractAddress,
1218
+ offerStore,
1219
+ collectorBlockStore,
1220
+ options: { maxBatchSize = 1e3, interval = 3e4 } = {},
1221
+ chainId
1222
+ } = parameters;
1223
+ const collector = "consumed_events";
1224
+ const collect = mempool.Utils.lazy((emit) => {
1225
+ const logger = getLogger();
1226
+ logger.info({
1227
+ collector,
1228
+ chainId: chainId.toString(),
1229
+ msg: "start",
1230
+ interval,
1231
+ maxBatchSize
1232
+ });
1233
+ return mempool.Utils.poll(
1234
+ async () => {
1235
+ const lastSyncedBlock = await collectorBlockStore.getBlockNumber({
1236
+ collectorName: collector,
1237
+ chainId
1238
+ });
1239
+ logger.debug({ collector, chainId, msg: "stream started", blockNumber: lastSyncedBlock });
1240
+ const stream = mempool.Chain.streamLogs({
1241
+ client,
1242
+ contractAddress,
1243
+ event: consumedEvent,
1244
+ blockNumberGte: lastSyncedBlock,
1245
+ order: "asc",
1246
+ options: { maxBatchSize }
1247
+ });
1248
+ let blockNumber = lastSyncedBlock;
1249
+ for await (const { logs, blockNumber: lastStreamBlockNumber } of stream) {
1250
+ try {
1251
+ const parsedLogs = viem.parseEventLogs({ abi: [consumedEvent], logs });
1252
+ logger.debug({ collector, chainId, msg: `found ${parsedLogs.length} consumed events` });
1253
+ for (const log of parsedLogs) {
1254
+ if (log.blockNumber === null || log.logIndex === null || log.transactionHash === null) {
1255
+ logger.debug({
1256
+ collector,
1257
+ chainId,
1258
+ msg: "skipping log because it is missing required fields"
1259
+ });
1260
+ continue;
1261
+ }
1262
+ const offer = fromConsumedLog({
1263
+ blockNumber: log.blockNumber,
1264
+ logIndex: log.logIndex,
1265
+ chainId: Number(chainId),
1266
+ transactionHash: log.transactionHash,
1267
+ user: log.args.user,
1268
+ nonce: log.args.nonce,
1269
+ amount: log.args.amount
1270
+ });
1271
+ await offerStore.updateConsumedAmount({
1272
+ id: offer.id,
1273
+ chainId: offer.chainId,
1274
+ offering: offer.offering,
1275
+ nonce: offer.nonce,
1276
+ consumed: offer.amount
1277
+ });
1278
+ }
1279
+ blockNumber = lastStreamBlockNumber;
1280
+ await collectorBlockStore.saveBlockNumber({
1281
+ collectorName: collector,
1282
+ chainId,
1283
+ blockNumber
1284
+ });
1285
+ emit(blockNumber);
1286
+ } catch (err) {
1287
+ logger.error({ err, msg: "Failed to process offer_consumed events" });
1288
+ }
1077
1289
  }
1078
- return sortOrder === "asc" ? a.expiry > b.expiry ? 1 : -1 : b.expiry > a.expiry ? 1 : -1;
1079
- }
1290
+ logger.info({
1291
+ collector,
1292
+ chainId,
1293
+ msg: "stream finished",
1294
+ startBlock: lastSyncedBlock,
1295
+ endBlock: blockNumber
1296
+ });
1297
+ },
1298
+ { interval }
1299
+ );
1300
+ });
1301
+ const onReorg = (_lastFinalizedBlockNumber) => {
1080
1302
  };
1081
1303
  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");
1304
+ name: collector,
1305
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName: collector, chainId }),
1306
+ onReorg,
1307
+ collect
1308
+ };
1309
+ }
1310
+
1311
+ // src/core/Validation.ts
1312
+ var Validation_exports = {};
1313
+ __export(Validation_exports, {
1314
+ run: () => run
1315
+ });
1316
+ async function run(parameters) {
1317
+ const { items, rules, ctx = {}, chunkSize } = parameters;
1318
+ const issues = [];
1319
+ let validItems = items.slice();
1320
+ for (const rule of rules) {
1321
+ if (validItems.length === 0) return { valid: [], issues };
1322
+ const indicesToRemove = /* @__PURE__ */ new Set();
1323
+ if (rule.kind === "single") {
1324
+ for (let i = 0; i < validItems.length; i++) {
1325
+ const item = validItems[i];
1326
+ const issue = await rule.run(item, ctx);
1327
+ if (issue) {
1328
+ issues.push({ ...issue, ruleName: rule.name, item });
1329
+ indicesToRemove.add(i);
1276
1330
  }
1277
- offers = offers.filter(
1278
- (o) => sortOrder === "asc" ? o.rate >= BigInt(cursor.rate) : o.rate <= BigInt(cursor.rate)
1279
- );
1280
1331
  }
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;
1332
+ } else if (rule.kind === "batch") {
1333
+ const exec = async (slice, offset) => {
1334
+ const map = await rule.run(slice, ctx);
1335
+ for (let i = 0; i < slice.length; i++) {
1336
+ const issue = map.get(i);
1337
+ if (issue !== void 0) {
1338
+ issues.push({ ...issue, ruleName: rule.name, item: slice[i] });
1339
+ indicesToRemove.add(offset + i);
1340
+ }
1333
1341
  }
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
1342
  };
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++;
1343
+ if (!chunkSize) await exec(validItems, 0);
1344
+ else {
1345
+ for (let i = 0; i < validItems.length; i += chunkSize) {
1346
+ await exec(validItems.slice(i, i + chunkSize), i);
1390
1347
  }
1391
1348
  }
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
1403
- });
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);
1417
1349
  }
1350
+ validItems = validItems.filter((_, i) => !indicesToRemove.has(i));
1351
+ }
1352
+ return {
1353
+ valid: validItems,
1354
+ issues
1418
1355
  };
1419
1356
  }
1420
1357
 
1421
- // src/core/router/Server.ts
1422
- async function serve(parameters) {
1423
- const app = new hono.Hono();
1424
- const store = parameters.store ? parameters.store : memory({
1425
- offers: /* @__PURE__ */ new Map(),
1426
- filled: /* @__PURE__ */ new Map()
1358
+ // src/core/ValidationRule.ts
1359
+ var ValidationRule_exports = {};
1360
+ __export(ValidationRule_exports, {
1361
+ batch: () => batch,
1362
+ morpho: () => morpho,
1363
+ single: () => single
1364
+ });
1365
+ function single(name, run2) {
1366
+ return { kind: "single", name, run: run2 };
1367
+ }
1368
+ function batch(name, run2) {
1369
+ return { kind: "batch", name, run: run2 };
1370
+ }
1371
+ function morpho() {
1372
+ const chainId = single("chain_id", (offer, { chain }) => {
1373
+ if (chain.id !== offer.chainId) {
1374
+ return {
1375
+ message: `Chain ID ${offer.chainId} is not the same as the chain ID in the context (${chain.id})`
1376
+ };
1377
+ }
1427
1378
  });
1428
- app.get("/v1/offers", async (c) => {
1429
- try {
1430
- const params = parse("get_offers", c.req.query());
1431
- const offers = await store.getAll({
1432
- query: {
1433
- creators: params.creators,
1434
- side: params.side,
1435
- chains: params.chains,
1436
- loanTokens: params.loan_tokens,
1437
- status: params.status,
1438
- callbackAddresses: params.callback_addresses,
1439
- minAmount: params.min_amount,
1440
- maxAmount: params.max_amount,
1441
- minRate: params.min_rate,
1442
- maxRate: params.max_rate,
1443
- minMaturity: params.min_maturity,
1444
- maxMaturity: params.max_maturity,
1445
- minExpiry: params.min_expiry,
1446
- maxExpiry: params.max_expiry,
1447
- collateralAssets: params.collateral_assets,
1448
- collateralOracles: params.collateral_oracles,
1449
- collateralTuple: params.collateral_tuple,
1450
- minLltv: params.min_lltv,
1451
- maxLltv: params.max_lltv,
1452
- sortBy: params.sort_by,
1453
- sortOrder: params.sort_order,
1454
- cursor: params.cursor,
1455
- limit: params.limit
1456
- }
1457
- });
1458
- return success(c, {
1459
- data: offers.offers.map(
1460
- (offer) => mempool.Format.stringifyBigint(mempool.Format.toSnakeCase(toResponse(offer)))
1461
- ),
1462
- cursor: offers.nextCursor ?? null
1463
- });
1464
- } catch (err) {
1465
- console.error(err);
1466
- return error(err, c);
1379
+ const loanToken = single("loan_token", (offer, { chain }) => {
1380
+ const tokens = new Set(
1381
+ Array.from(chain.whitelistedAssets.values()).map((a) => a.toLowerCase())
1382
+ );
1383
+ if (!tokens.has(offer.loanToken.toLowerCase())) {
1384
+ return {
1385
+ message: `Loan token ${offer.loanToken} is not whitelisted on chain ${offer.chainId}`
1386
+ };
1467
1387
  }
1468
1388
  });
1469
- app.get("/v1/offers/match", async (c) => {
1470
- try {
1471
- const params = parse("match_offers", c.req.query());
1472
- const offers = await store.findMatchingOffers({
1473
- side: params.side,
1474
- chainId: params.chain_id,
1475
- rate: params.rate,
1476
- collaterals: params.collaterals,
1477
- maturity: params.maturity,
1478
- minMaturity: params.min_maturity,
1479
- maxMaturity: params.max_maturity,
1480
- loanToken: params.loan_token,
1481
- creator: params.creator,
1482
- cursor: params.cursor,
1483
- limit: params.limit
1484
- });
1485
- return success(c, {
1486
- data: offers.offers.map(
1487
- (offer) => mempool.Format.stringifyBigint(mempool.Format.toSnakeCase(toResponse(offer)))
1488
- ),
1489
- cursor: offers.nextCursor ?? null
1490
- });
1491
- } catch (err) {
1492
- console.error(err);
1493
- return error(err, c);
1494
- }
1495
- });
1496
- nodeServer.serve(
1497
- {
1498
- fetch: app.fetch,
1499
- port: parameters.port
1500
- },
1501
- (info) => {
1502
- console.log(`Started local router @ http://localhost:${info.port}`);
1503
- }
1504
- );
1505
- }
1506
- function error(error2, c) {
1507
- if (error2 instanceof APIError) {
1508
- return handleAPIError(error2, c);
1509
- }
1510
- if (error2 instanceof SyntaxError) {
1511
- return handleAPIError(new BadRequestError(error2.message), c);
1512
- }
1513
- if (error2 instanceof v4.z.ZodError) {
1514
- return handleAPIError(handleZodError(error2), c);
1515
- }
1516
- return handleAPIError(new InternalServerError(), c);
1517
- }
1518
- function success(c, {
1519
- data,
1520
- cursor
1521
- }) {
1522
- return c.json({
1523
- status: "success",
1524
- cursor,
1525
- data,
1526
- meta: {
1527
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1389
+ const expiry = single("expiry", (offer, _) => {
1390
+ if (offer.expiry < Math.floor(Date.now() / 1e3)) {
1391
+ return { message: "Expiry mismatch" };
1528
1392
  }
1529
1393
  });
1530
- }
1531
- var APIError = class extends Error {
1532
- constructor(statusCode, message, code, details) {
1533
- super(message);
1534
- this.statusCode = statusCode;
1535
- this.code = code;
1536
- this.details = details;
1537
- this.name = "APIError";
1538
- }
1539
- };
1540
- var ValidationError = class extends APIError {
1541
- constructor(message, details) {
1542
- super(400, message, "VALIDATION_ERROR", details);
1543
- }
1544
- };
1545
- var NotFoundError = class extends APIError {
1546
- constructor(message) {
1547
- super(404, message, "NOT_FOUND");
1548
- }
1549
- };
1550
- var InternalServerError = class extends APIError {
1551
- constructor(message = "Internal server error") {
1552
- super(500, message, "INTERNAL_SERVER_ERROR");
1553
- }
1554
- };
1555
- var BadRequestError = class extends APIError {
1556
- constructor(message = "Invalid JSON format", details) {
1557
- super(400, message, "BAD_REQUEST", details);
1558
- }
1559
- };
1560
- function handleZodError(error2) {
1561
- const formattedErrors = error2.issues.map((err) => {
1562
- const field = err.path.join(".");
1563
- let issue = err.message;
1564
- if (err.code === "invalid_type") {
1565
- if (err.message.includes("received undefined")) {
1566
- issue = `${field} is required`;
1567
- } else {
1568
- issue = err.message;
1569
- }
1570
- } else if (err.code === "invalid_format") {
1571
- issue = `${field} has an invalid format`;
1394
+ const callback = single("empty_callback", (offer, _) => {
1395
+ if (!offer.buy || offer.callback.data !== "0x") {
1396
+ return { message: "Callback not supported yet." };
1572
1397
  }
1573
- return {
1574
- field,
1575
- issue
1576
- };
1577
1398
  });
1578
- return new ValidationError("Validation failed", formattedErrors);
1399
+ return [
1400
+ chainId,
1401
+ loanToken,
1402
+ expiry,
1403
+ // note: callback rule should be the last one, since it does not mean that the offer is forever invalid
1404
+ // integrators should be able to choose if they want to keep the offer or not
1405
+ callback
1406
+ ];
1579
1407
  }
1580
- function handleAPIError(error2, c) {
1581
- return c.json({
1582
- statusCode: error2.statusCode,
1583
- body: JSON.stringify({
1584
- status: "error",
1585
- error: {
1586
- code: error2.code,
1587
- message: error2.message,
1588
- ...error2.details && typeof error2.details === "object" ? { details: error2.details } : {}
1408
+
1409
+ // src/core/Collector/MempoolCollector.ts
1410
+ function createMempoolCollector(parameters) {
1411
+ const {
1412
+ mempool: mempool$1,
1413
+ offerStore,
1414
+ collectorBlockStore,
1415
+ chain,
1416
+ options: { maxBatchSize = 1e3, interval = 3e4 } = {}
1417
+ } = parameters;
1418
+ const collector = "mempool_offers";
1419
+ const collect = mempool.Utils.lazy((emit) => {
1420
+ const logger = getLogger();
1421
+ logger.info({
1422
+ collector,
1423
+ chainId: chain.id,
1424
+ msg: "start",
1425
+ interval,
1426
+ maxBatchSize
1427
+ });
1428
+ let lastSyncedBlock = null;
1429
+ return mempool$1.watch({
1430
+ lastSyncedBlock: async () => {
1431
+ lastSyncedBlock = await collectorBlockStore.getBlockNumber({
1432
+ collectorName: collector,
1433
+ chainId: chain.id
1434
+ });
1435
+ logger.debug({
1436
+ collector,
1437
+ chainId: chain.id,
1438
+ msg: "stream started",
1439
+ blockNumber: lastSyncedBlock
1440
+ });
1441
+ return lastSyncedBlock;
1589
1442
  },
1590
- meta: {
1591
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1443
+ polling: { maxBatchSize, interval },
1444
+ onOffers: async (offers2, blockNumber) => {
1445
+ logger.debug({ collector, chainId: chain.id, msg: `found ${offers2.length} offers` });
1446
+ try {
1447
+ const { valid: validOffers, issues } = await run({
1448
+ items: offers2,
1449
+ rules: morpho(),
1450
+ ctx: { chain }
1451
+ });
1452
+ const invalidOffersToSave = [];
1453
+ const issueToStatus = {
1454
+ empty_callback: "callback_not_supported",
1455
+ sell_offers_empty_callback: "callback_not_supported",
1456
+ buy_offers_empty_callback: "callback_error"
1457
+ };
1458
+ for (const issue of issues) {
1459
+ const status = issueToStatus[issue.ruleName];
1460
+ if (status) {
1461
+ invalidOffersToSave.push({
1462
+ offer: issue.item,
1463
+ status,
1464
+ metadata: { issue: issue.ruleName }
1465
+ });
1466
+ }
1467
+ }
1468
+ await offerStore.createMany([
1469
+ ...validOffers.map((offer) => ({
1470
+ offer,
1471
+ status: "valid"
1472
+ })),
1473
+ ...invalidOffersToSave
1474
+ ]);
1475
+ } catch (err) {
1476
+ logger.error({
1477
+ err,
1478
+ msg: "Failed to process offer_created events, falling back to unverified status"
1479
+ });
1480
+ await offerStore.createMany(
1481
+ offers2.map((offer) => ({
1482
+ offer,
1483
+ status: "unverified",
1484
+ metadata: { issue: "Offer processing failed" }
1485
+ }))
1486
+ );
1487
+ }
1488
+ await collectorBlockStore.saveBlockNumber({
1489
+ collectorName: collector,
1490
+ chainId: chain.id,
1491
+ blockNumber
1492
+ });
1493
+ emit(blockNumber);
1494
+ if (lastSyncedBlock === null) return;
1495
+ logger.info({
1496
+ collector,
1497
+ chainId: chain.id,
1498
+ msg: "stream finished",
1499
+ startBlock: lastSyncedBlock,
1500
+ endBlock: blockNumber
1501
+ });
1592
1502
  }
1593
- })
1503
+ });
1594
1504
  });
1505
+ const onReorg = (_lastFinalizedBlockNumber) => {
1506
+ };
1507
+ return {
1508
+ name: collector,
1509
+ lastSyncedBlock: async () => await collectorBlockStore.getBlockNumber({ collectorName: collector, chainId: chain.id }),
1510
+ collect,
1511
+ onReorg
1512
+ };
1595
1513
  }
1596
1514
 
1597
- // src/Liquidity.ts
1598
- var Liquidity_exports = {};
1599
- __export(Liquidity_exports, {
1600
- fetch: () => fetch2,
1601
- fetchBalancesAndAllowances: () => fetchBalancesAndAllowances,
1602
- serialize: () => serialize
1515
+ // src/core/CollectorBlockStore.ts
1516
+ var CollectorBlockStore_exports = {};
1517
+ __export(CollectorBlockStore_exports, {
1518
+ create: () => create,
1519
+ memory: () => memory
1603
1520
  });
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]
1521
+
1522
+ // src/core/OfferStore/drizzle/schema.ts
1523
+ var schema_exports = {};
1524
+ __export(schema_exports, {
1525
+ VERSION: () => VERSION,
1526
+ availableLiquidityPools: () => availableLiquidityPools,
1527
+ availableLiquidityQueues: () => availableLiquidityQueues,
1528
+ collectorBlockNumbers: () => collectorBlockNumbers,
1529
+ consumed: () => consumed,
1530
+ offerCollaterals: () => offerCollaterals,
1531
+ offerStatus: () => offerStatus,
1532
+ offers: () => offers,
1533
+ userPositions: () => userPositions
1534
+ });
1535
+ var VERSION = "offers_v1.1";
1536
+ var s = pgCore.pgSchema(VERSION);
1537
+ var offers = s.table(
1538
+ "offers",
1539
+ {
1540
+ hash: pgCore.varchar("hash", { length: 66 }).primaryKey(),
1541
+ offering: pgCore.varchar("offering", { length: 42 }).notNull(),
1542
+ assets: pgCore.numeric("assets", { precision: 78, scale: 0 }).notNull(),
1543
+ rate: pgCore.bigint("rate", { mode: "bigint" }).notNull(),
1544
+ maturity: pgCore.integer("maturity").notNull(),
1545
+ expiry: pgCore.integer("expiry").notNull(),
1546
+ start: pgCore.integer("start").notNull(),
1547
+ nonce: pgCore.bigint("nonce", { mode: "bigint" }).notNull(),
1548
+ buy: pgCore.boolean("buy").notNull(),
1549
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
1550
+ loanToken: pgCore.varchar("loan_token", { length: 42 }).notNull(),
1551
+ callbackAddress: pgCore.varchar("callback_address", { length: 42 }).notNull(),
1552
+ callbackData: pgCore.text("callback_data").notNull(),
1553
+ callbackGasLimit: pgCore.bigint("callback_gas_limit", { mode: "bigint" }).notNull(),
1554
+ signature: pgCore.varchar("signature", { length: 132 }),
1555
+ callbackId: pgCore.varchar("callback_id", { length: 256 }),
1556
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
1557
+ },
1558
+ (table) => [
1559
+ pgCore.index("offers_offering_idx").on(table.offering),
1560
+ pgCore.index("offers_buy_idx").on(table.buy),
1561
+ pgCore.index("offers_chain_id_idx").on(table.chainId),
1562
+ pgCore.index("offers_loan_token_idx").on(table.loanToken),
1563
+ pgCore.index("offers_maturity_idx").on(table.maturity),
1564
+ pgCore.index("offers_expiry_idx").on(table.expiry),
1565
+ pgCore.index("offers_rate_idx").on(table.rate),
1566
+ pgCore.index("offers_assets_idx").on(table.assets),
1567
+ // Compound indices for cursor pagination with hash
1568
+ pgCore.index("offers_rate_hash_idx").on(table.rate, table.hash),
1569
+ pgCore.index("offers_maturity_hash_idx").on(table.maturity, table.hash),
1570
+ pgCore.index("offers_expiry_hash_idx").on(table.expiry, table.hash),
1571
+ pgCore.index("offers_assets_hash_idx").on(table.assets, table.hash)
1572
+ ]
1573
+ );
1574
+ var offerCollaterals = s.table(
1575
+ "offer_collaterals",
1576
+ {
1577
+ id: pgCore.serial("id").primaryKey(),
1578
+ offerHash: pgCore.varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
1579
+ asset: pgCore.varchar("asset", { length: 42 }).notNull(),
1580
+ oracle: pgCore.varchar("oracle", { length: 42 }).notNull(),
1581
+ lltv: pgCore.bigint("lltv", { mode: "bigint" }).notNull()
1582
+ },
1583
+ (table) => [
1584
+ pgCore.index("offer_collaterals_offer_hash_idx").on(table.offerHash),
1585
+ pgCore.index("offer_collaterals_asset_idx").on(table.asset),
1586
+ pgCore.index("offer_collaterals_oracle_idx").on(table.oracle),
1587
+ // Composite index
1588
+ pgCore.index("offer_collaterals_tuple_idx").on(table.asset, table.oracle, table.lltv)
1589
+ ]
1590
+ );
1591
+ var offerStatus = s.table(
1592
+ "offer_status",
1593
+ {
1594
+ id: pgCore.serial("id").primaryKey(),
1595
+ offerHash: pgCore.varchar("offer_hash", { length: 66 }).notNull().references(() => offers.hash, { onDelete: "cascade" }),
1596
+ status: pgCore.text("status").$type().notNull(),
1597
+ metadata: pgCore.jsonb("metadata"),
1598
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
1599
+ },
1600
+ (table) => [
1601
+ pgCore.index("offer_status_offer_hash_created_at_idx").on(table.offerHash, drizzleOrm.desc(table.createdAt)),
1602
+ pgCore.index("offer_status_status_idx").on(table.status)
1603
+ ]
1604
+ );
1605
+ var consumed = s.table(
1606
+ "consumed_per_user_and_nonce",
1607
+ {
1608
+ id: pgCore.varchar("id", { length: 255 }).primaryKey(),
1609
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
1610
+ offering: pgCore.varchar("offering", { length: 42 }).notNull(),
1611
+ nonce: pgCore.bigint("nonce", { mode: "bigint" }).notNull(),
1612
+ consumed: pgCore.numeric("consumed", { precision: 78, scale: 0 }).notNull(),
1613
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
1614
+ },
1615
+ (table) => [
1616
+ pgCore.index("consumed_per_user_and_nonce_chain_id_offering_nonce_created_at_idx").on(
1617
+ table.chainId,
1618
+ table.offering,
1619
+ table.nonce,
1620
+ drizzleOrm.desc(table.createdAt)
1621
+ )
1622
+ ]
1623
+ );
1624
+ var collectorBlockNumbers = s.table(
1625
+ "collector_block_numbers",
1626
+ {
1627
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
1628
+ name: pgCore.text("name").notNull(),
1629
+ blockNumber: pgCore.bigint("block_number", { mode: "number" }).notNull(),
1630
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
1631
+ },
1632
+ (table) => [pgCore.uniqueIndex("collector_block_numbers_chain_name_idx").on(table.chainId, table.name)]
1633
+ );
1634
+ var availableLiquidityPools = s.table("available_liquidity_pools", {
1635
+ id: pgCore.varchar("id", { length: 255 }).primaryKey(),
1636
+ amount: pgCore.numeric("amount", { precision: 78, scale: 0 }).notNull(),
1637
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
1638
+ });
1639
+ var availableLiquidityQueues = s.table(
1640
+ "available_liquidity_queues",
1641
+ {
1642
+ queueId: pgCore.varchar("queue_id", { length: 255 }).notNull(),
1643
+ availableLiquidityPoolId: pgCore.varchar("available_liquidity_pool_id", { length: 255 }).notNull().references(() => availableLiquidityPools.id, { onDelete: "cascade" }),
1644
+ index: pgCore.integer("index").notNull(),
1645
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
1646
+ },
1647
+ (table) => [
1648
+ pgCore.primaryKey({
1649
+ columns: [table.queueId, table.availableLiquidityPoolId],
1650
+ name: "available_liquidity_queues_pk"
1651
+ }),
1652
+ pgCore.index("available_liquidity_queues_queue_index_idx").on(table.queueId, table.index)
1653
+ ]
1654
+ );
1655
+ var userPositions = s.table(
1656
+ "user_positions",
1657
+ {
1658
+ id: pgCore.varchar("id", { length: 255 }).primaryKey(),
1659
+ availableLiquidityQueueId: pgCore.varchar("available_liquidity_queue_id", { length: 255 }).notNull(),
1660
+ user: pgCore.varchar("user", { length: 255 }).notNull(),
1661
+ chainId: pgCore.bigint("chain_id", { mode: "bigint" }).notNull(),
1662
+ amount: pgCore.numeric("amount", { precision: 78, scale: 0 }).notNull(),
1663
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
1664
+ },
1665
+ (table) => [
1666
+ pgCore.index("user_positions_available_liquidity_queue_id_idx").on(table.availableLiquidityQueueId)
1667
+ ]
1668
+ );
1669
+
1670
+ // src/core/CollectorBlockStore.ts
1671
+ var create = (config) => {
1672
+ const db = config.db;
1673
+ return {
1674
+ getBlockNumber: async (parameters) => {
1675
+ const name = parameters.collectorName.toLowerCase();
1676
+ const result = await db.select({ blockNumber: collectorBlockNumbers.blockNumber }).from(collectorBlockNumbers).where(
1677
+ drizzleOrm.and(
1678
+ drizzleOrm.eq(collectorBlockNumbers.name, name),
1679
+ drizzleOrm.eq(collectorBlockNumbers.chainId, parameters.chainId)
1680
+ )
1681
+ ).limit(1);
1682
+ if (result.length === 0)
1683
+ return mempool.Chain.getChain(parameters.chainId)?.mempool.deploymentBlock || 0;
1684
+ return Number(result[0].blockNumber);
1685
+ },
1686
+ saveBlockNumber: async (parameters) => {
1687
+ const name = parameters.collectorName.toLowerCase();
1688
+ await db.insert(collectorBlockNumbers).values({
1689
+ chainId: parameters.chainId,
1690
+ name,
1691
+ blockNumber: parameters.blockNumber,
1692
+ updatedAt: /* @__PURE__ */ new Date()
1693
+ }).onConflictDoUpdate({
1694
+ target: [collectorBlockNumbers.chainId, collectorBlockNumbers.name],
1695
+ set: {
1696
+ blockNumber: parameters.blockNumber,
1697
+ updatedAt: /* @__PURE__ */ new Date()
1698
+ }
1627
1699
  });
1628
1700
  }
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);
1701
+ };
1702
+ };
1703
+ function memory() {
1704
+ const blockNumbers = /* @__PURE__ */ new Map();
1705
+ return {
1706
+ getBlockNumber: async (parameters) => {
1707
+ const name = parameters.collectorName.toLowerCase();
1708
+ return blockNumbers.get(name)?.get(parameters.chainId) || mempool.Chain.getChain(parameters.chainId)?.mempool.deploymentBlock || 0;
1709
+ },
1710
+ saveBlockNumber: async (parameters) => {
1711
+ const name = parameters.collectorName.toLowerCase();
1712
+ if (!blockNumbers.has(name)) {
1713
+ blockNumbers.set(name, /* @__PURE__ */ new Map());
1657
1714
  }
1658
- perUser.set(token, { balance, allowance });
1715
+ if (!blockNumbers.get(name)?.has(parameters.chainId)) {
1716
+ blockNumbers.get(name).set(parameters.chainId, parameters.blockNumber);
1717
+ }
1718
+ blockNumbers.get(name).set(parameters.chainId, parameters.blockNumber);
1659
1719
  }
1660
- }
1661
- return out;
1720
+ };
1662
1721
  }
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
- })
1722
+
1723
+ // src/core/LiquidityStore.ts
1724
+ var LiquidityStore_exports = {};
1725
+ __export(LiquidityStore_exports, {
1726
+ create: () => create2,
1727
+ memory: () => memory2
1728
+ });
1729
+ var create2 = (config) => {
1730
+ const db = config.db;
1731
+ return {
1732
+ getByUserPositionId: async (parameters) => {
1733
+ const up = await db.select().from(userPositions).where(drizzleOrm.eq(userPositions.id, parameters.userPositionId)).limit(1);
1734
+ if (up.length === 0) return null;
1735
+ const userPositionRow = up[0];
1736
+ const rows = await db.select({ queue: availableLiquidityQueues, pool: availableLiquidityPools }).from(availableLiquidityQueues).innerJoin(
1737
+ availableLiquidityPools,
1738
+ drizzleOrm.eq(availableLiquidityPools.id, availableLiquidityQueues.availableLiquidityPoolId)
1739
+ ).where(drizzleOrm.eq(availableLiquidityQueues.queueId, userPositionRow.availableLiquidityQueueId));
1740
+ const queues = rows.map((row) => ({
1741
+ queue: row.queue,
1742
+ pool: row.pool
1743
+ }));
1744
+ return { userPosition: userPositionRow, queues };
1745
+ },
1746
+ getAll: async () => {
1747
+ const rows = await db.select({
1748
+ userPosition: userPositions,
1749
+ queue: availableLiquidityQueues,
1750
+ pool: availableLiquidityPools
1751
+ }).from(userPositions).innerJoin(
1752
+ availableLiquidityQueues,
1753
+ drizzleOrm.eq(availableLiquidityQueues.queueId, userPositions.availableLiquidityQueueId)
1754
+ ).innerJoin(
1755
+ availableLiquidityPools,
1756
+ drizzleOrm.eq(availableLiquidityPools.id, availableLiquidityQueues.availableLiquidityPoolId)
1686
1757
  );
1758
+ const byUserPosition = /* @__PURE__ */ new Map();
1759
+ for (const row of rows) {
1760
+ const id = row.userPosition.id;
1761
+ if (!byUserPosition.has(id)) {
1762
+ byUserPosition.set(id, { userPosition: row.userPosition, queues: [] });
1763
+ }
1764
+ byUserPosition.get(id).queues.push({ queue: row.queue, pool: row.pool });
1765
+ }
1766
+ return Array.from(byUserPosition.values());
1767
+ },
1768
+ save: async (parameters) => {
1769
+ const { liquidity } = parameters;
1770
+ await db.transaction(async (tx) => {
1771
+ for (const qp of liquidity.queues) {
1772
+ await tx.insert(availableLiquidityPools).values({
1773
+ id: qp.pool.id,
1774
+ amount: qp.pool.amount,
1775
+ updatedAt: /* @__PURE__ */ new Date()
1776
+ }).onConflictDoUpdate({
1777
+ target: availableLiquidityPools.id,
1778
+ set: {
1779
+ amount: qp.pool.amount,
1780
+ updatedAt: /* @__PURE__ */ new Date()
1781
+ }
1782
+ });
1783
+ }
1784
+ for (const qp of liquidity.queues) {
1785
+ await tx.insert(availableLiquidityQueues).values({
1786
+ queueId: qp.queue.queueId,
1787
+ availableLiquidityPoolId: qp.pool.id,
1788
+ index: qp.queue.index,
1789
+ updatedAt: /* @__PURE__ */ new Date()
1790
+ }).onConflictDoUpdate({
1791
+ target: [
1792
+ availableLiquidityQueues.queueId,
1793
+ availableLiquidityQueues.availableLiquidityPoolId
1794
+ ],
1795
+ set: {
1796
+ index: qp.queue.index,
1797
+ updatedAt: /* @__PURE__ */ new Date()
1798
+ }
1799
+ });
1800
+ }
1801
+ await tx.insert(userPositions).values({
1802
+ id: liquidity.userPosition.id,
1803
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
1804
+ user: liquidity.userPosition.user,
1805
+ chainId: liquidity.userPosition.chainId,
1806
+ amount: liquidity.userPosition.amount,
1807
+ updatedAt: /* @__PURE__ */ new Date()
1808
+ }).onConflictDoUpdate({
1809
+ target: userPositions.id,
1810
+ set: {
1811
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
1812
+ user: liquidity.userPosition.user,
1813
+ chainId: liquidity.userPosition.chainId,
1814
+ amount: liquidity.userPosition.amount,
1815
+ updatedAt: /* @__PURE__ */ new Date()
1816
+ }
1817
+ });
1818
+ });
1687
1819
  }
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)
1820
+ };
1821
+ };
1822
+ function memory2() {
1823
+ const poolsById = /* @__PURE__ */ new Map();
1824
+ const queuesByComposite = /* @__PURE__ */ new Map();
1825
+ const queueIndexByQueueId = /* @__PURE__ */ new Map();
1826
+ const userPositionsById = /* @__PURE__ */ new Map();
1827
+ return {
1828
+ getByUserPositionId: async (parameters) => {
1829
+ const up = userPositionsById.get(parameters.userPositionId);
1830
+ if (!up) return null;
1831
+ const compositeKeys = queueIndexByQueueId.get(up.availableLiquidityQueueId) || /* @__PURE__ */ new Set();
1832
+ const queues = [];
1833
+ for (const key of compositeKeys) {
1834
+ const q = queuesByComposite.get(key);
1835
+ if (!q) continue;
1836
+ const p = poolsById.get(q.availableLiquidityPoolId);
1837
+ if (!p) continue;
1838
+ queues.push({ queue: q, pool: p });
1839
+ }
1840
+ return { userPosition: up, queues };
1699
1841
  },
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)
1842
+ getAll: async () => {
1843
+ const results = [];
1844
+ for (const up of userPositionsById.values()) {
1845
+ const compositeKeys = queueIndexByQueueId.get(up.availableLiquidityQueueId) || /* @__PURE__ */ new Set();
1846
+ const queues = [];
1847
+ for (const key of compositeKeys) {
1848
+ const q = queuesByComposite.get(key);
1849
+ if (!q) continue;
1850
+ const p = poolsById.get(q.availableLiquidityPoolId);
1851
+ if (!p) continue;
1852
+ queues.push({ queue: q, pool: p });
1853
+ }
1854
+ results.push({ userPosition: up, queues });
1709
1855
  }
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;
1856
+ return results;
1857
+ },
1858
+ save: async (parameters) => {
1859
+ const { liquidity } = parameters;
1860
+ for (const qp of liquidity.queues) {
1861
+ poolsById.set(qp.pool.id, {
1862
+ id: qp.pool.id,
1863
+ amount: qp.pool.amount,
1864
+ updatedAt: /* @__PURE__ */ new Date()
1865
+ });
1725
1866
  }
1726
- )
1867
+ for (const qp of liquidity.queues) {
1868
+ const qid = qp.queue.queueId;
1869
+ if (!qid) continue;
1870
+ const composite = `${qid}::${qp.pool.id}`;
1871
+ queuesByComposite.set(composite, {
1872
+ queueId: qid,
1873
+ availableLiquidityPoolId: qp.pool.id,
1874
+ index: qp.queue.index,
1875
+ updatedAt: /* @__PURE__ */ new Date()
1876
+ });
1877
+ if (!queueIndexByQueueId.has(qid)) queueIndexByQueueId.set(qid, /* @__PURE__ */ new Set());
1878
+ queueIndexByQueueId.get(qid).add(composite);
1879
+ }
1880
+ userPositionsById.set(liquidity.userPosition.id, {
1881
+ id: liquidity.userPosition.id,
1882
+ availableLiquidityQueueId: liquidity.userPosition.availableLiquidityQueueId,
1883
+ user: liquidity.userPosition.user,
1884
+ chainId: liquidity.userPosition.chainId,
1885
+ amount: liquidity.userPosition.amount,
1886
+ updatedAt: /* @__PURE__ */ new Date()
1887
+ });
1888
+ }
1727
1889
  };
1728
- return JSON.stringify(normalized);
1729
1890
  }
1730
1891
 
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
1892
+ // src/core/OfferStore/index.ts
1893
+ var OfferStore_exports = {};
1894
+ __export(OfferStore_exports, {
1895
+ create: () => create3
1739
1896
  });
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];
1897
+ function create3(config) {
1898
+ const db = config.db;
1759
1899
  return {
1760
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1761
- trace: isEnabled("trace") ? console.trace.bind(console) : () => {
1900
+ create: async (parameters) => {
1901
+ const { offer, status, metadata } = parameters;
1902
+ return await db.transaction(async (tx) => {
1903
+ const callbackId = getCallbackIdForOffer(offer);
1904
+ const result = await tx.insert(offers).values({
1905
+ hash: offer.hash.toLowerCase(),
1906
+ offering: offer.offering.toLowerCase(),
1907
+ assets: offer.assets.toString(),
1908
+ rate: offer.rate,
1909
+ maturity: offer.maturity,
1910
+ expiry: offer.expiry,
1911
+ start: offer.start,
1912
+ nonce: offer.nonce,
1913
+ buy: offer.buy,
1914
+ chainId: offer.chainId,
1915
+ loanToken: offer.loanToken.toLowerCase(),
1916
+ callbackAddress: offer.callback.address.toLowerCase(),
1917
+ callbackData: offer.callback.data,
1918
+ callbackGasLimit: offer.callback.gasLimit,
1919
+ signature: offer.signature,
1920
+ callbackId: callbackId ?? null
1921
+ }).onConflictDoNothing().returning();
1922
+ if (result.length === 0) {
1923
+ return offer.hash;
1924
+ }
1925
+ for (const collateral of offer.collaterals) {
1926
+ await tx.insert(offerCollaterals).values({
1927
+ offerHash: offer.hash.toLowerCase(),
1928
+ asset: collateral.asset.toLowerCase(),
1929
+ oracle: collateral.oracle.toLowerCase(),
1930
+ lltv: collateral.lltv
1931
+ });
1932
+ }
1933
+ await tx.insert(offerStatus).values({
1934
+ offerHash: offer.hash.toLowerCase(),
1935
+ status,
1936
+ metadata
1937
+ });
1938
+ return offer.hash;
1939
+ });
1762
1940
  },
1763
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1764
- debug: isEnabled("debug") ? console.debug.bind(console) : () => {
1941
+ createMany: async (parameters) => {
1942
+ return await db.transaction(async (tx) => {
1943
+ const hashes = [];
1944
+ for (const { offer, status, metadata } of parameters) {
1945
+ const callbackId = getCallbackIdForOffer(offer);
1946
+ const result = await tx.insert(offers).values({
1947
+ hash: offer.hash.toLowerCase(),
1948
+ offering: offer.offering.toLowerCase(),
1949
+ assets: offer.assets.toString(),
1950
+ rate: offer.rate,
1951
+ maturity: offer.maturity,
1952
+ expiry: offer.expiry,
1953
+ start: offer.start,
1954
+ nonce: offer.nonce,
1955
+ buy: offer.buy,
1956
+ chainId: offer.chainId,
1957
+ loanToken: offer.loanToken.toLowerCase(),
1958
+ callbackAddress: offer.callback.address.toLowerCase(),
1959
+ callbackData: offer.callback.data,
1960
+ callbackGasLimit: offer.callback.gasLimit,
1961
+ signature: offer.signature,
1962
+ callbackId: callbackId ?? null
1963
+ }).onConflictDoNothing().returning();
1964
+ if (result.length === 0) continue;
1965
+ for (const collateral of offer.collaterals) {
1966
+ await tx.insert(offerCollaterals).values({
1967
+ offerHash: offer.hash.toLowerCase(),
1968
+ asset: collateral.asset.toLowerCase(),
1969
+ oracle: collateral.oracle.toLowerCase(),
1970
+ lltv: collateral.lltv
1971
+ });
1972
+ }
1973
+ await tx.insert(offerStatus).values({
1974
+ offerHash: offer.hash.toLowerCase(),
1975
+ status,
1976
+ metadata
1977
+ });
1978
+ hashes.push(offer.hash);
1979
+ }
1980
+ return hashes;
1981
+ });
1765
1982
  },
1766
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1767
- info: isEnabled("info") ? console.info.bind(console) : () => {
1983
+ getAll: async (params) => {
1984
+ const { query } = params ?? {};
1985
+ const conditions = [];
1986
+ const now = mempool.Time.now();
1987
+ if (query?.creators && query.creators.length > 0) {
1988
+ conditions.push(
1989
+ drizzleOrm.inArray(
1990
+ offers.offering,
1991
+ query.creators.map((c) => c.toLowerCase())
1992
+ )
1993
+ );
1994
+ }
1995
+ if (query?.side) {
1996
+ conditions.push(drizzleOrm.eq(offers.buy, query.side === "buy"));
1997
+ }
1998
+ if (query?.chains && query.chains.length > 0) {
1999
+ conditions.push(
2000
+ drizzleOrm.inArray(
2001
+ offers.chainId,
2002
+ query.chains.map((chain) => BigInt(chain))
2003
+ )
2004
+ );
2005
+ }
2006
+ if (query?.loanTokens && query.loanTokens.length > 0) {
2007
+ conditions.push(
2008
+ drizzleOrm.inArray(
2009
+ offers.loanToken,
2010
+ query.loanTokens.map((t) => t.toLowerCase())
2011
+ )
2012
+ );
2013
+ }
2014
+ if (query?.callbackAddresses && query.callbackAddresses.length > 0) {
2015
+ conditions.push(
2016
+ drizzleOrm.inArray(
2017
+ offers.callbackAddress,
2018
+ query.callbackAddresses.map((c) => c.toLowerCase())
2019
+ )
2020
+ );
2021
+ }
2022
+ conditions.push(drizzleOrm.gte(offers.expiry, now));
2023
+ if (query?.minAmount !== void 0) {
2024
+ conditions.push(drizzleOrm.gte(offers.assets, query.minAmount.toString()));
2025
+ }
2026
+ if (query?.maxAmount !== void 0) {
2027
+ conditions.push(drizzleOrm.lte(offers.assets, query.maxAmount.toString()));
2028
+ }
2029
+ if (query?.minRate !== void 0) {
2030
+ conditions.push(drizzleOrm.gte(offers.rate, query.minRate));
2031
+ }
2032
+ if (query?.maxRate !== void 0) {
2033
+ conditions.push(drizzleOrm.lte(offers.rate, query.maxRate));
2034
+ }
2035
+ if (query?.minMaturity !== void 0) {
2036
+ conditions.push(drizzleOrm.gte(offers.maturity, query.minMaturity));
2037
+ }
2038
+ if (query?.maxMaturity !== void 0) {
2039
+ conditions.push(drizzleOrm.lte(offers.maturity, query.maxMaturity));
2040
+ }
2041
+ if (query?.minExpiry !== void 0) {
2042
+ conditions.push(drizzleOrm.gte(offers.expiry, query.minExpiry));
2043
+ }
2044
+ if (query?.maxExpiry !== void 0) {
2045
+ conditions.push(drizzleOrm.lte(offers.expiry, query.maxExpiry));
2046
+ }
2047
+ if (query?.collateralAssets && query.collateralAssets.length > 0) {
2048
+ conditions.push(
2049
+ drizzleOrm.inArray(
2050
+ offerCollaterals.asset,
2051
+ query.collateralAssets.map((a) => a.toLowerCase())
2052
+ )
2053
+ );
2054
+ }
2055
+ if (query?.collateralOracles && query.collateralOracles.length > 0) {
2056
+ conditions.push(
2057
+ drizzleOrm.inArray(
2058
+ offerCollaterals.oracle,
2059
+ query.collateralOracles.map((o) => o.toLowerCase())
2060
+ )
2061
+ );
2062
+ }
2063
+ if (query?.collateralTuple && query.collateralTuple.length > 0) {
2064
+ const tupleClauses = query.collateralTuple.map((tuple) => {
2065
+ const parts = [
2066
+ drizzleOrm.sql`${offerCollaterals.asset} = ${tuple.asset.toLowerCase()}`
2067
+ ];
2068
+ if (tuple.oracle) {
2069
+ parts.push(drizzleOrm.sql`${offerCollaterals.oracle} = ${tuple.oracle.toLowerCase()}`);
2070
+ }
2071
+ if (tuple.lltv !== void 0) {
2072
+ parts.push(drizzleOrm.sql`${offerCollaterals.lltv} = ${tuple.lltv}`);
2073
+ }
2074
+ let clause = parts[0];
2075
+ for (let i = 1; i < parts.length; i++) {
2076
+ clause = drizzleOrm.sql`${clause} AND ${parts[i]}`;
2077
+ }
2078
+ return drizzleOrm.sql`(${clause})`;
2079
+ }).filter((c) => c !== void 0);
2080
+ if (tupleClauses.length > 0) {
2081
+ let combined = tupleClauses[0];
2082
+ for (let i = 1; i < tupleClauses.length; i++) {
2083
+ combined = drizzleOrm.sql`${combined} OR ${tupleClauses[i]}`;
2084
+ }
2085
+ conditions.push(drizzleOrm.sql`(${combined})`);
2086
+ }
2087
+ }
2088
+ if (query?.minLltv !== void 0) {
2089
+ conditions.push(drizzleOrm.gte(offerCollaterals.lltv, viem.parseUnits(query.minLltv.toString(), 16)));
2090
+ }
2091
+ if (query?.maxLltv !== void 0) {
2092
+ conditions.push(drizzleOrm.lte(offerCollaterals.lltv, viem.parseUnits(query.maxLltv.toString(), 16)));
2093
+ }
2094
+ const sortBy = query?.sortBy ?? "expiry";
2095
+ const sortOrder = query?.sortOrder ?? "desc";
2096
+ const sortColumn = (() => {
2097
+ switch (sortBy) {
2098
+ case "rate":
2099
+ return offers.rate;
2100
+ case "maturity":
2101
+ return offers.maturity;
2102
+ case "expiry":
2103
+ return offers.expiry;
2104
+ case "amount":
2105
+ return offers.assets;
2106
+ default:
2107
+ return offers.expiry;
2108
+ }
2109
+ })();
2110
+ const cursor = decode(query?.cursor);
2111
+ if (cursor) {
2112
+ if (cursor.sort !== sortBy || cursor.dir !== sortOrder) {
2113
+ throw new Error("Cursor does not match the current sort parameters");
2114
+ }
2115
+ const op = sortOrder === "asc" ? drizzleOrm.sql`>` : drizzleOrm.sql`<`;
2116
+ const cursorVal = (() => {
2117
+ switch (sortBy) {
2118
+ case "rate":
2119
+ return BigInt(cursor.rate);
2120
+ case "amount":
2121
+ return BigInt(cursor.assets);
2122
+ case "maturity":
2123
+ return cursor.maturity;
2124
+ case "expiry":
2125
+ return cursor.expiry;
2126
+ default:
2127
+ return cursor.expiry;
2128
+ }
2129
+ })();
2130
+ conditions.push(drizzleOrm.sql`(${sortColumn}, ${offers.hash}) ${op} (${cursorVal}, ${cursor.hash})`);
2131
+ }
2132
+ const limit = query?.limit ?? 20;
2133
+ const latestStatus = db.select({
2134
+ status: offerStatus.status,
2135
+ metadata: offerStatus.metadata
2136
+ }).from(offerStatus).where(drizzleOrm.eq(offerStatus.offerHash, offers.hash)).orderBy(drizzleOrm.desc(offerStatus.createdAt)).limit(1).as("latest_status");
2137
+ const sumConsumed = db.select({
2138
+ consumed: drizzleOrm.sql`COALESCE(SUM(${consumed.consumed}), 0)`.as("consumed")
2139
+ }).from(consumed).where(
2140
+ drizzleOrm.and(
2141
+ drizzleOrm.eq(consumed.offering, offers.offering),
2142
+ drizzleOrm.eq(consumed.nonce, offers.nonce),
2143
+ drizzleOrm.eq(consumed.chainId, offers.chainId)
2144
+ )
2145
+ ).as("sum_consumed");
2146
+ const results = await db.select({
2147
+ hash: offers.hash,
2148
+ offering: offers.offering,
2149
+ assets: offers.assets,
2150
+ consumed: sumConsumed.consumed,
2151
+ rate: offers.rate,
2152
+ maturity: offers.maturity,
2153
+ expiry: offers.expiry,
2154
+ start: offers.start,
2155
+ nonce: offers.nonce,
2156
+ buy: offers.buy,
2157
+ chainId: offers.chainId,
2158
+ loanToken: offers.loanToken,
2159
+ callbackAddress: offers.callbackAddress,
2160
+ callbackData: offers.callbackData,
2161
+ callbackGasLimit: offers.callbackGasLimit,
2162
+ signature: offers.signature,
2163
+ createdAt: offers.createdAt,
2164
+ collateralAsset: offerCollaterals.asset,
2165
+ collateralOracle: offerCollaterals.oracle,
2166
+ collateralLltv: offerCollaterals.lltv,
2167
+ status: latestStatus.status,
2168
+ metadata: latestStatus.metadata
2169
+ }).from(offers).leftJoin(offerCollaterals, drizzleOrm.eq(offers.hash, offerCollaterals.offerHash)).leftJoinLateral(latestStatus, drizzleOrm.sql`true`).leftJoinLateral(sumConsumed, drizzleOrm.sql`true`).where(
2170
+ drizzleOrm.and(
2171
+ conditions.length > 0 ? drizzleOrm.and(...conditions) : drizzleOrm.sql`true`,
2172
+ query?.status && query.status.length > 0 ? drizzleOrm.inArray(latestStatus.status, query.status) : drizzleOrm.eq(latestStatus.status, "valid"),
2173
+ drizzleOrm.sql`( ${offers.assets} - COALESCE(${sumConsumed.consumed}, 0) ) > 0`
2174
+ )
2175
+ ).orderBy(
2176
+ ...sortOrder === "asc" ? [drizzleOrm.asc(sortColumn), drizzleOrm.asc(offers.hash)] : [drizzleOrm.desc(sortColumn), drizzleOrm.desc(offers.hash)]
2177
+ ).limit(limit);
2178
+ const offerMap = /* @__PURE__ */ new Map();
2179
+ for (const row of results) {
2180
+ const offer = offerMap.get(row.hash) || {
2181
+ hash: row.hash,
2182
+ offering: row.offering,
2183
+ assets: BigInt(row.assets),
2184
+ consumed: BigInt(row.consumed),
2185
+ rate: row.rate,
2186
+ maturity: row.maturity,
2187
+ expiry: row.expiry,
2188
+ start: row.start,
2189
+ nonce: row.nonce,
2190
+ buy: row.buy,
2191
+ chainId: row.chainId,
2192
+ loanToken: row.loanToken,
2193
+ callbackAddress: row.callbackAddress,
2194
+ callbackData: row.callbackData,
2195
+ callbackGasLimit: row.callbackGasLimit,
2196
+ signature: row.signature,
2197
+ createdAt: row.createdAt,
2198
+ collaterals: [],
2199
+ status: row.status,
2200
+ metadata: row.metadata
2201
+ };
2202
+ if (row.collateralAsset && row.collateralOracle && row.collateralLltv) {
2203
+ offer.collaterals.push({
2204
+ asset: row.collateralAsset,
2205
+ oracle: row.collateralOracle,
2206
+ lltv: mempool.LLTV.from(row.collateralLltv)
2207
+ });
2208
+ }
2209
+ offerMap.set(row.hash, offer);
2210
+ }
2211
+ let nextCursor = null;
2212
+ if (results.length === limit && results.length > 0) {
2213
+ const lastRow = results[results.length - 1];
2214
+ const base = {
2215
+ sort: sortBy,
2216
+ dir: sortOrder,
2217
+ hash: lastRow.hash
2218
+ };
2219
+ switch (sortBy) {
2220
+ case "rate":
2221
+ base.rate = lastRow.rate.toString();
2222
+ break;
2223
+ case "amount":
2224
+ base.assets = lastRow.assets.toString();
2225
+ break;
2226
+ case "maturity":
2227
+ base.maturity = lastRow.maturity;
2228
+ break;
2229
+ default:
2230
+ base.expiry = lastRow.expiry;
2231
+ }
2232
+ nextCursor = encode(base);
2233
+ }
2234
+ const transformedResults = [];
2235
+ for (const offerData of offerMap.values()) {
2236
+ const offer = mempool.Offer.from({
2237
+ offering: offerData.offering,
2238
+ assets: offerData.assets,
2239
+ rate: offerData.rate,
2240
+ maturity: mempool.Maturity.from(offerData.maturity),
2241
+ expiry: offerData.expiry,
2242
+ start: offerData.start,
2243
+ nonce: offerData.nonce,
2244
+ buy: offerData.buy,
2245
+ chainId: offerData.chainId,
2246
+ loanToken: offerData.loanToken,
2247
+ collaterals: offerData.collaterals.map((c) => ({
2248
+ asset: c.asset,
2249
+ oracle: c.oracle,
2250
+ lltv: mempool.LLTV.from(c.lltv)
2251
+ })).sort((a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())),
2252
+ callback: {
2253
+ address: offerData.callbackAddress,
2254
+ data: offerData.callbackData,
2255
+ gasLimit: offerData.callbackGasLimit
2256
+ },
2257
+ ...offerData.signature !== null ? { signature: offerData.signature } : void 0
2258
+ });
2259
+ transformedResults.push({
2260
+ ...offer,
2261
+ consumed: offerData.consumed,
2262
+ status: offerData.status,
2263
+ ...offerData.metadata !== null ? { metadata: offerData.metadata } : void 0
2264
+ });
2265
+ }
2266
+ return { offers: transformedResults, nextCursor };
2267
+ },
2268
+ /**
2269
+ * Returns offers that match specified offer based on the provided parameters.
2270
+ *
2271
+ * Rules for filtering:
2272
+ * - If offer expiry is in the past compared to now, it should not be included
2273
+ * - If offer maturity is in the past compared to now, it should not be included
2274
+ * - It should return if buyOffer.rate >= sellOffer.rate
2275
+ * - It should return if buyOffer.userAddress != sellOffer.userAddress
2276
+ * - It should return if maturity matches
2277
+ * - It should return if loanToken matches
2278
+ * - If the incoming intent is a buy / lend offer, only consider existing sell / borrow offers.
2279
+ * - If the incoming intent is a sell / borrow offer, only consider existing buy / lend offers.
2280
+ * - When the intent is lend: require offer_collaterals ⊆ intent_collaterals
2281
+ * - When the intent is borrow: require intent_collaterals ⊆ offer_collaterals
2282
+ * - It should sort by increasing rate if the input offer is a buy/lend offer
2283
+ * - It should sort by decreasing rate if the input offer is a sell/borrow offer
2284
+ */
2285
+ findMatchingOffers: async (params) => {
2286
+ const {
2287
+ side,
2288
+ chainId,
2289
+ rate,
2290
+ collaterals = [],
2291
+ maturity,
2292
+ minMaturity,
2293
+ maxMaturity,
2294
+ loanToken,
2295
+ creator,
2296
+ cursor,
2297
+ limit = 20
2298
+ } = params;
2299
+ const isIncomingBuy = side === "buy";
2300
+ const nowEpochSeconds = Math.floor(Date.now() / 1e3);
2301
+ const rateSortDirection = isIncomingBuy ? "desc" : "asc";
2302
+ const baseWhereClauses = [];
2303
+ baseWhereClauses.push(drizzleOrm.eq(offers.buy, !isIncomingBuy));
2304
+ baseWhereClauses.push(drizzleOrm.eq(offers.chainId, BigInt(chainId)));
2305
+ baseWhereClauses.push(drizzleOrm.gte(offers.expiry, nowEpochSeconds));
2306
+ baseWhereClauses.push(drizzleOrm.gte(offers.maturity, nowEpochSeconds));
2307
+ if (maturity) baseWhereClauses.push(drizzleOrm.eq(offers.maturity, maturity));
2308
+ if (minMaturity) baseWhereClauses.push(drizzleOrm.gte(offers.maturity, minMaturity));
2309
+ if (maxMaturity) baseWhereClauses.push(drizzleOrm.lte(offers.maturity, maxMaturity));
2310
+ if (loanToken) baseWhereClauses.push(drizzleOrm.eq(offers.loanToken, loanToken.toLowerCase()));
2311
+ if (creator) baseWhereClauses.push(drizzleOrm.eq(offers.offering, creator.toLowerCase()));
2312
+ if (rate)
2313
+ baseWhereClauses.push(isIncomingBuy ? drizzleOrm.gte(offers.rate, rate) : drizzleOrm.lte(offers.rate, rate));
2314
+ const parsedCursor = decode(cursor);
2315
+ if (parsedCursor) {
2316
+ if (parsedCursor.sort !== "rate" || parsedCursor.dir !== rateSortDirection) {
2317
+ throw new Error("Cursor does not match the current sort parameters");
2318
+ }
2319
+ }
2320
+ if (collaterals.length > 0) {
2321
+ const collateralValues = collaterals.map(
2322
+ (c) => `('${c.asset.toLowerCase()}', '${c.oracle.toLowerCase()}', ${c.lltv.toString()})`
2323
+ ).join(", ");
2324
+ const userCollaterals = drizzleOrm.sql`(VALUES ${drizzleOrm.sql.raw(collateralValues)}) AS user_collaterals(asset, oracle, lltv)`;
2325
+ const sellPredicate = drizzleOrm.sql`
2326
+ NOT EXISTS (
2327
+ SELECT 1 FROM ${userCollaterals}
2328
+ WHERE NOT EXISTS (
2329
+ SELECT 1 FROM ${offerCollaterals} offer_collaterals
2330
+ WHERE offer_collaterals.offer_hash = ${offers.hash}
2331
+ AND offer_collaterals.asset = user_collaterals.asset
2332
+ AND offer_collaterals.oracle = user_collaterals.oracle
2333
+ AND offer_collaterals.lltv = user_collaterals.lltv
2334
+ )
2335
+ )
2336
+ `;
2337
+ const buyPredicate = drizzleOrm.sql`
2338
+ NOT EXISTS (
2339
+ SELECT 1 FROM ${offerCollaterals} offer_collaterals
2340
+ WHERE offer_collaterals.offer_hash = ${offers.hash}
2341
+ AND NOT EXISTS (
2342
+ SELECT 1 FROM ${userCollaterals}
2343
+ WHERE user_collaterals.asset = offer_collaterals.asset
2344
+ AND user_collaterals.oracle = offer_collaterals.oracle
2345
+ AND user_collaterals.lltv = offer_collaterals.lltv
2346
+ )
2347
+ )
2348
+ `;
2349
+ baseWhereClauses.push(isIncomingBuy ? buyPredicate : sellPredicate);
2350
+ }
2351
+ const latestStatus = db.select({
2352
+ status: offerStatus.status,
2353
+ metadata: offerStatus.metadata
2354
+ }).from(offerStatus).where(drizzleOrm.eq(offerStatus.offerHash, offers.hash)).orderBy(drizzleOrm.desc(offerStatus.createdAt)).limit(1).as("latest_status");
2355
+ const sumConsumed = db.select({
2356
+ consumed: drizzleOrm.sql`COALESCE(SUM(${consumed.consumed}), 0)`.as("consumed")
2357
+ }).from(consumed).where(
2358
+ drizzleOrm.and(
2359
+ drizzleOrm.eq(consumed.offering, offers.offering),
2360
+ drizzleOrm.eq(consumed.nonce, offers.nonce),
2361
+ drizzleOrm.eq(consumed.chainId, offers.chainId)
2362
+ )
2363
+ ).as("sum_consumed");
2364
+ const statusCondition = drizzleOrm.eq(latestStatus.status, "valid");
2365
+ const bestOffers = db.selectDistinctOn(
2366
+ // group key
2367
+ [offers.offering, offers.nonce, offers.buy],
2368
+ {
2369
+ hash: offers.hash,
2370
+ offering: offers.offering,
2371
+ assets: offers.assets,
2372
+ consumed: sumConsumed.consumed,
2373
+ remaining: drizzleOrm.sql`${offers.assets} - COALESCE(${sumConsumed.consumed}, 0)`.as("remaining"),
2374
+ rate: offers.rate,
2375
+ maturity: offers.maturity,
2376
+ expiry: offers.expiry,
2377
+ start: offers.start,
2378
+ nonce: offers.nonce,
2379
+ buy: offers.buy,
2380
+ chainId: offers.chainId,
2381
+ loanToken: offers.loanToken,
2382
+ callbackAddress: offers.callbackAddress,
2383
+ callbackData: offers.callbackData,
2384
+ callbackGasLimit: offers.callbackGasLimit,
2385
+ signature: offers.signature,
2386
+ callbackId: offers.callbackId,
2387
+ status: latestStatus.status,
2388
+ metadata: latestStatus.metadata
2389
+ }
2390
+ ).from(offers).leftJoinLateral(latestStatus, drizzleOrm.sql`true`).leftJoinLateral(sumConsumed, drizzleOrm.sql`true`).where(
2391
+ drizzleOrm.and(
2392
+ drizzleOrm.and(...baseWhereClauses),
2393
+ statusCondition,
2394
+ drizzleOrm.sql`( ${offers.assets} - COALESCE(${sumConsumed.consumed}, 0) ) > 0`
2395
+ )
2396
+ ).orderBy(
2397
+ offers.offering,
2398
+ offers.nonce,
2399
+ offers.buy,
2400
+ // 1 price (direction depends on side)
2401
+ drizzleOrm.sql`CASE WHEN ${offers.buy} THEN ${offers.rate} ELSE -${offers.rate} END`,
2402
+ // 2 size (remaining)
2403
+ drizzleOrm.sql`( ${offers.assets} - COALESCE(${sumConsumed.consumed}, 0) ) DESC`,
2404
+ // 3 term (longer maturity)
2405
+ drizzleOrm.desc(offers.maturity)
2406
+ ).as("best_offers");
2407
+ const queueLiquidity = db.select({
2408
+ callbackId: userPositions.id,
2409
+ userAmount: userPositions.amount,
2410
+ // user's per-callback cap
2411
+ queueLiquidity: drizzleOrm.sql`COALESCE(SUM(${availableLiquidityPools.amount}), 0)`.as(
2412
+ "queue_liquidity"
2413
+ )
2414
+ }).from(userPositions).leftJoin(
2415
+ availableLiquidityQueues,
2416
+ drizzleOrm.eq(userPositions.availableLiquidityQueueId, availableLiquidityQueues.queueId)
2417
+ ).leftJoin(
2418
+ availableLiquidityPools,
2419
+ drizzleOrm.eq(availableLiquidityQueues.availableLiquidityPoolId, availableLiquidityPools.id)
2420
+ ).groupBy(userPositions.id).as("queue_liquidity");
2421
+ const sortExpr = drizzleOrm.sql`CASE WHEN ${bestOffers.buy} THEN ${bestOffers.rate} ELSE -${bestOffers.rate} END`;
2422
+ const offersWithEligibility = db.select({
2423
+ hash: bestOffers.hash,
2424
+ offering: bestOffers.offering,
2425
+ assets: bestOffers.assets,
2426
+ consumed: bestOffers.consumed,
2427
+ remaining: bestOffers.remaining,
2428
+ rate: bestOffers.rate,
2429
+ maturity: bestOffers.maturity,
2430
+ expiry: bestOffers.expiry,
2431
+ start: bestOffers.start,
2432
+ nonce: bestOffers.nonce,
2433
+ buy: bestOffers.buy,
2434
+ chainId: bestOffers.chainId,
2435
+ loanToken: bestOffers.loanToken,
2436
+ callbackAddress: bestOffers.callbackAddress,
2437
+ callbackData: bestOffers.callbackData,
2438
+ callbackGasLimit: bestOffers.callbackGasLimit,
2439
+ signature: bestOffers.signature,
2440
+ callbackId: bestOffers.callbackId,
2441
+ status: bestOffers.status,
2442
+ metadata: bestOffers.metadata,
2443
+ // liquidity caps
2444
+ userAmount: drizzleOrm.sql`COALESCE(${queueLiquidity.userAmount}, 0)`.as("user_amount"),
2445
+ queueLiquidity: drizzleOrm.sql`COALESCE(${queueLiquidity.queueLiquidity}, 0)`.as(
2446
+ "queue_liquidity"
2447
+ ),
2448
+ // running total of remaining per callback, ordered by the *same* priority as selection
2449
+ cumulativeRemaining: drizzleOrm.sql`
2450
+ SUM(${bestOffers.remaining}) OVER (
2451
+ PARTITION BY ${bestOffers.callbackId}
2452
+ ORDER BY ${sortExpr}, ${drizzleOrm.asc(bestOffers.hash)}
2453
+ ROWS UNBOUNDED PRECEDING
2454
+ )
2455
+ `.as("cumulative_remaining"),
2456
+ eligible: drizzleOrm.sql`
2457
+ CASE
2458
+ WHEN ${bestOffers.buy} = false THEN true
2459
+ WHEN ${bestOffers.remaining} <= 0 THEN false
2460
+ ELSE ( SUM(${bestOffers.remaining}) OVER (
2461
+ PARTITION BY ${bestOffers.callbackId}
2462
+ ORDER BY ${sortExpr}, ${drizzleOrm.asc(bestOffers.hash)}
2463
+ ROWS UNBOUNDED PRECEDING
2464
+ )
2465
+ <= LEAST(
2466
+ COALESCE(${queueLiquidity.userAmount}, 0),
2467
+ COALESCE(${queueLiquidity.queueLiquidity}, 0)
2468
+ )
2469
+ )
2470
+ END
2471
+ `.as("eligible")
2472
+ }).from(bestOffers).leftJoin(queueLiquidity, drizzleOrm.eq(bestOffers.callbackId, queueLiquidity.callbackId)).as("offers_with_eligibility");
2473
+ const hardCap = limit * 5 + 1 + (parsedCursor ? 1 : 0);
2474
+ const validOffers = db.select({
2475
+ // pass-through all fields + a global row_number for hard cap
2476
+ hash: offersWithEligibility.hash,
2477
+ offering: offersWithEligibility.offering,
2478
+ assets: offersWithEligibility.assets,
2479
+ consumed: offersWithEligibility.consumed,
2480
+ remaining: offersWithEligibility.remaining,
2481
+ rate: offersWithEligibility.rate,
2482
+ maturity: offersWithEligibility.maturity,
2483
+ expiry: offersWithEligibility.expiry,
2484
+ start: offersWithEligibility.start,
2485
+ nonce: offersWithEligibility.nonce,
2486
+ buy: offersWithEligibility.buy,
2487
+ chainId: offersWithEligibility.chainId,
2488
+ loanToken: offersWithEligibility.loanToken,
2489
+ callbackAddress: offersWithEligibility.callbackAddress,
2490
+ callbackData: offersWithEligibility.callbackData,
2491
+ callbackGasLimit: offersWithEligibility.callbackGasLimit,
2492
+ signature: offersWithEligibility.signature,
2493
+ callbackId: offersWithEligibility.callbackId,
2494
+ status: offersWithEligibility.status,
2495
+ metadata: offersWithEligibility.metadata,
2496
+ userAmount: offersWithEligibility.userAmount,
2497
+ queueLiquidity: offersWithEligibility.queueLiquidity,
2498
+ cumulativeRemaining: offersWithEligibility.cumulativeRemaining,
2499
+ eligible: offersWithEligibility.eligible,
2500
+ // sort expression is the same again as in offersWithEligibility
2501
+ rowNumber: drizzleOrm.sql`
2502
+ ROW_NUMBER() OVER (
2503
+ ORDER BY
2504
+ CASE WHEN ${offersWithEligibility.buy} THEN ${offersWithEligibility.rate} ELSE -${offersWithEligibility.rate} END,
2505
+ ${drizzleOrm.asc(offersWithEligibility.hash)}
2506
+ )
2507
+ `.as("row_number")
2508
+ }).from(offersWithEligibility).where(drizzleOrm.sql`${offersWithEligibility.eligible} = true`).limit(hardCap).as("valid_offers");
2509
+ const cursorTuple = parsedCursor ? {
2510
+ rate: BigInt(parsedCursor.rate),
2511
+ hash: parsedCursor.hash
2512
+ } : null;
2513
+ const withCollats = await db.select({
2514
+ // base fields
2515
+ hash: drizzleOrm.sql`${validOffers.hash}`,
2516
+ offering: drizzleOrm.sql`${validOffers.offering}`,
2517
+ assets: validOffers.assets,
2518
+ consumed: validOffers.consumed,
2519
+ rate: validOffers.rate,
2520
+ maturity: validOffers.maturity,
2521
+ expiry: validOffers.expiry,
2522
+ start: validOffers.start,
2523
+ nonce: validOffers.nonce,
2524
+ buy: validOffers.buy,
2525
+ chainId: validOffers.chainId,
2526
+ loanToken: drizzleOrm.sql`${validOffers.loanToken}`,
2527
+ callbackAddress: drizzleOrm.sql`${validOffers.callbackAddress}`,
2528
+ callbackData: drizzleOrm.sql`${validOffers.callbackData}`,
2529
+ callbackGasLimit: validOffers.callbackGasLimit,
2530
+ signature: drizzleOrm.sql`${validOffers.signature}`,
2531
+ callbackId: validOffers.callbackId,
2532
+ status: drizzleOrm.sql`${validOffers.status}`,
2533
+ metadata: validOffers.metadata,
2534
+ // collateral fields
2535
+ collateralAsset: drizzleOrm.sql`${offerCollaterals.asset}`,
2536
+ collateralOracle: drizzleOrm.sql`${offerCollaterals.oracle}`,
2537
+ collateralLltv: offerCollaterals.lltv
2538
+ }).from(validOffers).leftJoin(offerCollaterals, drizzleOrm.eq(validOffers.hash, offerCollaterals.offerHash)).where(
2539
+ drizzleOrm.and(
2540
+ ...cursorTuple ? [
2541
+ drizzleOrm.sql`(${validOffers.rate}, ${validOffers.hash}) ${rateSortDirection === "asc" ? drizzleOrm.sql`>` : drizzleOrm.sql`<`} (${cursorTuple.rate}, ${cursorTuple.hash})`
2542
+ ] : []
2543
+ )
2544
+ ).orderBy(
2545
+ rateSortDirection === "asc" ? drizzleOrm.asc(validOffers.rate) : drizzleOrm.desc(validOffers.rate),
2546
+ drizzleOrm.asc(validOffers.hash)
2547
+ );
2548
+ const buildOffersMap = (rows, skipHash) => {
2549
+ const map = /* @__PURE__ */ new Map();
2550
+ for (const row of rows) {
2551
+ const entry = map.get(row.hash) ?? { base: row, collaterals: [] };
2552
+ if (row.collateralAsset && row.collateralOracle && row.collateralLltv) {
2553
+ entry.collaterals.push({
2554
+ asset: row.collateralAsset,
2555
+ oracle: row.collateralOracle,
2556
+ lltv: mempool.LLTV.from(row.collateralLltv)
2557
+ });
2558
+ }
2559
+ map.set(row.hash, entry);
2560
+ }
2561
+ return map;
2562
+ };
2563
+ const allEntries = Array.from(buildOffersMap(withCollats).values());
2564
+ const pageEntries = allEntries.slice(0, limit);
2565
+ let nextCursor = null;
2566
+ if (allEntries.length > limit) {
2567
+ const last = pageEntries[pageEntries.length - 1].base;
2568
+ nextCursor = encode({
2569
+ sort: "rate",
2570
+ dir: rateSortDirection,
2571
+ hash: last.hash,
2572
+ rate: last.rate.toString()
2573
+ });
2574
+ }
2575
+ const data = pageEntries.map(({ base, collaterals: cols }) => ({
2576
+ ...mempool.Offer.from({
2577
+ offering: base.offering,
2578
+ assets: BigInt(base.assets),
2579
+ rate: base.rate,
2580
+ maturity: mempool.Maturity.from(base.maturity),
2581
+ expiry: base.expiry,
2582
+ start: base.start,
2583
+ nonce: base.nonce,
2584
+ buy: base.buy,
2585
+ chainId: base.chainId,
2586
+ loanToken: base.loanToken,
2587
+ collaterals: cols.sort(
2588
+ (a, b) => a.asset.toLowerCase().localeCompare(b.asset.toLowerCase())
2589
+ ),
2590
+ callback: {
2591
+ address: base.callbackAddress,
2592
+ data: base.callbackData,
2593
+ gasLimit: base.callbackGasLimit
2594
+ },
2595
+ ...base.signature !== null ? { signature: base.signature } : void 0
2596
+ }),
2597
+ consumed: BigInt(base.consumed),
2598
+ status: base.status,
2599
+ ...base.metadata ? { metadata: base.metadata } : {}
2600
+ }));
2601
+ return { offers: data, nextCursor };
1768
2602
  },
1769
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1770
- warn: isEnabled("warn") ? console.warn.bind(console) : () => {
2603
+ delete: async (hash) => {
2604
+ const result = await db.delete(offers).where(drizzleOrm.eq(offers.hash, hash.toLowerCase()));
2605
+ return result.affectedRows > 0;
2606
+ },
2607
+ deleteMany: async (hashes) => {
2608
+ if (hashes.length === 0) {
2609
+ return 0;
2610
+ }
2611
+ return await db.transaction(async (tx) => {
2612
+ const normalizedHashes = hashes.map((hash) => hash.toLowerCase());
2613
+ const result = await tx.delete(offers).where(
2614
+ drizzleOrm.sql`${offers.hash} = ANY(${drizzleOrm.sql.raw(`ARRAY[${normalizedHashes.map((h) => `'${h}'`).join(", ")}]`)})`
2615
+ );
2616
+ return result.affectedRows;
2617
+ });
1771
2618
  },
1772
- // biome-ignore lint/suspicious/noConsole: console is used for logging
1773
- error: isEnabled("error") ? console.error.bind(console) : () => {
2619
+ updateStatus: async (parameters) => {
2620
+ 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);
2621
+ if (latest.length > 0 && latest[0].status === parameters.status) return;
2622
+ await db.insert(offerStatus).values({
2623
+ offerHash: parameters.offerHash.toLowerCase(),
2624
+ status: parameters.status,
2625
+ metadata: parameters.metadata
2626
+ });
1774
2627
  },
1775
- fatal: isEnabled("fatal") ? (...args) => console.error("[fatal]", ...args) : () => {
2628
+ updateConsumedAmount: async (parameters) => {
2629
+ await db.insert(consumed).values({
2630
+ id: parameters.id,
2631
+ chainId: parameters.chainId,
2632
+ offering: parameters.offering.toLowerCase(),
2633
+ nonce: parameters.nonce,
2634
+ consumed: parameters.consumed.toString()
2635
+ }).onConflictDoNothing();
2636
+ }
2637
+ };
2638
+ }
2639
+
2640
+ // src/core/OfferStore/PG.ts
2641
+ var PG_exports = {};
2642
+ __export(PG_exports, {
2643
+ applyMigrations: () => applyMigrations,
2644
+ clean: () => clean,
2645
+ connect: () => connect
2646
+ });
2647
+ function connect(parameters) {
2648
+ if (parameters.type === "pg") {
2649
+ const pool2 = new pg.Pool({ connectionString: parameters.endpoint });
2650
+ const client2 = nodePostgres.drizzle(pool2, { schema: schema_exports });
2651
+ return Object.assign(client2, { name: "pg", pool: pool2 });
2652
+ }
2653
+ const pool = new pglite.PGlite();
2654
+ const client = pglite$1.drizzle(pool, { schema: schema_exports });
2655
+ return Object.assign(client, { name: "pglite", pool });
2656
+ }
2657
+ async function applyMigrations(pg) {
2658
+ const migrationsFolder = process.env.AWS_LAMBDA_FUNCTION_NAME ? path__default.default.join(process.cwd(), "drizzle", VERSION) : path__default.default.join(__dirname, "drizzle", VERSION);
2659
+ await pg.execute(`create schema if not exists "${VERSION}"`);
2660
+ if (pg.name === "pg") {
2661
+ await migrator.migrate(pg, { migrationsFolder });
2662
+ return;
2663
+ }
2664
+ await migrator$1.migrate(pg, { migrationsFolder });
2665
+ }
2666
+ async function clean(pg) {
2667
+ await pg.execute(`drop schema if exists "${VERSION}" cascade`);
2668
+ await pg.execute(`create schema "${VERSION}"`);
2669
+ await pg.execute("drop schema if exists drizzle cascade");
2670
+ }
2671
+
2672
+ // src/core/router/index.ts
2673
+ var router_exports = {};
2674
+ __export(router_exports, {
2675
+ APIError: () => APIError,
2676
+ BadRequestError: () => BadRequestError,
2677
+ HttpForbiddenError: () => HttpForbiddenError,
2678
+ HttpGetOffersFailedError: () => HttpGetOffersFailedError,
2679
+ HttpRateLimitError: () => HttpRateLimitError,
2680
+ HttpUnauthorizedError: () => HttpUnauthorizedError,
2681
+ InternalServerError: () => InternalServerError,
2682
+ InvalidUrlError: () => InvalidUrlError,
2683
+ NotFoundError: () => NotFoundError,
2684
+ ValidationError: () => ValidationError,
2685
+ connect: () => connect2,
2686
+ error: () => error,
2687
+ get: () => get,
2688
+ handleZodError: () => handleZodError,
2689
+ match: () => match,
2690
+ serve: () => serve,
2691
+ success: () => success
2692
+ });
2693
+ function connect2(opts) {
2694
+ const u = new URL(opts?.url || "https://router.morpho.dev");
2695
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
2696
+ throw new InvalidUrlError(u.toString());
2697
+ }
2698
+ const headers = opts?.headers ?? new Headers();
2699
+ headers.set("Content-Type", "application/json");
2700
+ opts?.apiKey !== void 0 ? headers.set("X-API-Key", opts.apiKey) : null;
2701
+ const config = {
2702
+ url: u,
2703
+ headers
2704
+ };
2705
+ return {
2706
+ ...config,
2707
+ get: (parameters) => get(config, parameters),
2708
+ match: (parameters) => match(config, parameters)
2709
+ };
2710
+ }
2711
+ async function get(config, parameters) {
2712
+ const url = new URL(`${config.url.toString()}v1/offers`);
2713
+ if (parameters.creators?.length) {
2714
+ url.searchParams.set("creators", parameters.creators.join(","));
2715
+ }
2716
+ if (parameters.side) {
2717
+ url.searchParams.set("side", parameters.side);
2718
+ }
2719
+ if (parameters.chains?.length) {
2720
+ url.searchParams.set("chains", parameters.chains.join(","));
2721
+ }
2722
+ if (parameters.loanTokens?.length) {
2723
+ url.searchParams.set("loan_tokens", parameters.loanTokens.join(","));
2724
+ }
2725
+ if (parameters.status?.length) {
2726
+ url.searchParams.set("status", parameters.status.join(","));
2727
+ }
2728
+ if (parameters.callbackAddresses?.length) {
2729
+ url.searchParams.set("callback_addresses", parameters.callbackAddresses.join(","));
2730
+ }
2731
+ if (parameters.minAmount !== void 0) {
2732
+ url.searchParams.set("min_amount", parameters.minAmount.toString());
2733
+ }
2734
+ if (parameters.maxAmount !== void 0) {
2735
+ url.searchParams.set("max_amount", parameters.maxAmount.toString());
2736
+ }
2737
+ if (parameters.minRate !== void 0) {
2738
+ url.searchParams.set("min_rate", parameters.minRate.toString());
2739
+ }
2740
+ if (parameters.maxRate !== void 0) {
2741
+ url.searchParams.set("max_rate", parameters.maxRate.toString());
2742
+ }
2743
+ if (parameters.minMaturity !== void 0) {
2744
+ url.searchParams.set("min_maturity", parameters.minMaturity.toString());
2745
+ }
2746
+ if (parameters.maxMaturity !== void 0) {
2747
+ url.searchParams.set("max_maturity", parameters.maxMaturity.toString());
2748
+ }
2749
+ if (parameters.minExpiry !== void 0) {
2750
+ url.searchParams.set("min_expiry", parameters.minExpiry.toString());
2751
+ }
2752
+ if (parameters.maxExpiry !== void 0) {
2753
+ url.searchParams.set("max_expiry", parameters.maxExpiry.toString());
2754
+ }
2755
+ if (parameters.collateralAssets?.length) {
2756
+ url.searchParams.set("collateral_assets", parameters.collateralAssets.join(","));
2757
+ }
2758
+ if (parameters.collateralOracles?.length) {
2759
+ url.searchParams.set("collateral_oracles", parameters.collateralOracles.join(","));
2760
+ }
2761
+ if (parameters.collateralTuple?.length) {
2762
+ const tupleStr = parameters.collateralTuple.map(({ asset, oracle, lltv }) => {
2763
+ let result = asset;
2764
+ if (oracle) {
2765
+ result += `:${oracle}`;
2766
+ } else if (lltv !== void 0) {
2767
+ result += `:`;
2768
+ }
2769
+ if (lltv !== void 0) result += `:${viem.formatUnits(lltv, 16)}`;
2770
+ return result;
2771
+ }).join("#");
2772
+ url.searchParams.set("collateral_tuple", tupleStr);
2773
+ }
2774
+ if (parameters.minLltv !== void 0) {
2775
+ url.searchParams.set("min_lltv", viem.formatUnits(parameters.minLltv, 16));
2776
+ }
2777
+ if (parameters.maxLltv !== void 0) {
2778
+ url.searchParams.set("max_lltv", viem.formatUnits(parameters.maxLltv, 16));
2779
+ }
2780
+ if (parameters.sortBy) {
2781
+ url.searchParams.set("sort_by", parameters.sortBy);
2782
+ }
2783
+ if (parameters.sortOrder) {
2784
+ url.searchParams.set("sort_order", parameters.sortOrder);
2785
+ }
2786
+ if (parameters.cursor) {
2787
+ url.searchParams.set("cursor", parameters.cursor);
2788
+ }
2789
+ if (parameters.limit !== void 0) {
2790
+ url.searchParams.set("limit", parameters.limit.toString());
2791
+ }
2792
+ const { cursor: returnedCursor, data: offers2 } = await getApi(config, url);
2793
+ const routerOffers = offers2.map(mempool.Format.fromSnakeCase).map(fromResponse);
2794
+ return {
2795
+ cursor: returnedCursor,
2796
+ offers: routerOffers.map(from).map(toResponse)
2797
+ };
2798
+ }
2799
+ async function match(config, parameters) {
2800
+ const url = new URL(`${config.url.toString()}v1/offers/match`);
2801
+ url.searchParams.set("side", parameters.side);
2802
+ url.searchParams.set("chain_id", parameters.chainId.toString());
2803
+ if (parameters.rate !== void 0) {
2804
+ url.searchParams.set("rate", parameters.rate.toString());
2805
+ }
2806
+ if (parameters.collaterals?.length) {
2807
+ const collateralsStr = parameters.collaterals.map(({ asset, oracle, lltv }) => `${asset}:${oracle}:${viem.formatUnits(lltv, 16)}`).join("#");
2808
+ url.searchParams.set("collaterals", collateralsStr);
2809
+ }
2810
+ if (parameters.maturity !== void 0) {
2811
+ url.searchParams.set("maturity", parameters.maturity.toString());
2812
+ }
2813
+ if (parameters.minMaturity !== void 0) {
2814
+ url.searchParams.set("min_maturity", parameters.minMaturity.toString());
2815
+ }
2816
+ if (parameters.maxMaturity !== void 0) {
2817
+ url.searchParams.set("max_maturity", parameters.maxMaturity.toString());
2818
+ }
2819
+ if (parameters.loanToken) {
2820
+ url.searchParams.set("loan_token", parameters.loanToken);
2821
+ }
2822
+ if (parameters.creator) {
2823
+ url.searchParams.set("creator", parameters.creator);
2824
+ }
2825
+ if (parameters.status?.length) {
2826
+ url.searchParams.set("status", parameters.status.join(","));
2827
+ }
2828
+ if (parameters.cursor) {
2829
+ url.searchParams.set("cursor", parameters.cursor);
2830
+ }
2831
+ if (parameters.limit !== void 0) {
2832
+ url.searchParams.set("limit", parameters.limit.toString());
2833
+ }
2834
+ const { cursor: returnedCursor, data: offers2 } = await getApi(config, url);
2835
+ const routerOffers = offers2.map(mempool.Format.fromSnakeCase).map(fromResponse);
2836
+ return {
2837
+ cursor: returnedCursor,
2838
+ offers: routerOffers.map(from).map(toResponse)
2839
+ };
2840
+ }
2841
+ async function getApi(config, url) {
2842
+ const pathname = url.pathname;
2843
+ let action;
2844
+ switch (true) {
2845
+ case pathname.includes("/v1/offers/match"):
2846
+ action = "match_offers";
2847
+ break;
2848
+ case pathname.includes("/v1/offers"):
2849
+ action = "get_offers";
2850
+ break;
2851
+ default:
2852
+ throw new HttpGetOffersFailedError("Unknown endpoint", {
2853
+ details: `Unsupported path: ${pathname}`
2854
+ });
2855
+ }
2856
+ const schemaParseResult = safeParse(action, Object.fromEntries(url.searchParams));
2857
+ if (!schemaParseResult.success) {
2858
+ throw new HttpGetOffersFailedError(`Invalid URL parameters`, {
2859
+ details: schemaParseResult.error.issues[0]?.message
2860
+ });
2861
+ }
2862
+ const response = await fetch(url.toString(), {
2863
+ method: "GET",
2864
+ headers: config.headers
2865
+ });
2866
+ if (!response.ok) {
2867
+ switch (response.status) {
2868
+ case 401:
2869
+ throw new HttpUnauthorizedError();
2870
+ case 403:
2871
+ throw new HttpForbiddenError();
2872
+ case 429:
2873
+ throw new HttpRateLimitError();
2874
+ }
2875
+ throw new HttpGetOffersFailedError(`GET request returned ${response.status}`, {
2876
+ details: await response.text()
2877
+ });
2878
+ }
2879
+ return response.json();
2880
+ }
2881
+ var InvalidUrlError = class extends mempool.Errors.BaseError {
2882
+ name = "Router.InvalidUrlError";
2883
+ constructor(url) {
2884
+ super(`URL "${url}" is not http/https.`);
2885
+ }
2886
+ };
2887
+ var HttpUnauthorizedError = class extends mempool.Errors.BaseError {
2888
+ name = "Router.HttpUnauthorizedError";
2889
+ constructor() {
2890
+ super("Unauthorized.", {
2891
+ metaMessages: ["Ensure that an API key is provided."]
2892
+ });
2893
+ }
2894
+ };
2895
+ var HttpForbiddenError = class extends mempool.Errors.BaseError {
2896
+ name = "Router.HttpForbiddenError";
2897
+ constructor() {
2898
+ super("Forbidden.", {
2899
+ metaMessages: ["Ensure that the API key is valid."]
2900
+ });
2901
+ }
2902
+ };
2903
+ var HttpRateLimitError = class extends mempool.Errors.BaseError {
2904
+ name = "Router.HttpRateLimitError";
2905
+ constructor() {
2906
+ super("Rate limit exceeded.", {
2907
+ metaMessages: [
2908
+ "The number of allowed requests has been exceeded. You must wait for the rate limit to reset."
2909
+ ]
2910
+ });
2911
+ }
2912
+ };
2913
+ var HttpGetOffersFailedError = class extends mempool.Errors.BaseError {
2914
+ name = "Router.HttpGetOffersFailedError";
2915
+ constructor(message, { details } = {}) {
2916
+ super(message, {
2917
+ metaMessages: [details]
2918
+ });
2919
+ }
2920
+ };
2921
+ async function serve(parameters) {
2922
+ const app = new hono.Hono();
2923
+ const store = parameters.store;
2924
+ const logger = getLogger();
2925
+ app.get("/v1/offers", async (c) => {
2926
+ try {
2927
+ const params = parse("get_offers", c.req.query());
2928
+ const offers2 = await store.getAll({
2929
+ query: {
2930
+ creators: params.creators,
2931
+ side: params.side,
2932
+ chains: params.chains,
2933
+ loanTokens: params.loan_tokens,
2934
+ status: params.status,
2935
+ callbackAddresses: params.callback_addresses,
2936
+ minAmount: params.min_amount,
2937
+ maxAmount: params.max_amount,
2938
+ minRate: params.min_rate,
2939
+ maxRate: params.max_rate,
2940
+ minMaturity: params.min_maturity,
2941
+ maxMaturity: params.max_maturity,
2942
+ minExpiry: params.min_expiry,
2943
+ maxExpiry: params.max_expiry,
2944
+ collateralAssets: params.collateral_assets,
2945
+ collateralOracles: params.collateral_oracles,
2946
+ collateralTuple: params.collateral_tuple,
2947
+ minLltv: params.min_lltv,
2948
+ maxLltv: params.max_lltv,
2949
+ sortBy: params.sort_by,
2950
+ sortOrder: params.sort_order,
2951
+ cursor: params.cursor,
2952
+ limit: params.limit
2953
+ }
2954
+ });
2955
+ return success(c, {
2956
+ data: offers2.offers.map(
2957
+ (offer) => mempool.Format.stringifyBigint(mempool.Format.toSnakeCase(toResponse(offer)))
2958
+ ),
2959
+ cursor: offers2.nextCursor ?? null
2960
+ });
2961
+ } catch (err) {
2962
+ console.error(err);
2963
+ return error(err, c);
1776
2964
  }
1777
- };
1778
- }
1779
- function silentLogger() {
1780
- return {
1781
- trace: () => {
1782
- },
1783
- debug: () => {
1784
- },
1785
- info: () => {
1786
- },
1787
- warn: () => {
1788
- },
1789
- error: () => {
2965
+ });
2966
+ app.get("/v1/offers/match", async (c) => {
2967
+ try {
2968
+ const params = parse("match_offers", c.req.query());
2969
+ const offers2 = await store.findMatchingOffers({
2970
+ side: params.side,
2971
+ chainId: params.chain_id,
2972
+ rate: params.rate,
2973
+ collaterals: params.collaterals,
2974
+ maturity: params.maturity,
2975
+ minMaturity: params.min_maturity,
2976
+ maxMaturity: params.max_maturity,
2977
+ loanToken: params.loan_token,
2978
+ creator: params.creator,
2979
+ cursor: params.cursor,
2980
+ limit: params.limit
2981
+ });
2982
+ return success(c, {
2983
+ data: offers2.offers.map(
2984
+ (offer) => mempool.Format.stringifyBigint(mempool.Format.toSnakeCase(toResponse(offer)))
2985
+ ),
2986
+ cursor: offers2.nextCursor ?? null
2987
+ });
2988
+ } catch (err) {
2989
+ console.error(err);
2990
+ return error(err, c);
2991
+ }
2992
+ });
2993
+ nodeServer.serve(
2994
+ {
2995
+ fetch: app.fetch,
2996
+ port: parameters.port
1790
2997
  },
1791
- fatal: () => {
2998
+ (info) => {
2999
+ logger.info({ service: "api", msg: "Router server started", port: info.port });
1792
3000
  }
1793
- };
3001
+ );
1794
3002
  }
1795
- var loggerContext = new async_hooks.AsyncLocalStorage();
1796
- function runWithLogger(logger, fn) {
1797
- return loggerContext.run(logger, fn);
3003
+ function error(error2, c) {
3004
+ if (error2 instanceof APIError) {
3005
+ return handleAPIError(error2, c);
3006
+ }
3007
+ if (error2 instanceof SyntaxError) {
3008
+ return handleAPIError(new BadRequestError(error2.message), c);
3009
+ }
3010
+ if (error2 instanceof v4.z.ZodError) {
3011
+ return handleAPIError(handleZodError(error2), c);
3012
+ }
3013
+ return handleAPIError(new InternalServerError(), c);
1798
3014
  }
1799
- function getLogger() {
1800
- return loggerContext.getStore() ?? defaultLogger();
3015
+ function success(c, {
3016
+ data,
3017
+ cursor
3018
+ }) {
3019
+ return c.json({
3020
+ status: "success",
3021
+ cursor,
3022
+ data,
3023
+ meta: {
3024
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3025
+ }
3026
+ });
1801
3027
  }
1802
-
1803
- // src/Validation.ts
1804
- var Validation_exports = {};
1805
- __export(Validation_exports, {
1806
- run: () => run
1807
- });
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);
1822
- }
1823
- }
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);
1832
- }
1833
- }
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);
1839
- }
3028
+ var APIError = class extends Error {
3029
+ constructor(statusCode, message, code, details) {
3030
+ super(message);
3031
+ this.statusCode = statusCode;
3032
+ this.code = code;
3033
+ this.details = details;
3034
+ this.name = "APIError";
3035
+ }
3036
+ };
3037
+ var ValidationError = class extends APIError {
3038
+ constructor(message, details) {
3039
+ super(400, message, "VALIDATION_ERROR", details);
3040
+ }
3041
+ };
3042
+ var NotFoundError = class extends APIError {
3043
+ constructor(message) {
3044
+ super(404, message, "NOT_FOUND");
3045
+ }
3046
+ };
3047
+ var InternalServerError = class extends APIError {
3048
+ constructor(message = "Internal server error") {
3049
+ super(500, message, "INTERNAL_SERVER_ERROR");
3050
+ }
3051
+ };
3052
+ var BadRequestError = class extends APIError {
3053
+ constructor(message = "Invalid JSON format", details) {
3054
+ super(400, message, "BAD_REQUEST", details);
3055
+ }
3056
+ };
3057
+ function handleZodError(error2) {
3058
+ const formattedErrors = error2.issues.map((err) => {
3059
+ const field = err.path.join(".");
3060
+ let issue = err.message;
3061
+ if (err.code === "invalid_type") {
3062
+ if (err.message.includes("received undefined")) {
3063
+ issue = `${field} is required`;
3064
+ } else {
3065
+ issue = err.message;
1840
3066
  }
3067
+ } else if (err.code === "invalid_format") {
3068
+ issue = `${field} has an invalid format`;
1841
3069
  }
1842
- validItems = validItems.filter((_, i) => !indicesToRemove.has(i));
1843
- }
1844
- return {
1845
- valid: validItems,
1846
- issues
1847
- };
3070
+ return {
3071
+ field,
3072
+ issue
3073
+ };
3074
+ });
3075
+ return new ValidationError("Validation failed", formattedErrors);
3076
+ }
3077
+ function handleAPIError(error2, c) {
3078
+ return c.json({
3079
+ statusCode: error2.statusCode,
3080
+ body: JSON.stringify({
3081
+ status: "error",
3082
+ error: {
3083
+ code: error2.code,
3084
+ message: error2.message,
3085
+ ...error2.details && typeof error2.details === "object" ? { details: error2.details } : {}
3086
+ },
3087
+ meta: {
3088
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3089
+ }
3090
+ })
3091
+ });
1848
3092
  }
1849
3093
 
1850
- // src/ValidationRule.ts
1851
- var ValidationRule_exports = {};
1852
- __export(ValidationRule_exports, {
1853
- batch: () => batch,
1854
- morpho: () => morpho,
1855
- single: () => single
3094
+ // src/core/Services.ts
3095
+ var Services_exports = {};
3096
+ __export(Services_exports, {
3097
+ from: () => from2
1856
3098
  });
1857
- function single(name, run2) {
1858
- return { kind: "single", name, run: run2 };
1859
- }
1860
- function batch(name, run2) {
1861
- return { kind: "batch", name, run: run2 };
1862
- }
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
- }
3099
+ function from2(config) {
3100
+ const { chain, rpcUrl, dbConfig } = config;
3101
+ const walletClient = viem.createWalletClient({
3102
+ chain,
3103
+ transport: viem.http(rpcUrl)
3104
+ }).extend(viem.publicActions);
3105
+ if (dbConfig.type === "pg" && !dbConfig.endpoint) {
3106
+ throw new Error("dbOffer.endpoint is required when dbOffer.type is pg");
3107
+ }
3108
+ const DB = dbConfig.type === "pg" ? connect({ type: "pg", endpoint: dbConfig.endpoint }) : connect({ type: "pglite" });
3109
+ const offerStore = create3({ db: DB });
3110
+ const collectorBlockStore = create({
3111
+ db: DB
1870
3112
  });
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
- };
3113
+ const liquidityStore = create2({ db: DB });
3114
+ const interval = 1e4;
3115
+ const maxBatchSize = 1e3;
3116
+ const mempoolOffersCollector = createMempoolCollector({
3117
+ mempool: mempool.Mempool.connect({
3118
+ client: walletClient,
3119
+ mempoolAddress: chain.mempool.address
3120
+ }),
3121
+ offerStore,
3122
+ collectorBlockStore,
3123
+ chain,
3124
+ options: {
3125
+ interval,
3126
+ maxBatchSize
1879
3127
  }
1880
3128
  });
1881
- const expiry = single("expiry", (offer, _) => {
1882
- if (offer.expiry < Math.floor(Date.now() / 1e3)) {
1883
- return { message: "Expiry mismatch" };
3129
+ const consumedEventsCollector = createConsumedEventsCollector({
3130
+ client: walletClient,
3131
+ contractAddress: chain.morpho,
3132
+ offerStore,
3133
+ collectorBlockStore,
3134
+ chainId: chain.id,
3135
+ options: {
3136
+ interval,
3137
+ maxBatchSize
1884
3138
  }
1885
3139
  });
1886
- const callback = single("empty_callback", (offer, _) => {
1887
- if (!offer.buy || offer.callback.data !== "0x") {
1888
- return { message: "Callback not supported yet." };
3140
+ const buyWithEmptyCallbackLiquidityCollector = createBuyWithEmptyCallbackLiquidityCollector({
3141
+ client: walletClient,
3142
+ offerStore,
3143
+ collectorBlockStore,
3144
+ liquidityStore,
3145
+ chain,
3146
+ options: {
3147
+ interval,
3148
+ maxBatchSize
1889
3149
  }
1890
3150
  });
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
- ];
3151
+ return {
3152
+ offerStore,
3153
+ collectorBlockStore,
3154
+ liquidityStore,
3155
+ mempoolOffersCollector,
3156
+ consumedEventsCollector,
3157
+ buyWithEmptyCallbackLiquidityCollector,
3158
+ DB
3159
+ };
1899
3160
  }
1900
3161
 
1901
3162
  exports.ApiSchema = apiSchema_exports;
1902
3163
  exports.Callback = Callback_exports;
3164
+ exports.Collector = Collector_exports;
3165
+ exports.CollectorBlockStore = CollectorBlockStore_exports;
1903
3166
  exports.Cursor = Cursor_exports;
1904
3167
  exports.Liquidity = Liquidity_exports;
3168
+ exports.LiquidityStore = LiquidityStore_exports;
1905
3169
  exports.Logger = Logger_exports;
1906
3170
  exports.OfferStore = OfferStore_exports;
3171
+ exports.OffersSchema = schema_exports;
3172
+ exports.PG = PG_exports;
1907
3173
  exports.Router = router_exports;
1908
3174
  exports.RouterOffer = RouterOffer_exports;
3175
+ exports.Services = Services_exports;
1909
3176
  exports.Validation = Validation_exports;
1910
3177
  exports.ValidationRule = ValidationRule_exports;
1911
3178
  Object.keys(mempool).forEach(function (k) {