@mento-protocol/mento-sdk 3.1.0-beta.4 → 3.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/core/types/liquidity.d.ts +20 -0
  2. package/dist/esm/index.js +25 -9
  3. package/dist/esm/services/borrow/internal/borrowReadService.js +63 -34
  4. package/dist/esm/services/borrow/internal/borrowRegistryReader.js +45 -28
  5. package/dist/esm/services/index.js +1 -0
  6. package/dist/esm/services/liquidity/LiquidityService.js +8 -2
  7. package/dist/esm/services/liquidity/liquidityHelpers.js +27 -1
  8. package/dist/esm/services/liquidity/zapHelpers.js +20 -24
  9. package/dist/esm/services/liquidity/zapIn.js +68 -75
  10. package/dist/esm/services/liquidity/zapOut.js +89 -77
  11. package/dist/esm/services/pools/PoolService.js +117 -22
  12. package/dist/esm/services/pools/poolDetails.js +79 -38
  13. package/dist/esm/services/quotes/QuoteService.js +22 -20
  14. package/dist/esm/services/routes/RouteService.js +82 -24
  15. package/dist/esm/services/swap/SwapService.js +81 -37
  16. package/dist/esm/services/tokens/tokenService.js +142 -29
  17. package/dist/esm/services/trading/TradingService.js +51 -12
  18. package/dist/esm/utils/multicall.js +29 -2
  19. package/dist/index.d.ts +11 -1
  20. package/dist/index.js +25 -9
  21. package/dist/services/borrow/internal/borrowReadService.d.ts +1 -0
  22. package/dist/services/borrow/internal/borrowReadService.js +63 -34
  23. package/dist/services/borrow/internal/borrowRegistryReader.js +45 -28
  24. package/dist/services/index.d.ts +1 -0
  25. package/dist/services/index.js +1 -0
  26. package/dist/services/liquidity/LiquidityService.d.ts +3 -1
  27. package/dist/services/liquidity/LiquidityService.js +6 -0
  28. package/dist/services/liquidity/liquidityHelpers.d.ts +6 -0
  29. package/dist/services/liquidity/liquidityHelpers.js +27 -0
  30. package/dist/services/liquidity/zapHelpers.js +20 -24
  31. package/dist/services/liquidity/zapIn.d.ts +2 -1
  32. package/dist/services/liquidity/zapIn.js +67 -73
  33. package/dist/services/liquidity/zapOut.d.ts +2 -10
  34. package/dist/services/liquidity/zapOut.js +90 -77
  35. package/dist/services/pools/PoolService.d.ts +5 -0
  36. package/dist/services/pools/PoolService.js +116 -21
  37. package/dist/services/pools/poolDetails.d.ts +2 -0
  38. package/dist/services/pools/poolDetails.js +81 -38
  39. package/dist/services/quotes/QuoteService.d.ts +1 -0
  40. package/dist/services/quotes/QuoteService.js +24 -21
  41. package/dist/services/routes/RouteService.d.ts +8 -0
  42. package/dist/services/routes/RouteService.js +82 -24
  43. package/dist/services/swap/SwapService.d.ts +19 -0
  44. package/dist/services/swap/SwapService.js +81 -37
  45. package/dist/services/tokens/tokenService.d.ts +7 -0
  46. package/dist/services/tokens/tokenService.js +142 -29
  47. package/dist/services/trading/TradingService.d.ts +3 -0
  48. package/dist/services/trading/TradingService.js +51 -12
  49. package/dist/utils/multicall.d.ts +5 -1
  50. package/dist/utils/multicall.js +29 -2
  51. package/package.json +1 -1
@@ -5,79 +5,84 @@ import { validateAddress } from '../../utils/validation';
5
5
  import { encodeRoutePath } from '../../utils/pathEncoder';
6
6
  import { buildApprovalParams, getAllowance, calculateMinAmount, getPoolInfo } from './liquidityHelpers';
7
7
  import { encodeZapOutCall, findZapOutRoutes } from './zapHelpers';
8
- // ========== ZAP OUT OPERATIONS ==========
9
8
  const INSUFFICIENT_LIQUIDITY_SELECTOR = '0xbb55fd27';
10
9
  const MAX_ROUTE_CANDIDATES_PER_LEG = 8;
11
10
  const MAX_ROUTE_COMBINATIONS = 48;
