@mento-protocol/mento-sdk 3.1.0-beta.3 → 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 +80 -40
- package/dist/esm/services/pools/poolDiscovery.js +3 -2
- 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/chainConfig.js +12 -0
- package/dist/esm/utils/costUtils.js +8 -9
- package/dist/esm/utils/multicall.js +42 -0
- 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 +82 -40
- package/dist/services/pools/poolDiscovery.js +3 -2
- 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/chainConfig.js +12 -0
- package/dist/utils/costUtils.js +8 -9
- package/dist/utils/multicall.d.ts +30 -0
- package/dist/utils/multicall.js +47 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { ERC20_ABI } from '../../core/abis';
|
|
|
2
2
|
import { RouteNotFoundError } from '../../core/errors';
|
|
3
3
|
import { buildConnectivityStructures, generateAllRoutes, selectOptimalRoutes } from '../../utils/routeUtils';
|
|
4
4
|
import { canonicalSymbolKey } from '../../utils/sortUtils';
|
|
5
|
+
import { multicall } from '../../utils/multicall';
|
|
5
6
|
/**
|
|
6
7
|
* Service for discovering and managing trading routes in the Mento protocol.
|
|
7
8
|
* Handles route discovery for both direct (single-hop) and multi-hop trading paths.
|
|
@@ -16,6 +17,9 @@ export class RouteService {
|
|
|
16
17
|
this.chainId = chainId;
|
|
17
18
|
this.poolService = poolService;
|
|
18
19
|
this.symbolCache = new Map();
|
|
20
|
+
this.routeCache = new Map();
|
|
21
|
+
this.routeLookupCache = new Map();
|
|
22
|
+
this.routePromises = new Map();
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
21
25
|
* Generates all direct (single-hop) routes from available pools
|
|
@@ -43,7 +47,7 @@ export class RouteService {
|
|
|
43
47
|
});
|
|
44
48
|
// Fetch symbols for all tokens in parallel. Used for the route ids
|
|
45
49
|
const tokenAddresses = Array.from(uniqueTokens);
|
|
46
|
-
await
|
|
50
|
+
await this.hydrateTokenSymbols(tokenAddresses);
|
|
47
51
|
const routes = [];
|
|
48
52
|
// Loop all pools
|
|
49
53
|
for (const pool of pools) {
|
|
@@ -96,20 +100,31 @@ export class RouteService {
|
|
|
96
100
|
async getRoutes(options) {
|
|
97
101
|
const cached = options?.cached ?? true;
|
|
98
102
|
const returnAllRoutes = options?.returnAllRoutes ?? false;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
const cacheKey = this.getCacheKey(cached, returnAllRoutes);
|
|
104
|
+
const cachedRoutes = this.routeCache.get(cacheKey);
|
|
105
|
+
if (cachedRoutes) {
|
|
106
|
+
return cachedRoutes;
|
|
107
|
+
}
|
|
108
|
+
const inFlight = this.routePromises.get(cacheKey);
|
|
109
|
+
if (inFlight) {
|
|
110
|
+
return inFlight;
|
|
111
|
+
}
|
|
112
|
+
const promise = this.loadRoutes(cached, returnAllRoutes);
|
|
113
|
+
this.routePromises.set(cacheKey, promise);
|
|
114
|
+
try {
|
|
115
|
+
const routes = await promise;
|
|
116
|
+
this.routeCache.set(cacheKey, routes);
|
|
117
|
+
if (!returnAllRoutes) {
|
|
118
|
+
this.routeLookupCache.set(cacheKey, this.buildLookup(routes));
|
|
109
119
|
}
|
|
120
|
+
return routes;
|
|
110
121
|
}
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
finally {
|
|
123
|
+
this.routePromises.delete(cacheKey);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async warm(options) {
|
|
127
|
+
return this.getRoutes(options);
|
|
113
128
|
}
|
|
114
129
|
/**
|
|
115
130
|
* Looks up the tradable route between two tokens (direct or multi-hop)
|
|
@@ -134,17 +149,12 @@ export class RouteService {
|
|
|
134
149
|
* ```
|
|
135
150
|
*/
|
|
136
151
|
async findRoute(tokenIn, tokenOut, options) {
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
const matchingRoute =
|
|
143
|
-
const a0 = route.tokens[0].address.toLowerCase();
|
|
144
|
-
const a1 = route.tokens[1].address.toLowerCase();
|
|
145
|
-
// Match either direction: (t0,t1) or (t1,t0)
|
|
146
|
-
return (a0 === t0 && a1 === t1) || (a0 === t1 && a1 === t0);
|
|
147
|
-
});
|
|
152
|
+
const cached = options?.cached ?? true;
|
|
153
|
+
const cacheKey = this.getCacheKey(cached, false);
|
|
154
|
+
const routes = await this.getRoutes({ cached, returnAllRoutes: false });
|
|
155
|
+
const lookup = this.routeLookupCache.get(cacheKey) ?? this.buildLookup(routes);
|
|
156
|
+
this.routeLookupCache.set(cacheKey, lookup);
|
|
157
|
+
const matchingRoute = lookup.get(makeTokenPairKey(tokenIn, tokenOut));
|
|
148
158
|
if (!matchingRoute) {
|
|
149
159
|
throw new RouteNotFoundError(tokenIn, tokenOut);
|
|
150
160
|
}
|
|
@@ -178,6 +188,50 @@ export class RouteService {
|
|
|
178
188
|
const cachedRoutes = await getCachedRoutes(this.chainId);
|
|
179
189
|
return cachedRoutes || [];
|
|
180
190
|
}
|
|
191
|
+
async loadRoutes(cached, returnAllRoutes) {
|
|
192
|
+
if (cached) {
|
|
193
|
+
try {
|
|
194
|
+
const cachedRoutes = await this.loadCachedRoutes();
|
|
195
|
+
if (cachedRoutes.length > 0) {
|
|
196
|
+
return returnAllRoutes ? cachedRoutes : cachedRoutes;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Cache miss or corrupt - silently fall through to fresh generation
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return this.generateFreshRoutes(returnAllRoutes);
|
|
204
|
+
}
|
|
205
|
+
getCacheKey(cached, returnAllRoutes) {
|
|
206
|
+
return `${cached ? 'cached' : 'fresh'}:${returnAllRoutes ? 'all' : 'best'}`;
|
|
207
|
+
}
|
|
208
|
+
buildLookup(routes) {
|
|
209
|
+
const lookup = new Map();
|
|
210
|
+
for (const route of routes) {
|
|
211
|
+
lookup.set(makeTokenPairKey(route.tokens[0].address, route.tokens[1].address), route);
|
|
212
|
+
}
|
|
213
|
+
return lookup;
|
|
214
|
+
}
|
|
215
|
+
async hydrateTokenSymbols(addresses) {
|
|
216
|
+
const missingAddresses = addresses.filter((address) => !this.symbolCache.has(address));
|
|
217
|
+
if (missingAddresses.length === 0) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const results = await multicall(this.publicClient, missingAddresses.map((address) => ({
|
|
221
|
+
address: address,
|
|
222
|
+
abi: ERC20_ABI,
|
|
223
|
+
functionName: 'symbol',
|
|
224
|
+
args: [],
|
|
225
|
+
})));
|
|
226
|
+
for (const [index, address] of missingAddresses.entries()) {
|
|
227
|
+
const result = results[index];
|
|
228
|
+
if (!result || result.status === 'failure') {
|
|
229
|
+
this.symbolCache.set(address, address);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
this.symbolCache.set(address, result.result);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
181
235
|
/**
|
|
182
236
|
* Helper: Fetch token symbol from on-chain
|
|
183
237
|
* Results are cached to avoid redundant RPC calls
|
|
@@ -208,3 +262,7 @@ export class RouteService {
|
|
|
208
262
|
}
|
|
209
263
|
}
|
|
210
264
|
}
|
|
265
|
+
function makeTokenPairKey(tokenA, tokenB) {
|
|
266
|
+
const [first, second] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
|
|
267
|
+
return `${first}:${second}`;
|
|
268
|
+
}
|
|
@@ -4,6 +4,7 @@ import { getContractAddress } from '../../core/constants';
|
|
|
4
4
|
import { encodeRoutePath } from '../../utils/pathEncoder';
|
|
5
5
|
import { validateAddress } from '../../utils/validation';
|
|
6
6
|
import { retryOperation } from '../../utils';
|
|
7
|
+
import { getAmountOutForRoute } from '../quotes/QuoteService';
|
|
7
8
|
/**
|
|
8
9
|
* Service for building token swap transactions on the Mento protocol.
|
|
9
10
|
* Returns transaction parameters that can be executed by any wallet.
|
|
@@ -55,18 +56,31 @@ export class SwapService {
|
|
|
55
56
|
* ```
|
|
56
57
|
*/
|
|
57
58
|
async buildSwapTransaction(tokenIn, tokenOut, amountIn, recipient, owner, options, route) {
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
const prepared = await this.prepareSwap({
|
|
60
|
+
amountIn,
|
|
61
|
+
deadline: options.deadline,
|
|
62
|
+
owner,
|
|
63
|
+
recipient,
|
|
64
|
+
route,
|
|
65
|
+
slippageTolerance: options.slippageTolerance,
|
|
66
|
+
tokenIn,
|
|
67
|
+
tokenOut,
|
|
68
|
+
});
|
|
69
|
+
if (!prepared.params) {
|
|
70
|
+
throw new Error('Swap params were not prepared');
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
approval: prepared.approval ?? null,
|
|
74
|
+
swap: {
|
|
75
|
+
params: prepared.params,
|
|
76
|
+
route: prepared.route,
|
|
77
|
+
routerRoutes: prepared.routerRoutes,
|
|
78
|
+
amountIn,
|
|
79
|
+
amountOutMin: prepared.amountOutMin,
|
|
80
|
+
expectedAmountOut: prepared.expectedAmountOut,
|
|
81
|
+
deadline: options.deadline,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
70
84
|
}
|
|
71
85
|
/**
|
|
72
86
|
* Builds swap transaction parameters without executing the transaction.
|
|
@@ -101,37 +115,67 @@ export class SwapService {
|
|
|
101
115
|
* ```
|
|
102
116
|
*/
|
|
103
117
|
async buildSwapParams(tokenIn, tokenOut, amountIn, recipient, options, route) {
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
route = await this.routeService.findRoute(tokenIn, tokenOut);
|
|
118
|
+
const prepared = await this.prepareSwap({
|
|
119
|
+
amountIn,
|
|
120
|
+
deadline: options.deadline,
|
|
121
|
+
recipient,
|
|
122
|
+
route,
|
|
123
|
+
slippageTolerance: options.slippageTolerance,
|
|
124
|
+
tokenIn,
|
|
125
|
+
tokenOut,
|
|
126
|
+
});
|
|
127
|
+
if (!prepared.params) {
|
|
128
|
+
throw new Error('Swap params were not prepared');
|
|
116
129
|
}
|
|
117
|
-
const expectedAmountOut = await this.quoteService.getAmountOut(tokenIn, tokenOut, amountIn, route);
|
|
118
|
-
const amountOutMin = this.calculateMinAmountOut(expectedAmountOut, options.slippageTolerance);
|
|
119
|
-
const routerRoutes = encodeRoutePath(route.path, tokenIn, tokenOut);
|
|
120
|
-
const routerAddress = getContractAddress(this.chainId, 'Router');
|
|
121
|
-
const data = this.encodeSwapCall(amountIn, amountOutMin, routerRoutes, recipient, deadline);
|
|
122
130
|
return {
|
|
123
|
-
params:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
params: prepared.params,
|
|
132
|
+
route: prepared.route,
|
|
133
|
+
routerRoutes: prepared.routerRoutes,
|
|
134
|
+
amountIn,
|
|
135
|
+
amountOutMin: prepared.amountOutMin,
|
|
136
|
+
expectedAmountOut: prepared.expectedAmountOut,
|
|
137
|
+
deadline: options.deadline,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async prepareSwap(input) {
|
|
141
|
+
this.validateAmountIn(input.amountIn);
|
|
142
|
+
validateAddress(input.tokenIn, 'tokenIn');
|
|
143
|
+
validateAddress(input.tokenOut, 'tokenOut');
|
|
144
|
+
if (input.recipient) {
|
|
145
|
+
validateAddress(input.recipient, 'recipient');
|
|
146
|
+
}
|
|
147
|
+
if (input.owner) {
|
|
148
|
+
validateAddress(input.owner, 'owner');
|
|
149
|
+
}
|
|
150
|
+
if (input.deadline !== undefined && input.deadline <= BigInt(Date.now()) / 1000n) {
|
|
151
|
+
throw new Error('Deadline must be in the future');
|
|
152
|
+
}
|
|
153
|
+
const route = input.route ?? await this.routeService.findRoute(input.tokenIn, input.tokenOut);
|
|
154
|
+
const routerRoutes = encodeRoutePath(route.path, input.tokenIn, input.tokenOut);
|
|
155
|
+
const expectedAmountOut = await getAmountOutForRoute(this.publicClient, this.chainId, input.tokenIn, input.tokenOut, input.amountIn, route);
|
|
156
|
+
const amountOutMin = this.calculateMinAmountOut(expectedAmountOut, input.slippageTolerance);
|
|
157
|
+
const prepared = {
|
|
128
158
|
route,
|
|
129
159
|
routerRoutes,
|
|
130
|
-
amountIn,
|
|
131
|
-
amountOutMin,
|
|
132
160
|
expectedAmountOut,
|
|
133
|
-
|
|
161
|
+
amountOutMin,
|
|
134
162
|
};
|
|
163
|
+
if (input.owner) {
|
|
164
|
+
const currentAllowance = await this.getAllowance(input.tokenIn, input.owner);
|
|
165
|
+
prepared.approval = currentAllowance < input.amountIn
|
|
166
|
+
? this.buildApprovalParams(input.tokenIn, input.amountIn)
|
|
167
|
+
: null;
|
|
168
|
+
}
|
|
169
|
+
if (input.recipient && input.deadline !== undefined) {
|
|
170
|
+
const routerAddress = getContractAddress(this.chainId, 'Router');
|
|
171
|
+
const data = this.encodeSwapCall(input.amountIn, amountOutMin, routerRoutes, input.recipient, input.deadline);
|
|
172
|
+
prepared.params = {
|
|
173
|
+
to: routerAddress,
|
|
174
|
+
data,
|
|
175
|
+
value: '0',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return prepared;
|
|
135
179
|
}
|
|
136
180
|
/**
|
|
137
181
|
* Builds approval transaction params for the Router to spend tokenIn
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RESERVE_ABI, RESERVE_V2_ABI, BIPOOL_MANAGER_ABI, ERC20_ABI } from '../../core/abis';
|
|
2
2
|
import { getContractAddress, tryGetContractAddress, ChainId, RESERVE, BIPOOLMANAGER, } from '../../core/constants';
|
|
3
3
|
import { retryOperation } from '../../utils';
|
|
4
|
+
import { multicall } from '../../utils/multicall';
|
|
4
5
|
/**
|
|
5
6
|
* Chains that use ReserveV2 (v3) instead of the legacy Reserve contract.
|
|
6
7
|
*/
|
|
@@ -9,6 +10,7 @@ export class TokenService {
|
|
|
9
10
|
constructor(publicClient, chainId) {
|
|
10
11
|
this.publicClient = publicClient;
|
|
11
12
|
this.chainId = chainId;
|
|
13
|
+
this.tokenMetadataCache = new Map();
|
|
12
14
|
}
|
|
13
15
|
isReserveV2() {
|
|
14
16
|
return RESERVE_V2_CHAINS.has(this.chainId);
|
|
@@ -19,6 +21,76 @@ export class TokenService {
|
|
|
19
21
|
* @returns Token metadata
|
|
20
22
|
*/
|
|
21
23
|
async getTokenMetadata(address) {
|
|
24
|
+
const cacheKey = address.toLowerCase();
|
|
25
|
+
const cached = this.tokenMetadataCache.get(cacheKey);
|
|
26
|
+
if (cached) {
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
const [metadata] = await this.getTokenMetadataBatch([address]);
|
|
30
|
+
this.tokenMetadataCache.set(cacheKey, metadata);
|
|
31
|
+
return metadata;
|
|
32
|
+
}
|
|
33
|
+
async getTokenMetadataBatch(addresses) {
|
|
34
|
+
if (addresses.length === 0) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const results = new Array(addresses.length);
|
|
38
|
+
const missing = [];
|
|
39
|
+
for (const [index, address] of addresses.entries()) {
|
|
40
|
+
const cached = this.tokenMetadataCache.get(address.toLowerCase());
|
|
41
|
+
if (cached) {
|
|
42
|
+
results[index] = cached;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
missing.push({ address, index });
|
|
46
|
+
}
|
|
47
|
+
if (missing.length === 0) {
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
const multicallResults = await multicall(this.publicClient, missing.flatMap(({ address }) => ([
|
|
51
|
+
{
|
|
52
|
+
address: address,
|
|
53
|
+
abi: ERC20_ABI,
|
|
54
|
+
functionName: 'name',
|
|
55
|
+
args: [],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
address: address,
|
|
59
|
+
abi: ERC20_ABI,
|
|
60
|
+
functionName: 'symbol',
|
|
61
|
+
args: [],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
address: address,
|
|
65
|
+
abi: ERC20_ABI,
|
|
66
|
+
functionName: 'decimals',
|
|
67
|
+
args: [],
|
|
68
|
+
},
|
|
69
|
+
])), { allowFailure: true });
|
|
70
|
+
const hydrated = await Promise.all(missing.map(async ({ address }, index) => {
|
|
71
|
+
const resultOffset = index * 3;
|
|
72
|
+
const name = multicallResults[resultOffset];
|
|
73
|
+
const symbol = multicallResults[resultOffset + 1];
|
|
74
|
+
const decimals = multicallResults[resultOffset + 2];
|
|
75
|
+
if (name?.status === 'success' &&
|
|
76
|
+
symbol?.status === 'success' &&
|
|
77
|
+
decimals?.status === 'success') {
|
|
78
|
+
return {
|
|
79
|
+
name: name.result,
|
|
80
|
+
symbol: symbol.result,
|
|
81
|
+
decimals: Number(decimals.result),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return this.readTokenMetadataWithRetry(address);
|
|
85
|
+
}));
|
|
86
|
+
for (const [index, metadata] of hydrated.entries()) {
|
|
87
|
+
const address = missing[index].address;
|
|
88
|
+
this.tokenMetadataCache.set(address.toLowerCase(), metadata);
|
|
89
|
+
results[missing[index].index] = metadata;
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
async readTokenMetadataWithRetry(address) {
|
|
22
94
|
const [name, symbol, decimals] = await Promise.all([
|
|
23
95
|
retryOperation(() => this.publicClient.readContract({
|
|
24
96
|
address: address,
|
|
@@ -51,6 +123,28 @@ export class TokenService {
|
|
|
51
123
|
* @returns Total supply as string
|
|
52
124
|
*/
|
|
53
125
|
async getTotalSupply(address) {
|
|
126
|
+
const [totalSupply] = await this.getTotalSupplyBatch([address]);
|
|
127
|
+
return totalSupply;
|
|
128
|
+
}
|
|
129
|
+
async getTotalSupplyBatch(addresses) {
|
|
130
|
+
if (addresses.length === 0) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const results = await multicall(this.publicClient, addresses.map((address) => ({
|
|
134
|
+
address: address,
|
|
135
|
+
abi: ERC20_ABI,
|
|
136
|
+
functionName: 'totalSupply',
|
|
137
|
+
args: [],
|
|
138
|
+
})), { allowFailure: true });
|
|
139
|
+
return Promise.all(addresses.map(async (address, index) => {
|
|
140
|
+
const result = results[index];
|
|
141
|
+
if (result?.status === 'success') {
|
|
142
|
+
return result.result.toString();
|
|
143
|
+
}
|
|
144
|
+
return this.readTotalSupplyWithRetry(address);
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
async readTotalSupplyWithRetry(address) {
|
|
54
148
|
const totalSupply = await retryOperation(() => this.publicClient.readContract({
|
|
55
149
|
address: address,
|
|
56
150
|
abi: ERC20_ABI,
|
|
@@ -59,6 +153,32 @@ export class TokenService {
|
|
|
59
153
|
}));
|
|
60
154
|
return totalSupply.toString();
|
|
61
155
|
}
|
|
156
|
+
async getCollateralStatusBatch(reserveAddress, addresses) {
|
|
157
|
+
if (addresses.length === 0) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const results = await multicall(this.publicClient, addresses.map((address) => ({
|
|
161
|
+
address: reserveAddress,
|
|
162
|
+
abi: RESERVE_ABI,
|
|
163
|
+
functionName: 'isCollateralAsset',
|
|
164
|
+
args: [address],
|
|
165
|
+
})), { allowFailure: true });
|
|
166
|
+
return Promise.all(addresses.map(async (address, index) => {
|
|
167
|
+
const result = results[index];
|
|
168
|
+
if (result?.status === 'success') {
|
|
169
|
+
return result.result;
|
|
170
|
+
}
|
|
171
|
+
return this.readCollateralStatusWithRetry(reserveAddress, address);
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
async readCollateralStatusWithRetry(reserveAddress, address) {
|
|
175
|
+
return retryOperation(() => this.publicClient.readContract({
|
|
176
|
+
address: reserveAddress,
|
|
177
|
+
abi: RESERVE_ABI,
|
|
178
|
+
functionName: 'isCollateralAsset',
|
|
179
|
+
args: [address],
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
62
182
|
/**
|
|
63
183
|
* Get stable token addresses from the Reserve contract.
|
|
64
184
|
* Uses getStableAssets() on ReserveV2, getTokens() on legacy Reserve.
|
|
@@ -88,17 +208,14 @@ export class TokenService {
|
|
|
88
208
|
async getStableTokens(includeSupply = true) {
|
|
89
209
|
const reserveAddress = getContractAddress(this.chainId, RESERVE);
|
|
90
210
|
const tokenAddresses = await this.getStableTokenAddresses(reserveAddress);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
...metadata,
|
|
100
|
-
totalSupply,
|
|
101
|
-
};
|
|
211
|
+
const [metadataList, totalSupplies] = await Promise.all([
|
|
212
|
+
this.getTokenMetadataBatch(tokenAddresses),
|
|
213
|
+
includeSupply ? this.getTotalSupplyBatch(tokenAddresses) : Promise.resolve(tokenAddresses.map(() => '0')),
|
|
214
|
+
]);
|
|
215
|
+
const tokens = tokenAddresses.map((address, index) => ({
|
|
216
|
+
address,
|
|
217
|
+
...metadataList[index],
|
|
218
|
+
totalSupply: totalSupplies[index],
|
|
102
219
|
}));
|
|
103
220
|
return tokens;
|
|
104
221
|
}
|
|
@@ -125,9 +242,10 @@ export class TokenService {
|
|
|
125
242
|
functionName: 'getCollateralAssets',
|
|
126
243
|
args: [],
|
|
127
244
|
})));
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
245
|
+
const metadataList = await this.getTokenMetadataBatch(collateralAddresses);
|
|
246
|
+
const assets = collateralAddresses.map((address, index) => ({
|
|
247
|
+
address,
|
|
248
|
+
...metadataList[index],
|
|
131
249
|
}));
|
|
132
250
|
return assets;
|
|
133
251
|
}
|
|
@@ -151,22 +269,17 @@ export class TokenService {
|
|
|
151
269
|
for (const exchange of exchanges) {
|
|
152
270
|
exchange.assets.forEach((address) => uniqueAddresses.add(address));
|
|
153
271
|
}
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
})),
|
|
163
|
-
this.getTokenMetadata(address),
|
|
164
|
-
]);
|
|
165
|
-
if (isCollateral) {
|
|
166
|
-
return { address, ...metadata };
|
|
272
|
+
const addresses = Array.from(uniqueAddresses);
|
|
273
|
+
const [collateralStatuses, metadataList] = await Promise.all([
|
|
274
|
+
this.getCollateralStatusBatch(reserveAddress, addresses),
|
|
275
|
+
this.getTokenMetadataBatch(addresses),
|
|
276
|
+
]);
|
|
277
|
+
const results = addresses.map((address, index) => {
|
|
278
|
+
if (!collateralStatuses[index]) {
|
|
279
|
+
return null;
|
|
167
280
|
}
|
|
168
|
-
return
|
|
169
|
-
})
|
|
281
|
+
return { address, ...metadataList[index] };
|
|
282
|
+
});
|
|
170
283
|
return results.filter((asset) => asset !== null);
|
|
171
284
|
}
|
|
172
285
|
}
|
|
@@ -2,6 +2,7 @@ import { isTradingEnabled, } from '../../core/types';
|
|
|
2
2
|
import { TradingLimitsService } from './TradingLimitsService';
|
|
3
3
|
import { BREAKERBOX_ABI, FPMM_ABI } from '../../core/abis';
|
|
4
4
|
import { getContractAddress } from '../../core/constants';
|
|
5
|
+
import { multicall } from '../../utils/multicall';
|
|
5
6
|
/**
|
|
6
7
|
* Service for checking trading status and circuit breaker state in the Mento protocol.
|
|
7
8
|
* Provides methods to query whether trading is enabled for specific rate feeds,
|
|
@@ -78,14 +79,9 @@ export class TradingService {
|
|
|
78
79
|
* ```
|
|
79
80
|
*/
|
|
80
81
|
async isRouteTradable(route) {
|
|
81
|
-
|
|
82
|
-
const rateFeedChecks = await Promise.all(route.path.map(async (pool) => {
|
|
83
|
-
const rateFeedId = await this.getPoolRateFeedId(pool);
|
|
84
|
-
const tradingMode = await this.getRateFeedTradingMode(rateFeedId);
|
|
85
|
-
return isTradingEnabled(tradingMode);
|
|
86
|
-
}));
|
|
82
|
+
const tradingModes = await this.getTradingModesForPools(route.path);
|
|
87
83
|
// All rate feeds must have trading enabled for the route to be tradable
|
|
88
|
-
return
|
|
84
|
+
return tradingModes.every((tradingMode) => isTradingEnabled(tradingMode));
|
|
89
85
|
}
|
|
90
86
|
/**
|
|
91
87
|
* Get trading limits for a pool.
|
|
@@ -123,11 +119,10 @@ export class TradingService {
|
|
|
123
119
|
* ```
|
|
124
120
|
*/
|
|
125
121
|
async getPoolTradabilityStatus(pool) {
|
|
126
|
-
const [
|
|
127
|
-
this.
|
|
122
|
+
const [[tradingMode], limits] = await Promise.all([
|
|
123
|
+
this.getTradingModesForPools([pool]),
|
|
128
124
|
this.tradingLimitsService.getPoolTradingLimits(pool),
|
|
129
125
|
]);
|
|
130
|
-
const tradingMode = await this.getRateFeedTradingMode(rateFeedId);
|
|
131
126
|
const circuitBreakerOk = isTradingEnabled(tradingMode);
|
|
132
127
|
// Limits are OK if no limits configured OR all limits have capacity
|
|
133
128
|
const limitsOk = limits.length === 0 || limits.every((l) => l.maxIn > 0n && l.maxOut > 0n);
|
|
@@ -147,11 +142,55 @@ export class TradingService {
|
|
|
147
142
|
* @returns The rate feed ID address
|
|
148
143
|
*/
|
|
149
144
|
async getPoolRateFeedId(pool) {
|
|
150
|
-
const rateFeedId = await this.
|
|
145
|
+
const [rateFeedId] = await this.getPoolRateFeedIds([pool]);
|
|
146
|
+
return rateFeedId;
|
|
147
|
+
}
|
|
148
|
+
async getTradingModesForPools(pools) {
|
|
149
|
+
const rateFeedIds = await this.getPoolRateFeedIds(pools);
|
|
150
|
+
return this.getTradingModesForRateFeeds(rateFeedIds);
|
|
151
|
+
}
|
|
152
|
+
async getPoolRateFeedIds(pools) {
|
|
153
|
+
if (pools.length === 0) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const results = await multicall(this.publicClient, pools.map((pool) => ({
|
|
151
157
|
address: pool.poolAddr,
|
|
152
158
|
abi: FPMM_ABI,
|
|
153
159
|
functionName: 'referenceRateFeedID',
|
|
160
|
+
args: [],
|
|
161
|
+
})), { allowFailure: false });
|
|
162
|
+
return results.map((result) => {
|
|
163
|
+
if (result.status === 'failure') {
|
|
164
|
+
throw result.error;
|
|
165
|
+
}
|
|
166
|
+
return result.result;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async getTradingModesForRateFeeds(rateFeedIds) {
|
|
170
|
+
if (rateFeedIds.length === 0) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const breakerBoxAddr = getContractAddress(this.chainId, 'BreakerBox');
|
|
174
|
+
const uniqueRateFeeds = Array.from(new Map(rateFeedIds.map((rateFeedId) => [rateFeedId.toLowerCase(), rateFeedId])).values());
|
|
175
|
+
const results = await multicall(this.publicClient, uniqueRateFeeds.map((rateFeedId) => ({
|
|
176
|
+
address: breakerBoxAddr,
|
|
177
|
+
abi: BREAKERBOX_ABI,
|
|
178
|
+
functionName: 'getRateFeedTradingMode',
|
|
179
|
+
args: [rateFeedId],
|
|
180
|
+
})), { allowFailure: false });
|
|
181
|
+
const tradingModes = new Map();
|
|
182
|
+
for (const [index, result] of results.entries()) {
|
|
183
|
+
if (result.status === 'failure') {
|
|
184
|
+
throw result.error;
|
|
185
|
+
}
|
|
186
|
+
tradingModes.set(uniqueRateFeeds[index].toLowerCase(), Number(result.result));
|
|
187
|
+
}
|
|
188
|
+
return rateFeedIds.map((rateFeedId) => {
|
|
189
|
+
const tradingMode = tradingModes.get(rateFeedId.toLowerCase());
|
|
190
|
+
if (tradingMode === undefined) {
|
|
191
|
+
throw new Error(`Trading mode not found for rate feed ${rateFeedId}`);
|
|
192
|
+
}
|
|
193
|
+
return tradingMode;
|
|
154
194
|
});
|
|
155
|
-
return rateFeedId;
|
|
156
195
|
}
|
|
157
196
|
}
|
|
@@ -42,6 +42,12 @@ const monadTestnet = defineChain({
|
|
|
42
42
|
url: 'https://testnet.monadexplorer.com',
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
|
+
contracts: {
|
|
46
|
+
multicall3: {
|
|
47
|
+
address: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
|
48
|
+
blockCreated: 251449,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
45
51
|
testnet: true,
|
|
46
52
|
});
|
|
47
53
|
const monad = defineChain({
|
|
@@ -63,6 +69,12 @@ const monad = defineChain({
|
|
|
63
69
|
url: 'https://monadvision.com',
|
|
64
70
|
},
|
|
65
71
|
},
|
|
72
|
+
contracts: {
|
|
73
|
+
multicall3: {
|
|
74
|
+
address: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
|
75
|
+
blockCreated: 9248132,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
66
78
|
});
|
|
67
79
|
/**
|
|
68
80
|
* Get the default RPC URL for a given chain ID
|