@morpho-dev/router 0.1.9 → 0.1.11

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.
@@ -2,11 +2,11 @@ import { Errors, LLTV, Offer, Format, Utils, Chain, Maturity, Time, Mempool } fr
2
2
  export * from '@morpho-dev/mempool';
3
3
  import { z } from 'zod/v4';
4
4
  import { createDocument } from 'zod-openapi';
5
- import { parseUnits, maxUint256, publicActions, parseEventLogs, formatUnits, createWalletClient, http, erc20Abi, stringify } from 'viem';
5
+ import { parseUnits, maxUint256, encodeAbiParameters, publicActions, parseEventLogs, formatUnits, createWalletClient, http, decodeAbiParameters, erc20Abi, stringify } from 'viem';
6
6
  import { Base64 } from 'js-base64';
7
7
  import { getBlockNumber } from 'viem/actions';
8
8
  import { AsyncLocalStorage } from 'async_hooks';
9
- import { desc, and, eq, sql, gte, lte, asc, inArray } from 'drizzle-orm';
9
+ import { asc, desc, and, eq, sql, gte, lte, inArray } from 'drizzle-orm';
10
10
  import { pgSchema, timestamp, varchar, bigint, text, boolean, integer, numeric, index, serial, jsonb, uniqueIndex, primaryKey } from 'drizzle-orm/pg-core';
11
11
  import path from 'path';
12
12
  import { PGlite } from '@electric-sql/pglite';