12
- /**
13
- * Builds a complete zap out transaction including approval if needed
14
- */
11
+ const ROUTE_SIMULATION_BATCH_SIZE = 6;
15
12
  export async function buildZapOutTransactionInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, owner, options) {
16
- validateAddress(owner, 'owner');
17
- // Build zap out params
18
- let zapOut = await buildZapOutParamsInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, options);
19
- // Check LP token allowance
20
- const poolAddr = poolAddress;
21
- const ownerAddr = owner;
22
- const currentAllowance = await getAllowance(publicClient, poolAddr, ownerAddr, chainId);
23
- // Build approval if needed
24
- const approval = currentAllowance < liquidity
25
- ? { token: poolAddress, amount: liquidity, params: buildApprovalParams(chainId, poolAddr, liquidity) }
26
- : null;
27
- // We can only preflight/simulate a real zap call when approval is already sufficient.
28
- // Before approval, transferFrom in zapOut() would fail and make simulation meaningless.
29
- if (currentAllowance >= liquidity) {
13
+ const prepared = await prepareZapOutInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, owner, options);
14
+ return {
15
+ approval: prepared.approval ?? null,
16
+ zapOut: prepared.details,
17
+ };
18
+ }
19
+ export async function buildZapOutParamsInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, options) {
20
+ const prepared = await prepareZapOutInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, undefined, options);
21
+ return prepared.details;
22
+ }
23
+ export async function quoteZapOutInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, options) {
24
+ const prepared = await prepareZapOutInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, tokenOut, undefined, options);
25
+ return prepared.quote;
26
+ }
27
+ export async function prepareZapOutInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, owner, options) {
28
+ if (owner) {
29
+ validateAddress(owner, 'owner');
30
+ }
31
+ const [context, currentAllowance] = await Promise.all([
32
+ prepareZapOutContextInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, options),
33
+ owner ? getAllowance(publicClient, poolAddress, owner, chainId) : Promise.resolve(null),
34
+ ]);
35
+ let details = context.details;
36
+ const approval = owner && currentAllowance !== null && currentAllowance < liquidity
37
+ ? { token: poolAddress, amount: liquidity, params: buildApprovalParams(chainId, poolAddress, liquidity) }
38
+ : owner
39
+ ? null
40
+ : undefined;
41
+ if (owner && currentAllowance !== null && currentAllowance >= liquidity) {
42
+ const ownerAddr = owner;
30
43
  const routerAddress = getContractAddress(chainId, 'Router');
31
44
  try {
32
- await simulateZapOut(publicClient, ownerAddr, routerAddress, zapOut.params.data);
45
+ await simulateZapOut(publicClient, ownerAddr, routerAddress, details.params.data);
33
46
  }
34
47
  catch (error) {
35
- // Only attempt route fallback for the known liquidity failure.
36
48
  if (!isInsufficientLiquidityError(error)) {
37
49
  throw error;
38
50
  }
39
- zapOut = await findViableZapOutDetails(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, ownerAddr, options);
51
+ details = await findViableZapOutDetails(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, ownerAddr, options);
40
52
  }
41
53
  }
42
- return { approval, zapOut };
54
+ return {
55
+ routesA: details.routesA,
56
+ routesB: details.routesB,
57
+ quote: {
58
+ amountOutFromA: details.zapParams.amountOutMinA,
59
+ amountOutFromB: details.zapParams.amountOutMinB,
60
+ amountAMin: details.zapParams.amountAMin,
61
+ amountBMin: details.zapParams.amountBMin,
62
+ estimatedMinTokenOut: details.estimatedMinTokenOut,
63
+ },
64
+ approval,
65
+ details,
66
+ };
43
67
  }
