@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.
Files changed (57) 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 +80 -40
  13. package/dist/esm/services/pools/poolDiscovery.js +3 -2
  14. package/dist/esm/services/quotes/QuoteService.js +22 -20
  15. package/dist/esm/services/routes/RouteService.js +82 -24
  16. package/dist/esm/services/swap/SwapService.js +81 -37
  17. package/dist/esm/services/tokens/tokenService.js +142 -29
  18. package/dist/esm/services/trading/TradingService.js +51 -12
  19. package/dist/esm/utils/chainConfig.js +12 -0
  20. package/dist/esm/utils/costUtils.js +8 -9
  21. package/dist/esm/utils/multicall.js +42 -0
  22. package/dist/index.d.ts +11 -1
  23. package/dist/index.js +25 -9
  24. package/dist/services/borrow/internal/borrowReadService.d.ts +1 -0
  25. package/dist/services/borrow/internal/borrowReadService.js +63 -34
  26. package/dist/services/borrow/internal/borrowRegistryReader.js +45 -28
  27. package/dist/services/index.d.ts +1 -0
  28. package/dist/services/index.js +1 -0
  29. package/dist/services/liquidity/LiquidityService.d.ts +3 -1
  30. package/dist/services/liquidity/LiquidityService.js +6 -0
  31. package/dist/services/liquidity/liquidityHelpers.d.ts +6 -0
  32. package/dist/services/liquidity/liquidityHelpers.js +27 -0
  33. package/dist/services/liquidity/zapHelpers.js +20 -24
  34. package/dist/services/liquidity/zapIn.d.ts +2 -1
  35. package/dist/services/liquidity/zapIn.js +67 -73
  36. package/dist/services/liquidity/zapOut.d.ts +2 -10
  37. package/dist/services/liquidity/zapOut.js +90 -77
  38. package/dist/services/pools/PoolService.d.ts +5 -0
  39. package/dist/services/pools/PoolService.js +116 -21
  40. package/dist/services/pools/poolDetails.d.ts +2 -0
  41. package/dist/services/pools/poolDetails.js +82 -40
  42. package/dist/services/pools/poolDiscovery.js +3 -2
  43. package/dist/services/quotes/QuoteService.d.ts +1 -0
  44. package/dist/services/quotes/QuoteService.js +24 -21
  45. package/dist/services/routes/RouteService.d.ts +8 -0
  46. package/dist/services/routes/RouteService.js +82 -24
  47. package/dist/services/swap/SwapService.d.ts +19 -0
  48. package/dist/services/swap/SwapService.js +81 -37
  49. package/dist/services/tokens/tokenService.d.ts +7 -0
  50. package/dist/services/tokens/tokenService.js +142 -29
  51. package/dist/services/trading/TradingService.d.ts +3 -0
  52. package/dist/services/trading/TradingService.js +51 -12
  53. package/dist/utils/chainConfig.js +12 -0
  54. package/dist/utils/costUtils.js +8 -9
  55. package/dist/utils/multicall.d.ts +30 -0
  56. package/dist/utils/multicall.js +47 -0
  57. 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 Promise.all(tokenAddresses.map((addr) => this.fetchTokenSymbol(addr)));
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
- if (cached) {
100
- // Try to load from static cache
101
- try {
102
- const cachedRoutes = await this.loadCachedRoutes();
103
- if (cachedRoutes.length > 0) {
104
- return cachedRoutes;
105
- }
106
- }
107
- catch {
108
- // Cache miss or corrupt - silently fall through to fresh generation
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
- // Generate fresh routes from blockchain
112
- return this.generateFreshRoutes(returnAllRoutes);
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
- // Get all tradable routes
138
- const allRoutes = await this.getRoutes(options);
139
- const t0 = tokenIn.toLowerCase();
140
- const t1 = tokenOut.toLowerCase();
141
- // Search for matching route (bidirectional)
142
- const matchingRoute = allRoutes.find((route) => {
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.validateAmountIn(amountIn);
59
- // Validate all address inputs
60
- validateAddress(tokenIn, 'tokenIn');
61
- validateAddress(tokenOut, 'tokenOut');
62
- validateAddress(recipient, 'recipient');
63
- validateAddress(owner, 'owner');
64
- // Build swap params first
65
- const swap = await this.buildSwapParams(tokenIn, tokenOut, amountIn, recipient, options, route);
66
- // Check if approval is needed
67
- const currentAllowance = await this.getAllowance(tokenIn, owner);
68
- const approval = currentAllowance < amountIn ? this.buildApprovalParams(tokenIn, amountIn) : null;
69
- return { approval, swap };
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.validateAmountIn(amountIn);
105
- const deadline = options.deadline;
106
- if (deadline <= BigInt(Date.now()) / 1000n) {
107
- throw new Error('Deadline must be in the future');
108
- }
109
- // Validate all address inputs
110
- validateAddress(tokenIn, 'tokenIn');
111
- validateAddress(tokenOut, 'tokenOut');
112
- validateAddress(recipient, 'recipient');
113
- // Find route if not provided
114
- if (!route) {
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
- to: routerAddress,
125
- data,
126
- value: '0',
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
- deadline,
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
- // Fetch metadata and totalSupply for all tokens concurrently
92
- const tokens = await Promise.all(tokenAddresses.map(async (address) => {
93
- const [metadata, totalSupply] = await Promise.all([
94
- this.getTokenMetadata(address),
95
- includeSupply ? this.getTotalSupply(address) : Promise.resolve('0'),
96
- ]);
97
- return {
98
- address,
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 assets = await Promise.all(collateralAddresses.map(async (address) => {
129
- const metadata = await this.getTokenMetadata(address);
130
- return { address, ...metadata };
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
- // Check which tokens are collateral assets and get their info in parallel
155
- const results = await Promise.all(Array.from(uniqueAddresses).map(async (address) => {
156
- const [isCollateral, metadata] = await Promise.all([
157
- retryOperation(() => this.publicClient.readContract({
158
- address: reserveAddress,
159
- abi: RESERVE_ABI,
160
- functionName: 'isCollateralAsset',
161
- args: [address],
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 null;
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
- // Get rate feed IDs for each pool in the path and check trading modes
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 rateFeedChecks.every((isEnabled) => isEnabled);
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 [rateFeedId, limits] = await Promise.all([
127
- this.getPoolRateFeedId(pool),
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.publicClient.readContract({
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