@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.
- package/dist/core/types/liquidity.d.ts +20 -0
- package/dist/esm/index.js +25 -9
- package/dist/esm/services/borrow/internal/borrowReadService.js +63 -34
- package/dist/esm/services/borrow/internal/borrowRegistryReader.js +45 -28
- package/dist/esm/services/index.js +1 -0
- package/dist/esm/services/liquidity/LiquidityService.js +8 -2
- package/dist/esm/services/liquidity/liquidityHelpers.js +27 -1
- package/dist/esm/services/liquidity/zapHelpers.js +20 -24
- package/dist/esm/services/liquidity/zapIn.js +68 -75
- package/dist/esm/services/liquidity/zapOut.js +89 -77
- package/dist/esm/services/pools/PoolService.js +117 -22
- package/dist/esm/services/pools/poolDetails.js +79 -38
- package/dist/esm/services/quotes/QuoteService.js +22 -20
- package/dist/esm/services/routes/RouteService.js +82 -24
- package/dist/esm/services/swap/SwapService.js +81 -37
- package/dist/esm/services/tokens/tokenService.js +142 -29
- package/dist/esm/services/trading/TradingService.js +51 -12
- package/dist/esm/utils/multicall.js +29 -2
- package/dist/index.d.ts +11 -1
- package/dist/index.js +25 -9
- package/dist/services/borrow/internal/borrowReadService.d.ts +1 -0
- package/dist/services/borrow/internal/borrowReadService.js +63 -34
- package/dist/services/borrow/internal/borrowRegistryReader.js +45 -28
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/liquidity/LiquidityService.d.ts +3 -1
- package/dist/services/liquidity/LiquidityService.js +6 -0
- package/dist/services/liquidity/liquidityHelpers.d.ts +6 -0
- package/dist/services/liquidity/liquidityHelpers.js +27 -0
- package/dist/services/liquidity/zapHelpers.js +20 -24
- package/dist/services/liquidity/zapIn.d.ts +2 -1
- package/dist/services/liquidity/zapIn.js +67 -73
- package/dist/services/liquidity/zapOut.d.ts +2 -10
- package/dist/services/liquidity/zapOut.js +90 -77
- package/dist/services/pools/PoolService.d.ts +5 -0
- package/dist/services/pools/PoolService.js +116 -21
- package/dist/services/pools/poolDetails.d.ts +2 -0
- package/dist/services/pools/poolDetails.js +81 -38
- package/dist/services/quotes/QuoteService.d.ts +1 -0
- package/dist/services/quotes/QuoteService.js +24 -21
- package/dist/services/routes/RouteService.d.ts +8 -0
- package/dist/services/routes/RouteService.js +82 -24
- package/dist/services/swap/SwapService.d.ts +19 -0
- package/dist/services/swap/SwapService.js +81 -37
- package/dist/services/tokens/tokenService.d.ts +7 -0
- package/dist/services/tokens/tokenService.js +142 -29
- package/dist/services/trading/TradingService.d.ts +3 -0
- package/dist/services/trading/TradingService.js +51 -12
- package/dist/utils/multicall.d.ts +5 -1
- package/dist/utils/multicall.js +29 -2
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
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
|
-
|
|
51
|
+
details = await findViableZapOutDetails(publicClient, chainId, poolService, routeService, poolAddress, tokenOut, liquidity, ownerAddr, options);
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
|
-
return {
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
124
|
-
const routesBOptions = await
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
136
|
+
routeCombinations.push({ routesA, routesB });
|
|
137
|
+
if (routeCombinations.length >= MAX_ROUTE_COMBINATIONS) {
|
|
130
138
|
break outer;
|
|
131
139
|
}
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
best = candidate;
|
|
138
|
-
}
|
|
149
|
+
return candidate;
|
|
139
150
|
}
|
|
140
151
|
catch {
|
|
141
|
-
|
|
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
|
|
175
|
+
// Continue; we'll try the broader route set next.
|
|
161
176
|
}
|
|
162
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 {
|
|
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
|
-
|
|
40
|
-
|
|
42
|
+
if (this.poolsPromise) {
|
|
43
|
+
return this.poolsPromise;
|
|
44
|
+
}
|
|
45
|
+
this.poolsPromise = this.loadPools();
|
|
41
46
|
try {
|
|
42
|
-
|
|
43
|
-
pools.push(...fpmmPools);
|
|
47
|
+
return await this.poolsPromise;
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
warnings.push(message);
|
|
49
|
+
finally {
|
|
50
|
+
this.poolsPromise = null;
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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 (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
60
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
}
|