@@ -719,54 +719,147 @@ function fromResponse(offerResponse) {
719
719
  var Callback_exports = {};
720
720
  __export(Callback_exports, {
721
721
  CallbackType: () => CallbackType,
722
+ WhitelistedCallbackAddresses: () => WhitelistedCallbackAddresses,
722
723
  buildLiquidity: () => buildLiquidity,
724
+ decode: () => decode2,
725
+ encode: () => encode2,
723
726
  getCallbackIdForOffer: () => getCallbackIdForOffer
724
727
  });
725
728
  var CallbackType = /* @__PURE__ */ ((CallbackType2) => {
726
729
  CallbackType2["BuyWithEmptyCallback"] = "buy_with_empty_callback";
730
+ CallbackType2["SellWithdrawFromWallet"] = "sell_withdraw_from_wallet";
727
731
  return CallbackType2;
728
732
  })(CallbackType || {});
733
+ var WhitelistedCallbackAddresses = {
734
+ ["buy_with_empty_callback" /* BuyWithEmptyCallback */]: [],
735
+ ["sell_withdraw_from_wallet" /* SellWithdrawFromWallet */]: [
736
+ "0x1111111111111111111111111111111111111111",
737
+ "0x2222222222222222222222222222222222222222"
738
+ // @TODO: update once deployed and add mapping per chain if needed
739
+ ].map((address) => address.toLowerCase())
740
+ };
729
741
  function buildLiquidity(parameters) {
730
- const { type, user, contract, chainId, amount, index: index2 = 0, updatedAt = /* @__PURE__ */ new Date() } = parameters;
731
- if (type !== "buy_with_empty_callback" /* BuyWithEmptyCallback */)
732
- throw new Error(`CallbackType not implemented: ${type}`);
733
- const amountStr = amount.toString();
734
- const id = `${user}-${chainId.toString()}-${type}-${contract}`.toLowerCase();
735
- return {
736
- userPosition: {
737
- id,
738
- availableLiquidityQueueId: id,
739
- user: user.toLowerCase(),
740
- chainId,
741
- amount: amountStr,
742
- updatedAt
743
- },
744
- queues: [
745
- {
746
- queue: {
747
- queueId: id,
748
- availableLiquidityPoolId: id,
749
- index: index2,
742
+ switch (parameters.type) {
743
+ case "buy_with_empty_callback" /* BuyWithEmptyCallback */: {
744
+ const { user, loanToken, chainId, amount, index: index2 = 0, updatedAt = /* @__PURE__ */ new Date() } = parameters;
745
+ const amountStr = amount.toString();
746
+ const id = `${user}-${chainId.toString()}-${parameters.type}-${loanToken}`.toLowerCase();
747
+ const poolId = `${user}-${chainId.toString()}-${loanToken}`.toLowerCase();
748
+ return {
749
+ userPosition: {
750
+ id,
751
+ availableLiquidityQueueId: id,
752
+ user: user.toLowerCase(),
753
+ chainId,
754
+ amount: amountStr,
750
755
  updatedAt
751
756
  },
752
- pool: {
757
+ queues: [
758
+ {
759
+ queue: {
760
+ queueId: id,
761
+ availableLiquidityPoolId: poolId,
762
+ index: index2,
763
+ callbackAmount: "0",
764
+ updatedAt
765
+ },
766
+ pool: {
767
+ id: poolId,
768
+ amount: amountStr,
769
+ updatedAt
770
+ }
771
+ }
772
+ ]
773
+ };
774
+ }
775
+ case "sell_withdraw_from_wallet" /* SellWithdrawFromWallet */: {
776
+ const {
777
+ user,
778
+ termId,
779
+ chainId,
780
+ amount,
781
+ collaterals,
782
+ index: index2 = 0,
783
+ updatedAt = /* @__PURE__ */ new Date()
784
+ } = parameters;
785
+ const amountStr = amount.toString();
786
+ const id = `${user}-${chainId.toString()}-${parameters.type}-${termId}`.toLowerCase();
787
+ return {
788
+ userPosition: {
753
789
  id,
790
+ availableLiquidityQueueId: id,
791
+ user: user.toLowerCase(),
792
+ chainId,
754
793
  amount: amountStr,
755
794
  updatedAt
756
- }
757
- }
758
- ]
759
- };
795
+ },
796
+ queues: collaterals.map((collateral) => {
797
+ const poolId = `${user}-${chainId.toString()}-${collateral.collateralAddress}`.toLowerCase();
798
+ return {
799
+ queue: {
800
+ queueId: id,
801
+ availableLiquidityPoolId: poolId,
802
+ index: index2,
803
+ callbackAmount: collateral.callbackAmount.toString(),
804
+ updatedAt
805
+ },
806
+ pool: {
807
+ id: poolId,
808
+ amount: collateral.balance.toString(),
809
+ updatedAt
810
+ }
811
+ };
812
+ })
813
+ };
814
+ }
815
+ default: {
816
+ throw new Error(`CallbackType not implemented`);
817
+ }
818
+ }
760
819
  }
761
820
  function getCallbackIdForOffer(offer) {
762
821
  if (offer.buy && offer.callback.data === "0x") {
763
- const type = "buy_with_empty_callback" /* BuyWithEmptyCallback */;
764
- const user = offer.offering;
765
- const loanToken = offer.loanToken;
766
- return `${user}-${offer.chainId.toString()}-${type}-${loanToken}`.toLowerCase();
822
+ return `${offer.offering}-${offer.chainId.toString()}-${"buy_with_empty_callback" /* BuyWithEmptyCallback */}-${offer.loanToken}`.toLowerCase();
823
+ }
824
+ if (!offer.buy && offer.callback.data !== "0x" && WhitelistedCallbackAddresses["sell_withdraw_from_wallet" /* SellWithdrawFromWallet */].includes(
825
+ offer.callback.address.toLowerCase()
826
+ )) {
827
+ return `${offer.offering}-${offer.chainId.toString()}-${"sell_withdraw_from_wallet" /* SellWithdrawFromWallet */}-${Offer.termId(offer)}`.toLowerCase();
767
828
  }
768
829
  return null;
769
830
  }
831
+ function decodeSellWithdrawFromWalletData(data) {
832
+ if (!data || data === "0x") throw new Error("Empty callback data");
833
+ try {
834
+ const [collaterals, amounts] = decodeAbiParameters(
835
+ [{ type: "address[]" }, { type: "uint256[]" }],
836
+ data
837
+ );
838
+ if (collaterals.length !== amounts.length) {
839
+ throw new Error("Mismatched array lengths");
840
+ }
841
+ return collaterals.map((c, i) => ({ collateral: c, amount: amounts[i] }));
842
+ } catch (_) {
843
+ throw new Error("Invalid SellWithdrawFromWallet callback data");
844
+ }
845
+ }
846
+ function decode2(parameters) {
847
+ const { type, data } = parameters;
848
+ if (type === "sell_withdraw_from_wallet" /* SellWithdrawFromWallet */) {
849
+ return decodeSellWithdrawFromWalletData(data);
850
+ }
851
+ throw new Error(`CallbackType not implemented: ${type}`);
852
+ }
853
+ function encode2(parameters) {
854
+ const { type, data } = parameters;
855
+ if (type === "sell_withdraw_from_wallet" /* SellWithdrawFromWallet */) {
856
+ return encodeAbiParameters(
857
+ [{ type: "address[]" }, { type: "uint256[]" }],
858
+ [data.collaterals, data.amounts]
859
+ );
860
+ }
861
+ throw new Error(`CallbackType not implemented: ${type}`);
862
+ }
770
863
 
771
864
  // src/core/Collector/index.ts
772
865
  var Collector_exports = {};
@@ -851,20 +944,20 @@ async function fetch2(parameters) {
851
944
  const map = await fetchBalancesAndAllowances({
852
945
  client,
853
946
  spender,
854
- pairs: pairs.map(({ user, contract }) => ({ user, token: contract })),
947
+ pairs: pairs.map(({ user, loanToken }) => ({ user, token: loanToken })),
855
948
  options
856
949
  });
857
950
  const out = [];
858
- for (const [user, perContract] of map) {
859
- for (const [contract, { balance, allowance }] of perContract) {
951
+ for (const [user, perLoanToken] of map) {
952
+ for (const [loanToken, { balance, allowance }] of perLoanToken) {
860
953
  const amount = balance < allowance ? balance : allowance;
861
954
  out.push(
862
955
  buildLiquidity({
863
956
  type,
864
957
  user,
865
- contract,
958
+ loanToken,
866
959
  chainId,
867
- amount: amount.toString(),
960
+ amount,
868
961
  index: 0
869
962
  })
870
963
  );
@@ -922,16 +1015,16 @@ __export(Logger_exports, {
922
1015
  silentLogger: () => silentLogger
923
1016
  });
924
1017
  var LogLevelValues = [
925
- "silent",
926
1018
  "trace",
927
1019
  "debug",
928
1020
  "info",
929
1021
  "warn",
930
1022
  "error",
931
- "fatal"
1023
+ "fatal",
1024
+ "silent"
932
1025
  ];
933
1026
  function defaultLogger(minLevel) {
934
- const threshold = minLevel ?? "info";
1027
+ const threshold = minLevel ?? process.env.ROUTER_LOG_LEVEL ?? "info";
935
1028
  const levelIndexByName = LogLevelValues.reduce(
936
1029
  (acc, lvl, idx) => {
937
1030
  acc[lvl] = idx;
@@ -1027,7 +1120,7 @@ function createBuyWithEmptyCallbackLiquidityCollector(params) {
1027
1120
  chainId: chain.id,
1028
1121
  spender: chain.morpho,
1029
1122
  type: "buy_with_empty_callback" /* BuyWithEmptyCallback */,
1030
- pairs: pairs.map(({ user, token }) => ({ user, contract: token })),
1123
+ pairs: pairs.map(({ user, token }) => ({ user, loanToken: token })),
1031
1124
  options: {
1032
1125
  blockNumber: latestBlockNumber,
1033
1126
  batchSize: maxBatchSize
@@ -1078,14 +1171,15 @@ function createBuyWithEmptyCallbackLiquidityCollector(params) {
1078
1171
  };
1079
1172
  }
1080
1173
  function createChainReorgsCollector(parameters) {
1081
- const collectorName = "chain_reorgs";
1174
+ const collector = "chain_reorgs";
1082
1175
  const {
1083
1176
  client,
1084
1177
  subscribers,
1085
1178
  collectorStore,
1086
1179
  chain,
1087
- options: { maxBatchSize = 25, interval } = {}
1180
+ options: { maxBatchSize = 25, interval = 3e4, maxBlockNumber } = {}
1088
1181
  } = parameters;
1182
+ const maxBlockNumberBI = maxBlockNumber !== void 0 ? BigInt(maxBlockNumber) : void 0;
1089
1183
  let finalizedBlock = null;
1090
1184
  let unfinalizedBlocks = [];
1091
1185
  const commonAncestor = (block) => {
@@ -1094,67 +1188,79 @@ function createChainReorgsCollector(parameters) {
1094
1188
  if (finalizedBlock == null) throw new Error("Failed to get common ancestor");
1095
1189
  return finalizedBlock;
1096
1190
  };
1097
- const reconcile = async (block) => {
1098
- if (block.hash === null || block.number === null || block.parentHash === null)
1099
- throw new Error("Failed to get block");
1100
- const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
1101
- if (latestBlock === void 0) {
1102
- unfinalizedBlocks.push({
1191
+ const collect = Utils.lazy((emit, { stop }) => {
1192
+ const logger = getLogger();
1193
+ const reconcile = async (block) => {
1194
+ if (block.hash === null || block.number === null || block.parentHash === null)
1195
+ throw new Error("Failed to get block");
1196
+ const latestBlock = unfinalizedBlocks[unfinalizedBlocks.length - 1];
1197
+ if (latestBlock === void 0) {
1198
+ const newBlock2 = {
1199
+ hash: block.hash,
1200
+ number: block.number,
1201
+ parentHash: block.parentHash
1202
+ };
1203
+ unfinalizedBlocks.push(newBlock2);
1204
+ return newBlock2;
1205
+ }
1206
+ if (latestBlock.hash === block.hash) return latestBlock;
1207
+ if (latestBlock.number >= block.number) {
1208
+ const ancestor = commonAncestor(block);
1209
+ logger.info({
1210
+ collector,
1211
+ chainId: chain.id,
1212
+ msg: `reorg detected, latestBlock.number: ${latestBlock.number} >= block.number: ${block.number} on chain ${chain.id}. Ancestor: ${ancestor.number}`
1213
+ });
1214
+ subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
1215
+ unfinalizedBlocks = [];
1216
+ return ancestor;
1217
+ }
1218
+ if (latestBlock.number + 1n < block.number) {
1219
+ logger.debug({
1220
+ collector,
1221
+ chainId: chain.id,
1222
+ msg: `missing blocks between block ${latestBlock.number} and block ${block.number} on chain ${chain.id}`
1223
+ });
1224
+ const missingBlockNumbers = (() => {
1225
+ const missingBlockNumbers2 = [];
1226
+ let start2 = latestBlock.number + 1n;
1227
+ const threshold = latestBlock.number + BigInt(maxBatchSize) > block.number ? block.number : latestBlock.number + BigInt(maxBatchSize);
1228
+ while (start2 < threshold) {
1229
+ missingBlockNumbers2.push(start2);
1230
+ start2 = start2 + 1n;
1231
+ }
1232
+ return missingBlockNumbers2;
1233
+ })();
1234
+ const missingBlocks = await Promise.all(
1235
+ missingBlockNumbers.map(
1236
+ (blockNumber) => client.getBlock({ blockNumber, includeTransactions: false })
1237
+ )
1238
+ );
1239
+ for (const missingBlock of missingBlocks) {
1240
+ const returnedBlock = await reconcile(missingBlock);
1241
+ if (returnedBlock.number !== missingBlock.number) return returnedBlock;
1242
+ }
1243
+ return reconcile(block);
1244
+ }
1245
+ if (block.parentHash !== latestBlock.hash) {
1246
+ const ancestor = commonAncestor(block);
1247
+ logger.info({
1248
+ collector,
1249
+ chainId: chain.id,
1250
+ msg: `reorg detected, block.parentHash: ${block.parentHash} !== latestBlock.hash: ${latestBlock.hash} on chain ${chain.id}. Ancestor: ${ancestor.number}`
1251
+ });
1252
+ subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
1253
+ unfinalizedBlocks = [];
1254
+ return ancestor;
1255
+ }
1256
+ const newBlock = {
1103
1257
  hash: block.hash,
1104
1258
  number: block.number,
1105
1259
  parentHash: block.parentHash
1106
- });
1107
- return;
1108
- }
1109
- if (latestBlock.hash === block.hash) return;
1110
- if (latestBlock.number >= block.number) {
1111
- const ancestor = commonAncestor(block);
1112
- console.log(
1113
- `reorg detected, latestBlock.number: ${latestBlock.number} > block.number: ${block.number} on chain ${chain.id}. Ancestor: ${ancestor.number}`
1114
- );
1115
- subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
1116
- unfinalizedBlocks = [];
1117
- return;
1118
- }
1119
- if (latestBlock.number + 1n < block.number) {
1120
- console.log(
1121
- `missing blocks between block ${latestBlock.number} and block ${block.number} on chain ${chain.id}`
1122
- );
1123
- const missingBlockNumbers = (() => {
1124
- const missingBlockNumbers2 = [];
1125
- let start2 = latestBlock.number + 1n;
1126
- const threshold = latestBlock.number + BigInt(maxBatchSize) > block.number ? block.number : latestBlock.number + BigInt(maxBatchSize);
1127
- while (start2 < threshold) {
1128
- missingBlockNumbers2.push(start2);
1129
- start2 = start2 + 1n;
1130
- }
1131
- return missingBlockNumbers2;
1132
- })();
1133
- const missingBlocks = await Promise.all(
1134
- missingBlockNumbers.map(
1135
- (blockNumber) => client.getBlock({ blockNumber, includeTransactions: false })
1136
- )
1137
- );
1138
- for (const missingBlock of missingBlocks) await reconcile(missingBlock);
1139
- await reconcile(block);
1140
- return;
1141
- }
1142
- if (block.parentHash !== latestBlock.hash) {
1143
- const ancestor = commonAncestor(block);
1144
- console.log(
1145
- `reorg detected, block.parentHash: ${block.parentHash} !== latestBlock.hash: ${latestBlock.hash} on chain ${chain.id}. Ancestor: ${ancestor.number}`
1146
- );
1147
- subscribers.forEach((subscriber) => subscriber.onReorg(Number(ancestor.number)));
1148
- unfinalizedBlocks = [];
1149
- return;
1150
- }
1151
- unfinalizedBlocks.push({
1152
- hash: block.hash,
1153
- number: block.number,
1154
- parentHash: block.parentHash
1155
- });
1156
- };
1157
- const collect = Utils.lazy((emit) => {
1260
+ };
1261
+ unfinalizedBlocks.push(newBlock);
1262
+ return newBlock;
1263
+ };
1158
1264
  let tick = 0;
1159
1265
  const fetchFinalizedBlock = async () => {
1160
1266
  if (tick % 20 === 0 || finalizedBlock === null) {
@@ -1162,32 +1268,58 @@ function createChainReorgsCollector(parameters) {
1162
1268
  blockTag: "finalized",
1163
1269
  includeTransactions: false
1164
1270
  });
1165
- if (finalizedBlock === null) throw new Error("Failed to get finalized block");
1271
+ if (finalizedBlock === null) {
1272
+ const msg = "Failed to get finalized block";
1273
+ logger.fatal({ collector, chainId: chain.id, msg });
1274
+ throw new Error(msg);
1275
+ }
1276
+ unfinalizedBlocks = unfinalizedBlocks.filter((b) => b.number >= finalizedBlock.number);
1166
1277
  }
1167
1278
  tick++;
1168
1279
  };
1169
- return Utils.poll(
1280
+ let isMaxBlockNumberReached = false;
1281
+ const unpoll = Utils.poll(
1170
1282
  async () => {
1171
- await fetchFinalizedBlock();
1172
- const latestBlock = await client.getBlock({
1283
+ if (isMaxBlockNumberReached) {
1284
+ stop();
1285
+ return;
1286
+ }
1287
+ const head = await client.getBlock({
1173
1288
  blockTag: "latest",
1174
1289
  includeTransactions: false
1175
1290
  });
1176
- await reconcile(latestBlock);
1177
- const lastBlockNumber = Number(latestBlock.number);
1291
+ if (maxBlockNumberBI !== void 0 && head.number >= maxBlockNumberBI) {
1292
+ logger.info({
1293
+ collector,
1294
+ chainId: chain.id,
1295
+ msg: `head is greater than max block number, head.number: ${head.number} > maxBlockNumber: ${maxBlockNumber} on chain ${chain.id}.`
1296
+ });
1297
+ isMaxBlockNumberReached = true;
1298
+ subscribers.forEach((subscriber) => subscriber.onReorg(maxBlockNumber));
1299
+ await collectorStore.saveBlockNumber({
1300
+ collectorName: collector,
1301
+ chainId: chain.id,
1302
+ blockNumber: maxBlockNumber
1303
+ });
1304
+ emit(maxBlockNumber);
1305
+ return;
1306
+ }
1307
+ await fetchFinalizedBlock();
1308
+ const blockNumber = Number((await reconcile(head)).number);
1178
1309
  await collectorStore.saveBlockNumber({
1179
- collectorName: "chain_reorgs",
1310
+ collectorName: collector,
1180
1311
  chainId: chain.id,
1181
- blockNumber: lastBlockNumber
1312
+ blockNumber
1182
1313
  });
1183
- emit(lastBlockNumber);
1314
+ emit(blockNumber);
1184
1315
  },
1185
1316
  { interval: interval ?? 3e4 }
1186
1317
  );
1318
+ return unpoll;
1187
1319
  });
1188
1320
  return {
1189
- name: collectorName,
1190
- lastSyncedBlock: async () => await collectorStore.getBlockNumber({ collectorName, chainId: chain.id }),
1321
+ name: collector,
1322
+ lastSyncedBlock: async () => await collectorStore.getBlockNumber({ collectorName: collector, chainId: chain.id }),
1191
1323
  collect,
1192
1324
  onReorg: (_) => {
1193
1325
  }
@@ -1386,18 +1518,89 @@ function morpho() {
1386
1518
  return { message: "Expiry mismatch" };
1387
1519
  }
1388
1520
  });
1389
- const callback = single("empty_callback", (offer, _) => {
1390
- if (!offer.buy || offer.callback.data !== "0x") {
1391
- return { message: "Callback not supported yet." };
1521
+ const sellEmptyCallback = single(
1522
+ "sell_offers_empty_callback",
1523
+ (offer, _) => {
1524
+ if (!offer.buy && offer.callback.data === "0x") {
1525
+ return { message: "Sell offers require a non-empty callback." };
1526
+ }
1392
1527
  }
1393
- });
1528
+ );
1529
+ const buyNonEmptyCallback = single(
1530
+ "buy_offers_non_empty_callback",
1531
+ (offer, _) => {
1532
+ if (offer.buy && offer.callback.data !== "0x") {
1533
+ return { message: "Buy offers must use an empty callback." };
1534
+ }
1535
+ }
1536
+ );
1537
+ const sellNonWhitelistedCallback = single(
1538
+ "sell_offers_non_whitelisted_callback",
1539
+ (offer, _) => {
1540
+ if (!offer.buy && offer.callback.data !== "0x") {
1541
+ const allowed = new Set(
1542
+ WhitelistedCallbackAddresses["sell_withdraw_from_wallet" /* SellWithdrawFromWallet */].map(
1543
+ (a) => a.toLowerCase()
1544
+ )
1545
+ );
1546
+ const callbackAddress = offer.callback.address?.toLowerCase();
1547
+ if (!callbackAddress || !allowed.has(callbackAddress)) {
1548
+ return { message: "Sell offer callback address is not whitelisted." };
1549
+ }
1550
+ }
1551
+ }
1552
+ );
1553
+ const sellCallbackDataInvalid = single(
1554
+ "sell_offers_callback_data_invalid",
1555
+ (offer, _) => {
1556
+ if (!offer.buy && offer.callback.data !== "0x") {
1557
+ try {
1558
+ const decoded = decode2({
1559
+ type: "sell_withdraw_from_wallet" /* SellWithdrawFromWallet */,
1560
+ data: offer.callback.data
1561
+ });
1562
+ if (decoded.length === 0) {
1563
+ return { message: "Sell offer callback data must include at least one collateral." };
1564
+ }
1565
+ } catch (_2) {
1566
+ return { message: "Sell offer callback data cannot be decoded." };
1567
+ }
1568
+ }
1569
+ }
1570
+ );
1571
+ const sellCallbackCollateralInvalid = single(
1572
+ "sell_offers_callback_collateral_invalid",
1573
+ (offer, _) => {
1574
+ if (!offer.buy && offer.callback.data !== "0x") {
1575
+ try {
1576
+ const decoded = decode2({
1577
+ type: "sell_withdraw_from_wallet" /* SellWithdrawFromWallet */,
1578
+ data: offer.callback.data
1579
+ });
1580
+ const offerCollaterals2 = new Set(
1581
+ offer.collaterals.map((c) => c.asset.toLowerCase())
1582
+ );
1583
+ for (const { collateral } of decoded) {
1584
+ if (!offerCollaterals2.has(collateral.toLowerCase())) {
1585
+ return { message: "Sell callback collateral is not part of offer collaterals." };
1586
+ }
1587
+ }
1588
+ } catch (_2) {
1589
+ }
1590
+ }
1591
+ }
1592
+ );
1394
1593
  return [
1395
1594
  chainId,
1396
1595
  loanToken,
1397
1596
  expiry,
1398
- // note: callback rule should be the last one, since it does not mean that the offer is forever invalid
1597
+ // note: callback rules should be the last ones, since they do not mean that the offer is forever invalid
1399
1598
  // integrators should be able to choose if they want to keep the offer or not
1400
- callback
1599
+ sellEmptyCallback,
1600
+ buyNonEmptyCallback,
1601
+ sellNonWhitelistedCallback,
1602
+ sellCallbackDataInvalid,
1603
+ sellCallbackCollateralInvalid
1401
1604
  ];
1402
1605
  }
1403
1606
 
@@ -1446,9 +1649,11 @@ function createMempoolCollector(parameters) {
1446
1649
  });
1447
1650
  const invalidOffersToSave = [];
1448
1651
  const issueToStatus = {
1449
- empty_callback: "callback_not_supported",
1450
1652
  sell_offers_empty_callback: "callback_not_supported",
1451
- buy_offers_empty_callback: "callback_error"
1653
+ buy_offers_non_empty_callback: "callback_not_supported",
1654
+ sell_offers_non_whitelisted_callback: "callback_not_supported",
1655
+ sell_offers_callback_data_invalid: "callback_error",
1656
+ sell_offers_callback_collateral_invalid: "callback_error"
1452
1657
  };
1453
1658
  for (const issue of issues) {
1454
1659
  const status = issueToStatus[issue.ruleName];
@@ -1559,11 +1764,19 @@ var offers = s.table(
1559
1764
  index("offers_expiry_idx").on(table.expiry),
1560
1765
  index("offers_rate_idx").on(table.rate),
1561
1766
  index("offers_assets_idx").on(table.assets),
1767
+ index("offers_created_at_idx").on(table.createdAt),
1562
1768
  // Compound indices for cursor pagination with hash
1563
1769
  index("offers_rate_hash_idx").on(table.rate, table.hash),
1564
1770
  index("offers_maturity_hash_idx").on(table.maturity, table.hash),
1565
1771
  index("offers_expiry_hash_idx").on(table.expiry, table.hash),
1566
- index("offers_assets_hash_idx").on(table.assets, table.hash)
1772
+ index("offers_assets_hash_idx").on(table.assets, table.hash),
1773
+ // Compound index for multi-level sorting optimization (rate, createdAt, assets, hash)
1774
+ index("offers_rate_created_at_assets_hash_idx").on(
1775
+ table.rate,
1776
+ asc(table.createdAt),
1777
+ desc(table.assets),
1778
+ asc(table.hash)
1779
+ )
1567
1780
  ]
1568
1781
  );
1569
1782
  var offerCollaterals = s.table(
@@ -1637,6 +1850,7 @@ var availableLiquidityQueues = s.table(
1637
1850
  queueId: varchar("queue_id", { length: 255 }).notNull(),
1638
1851
  availableLiquidityPoolId: varchar("available_liquidity_pool_id", { length: 255 }).notNull().references(() => availableLiquidityPools.id, { onDelete: "cascade" }),
1639
1852
  index: integer("index").notNull(),
1853
+ callbackAmount: numeric("callback_amount", { precision: 78, scale: 0 }).default("0").notNull(),
1640
1854
  updatedAt: timestamp("updated_at").defaultNow().notNull()
1641
1855
  },
1642
1856
  (table) => [
@@ -1781,6 +1995,7 @@ var create2 = (config) => {
1781
1995
  queueId: qp.queue.queueId,
1782
1996
  availableLiquidityPoolId: qp.pool.id,
1783
1997
  index: qp.queue.index,
1998
+ callbackAmount: qp.queue.callbackAmount,
1784
1999
  updatedAt: /* @__PURE__ */ new Date()
1785
2000
  }).onConflictDoUpdate({
1786
2001
  target: [
@@ -1789,6 +2004,7 @@ var create2 = (config) => {
1789
2004
  ],
1790
2005
  set: {
1791
2006
  index: qp.queue.index,
2007
+ callbackAmount: qp.queue.callbackAmount,
1792
2008
  updatedAt: /* @__PURE__ */ new Date()
1793
2009
  }
1794
2010
  });
@@ -1867,6 +2083,7 @@ function memory2() {
1867
2083
  queueId: qid,
1868
2084
  availableLiquidityPoolId: qp.pool.id,
1869
2085
  index: qp.queue.index,
2086
+ callbackAmount: qp.queue.callbackAmount,
1870
2087
  updatedAt: /* @__PURE__ */ new Date()
1871
2088
  });
1872
2089
  if (!queueIndexByQueueId.has(qid)) queueIndexByQueueId.set(qid, /* @__PURE__ */ new Set());
@@ -2380,7 +2597,8 @@ function create3(config) {
2380
2597
  signature: offers.signature,
2381
2598
  callbackId: offers.callbackId,
2382
2599
  status: latestStatus.status,
2383
- metadata: latestStatus.metadata
2600
+ metadata: latestStatus.metadata,
2601
+ createdAt: offers.createdAt
2384
2602
  }
2385
2603
  ).from(offers).leftJoinLateral(latestStatus, sql`true`).leftJoinLateral(sumConsumed, sql`true`).where(
2386
2604
  and(
@@ -2435,6 +2653,7 @@ function create3(config) {
2435
2653
  callbackId: bestOffers.callbackId,
2436
2654
  status: bestOffers.status,
2437
2655
  metadata: bestOffers.metadata,
2656
+ createdAt: bestOffers.createdAt,
2438
2657
  // liquidity caps
2439
2658
  userAmount: sql`COALESCE(${queueLiquidity.userAmount}, 0)`.as("user_amount"),
2440
2659
  queueLiquidity: sql`COALESCE(${queueLiquidity.queueLiquidity}, 0)`.as(
@@ -2444,7 +2663,7 @@ function create3(config) {
2444
2663
  cumulativeRemaining: sql`
2445
2664
  SUM(${bestOffers.remaining}) OVER (
2446
2665
  PARTITION BY ${bestOffers.callbackId}
2447
- ORDER BY ${sortExpr}, ${asc(bestOffers.hash)}
2666
+ ORDER BY ${sortExpr}, ${asc(bestOffers.createdAt)}, ${bestOffers.assets} DESC, ${asc(bestOffers.hash)}
2448
2667
  ROWS UNBOUNDED PRECEDING
2449
2668
  )
2450
2669
  `.as("cumulative_remaining"),
@@ -2454,7 +2673,7 @@ function create3(config) {
2454
2673
  WHEN ${bestOffers.remaining} <= 0 THEN false
2455
2674
  ELSE ( SUM(${bestOffers.remaining}) OVER (
2456
2675
  PARTITION BY ${bestOffers.callbackId}
2457
- ORDER BY ${sortExpr}, ${asc(bestOffers.hash)}
2676
+ ORDER BY ${sortExpr}, ${asc(bestOffers.createdAt)}, ${bestOffers.assets} DESC, ${asc(bestOffers.hash)}
2458
2677
  ROWS UNBOUNDED PRECEDING
2459
2678
  )
2460
2679
  <= LEAST(
@@ -2488,6 +2707,7 @@ function create3(config) {
2488
2707
  callbackId: offersWithEligibility.callbackId,
2489
2708
  status: offersWithEligibility.status,
2490
2709
  metadata: offersWithEligibility.metadata,
2710
+ createdAt: offersWithEligibility.createdAt,
2491
2711
  userAmount: offersWithEligibility.userAmount,
2492
2712
  queueLiquidity: offersWithEligibility.queueLiquidity,
2493
2713
  cumulativeRemaining: offersWithEligibility.cumulativeRemaining,
@@ -2497,6 +2717,8 @@ function create3(config) {
2497
2717
  ROW_NUMBER() OVER (
2498
2718
  ORDER BY
2499
2719
  CASE WHEN ${offersWithEligibility.buy} THEN ${offersWithEligibility.rate} ELSE -${offersWithEligibility.rate} END,
2720
+ ${offersWithEligibility.createdAt},
2721
+ ${offersWithEligibility.assets} DESC,
2500
2722
  ${asc(offersWithEligibility.hash)}
2501
2723
  )
2502
2724
  `.as("row_number")
@@ -2538,6 +2760,10 @@ function create3(config) {
2538
2760
  )
2539
2761
  ).orderBy(
2540
2762
  rateSortDirection === "asc" ? asc(validOffers.rate) : desc(validOffers.rate),
2763
+ asc(validOffers.createdAt),
2764
+ // earlier createdAt first
2765
+ desc(validOffers.assets),
2766
+ // higher assets first
2541
2767
  asc(validOffers.hash)
2542
2768
  );
2543
2769
  const buildOffersMap = (rows, skipHash) => {