44
- /**
45
- * Builds zap out transaction parameters without checking approval
46
- */
47
- export async function buildZapOutParamsInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, options) {
68
+ async function prepareZapOutContextInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, recipient, options) {
48
69
  validateAddress(poolAddress, 'poolAddress');
49
70
  validateAddress(tokenOut, 'tokenOut');
50
71
  validateAddress(recipient, 'recipient');
51
- // Get pool info
52
- const { token0, token1, factoryAddr } = await getPoolInfo(poolService, poolAddress);
53
- // Find routes for swapping (from pool tokens to tokenOut)
54
- const { routesA, routesB } = await findZapOutRoutes(routeService, token0, token1, tokenOut);
55
- return buildZapOutDetailsForRoutes(publicClient, chainId, poolAddress, tokenOut, liquidity, token0, token1, factoryAddr, routesA, routesB, options);
56
- }
57
- /**
58
- * Quotes a zap out operation (read-only)
59
- */
60
- export async function quoteZapOutInternal(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, options) {
61
- validateAddress(poolAddress, 'poolAddress');
62
- validateAddress(tokenOut, 'tokenOut');
63
72
  const { token0, token1, factoryAddr } = await getPoolInfo(poolService, poolAddress);
64
- // Find routes for swapping (from pool tokens to tokenOut)
65
73
  const { routesA, routesB } = await findZapOutRoutes(routeService, token0, token1, tokenOut);
66
- const routerAddress = getContractAddress(chainId, 'Router');
67
- const [amountOutMinA, amountOutMinB, amountAMin, amountBMin] = (await publicClient.readContract({
68
- address: routerAddress,
69
- abi: ROUTER_ABI,
70
- functionName: 'generateZapOutParams',
71
- args: [token0, token1, factoryAddr, liquidity, routesA, routesB],
72
- }));
73
- const finalAmountOutFromA = calculateMinAmount(amountOutMinA, options.slippageTolerance);
74
- const finalAmountOutFromB = calculateMinAmount(amountOutMinB, options.slippageTolerance);
74
+ const details = await buildZapOutDetailsForRoutes(publicClient, chainId, poolAddress, tokenOut, liquidity, token0, token1, factoryAddr, routesA, routesB, options);
75
75
  return {
76
- amountOutFromA: finalAmountOutFromA,
77
- amountOutFromB: finalAmountOutFromB,
78
- amountAMin: calculateMinAmount(amountAMin, options.slippageTolerance),
79
- amountBMin: calculateMinAmount(amountBMin, options.slippageTolerance),
80
- estimatedMinTokenOut: finalAmountOutFromA + finalAmountOutFromB,
76
+ routesA,
77
+ routesB,
78
+ quote: {
79
+ amountOutFromA: details.zapParams.amountOutMinA,
80
+ amountOutFromB: details.zapParams.amountOutMinB,
81
+ amountAMin: details.zapParams.amountAMin,
82
+ amountBMin: details.zapParams.amountBMin,
83
+ estimatedMinTokenOut: details.estimatedMinTokenOut,
84
+ },
85
+ details,
81
86
  };
82
87
  }
83
88
  async function buildZapOutDetailsForRoutes(publicClient, chainId, poolAddress, tokenOut, liquidity, token0, token1, factoryAddr, routesA, routesB, options) {
@@ -120,25 +125,36 @@ async function buildZapOutDetailsForRoutes(publicClient, chainId, poolAddress, t
120
125
  async function findViableZapOutDetails(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, owner, options) {
121
126
  const { token0, token1, factoryAddr } = await getPoolInfo(poolService, poolAddress);
122
127
  const routerAddress = getContractAddress(chainId, 'Router');
123
- const routesAOptions = await getEncodedRouteCandidates(routeService, token0, tokenOut, poolAddress);
124
- const routesBOptions = await getEncodedRouteCandidates(routeService, token1, tokenOut, poolAddress);
125
- let best = null;
126
- let combinationsTried = 0;
128
+ const allRoutes = await routeService.getRoutes({ cached: false, returnAllRoutes: true });
129
+ const [routesAOptions, routesBOptions] = await Promise.all([
130
+ getEncodedRouteCandidates(routeService, token0, tokenOut, poolAddress, allRoutes),
131
+ getEncodedRouteCandidates(routeService, token1, tokenOut, poolAddress, allRoutes),
132
+ ]);
133
+ const routeCombinations = [];
127
134
  outer: for (const routesA of routesAOptions) {
128
135
  for (const routesB of routesBOptions) {
129
- if (combinationsTried >= MAX_ROUTE_COMBINATIONS) {
136
+ routeCombinations.push({ routesA, routesB });
137
+ if (routeCombinations.length >= MAX_ROUTE_COMBINATIONS) {
130
138
  break outer;
131
139
  }
132
- combinationsTried += 1;
140
+ }
141
+ }
142
+ let best = null;
143
+ for (let index = 0; index < routeCombinations.length; index += ROUTE_SIMULATION_BATCH_SIZE) {
144
+ const batch = routeCombinations.slice(index, index + ROUTE_SIMULATION_BATCH_SIZE);
145
+ const candidates = await Promise.all(batch.map(async ({ routesA, routesB }) => {
133
146
  try {
134
147
  const candidate = await buildZapOutDetailsForRoutes(publicClient, chainId, poolAddress, tokenOut, liquidity, token0, token1, factoryAddr, routesA, routesB, options);
135
148
  await simulateZapOut(publicClient, owner, routerAddress, candidate.params.data);
136
- if (!best || candidate.estimatedMinTokenOut > best.estimatedMinTokenOut) {
137
- best = candidate;
138
- }
149
+ return candidate;
139
150
  }
140
151
  catch {
141
- // Ignore non-viable route combination and continue searching.
152
+ return null;
153
+ }
154
+ }));
155
+ for (const candidate of candidates) {
156
+ if (candidate && (!best || candidate.estimatedMinTokenOut > best.estimatedMinTokenOut)) {
157
+ best = candidate;
142
158
  }
143
159
  }
144
160
  }
@@ -147,21 +163,18 @@ async function findViableZapOutDetails(publicClient, chainId, poolService, route
147
163
  }
148
164
  return best;
149
165
  }
150
- async function getEncodedRouteCandidates(routeService, tokenIn, tokenOut, sourcePoolAddress) {
166
+ async function getEncodedRouteCandidates(routeService, tokenIn, tokenOut, sourcePoolAddress, allRoutes) {
151
167
  if (tokenIn.toLowerCase() === tokenOut.toLowerCase()) {
152
168
  return [[]];
153
169
  }
154
170
  const rawCandidates = [];
155
- // Include the SDK default first for consistency.
156
171
  try {
157
172
  rawCandidates.push(await routeService.findRoute(tokenIn, tokenOut));
158
173
  }
159
174
  catch {
160
- // Continue; we'll try full route enumeration next.
175
+ // Continue; we'll try the broader route set next.
161
176
  }
162
- // Build a broader candidate set for fallback route selection.
163
- const allRoutes = await routeService.getRoutes({ cached: false, returnAllRoutes: true });
164
- const pairCandidates = allRoutes.filter((route) => {
177
+ const pairCandidates = (allRoutes ?? await routeService.getRoutes({ cached: false, returnAllRoutes: true })).filter((route) => {
165
178
  const a0 = route.tokens[0].address.toLowerCase();
166
179
  const a1 = route.tokens[1].address.toLowerCase();
167
180
  const t0 = tokenIn.toLowerCase();
@@ -172,14 +185,13 @@ async function getEncodedRouteCandidates(routeService, tokenIn, tokenOut, source
172
185
  if (rawCandidates.length === 0) {
173
186
  throw new RouteNotFoundError(tokenIn, tokenOut);
174
187
  }
175
- // Prioritize routes that avoid swapping through the same pool being zapped out.
176
- rawCandidates.sort((a, b) => {
177
- const aUsesSourcePool = routeUsesPool(a, sourcePoolAddress) ? 1 : 0;
178
- const bUsesSourcePool = routeUsesPool(b, sourcePoolAddress) ? 1 : 0;
179
- if (aUsesSourcePool !== bUsesSourcePool)
180
- return aUsesSourcePool - bUsesSourcePool;
181
- if (a.path.length !== b.path.length)
182
- return a.path.length - b.path.length;
188
+ rawCandidates.sort((routeA, routeB) => {
189
+ const routeAUsesSourcePool = routeUsesPool(routeA, sourcePoolAddress) ? 1 : 0;
190
+ const routeBUsesSourcePool = routeUsesPool(routeB, sourcePoolAddress) ? 1 : 0;
191
+ if (routeAUsesSourcePool !== routeBUsesSourcePool)
192
+ return routeAUsesSourcePool - routeBUsesSourcePool;
193
+ if (routeA.path.length !== routeB.path.length)
194
+ return routeA.path.length - routeB.path.length;
183
195
  return 0;
184
196
  });
185
197
  const encodedRoutes = [];
@@ -1,6 +1,6 @@
1
1
  import { PoolType } from '../../core/types';
2
2
  import { fetchFPMMPools, fetchVirtualPools } from './poolDiscovery';
3
- import { fetchFPMMPoolDetails, fetchVirtualPoolDetails } from './poolDetails';
3
+ import { fetchFPMMPoolDetailsBatch, fetchVirtualPoolDetailsBatch, } from './poolDetails';
4
4
  /**
5
5
  * Service for discovering liquidity pools in the Mento protocol.
6
6
  * Aggregates pools from multiple factory contracts (FPMM and VirtualPool).
@@ -11,6 +11,9 @@ export class PoolService {
11
11
  this.chainId = chainId;
12
12
  this.poolsCache = null;
13
13
  this.discoveryWarnings = [];
14
+ this.poolsPromise = null;
15
+ this.poolDetailsCache = new Map();
16
+ this.poolDetailPromises = new Map();
14
17
  }
15
18
  /**
16
19
  * Returns any warnings from the last pool discovery operation.
@@ -36,23 +39,36 @@ export class PoolService {
36
39
  if (this.poolsCache) {
37
40
  return this.poolsCache;
38
41
  }
39
- const pools = [];
40
- const warnings = [];
42
+ if (this.poolsPromise) {
43
+ return this.poolsPromise;
44
+ }
45
+ this.poolsPromise = this.loadPools();
41
46
  try {
42
- const fpmmPools = await fetchFPMMPools(this.publicClient, this.chainId);
43
- pools.push(...fpmmPools);
47
+ return await this.poolsPromise;
44
48
  }
45
- catch (error) {
46
- const message = `Failed to fetch FPMM pools: ${error instanceof Error ? error.message : String(error)}`;
47
- warnings.push(message);
49
+ finally {
50
+ this.poolsPromise = null;
48
51
  }
49
- try {
50
- const virtualPools = await fetchVirtualPools(this.publicClient, this.chainId);
51
- pools.push(...virtualPools);
52
+ }
53
+ async loadPools() {
54
+ const warnings = [];
55
+ const settled = await Promise.allSettled([
56
+ fetchFPMMPools(this.publicClient, this.chainId),
57
+ fetchVirtualPools(this.publicClient, this.chainId),
58
+ ]);
59
+ const pools = [];
60
+ const [fpmmResult, virtualResult] = settled;
61
+ if (fpmmResult.status === 'fulfilled') {
62
+ pools.push(...fpmmResult.value);
52
63
  }
53
- catch (error) {
54
- const message = `Failed to fetch Virtual pools: ${error instanceof Error ? error.message : String(error)}`;
55
- warnings.push(message);
64
+ else {
65
+ warnings.push(`Failed to fetch FPMM pools: ${fpmmResult.reason instanceof Error ? fpmmResult.reason.message : String(fpmmResult.reason)}`);
66
+ }
67
+ if (virtualResult.status === 'fulfilled') {
68
+ pools.push(...virtualResult.value);
69
+ }
70
+ else {
71
+ warnings.push(`Failed to fetch Virtual pools: ${virtualResult.reason instanceof Error ? virtualResult.reason.message : String(virtualResult.reason)}`);
56
72
  }
57
73
  this.discoveryWarnings = warnings;
58
74
  // Only throw if NO pools were discovered from any factory
@@ -85,16 +101,95 @@ export class PoolService {
85
101
  * ```
86
102
  */
87
103
  async getPoolDetails(poolAddr) {
104
+ const [details] = await this.getPoolDetailsBatch([poolAddr]);
105
+ return details;
106
+ }
107
+ async getPoolDetailsBatch(poolAddresses) {
88
108
  const pools = await this.getPools();
89
- const pool = pools.find((p) => p.poolAddr.toLowerCase() === poolAddr.toLowerCase());
90
- if (!pool) {
91
- throw new Error(`Pool not found: ${poolAddr}. ` + 'Ensure the address is a valid pool discovered by getPools().');
109
+ const targets = poolAddresses
110
+ ? poolAddresses.map((poolAddress) => {
111
+ const pool = pools.find((candidate) => candidate.poolAddr.toLowerCase() === poolAddress.toLowerCase());
112
+ if (!pool) {
113
+ throw new Error(`Pool not found: ${poolAddress}. Ensure the address is a valid pool discovered by getPools().`);
114
+ }
115
+ return pool;
116
+ })
117
+ : pools;
118
+ const results = new Array(targets.length);
119
+ const pendingResults = [];
120
+ const missingTargets = [];
121
+ for (const [index, pool] of targets.entries()) {
122
+ const key = pool.poolAddr.toLowerCase();
123
+ const cached = this.poolDetailsCache.get(key);
124
+ if (cached) {
125
+ results[index] = cached;
126
+ continue;
127
+ }
128
+ const inFlight = this.poolDetailPromises.get(key);
129
+ if (inFlight) {
130
+ pendingResults.push(inFlight.then((detail) => {
131
+ results[index] = detail;
132
+ }));
133
+ continue;
134
+ }
135
+ missingTargets.push({ pool, index, key });
92
136
  }
93
- if (pool.poolType === PoolType.FPMM) {
94
- return fetchFPMMPoolDetails(this.publicClient, this.chainId, pool);
95
- }
96
- else {
97
- return fetchVirtualPoolDetails(this.publicClient, pool);
137
+ if (missingTargets.length > 0) {
138
+ const grouped = {
139
+ fpmm: missingTargets.filter(({ pool }) => pool.poolType === PoolType.FPMM),
140
+ virtual: missingTargets.filter(({ pool }) => pool.poolType !== PoolType.FPMM),
141
+ };
142
+ const createdPromises = new Map();
143
+ const createdPendingResults = [];
144
+ for (const target of missingTargets) {
145
+ const deferred = createDeferred();
146
+ this.poolDetailPromises.set(target.key, deferred.promise);
147
+ createdPromises.set(target.key, deferred);
148
+ const pendingResult = deferred.promise.then((detail) => {
149
+ results[target.index] = detail;
150
+ });
151
+ pendingResults.push(pendingResult);
152
+ createdPendingResults.push(pendingResult);
153
+ }
154
+ try {
155
+ const [fpmmDetails, virtualDetails] = await Promise.all([
156
+ fetchFPMMPoolDetailsBatch(this.publicClient, this.chainId, grouped.fpmm.map(({ pool }) => pool)),
157
+ fetchVirtualPoolDetailsBatch(this.publicClient, grouped.virtual.map(({ pool }) => pool)),
158
+ ]);
159
+ for (const [groupIndex, detail] of fpmmDetails.entries()) {
160
+ const target = grouped.fpmm[groupIndex];
161
+ this.poolDetailsCache.set(target.key, detail);
162
+ createdPromises.get(target.key)?.resolve(detail);
163
+ }
164
+ for (const [groupIndex, detail] of virtualDetails.entries()) {
165
+ const target = grouped.virtual[groupIndex];
166
+ this.poolDetailsCache.set(target.key, detail);
167
+ createdPromises.get(target.key)?.resolve(detail);
168
+ }
169
+ }
170
+ catch (error) {
171
+ for (const target of missingTargets) {
172
+ createdPromises.get(target.key)?.reject(error);
173
+ }
174
+ await Promise.allSettled(createdPendingResults);
175
+ throw error;
176
+ }
177
+ finally {
178
+ for (const target of missingTargets) {
179
+ this.poolDetailPromises.delete(target.key);
180
+ }
181
+ }
98
182
  }
183
+ await Promise.all(pendingResults);
184
+ return results;
99
185
  }
100
186
  }
187
+ function createDeferred() {
188
+ let resolve;
189
+ let reject;
190
+ const promise = new Promise((resolvePromise, rejectPromise) => {
191
+ resolve = resolvePromise;
192
+ reject = rejectPromise;
193
+ });
194
+ return { promise, resolve, reject };
195
+ }
@@ -2,35 +2,51 @@ import { addresses } from '../../core/constants';
2
2
  import { FPMM_ABI, VIRTUAL_POOL_ABI } from '../../core/abis';
3
3
  import { getAddress } from 'viem';
4
4
  import { multicall } from '../../utils/multicall';
5
+ const FPMM_FIXED_RESULT_COUNT = 8;
6
+ const VIRTUAL_RESULT_COUNT = 3;
5
7
  /**
6
8
  * Fetches enriched details for an FPMM pool
7
9
  */
8
10
  export async function fetchFPMMPoolDetails(publicClient, chainId, pool) {
11
+ const [details] = await fetchFPMMPoolDetailsBatch(publicClient, chainId, [pool]);
12
+ return details;
13
+ }
14
+ export async function fetchFPMMPoolDetailsBatch(publicClient, chainId, pools) {
15
+ if (pools.length === 0) {
16
+ return [];
17
+ }
18
+ const knownStrategies = getKnownLiquidityStrategies(chainId);
19
+ const contracts = pools.flatMap((pool) => buildFPMMContracts(pool, knownStrategies));
20
+ const results = await multicall(publicClient, contracts);
21
+ const perPoolResultCount = FPMM_FIXED_RESULT_COUNT + knownStrategies.length + 1;
22
+ return pools.map((pool, index) => {
23
+ const offset = index * perPoolResultCount;
24
+ const poolResults = results.slice(offset, offset + perPoolResultCount);
25
+ return parseFPMMPoolDetails(pool, knownStrategies, poolResults);
26
+ });
27
+ }
28
+ function buildFPMMContracts(pool, knownStrategies) {
9
29
  const address = pool.poolAddr;
30
+ return [
31
+ { address, abi: FPMM_ABI, functionName: 'getReserves' },
32
+ { address, abi: FPMM_ABI, functionName: 'decimals0' },
33
+ { address, abi: FPMM_ABI, functionName: 'decimals1' },
34
+ { address, abi: FPMM_ABI, functionName: 'lpFee' },
35
+ { address, abi: FPMM_ABI, functionName: 'protocolFee' },
36
+ { address, abi: FPMM_ABI, functionName: 'rebalanceIncentive' },
37
+ { address, abi: FPMM_ABI, functionName: 'rebalanceThresholdAbove' },
38
+ { address, abi: FPMM_ABI, functionName: 'rebalanceThresholdBelow' },
39
+ ...knownStrategies.map((strategyAddr) => ({
40
+ address,
41
+ abi: FPMM_ABI,
42
+ functionName: 'liquidityStrategy',
43
+ args: [strategyAddr],
44
+ })),
45
+ { address, abi: FPMM_ABI, functionName: 'getRebalancingState' },
46
+ ];
47
+ }
48
+ function parseFPMMPoolDetails(pool, knownStrategies, results) {
10
49
  try {
11
- // Known liquidity strategy addresses for this chain
12
- const knownStrategies = getKnownLiquidityStrategies(chainId);
13
- // Build all contract reads for a single multicall
14
- const coreContracts = [
15
- { address, abi: FPMM_ABI, functionName: 'getReserves' },
16
- { address, abi: FPMM_ABI, functionName: 'decimals0' },
17
- { address, abi: FPMM_ABI, functionName: 'decimals1' },
18
- { address, abi: FPMM_ABI, functionName: 'lpFee' },
19
- { address, abi: FPMM_ABI, functionName: 'protocolFee' },
20
- { address, abi: FPMM_ABI, functionName: 'rebalanceIncentive' },
21
- { address, abi: FPMM_ABI, functionName: 'rebalanceThresholdAbove' },
22
- { address, abi: FPMM_ABI, functionName: 'rebalanceThresholdBelow' },
23
- ...knownStrategies.map((strategyAddr) => ({
24
- address,
25
- abi: FPMM_ABI,
26
- functionName: 'liquidityStrategy',
27
- args: [strategyAddr],
28
- })),
29
- // Include getRebalancingState in the same multicall (allowFailure handles FXMarketClosed)
30
- { address, abi: FPMM_ABI, functionName: 'getRebalancingState' },
31
- ];
32
- const results = await multicall(publicClient, coreContracts);
33
- // Parse core results (first 8 are fixed)
34
50
  const reservesRes = results[0];
35
51
  const decimals0Res = results[1];
36
52
  const decimals1Res = results[2];
@@ -39,8 +55,15 @@ export async function fetchFPMMPoolDetails(publicClient, chainId, pool) {
39
55
  const rebalanceIncentiveRes = results[5];
40
56
  const thresholdAboveRes = results[6];
41
57
  const thresholdBelowRes = results[7];
42
- // Check core results
43
- if (reservesRes.status === 'failure' ||
58
+ if (!reservesRes ||
59
+ !decimals0Res ||
60
+ !decimals1Res ||
61
+ !lpFeeRes ||
62
+ !protocolFeeRes ||
63
+ !rebalanceIncentiveRes ||
64
+ !thresholdAboveRes ||
65
+ !thresholdBelowRes ||
66
+ reservesRes.status === 'failure' ||
44
67
  decimals0Res.status === 'failure' ||
45
68
  decimals1Res.status === 'failure' ||
46
69
  lpFeeRes.status === 'failure' ||
@@ -56,15 +79,13 @@ export async function fetchFPMMPoolDetails(publicClient, chainId, pool) {
56
79
  const rebalanceIncentiveBps = rebalanceIncentiveRes.result;
57
80
  const thresholdAboveBps = thresholdAboveRes.result;
58
81
  const thresholdBelowBps = thresholdBelowRes.result;
59
- // Parse strategy results (indices 8 .. 8+N-1)
60
- const strategyResults = results.slice(8, 8 + knownStrategies.length);
61
- const activeIndex = strategyResults.findIndex((r) => r.status === 'success' && r.result === true);
82
+ const strategyResults = results.slice(FPMM_FIXED_RESULT_COUNT, FPMM_FIXED_RESULT_COUNT + knownStrategies.length);
83
+ const activeIndex = strategyResults.findIndex((result) => result.status === 'success' && result.result === true);
62
84
  const liquidityStrategy = activeIndex >= 0 ? knownStrategies[activeIndex] : null;
63
- // Parse getRebalancingState (last result) — graceful degradation when FX market is closed
64
- const rebalancingRes = results[8 + knownStrategies.length];
85
+ const rebalancingRes = results[FPMM_FIXED_RESULT_COUNT + knownStrategies.length];
65
86
  let pricing = null;
66
87
  let inBand = null;
67
- if (rebalancingRes.status === 'success') {
88
+ if (rebalancingRes?.status === 'success') {
68
89
  const [oraclePriceNum, oraclePriceDen, reservePriceNum, reservePriceDen, reservePriceAboveOraclePrice, rebalanceThreshold, priceDifference,] = rebalancingRes.result;
69
90
  pricing = {
70
91
  oraclePriceNum,
@@ -79,7 +100,6 @@ export async function fetchFPMMPoolDetails(publicClient, chainId, pool) {
79
100
  };
80
101
  inBand = priceDifference < BigInt(rebalanceThreshold);
81
102
  }
82
- // If rebalancingRes.status === 'failure' (likely FXMarketClosed) — pricing stays null
83
103
  return {
84
104
  ...pool,
85
105
  poolType: 'FPMM',
@@ -116,14 +136,35 @@ export async function fetchFPMMPoolDetails(publicClient, chainId, pool) {
116
136
  * Fetches enriched details for a Virtual pool
117
137
  */
118
138
  export async function fetchVirtualPoolDetails(publicClient, pool) {
139
+ const [details] = await fetchVirtualPoolDetailsBatch(publicClient, [pool]);
140
+ return details;
141
+ }
142
+ export async function fetchVirtualPoolDetailsBatch(publicClient, pools) {
143
+ if (pools.length === 0) {
144
+ return [];
145
+ }
146
+ const contracts = pools.flatMap((pool) => buildVirtualContracts(pool));
147
+ const results = await multicall(publicClient, contracts);
148
+ return pools.map((pool, index) => {
149
+ const offset = index * VIRTUAL_RESULT_COUNT;
150
+ const poolResults = results.slice(offset, offset + VIRTUAL_RESULT_COUNT);
151
+ return parseVirtualPoolDetails(pool, poolResults);
152
+ });
153
+ }
154
+ function buildVirtualContracts(pool) {
119
155
  const address = pool.poolAddr;
156
+ return [
157
+ { address, abi: VIRTUAL_POOL_ABI, functionName: 'getReserves' },
158
+ { address, abi: VIRTUAL_POOL_ABI, functionName: 'protocolFee' },
159
+ { address, abi: VIRTUAL_POOL_ABI, functionName: 'metadata' },
160
+ ];
161
+ }
162
+ function parseVirtualPoolDetails(pool, results) {
120
163
  try {
121
- const results = await multicall(publicClient, [
122
- { address, abi: VIRTUAL_POOL_ABI, functionName: 'getReserves' },
123
- { address, abi: VIRTUAL_POOL_ABI, functionName: 'protocolFee' },
124
- { address, abi: VIRTUAL_POOL_ABI, functionName: 'metadata' },
125
- ]);
126
- if (results[0].status === 'failure' || results[1].status === 'failure' || results[2].status === 'failure') {
164
+ if (results.length !== VIRTUAL_RESULT_COUNT ||
165
+ results[0].status === 'failure' ||
166
+ results[1].status === 'failure' ||
167
+ results[2].status === 'failure') {
127
168
  throw new Error('One or more virtual pool reads failed');
128
169
  }
129
170
  const [reserve0, reserve1, blockTimestampLast] = results[0].result;
@@ -57,27 +57,29 @@ export class QuoteService {
57
57
  if (!route) {
58
58
  route = await this.routeService.findRoute(tokenIn, tokenOut);
59
59
  }
60
- // Convert route.path to Router contract's Route[] format
61
- const routerRoutes = encodeRoutePath(route.path, tokenIn, tokenOut);
62
- const routerAddress = getContractAddress(this.chainId, 'Router');
63
- try {
64
- const amounts = (await this.publicClient.readContract({
65
- address: routerAddress,
66
- abi: ROUTER_ABI,
67
- functionName: 'getAmountsOut',
68
- args: [amountIn, routerRoutes],
69
- }));
70
- return amounts[amounts.length - 1];
71
- }
72
- catch (error) {
73
- if (error instanceof BaseError) {
74
- const revertError = error.walk((e) => e instanceof ContractFunctionRevertedError);
75
- if (revertError instanceof ContractFunctionRevertedError &&
76
- revertError.data?.errorName === 'FXMarketClosed') {
77
- throw new FXMarketClosedError();
78
- }
60
+ return getAmountOutForRoute(this.publicClient, this.chainId, tokenIn, tokenOut, amountIn, route);
61
+ }
62
+ }
63
+ export async function getAmountOutForRoute(publicClient, chainId, tokenIn, tokenOut, amountIn, route) {
64
+ const routerRoutes = encodeRoutePath(route.path, tokenIn, tokenOut);
65
+ const routerAddress = getContractAddress(chainId, 'Router');
66
+ try {
67
+ const amounts = (await publicClient.readContract({
68
+ address: routerAddress,
69
+ abi: ROUTER_ABI,
70
+ functionName: 'getAmountsOut',
71
+ args: [amountIn, routerRoutes],
72
+ }));
73
+ return amounts[amounts.length - 1];
74
+ }
75
+ catch (error) {
76
+ if (error instanceof BaseError) {
77
+ const revertError = error.walk((candidate) => candidate instanceof ContractFunctionRevertedError);
78
+ if (revertError instanceof ContractFunctionRevertedError &&
79
+ revertError.data?.errorName === 'FXMarketClosed') {
80
+ throw new FXMarketClosedError();
79
81
  }
80
- throw error;
81
82
  }
83
+ throw error;
82
84
  }
83
85
  }