@steerprotocol/sdk 1.29.3 → 1.30.1
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/cjs/base/VaultClient.js +459 -78
- package/dist/cjs/base/VaultClient.js.map +1 -1
- package/dist/cjs/utils/SubgraphVaultClient.js +80 -6
- package/dist/cjs/utils/SubgraphVaultClient.js.map +1 -1
- package/dist/cjs/utils/subgraph-types.js.map +1 -1
- package/dist/esm/base/VaultClient.js +460 -79
- package/dist/esm/base/VaultClient.js.map +1 -1
- package/dist/esm/utils/SubgraphVaultClient.js +80 -6
- package/dist/esm/utils/SubgraphVaultClient.js.map +1 -1
- package/dist/esm/utils/subgraph-types.js.map +1 -1
- package/dist/types/base/VaultClient.d.ts +60 -1
- package/dist/types/base/VaultClient.d.ts.map +1 -1
- package/dist/types/utils/SubgraphVaultClient.d.ts +6 -0
- package/dist/types/utils/SubgraphVaultClient.d.ts.map +1 -1
- package/dist/types/utils/subgraph-types.d.ts +9 -0
- package/dist/types/utils/subgraph-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/base/VaultClient.protocol-filter.test.ts +179 -0
- package/src/__tests__/base/VaultClient.test.ts +4 -3
- package/src/base/VaultClient.ts +568 -83
- package/src/utils/SubgraphVaultClient.ts +88 -6
- package/src/utils/subgraph-types.ts +10 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createClient } from '@steerprotocol/api-sdk';
|
|
2
|
-
import { chainIdToName, getBeaconNameByProtocol } from '../const';
|
|
2
|
+
import { chainIdToName, getBeaconNameByProtocol, getProtocolTypeByBeacon } from '../const';
|
|
3
|
+
import { getAmmConfig } from '../const/amm/configs/ammConfig.js';
|
|
3
4
|
import { getProtocolsForChainId } from '../const/amm/utils/protocol';
|
|
4
5
|
import { ChainId, MultiPositionManagers, Protocol } from '../const/chain';
|
|
5
6
|
import { steerSubgraphConfig } from '../const/subgraph';
|
|
@@ -145,8 +146,95 @@ export class VaultClient extends SubgraphClient {
|
|
|
145
146
|
this.subgraphVaultClient = new SubgraphVaultClient();
|
|
146
147
|
this.subgraphStudioKey = subgraphStudioKey || '';
|
|
147
148
|
}
|
|
149
|
+
normalizeProtocolValue(value) {
|
|
150
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
151
|
+
}
|
|
152
|
+
resolveProtocolEnum(protocol) {
|
|
153
|
+
if (!protocol) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const normalizedProtocol = this.normalizeProtocolValue(protocol);
|
|
157
|
+
const matchedProtocol = Object.values(Protocol).find(protocolValue => this.normalizeProtocolValue(protocolValue) === normalizedProtocol);
|
|
158
|
+
return matchedProtocol || null;
|
|
159
|
+
}
|
|
160
|
+
getProtocolBeaconNames(protocol) {
|
|
161
|
+
const resolvedProtocol = this.resolveProtocolEnum(protocol);
|
|
162
|
+
if (!resolvedProtocol) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const protocolConfig = getAmmConfig(this.subgraphStudioKey)[resolvedProtocol];
|
|
166
|
+
const beaconNames = [];
|
|
167
|
+
if (protocolConfig?.beaconContract) {
|
|
168
|
+
beaconNames.push(protocolConfig.beaconContract);
|
|
169
|
+
}
|
|
170
|
+
if (protocolConfig?.beaconContractSushiManaged) {
|
|
171
|
+
beaconNames.push(protocolConfig.beaconContractSushiManaged);
|
|
172
|
+
}
|
|
173
|
+
if (resolvedProtocol === Protocol.Blackhole) {
|
|
174
|
+
beaconNames.push(MultiPositionManagers.MultiPositionBlackholeOld);
|
|
175
|
+
}
|
|
176
|
+
const beaconAlias = getBeaconNameByProtocol(resolvedProtocol);
|
|
177
|
+
if (beaconAlias) {
|
|
178
|
+
beaconNames.push(beaconAlias);
|
|
179
|
+
}
|
|
180
|
+
return [...new Set(beaconNames.filter(name => name.length > 0))];
|
|
181
|
+
}
|
|
182
|
+
getPrimaryProtocolBeaconName(protocol) {
|
|
183
|
+
const resolvedProtocol = this.resolveProtocolEnum(protocol);
|
|
184
|
+
if (!resolvedProtocol) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const protocolConfig = getAmmConfig(this.subgraphStudioKey)[resolvedProtocol];
|
|
188
|
+
return protocolConfig?.beaconContract || null;
|
|
189
|
+
}
|
|
190
|
+
buildApiVaultFilter(filter) {
|
|
191
|
+
if (!filter) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const { protocol, ...rest } = filter;
|
|
195
|
+
const apiFilter = { ...rest };
|
|
196
|
+
if (!apiFilter.beaconName && protocol) {
|
|
197
|
+
const primaryBeaconName = this.getPrimaryProtocolBeaconName(protocol);
|
|
198
|
+
if (primaryBeaconName) {
|
|
199
|
+
apiFilter.beaconName = primaryBeaconName;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return apiFilter;
|
|
203
|
+
}
|
|
204
|
+
vaultMatchesProtocolFilter(vault, protocolFilter, resolvedProtocol) {
|
|
205
|
+
const normalizedFilter = this.normalizeProtocolValue(protocolFilter);
|
|
206
|
+
const normalizedBeaconName = this.normalizeProtocolValue(vault.beaconName);
|
|
207
|
+
const resolvedVaultProtocol = getProtocolTypeByBeacon(vault.beaconName);
|
|
208
|
+
const directCandidates = [vault.protocol, vault.protocolBaseType, resolvedVaultProtocol || ''];
|
|
209
|
+
const hasDirectMatch = directCandidates.some(candidate => {
|
|
210
|
+
if (!candidate) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return this.normalizeProtocolValue(candidate) === normalizedFilter;
|
|
214
|
+
});
|
|
215
|
+
if (hasDirectMatch) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
const expectedBeacon = resolvedProtocol
|
|
219
|
+
? getBeaconNameByProtocol(resolvedProtocol)
|
|
220
|
+
: protocolFilter;
|
|
221
|
+
const normalizedExpectedBeacon = this.normalizeProtocolValue(expectedBeacon);
|
|
222
|
+
return normalizedExpectedBeacon.length > 0 && normalizedBeaconName.includes(normalizedExpectedBeacon);
|
|
223
|
+
}
|
|
224
|
+
applyProtocolFilter(vaultsConnection, protocol) {
|
|
225
|
+
if (!protocol) {
|
|
226
|
+
return vaultsConnection;
|
|
227
|
+
}
|
|
228
|
+
const resolvedProtocol = this.resolveProtocolEnum(protocol);
|
|
229
|
+
const filteredEdges = vaultsConnection.edges.filter(edge => this.vaultMatchesProtocolFilter(edge.node, protocol, resolvedProtocol));
|
|
230
|
+
return {
|
|
231
|
+
...vaultsConnection,
|
|
232
|
+
edges: filteredEdges
|
|
233
|
+
};
|
|
234
|
+
}
|
|
148
235
|
/**
|
|
149
236
|
* Gets vaults with pagination support
|
|
237
|
+
* Fetches ALL data from both API (database) and subgraph in parallel, merges without duplicates, then paginates
|
|
150
238
|
* @param filter - Optional filter criteria
|
|
151
239
|
* @param first - Number of items to fetch (default: 50)
|
|
152
240
|
* @param after - Cursor for pagination (null for first page)
|
|
@@ -181,90 +269,388 @@ export class VaultClient extends SubgraphClient {
|
|
|
181
269
|
* ```
|
|
182
270
|
*/
|
|
183
271
|
async getVaults(filter, first = 50, after) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
endCursor: response.data.vaults.pageInfo.endCursor ?? null
|
|
238
|
-
},
|
|
239
|
-
totalCount: response.data.vaults.totalCount
|
|
240
|
-
};
|
|
272
|
+
const apiFilter = this.buildApiVaultFilter(filter);
|
|
273
|
+
// Fetch ALL vaults from both sources in parallel (no pagination at source level)
|
|
274
|
+
const [apiResult, subgraphResult] = await Promise.allSettled([
|
|
275
|
+
this.getAllVaultsFromApi(apiFilter),
|
|
276
|
+
filter?.chainId !== ChainId.Avalanche ? this.getAllVaultsFromSubgraph(filter) : Promise.reject(new Error('Avalanche not supported'))
|
|
277
|
+
]);
|
|
278
|
+
// Extract successful results
|
|
279
|
+
const apiVaults = apiResult.status === 'fulfilled' && apiResult.value.success && apiResult.value.data
|
|
280
|
+
? apiResult.value.data
|
|
281
|
+
: [];
|
|
282
|
+
const subgraphVaults = subgraphResult.status === 'fulfilled' && subgraphResult.value.success && subgraphResult.value.data
|
|
283
|
+
? subgraphResult.value.data
|
|
284
|
+
: [];
|
|
285
|
+
// If both failed, return error
|
|
286
|
+
if (apiVaults.length === 0 && subgraphVaults.length === 0) {
|
|
287
|
+
const apiError = apiResult.status === 'rejected' ? apiResult.reason : null;
|
|
288
|
+
const subgraphError = subgraphResult.status === 'rejected' ? subgraphResult.reason : null;
|
|
289
|
+
return {
|
|
290
|
+
data: null,
|
|
291
|
+
status: 500,
|
|
292
|
+
success: false,
|
|
293
|
+
error: `Both API and subgraph failed. API: ${apiError?.message || 'Unknown error'}. Subgraph: ${subgraphError?.message || 'Unknown error'}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Merge results and remove duplicates based on vaultAddress
|
|
297
|
+
const mergedVaults = this.mergeVaultResults(apiVaults, subgraphVaults);
|
|
298
|
+
// Apply protocol filter to merged results
|
|
299
|
+
const filteredVaults = mergedVaults.filter(edge => !filter?.protocol || this.vaultMatchesProtocolFilter(edge.node, filter.protocol, this.resolveProtocolEnum(filter.protocol)));
|
|
300
|
+
// Apply pagination to the complete merged and filtered dataset
|
|
301
|
+
const paginatedVaults = this.paginateVaults(filteredVaults, first, after);
|
|
302
|
+
return {
|
|
303
|
+
data: paginatedVaults,
|
|
304
|
+
status: 200,
|
|
305
|
+
success: true
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Fetches ALL vaults from API (database) by auto-paginating
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
async getAllVaultsFromApi(apiFilter) {
|
|
313
|
+
try {
|
|
314
|
+
const allVaults = [];
|
|
315
|
+
let hasNextPage = true;
|
|
316
|
+
let cursor = null;
|
|
317
|
+
const batchSize = 100; // Fetch in batches of 100
|
|
318
|
+
while (hasNextPage) {
|
|
319
|
+
const response = await this.getVaultsFromApi(apiFilter, batchSize, cursor);
|
|
320
|
+
if (!response.success || !response.data) {
|
|
321
|
+
// If we already have some vaults, return them; otherwise return error
|
|
322
|
+
if (allVaults.length > 0) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
241
325
|
return {
|
|
242
|
-
data:
|
|
326
|
+
data: null,
|
|
243
327
|
status: response.status,
|
|
244
|
-
success:
|
|
328
|
+
success: false,
|
|
329
|
+
error: response.error || 'Failed to fetch vaults from API'
|
|
245
330
|
};
|
|
246
331
|
}
|
|
332
|
+
allVaults.push(...response.data.edges);
|
|
333
|
+
hasNextPage = response.data.pageInfo.hasNextPage;
|
|
334
|
+
cursor = response.data.pageInfo.endCursor;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
data: allVaults,
|
|
338
|
+
status: 200,
|
|
339
|
+
success: true
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.warn('Failed to fetch all vaults from API:', error);
|
|
344
|
+
return {
|
|
345
|
+
data: null,
|
|
346
|
+
status: 500,
|
|
347
|
+
success: false,
|
|
348
|
+
error: error instanceof Error ? error.message : 'Failed to fetch all vaults from API'
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Fetches vaults from API (database) with pagination
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
async getVaultsFromApi(apiFilter, first = 50, after) {
|
|
357
|
+
try {
|
|
358
|
+
const response = await this.apiClient.vaults({
|
|
359
|
+
filter: apiFilter,
|
|
360
|
+
first,
|
|
361
|
+
after
|
|
362
|
+
});
|
|
363
|
+
if (!response.data?.vaults) {
|
|
364
|
+
return {
|
|
365
|
+
data: null,
|
|
366
|
+
status: response.status,
|
|
367
|
+
success: false,
|
|
368
|
+
error: 'No data returned from API'
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Transform the response to match our interface
|
|
372
|
+
const transformedData = {
|
|
373
|
+
edges: response.data.vaults.edges.map(edge => ({
|
|
374
|
+
cursor: edge.cursor,
|
|
375
|
+
node: {
|
|
376
|
+
id: edge.node.id,
|
|
377
|
+
chainId: edge.node.chainId,
|
|
378
|
+
vaultAddress: edge.node.vaultAddress,
|
|
379
|
+
protocol: edge.node.protocol,
|
|
380
|
+
beaconName: edge.node.beaconName,
|
|
381
|
+
protocolBaseType: edge.node.protocolBaseType,
|
|
382
|
+
name: edge.node.name || '',
|
|
383
|
+
feeApr: edge.node.feeApr || undefined,
|
|
384
|
+
stakingApr: edge.node.stakingApr || undefined,
|
|
385
|
+
merklApr: edge.node.merklApr || undefined,
|
|
386
|
+
pool: {
|
|
387
|
+
id: edge.node.pool?.id || '',
|
|
388
|
+
poolAddress: edge.node.pool?.poolAddress || '',
|
|
389
|
+
feeTier: edge.node.pool?.feeTier || '',
|
|
390
|
+
tick: undefined, // Not available in API response
|
|
391
|
+
liquidity: undefined, // Not available in API response
|
|
392
|
+
volumeUSD: undefined, // Not available in API response
|
|
393
|
+
totalValueLockedUSD: undefined // Not available in API response
|
|
394
|
+
},
|
|
395
|
+
token0: {
|
|
396
|
+
id: edge.node.token0?.id || '',
|
|
397
|
+
symbol: edge.node.token0?.symbol || '',
|
|
398
|
+
name: edge.node.token0?.name || '',
|
|
399
|
+
decimals: edge.node.token0?.decimals || 0,
|
|
400
|
+
address: edge.node.token0?.address || '',
|
|
401
|
+
chainId: edge.node.token0?.chainId || 0
|
|
402
|
+
},
|
|
403
|
+
token1: {
|
|
404
|
+
id: edge.node.token1?.id || '',
|
|
405
|
+
symbol: edge.node.token1?.symbol || '',
|
|
406
|
+
name: edge.node.token1?.name || '',
|
|
407
|
+
decimals: edge.node.token1?.decimals || 0,
|
|
408
|
+
address: edge.node.token1?.address || '',
|
|
409
|
+
chainId: edge.node.token1?.chainId || 0
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
})),
|
|
413
|
+
pageInfo: {
|
|
414
|
+
hasNextPage: response.data.vaults.pageInfo.hasNextPage,
|
|
415
|
+
endCursor: response.data.vaults.pageInfo.endCursor ?? null
|
|
416
|
+
},
|
|
417
|
+
totalCount: response.data.vaults.totalCount
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
data: transformedData,
|
|
421
|
+
status: response.status,
|
|
422
|
+
success: true
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
console.warn('API client failed:', error);
|
|
427
|
+
return {
|
|
428
|
+
data: null,
|
|
429
|
+
status: 500,
|
|
430
|
+
success: false,
|
|
431
|
+
error: error instanceof Error ? error.message : 'API request failed'
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Merges vault results from API and subgraph, removing duplicates
|
|
437
|
+
* Prioritizes API data when duplicates are found
|
|
438
|
+
* Generates consistent cursors for the merged dataset
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
mergeVaultResults(apiVaults, subgraphVaults) {
|
|
442
|
+
const vaultMap = new Map();
|
|
443
|
+
// Add API vaults first (they take priority)
|
|
444
|
+
apiVaults.forEach(edge => {
|
|
445
|
+
const key = edge.node.vaultAddress.toLowerCase();
|
|
446
|
+
vaultMap.set(key, edge);
|
|
447
|
+
});
|
|
448
|
+
// Add subgraph vaults only if not already present
|
|
449
|
+
subgraphVaults.forEach(edge => {
|
|
450
|
+
const key = edge.node.vaultAddress.toLowerCase();
|
|
451
|
+
if (!vaultMap.has(key)) {
|
|
452
|
+
vaultMap.set(key, edge);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// Merge additional data from subgraph if available (like pool details)
|
|
456
|
+
const existing = vaultMap.get(key);
|
|
457
|
+
const merged = {
|
|
458
|
+
...existing,
|
|
459
|
+
node: {
|
|
460
|
+
...existing.node,
|
|
461
|
+
// Merge pool data - prefer subgraph data for pool details if API doesn't have it
|
|
462
|
+
pool: {
|
|
463
|
+
id: existing.node.pool.id || edge.node.pool.id,
|
|
464
|
+
poolAddress: existing.node.pool.poolAddress || edge.node.pool.poolAddress,
|
|
465
|
+
feeTier: existing.node.pool.feeTier || edge.node.pool.feeTier,
|
|
466
|
+
tick: existing.node.pool.tick || edge.node.pool.tick,
|
|
467
|
+
liquidity: existing.node.pool.liquidity || edge.node.pool.liquidity,
|
|
468
|
+
volumeUSD: existing.node.pool.volumeUSD || edge.node.pool.volumeUSD,
|
|
469
|
+
totalValueLockedUSD: existing.node.pool.totalValueLockedUSD || edge.node.pool.totalValueLockedUSD
|
|
470
|
+
},
|
|
471
|
+
// Prefer API APR data, but use subgraph if API doesn't have it
|
|
472
|
+
feeApr: existing.node.feeApr ?? edge.node.feeApr,
|
|
473
|
+
stakingApr: existing.node.stakingApr ?? edge.node.stakingApr,
|
|
474
|
+
merklApr: existing.node.merklApr ?? edge.node.merklApr,
|
|
475
|
+
// Subgraph-only fields: always take from subgraph when available
|
|
476
|
+
positions: existing.node.positions ?? edge.node.positions,
|
|
477
|
+
tickRange: existing.node.tickRange ?? edge.node.tickRange,
|
|
478
|
+
fees: existing.node.fees ?? edge.node.fees,
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
vaultMap.set(key, merged);
|
|
247
482
|
}
|
|
248
|
-
|
|
249
|
-
|
|
483
|
+
});
|
|
484
|
+
// Convert to array and regenerate cursors for consistent pagination
|
|
485
|
+
const mergedArray = Array.from(vaultMap.values());
|
|
486
|
+
return mergedArray.map((edge, index) => ({
|
|
487
|
+
...edge,
|
|
488
|
+
cursor: `merged_${edge.node.chainId}_${index}_${edge.node.vaultAddress.toLowerCase()}`
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Applies pagination to vault results
|
|
493
|
+
* @private
|
|
494
|
+
*/
|
|
495
|
+
paginateVaults(vaults, first, after) {
|
|
496
|
+
let startIndex = 0;
|
|
497
|
+
// If cursor is provided, find the starting position
|
|
498
|
+
if (after) {
|
|
499
|
+
const cursorIndex = vaults.findIndex(edge => edge.cursor === after);
|
|
500
|
+
if (cursorIndex !== -1) {
|
|
501
|
+
startIndex = cursorIndex + 1;
|
|
250
502
|
}
|
|
251
503
|
}
|
|
252
|
-
//
|
|
504
|
+
// Get the slice of vaults for this page
|
|
505
|
+
const paginatedEdges = vaults.slice(startIndex, startIndex + first);
|
|
506
|
+
const hasNextPage = startIndex + first < vaults.length;
|
|
507
|
+
const endCursor = paginatedEdges.length > 0 ? paginatedEdges[paginatedEdges.length - 1].cursor : null;
|
|
508
|
+
return {
|
|
509
|
+
edges: paginatedEdges,
|
|
510
|
+
pageInfo: {
|
|
511
|
+
hasNextPage,
|
|
512
|
+
endCursor
|
|
513
|
+
},
|
|
514
|
+
totalCount: vaults.length
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Fetches ALL vaults from subgraph (no pagination)
|
|
519
|
+
* @param filter - Optional filter criteria
|
|
520
|
+
* @returns Promise resolving to all vaults data from subgraph
|
|
521
|
+
* @private
|
|
522
|
+
*/
|
|
523
|
+
async getAllVaultsFromSubgraph(filter) {
|
|
253
524
|
try {
|
|
254
|
-
|
|
525
|
+
// Extract chainId from filter
|
|
526
|
+
const chainId = filter?.chainId;
|
|
527
|
+
if (!chainId) {
|
|
528
|
+
throw new Error('ChainId is required for subgraph');
|
|
529
|
+
}
|
|
530
|
+
// Get chain enum from chainId
|
|
531
|
+
const chain = chainIdToName(chainId);
|
|
532
|
+
if (!chain) {
|
|
533
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
534
|
+
}
|
|
535
|
+
// Get subgraph URL for this chain
|
|
536
|
+
const subgraphUrl = steerSubgraphConfig[chain];
|
|
537
|
+
if (!subgraphUrl) {
|
|
538
|
+
throw new Error(`No subgraph configured for chain: ${chain}`);
|
|
539
|
+
}
|
|
540
|
+
const beaconNames = this.getProtocolBeaconNames(filter?.protocol);
|
|
541
|
+
if (filter?.beaconName) {
|
|
542
|
+
beaconNames.push(filter.beaconName);
|
|
543
|
+
}
|
|
544
|
+
const uniqueBeaconNames = [...new Set(beaconNames.filter(name => name.length > 0))];
|
|
545
|
+
// Fetch all vaults from subgraph
|
|
546
|
+
const subgraphVaults = await this.subgraphVaultClient.getAllVaultsFromSubgraph({
|
|
547
|
+
subgraphUrl,
|
|
548
|
+
chainId,
|
|
549
|
+
showDeprecated: false,
|
|
550
|
+
showCurrentProtocol: uniqueBeaconNames.length > 0,
|
|
551
|
+
beaconNames: uniqueBeaconNames
|
|
552
|
+
});
|
|
553
|
+
// Get all supported protocols for this chain
|
|
554
|
+
const supportedProtocols = getProtocolsForChainId(chainId, this.subgraphStudioKey);
|
|
555
|
+
// Fetch APR data for all protocols in parallel
|
|
556
|
+
const aprPromises = supportedProtocols.map(protocol => this.getAprs({ chainId, protocol }).catch(error => {
|
|
557
|
+
console.warn(`Failed to fetch APR for protocol ${protocol}:`, error);
|
|
558
|
+
return { success: false, data: null };
|
|
559
|
+
}));
|
|
560
|
+
const aprResults = await Promise.all(aprPromises);
|
|
561
|
+
// Create a map of vault address to APR for quick lookup
|
|
562
|
+
const aprMap = new Map();
|
|
563
|
+
aprResults && aprResults.forEach(aprResult => {
|
|
564
|
+
if (aprResult.success && aprResult.data) {
|
|
565
|
+
aprResult?.data?.vaults && aprResult.data.vaults.forEach(vaultApr => {
|
|
566
|
+
aprMap.set(vaultApr.vaultAddress.toLowerCase(), vaultApr.apr.apr);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
// Transform to VaultEdge array with APR data
|
|
571
|
+
const vaultEdges = subgraphVaults.map((vault, index) => {
|
|
572
|
+
// Parse positions and compute tick range
|
|
573
|
+
const positions = vault.positions?.map(pos => ({
|
|
574
|
+
id: pos.id,
|
|
575
|
+
upperTick: parseInt(pos.upperTick),
|
|
576
|
+
lowerTick: parseInt(pos.lowerTick),
|
|
577
|
+
relativeWeight: pos.relativeWeight
|
|
578
|
+
}));
|
|
579
|
+
// Compute tick range from positions
|
|
580
|
+
let tickRange;
|
|
581
|
+
if (positions && positions.length > 0) {
|
|
582
|
+
const lowerTicks = positions.map(p => p.lowerTick);
|
|
583
|
+
const upperTicks = positions.map(p => p.upperTick);
|
|
584
|
+
tickRange = {
|
|
585
|
+
minLowerTick: Math.min(...lowerTicks),
|
|
586
|
+
maxUpperTick: Math.max(...upperTicks)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
cursor: `subgraph_${chainId}_${index}`,
|
|
591
|
+
node: {
|
|
592
|
+
id: vault.id,
|
|
593
|
+
chainId: chainId,
|
|
594
|
+
vaultAddress: vault.id,
|
|
595
|
+
protocol: vault.beaconName || '',
|
|
596
|
+
beaconName: vault.beaconName || '',
|
|
597
|
+
protocolBaseType: vault.beaconName || '',
|
|
598
|
+
name: `${vault.token0Symbol}/${vault.token1Symbol}`,
|
|
599
|
+
feeApr: aprMap.get(vault.id.toLowerCase()),
|
|
600
|
+
stakingApr: undefined,
|
|
601
|
+
merklApr: undefined,
|
|
602
|
+
positions,
|
|
603
|
+
tickRange,
|
|
604
|
+
fees: vault.fees0 && vault.fees1 ? {
|
|
605
|
+
fees0: vault.fees0,
|
|
606
|
+
fees1: vault.fees1
|
|
607
|
+
} : undefined,
|
|
608
|
+
pool: {
|
|
609
|
+
id: vault.pool || '',
|
|
610
|
+
poolAddress: vault.pool || '',
|
|
611
|
+
feeTier: vault.feeTier || '',
|
|
612
|
+
tick: undefined,
|
|
613
|
+
liquidity: undefined,
|
|
614
|
+
volumeUSD: undefined,
|
|
615
|
+
totalValueLockedUSD: undefined
|
|
616
|
+
},
|
|
617
|
+
token0: {
|
|
618
|
+
id: vault.token0,
|
|
619
|
+
symbol: vault.token0Symbol,
|
|
620
|
+
name: vault.token0Symbol, // Use symbol as name since subgraph doesn't provide name
|
|
621
|
+
decimals: parseInt(vault.token0Decimals) || 18,
|
|
622
|
+
address: vault.token0,
|
|
623
|
+
chainId: chainId
|
|
624
|
+
},
|
|
625
|
+
token1: {
|
|
626
|
+
id: vault.token1,
|
|
627
|
+
symbol: vault.token1Symbol,
|
|
628
|
+
name: vault.token1Symbol, // Use symbol as name since subgraph doesn't provide name
|
|
629
|
+
decimals: parseInt(vault.token1Decimals) || 18,
|
|
630
|
+
address: vault.token1,
|
|
631
|
+
chainId: chainId
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
data: vaultEdges,
|
|
638
|
+
status: 200,
|
|
639
|
+
success: true
|
|
640
|
+
};
|
|
255
641
|
}
|
|
256
|
-
catch (
|
|
257
|
-
console.error('
|
|
642
|
+
catch (error) {
|
|
643
|
+
console.error('Subgraph vault fetch failed:', error);
|
|
258
644
|
return {
|
|
259
645
|
data: null,
|
|
260
646
|
status: 500,
|
|
261
647
|
success: false,
|
|
262
|
-
error:
|
|
648
|
+
error: error instanceof Error ? error.message : 'Failed to fetch vaults from subgraph'
|
|
263
649
|
};
|
|
264
650
|
}
|
|
265
651
|
}
|
|
266
652
|
/**
|
|
267
|
-
* Fallback method to fetch vaults from subgraph
|
|
653
|
+
* Fallback method to fetch vaults from subgraph with pagination
|
|
268
654
|
* @param filter - Optional filter criteria
|
|
269
655
|
* @param first - Number of items to fetch (default: 50)
|
|
270
656
|
* @param after - Cursor for pagination (null for first page)
|
|
@@ -288,24 +674,18 @@ export class VaultClient extends SubgraphClient {
|
|
|
288
674
|
if (!subgraphUrl) {
|
|
289
675
|
throw new Error(`No subgraph configured for chain: ${chain}`);
|
|
290
676
|
}
|
|
291
|
-
const beaconNames =
|
|
292
|
-
if (filter?.protocol) {
|
|
293
|
-
const beacon = getBeaconNameByProtocol(filter.protocol);
|
|
294
|
-
beaconNames.push(beacon);
|
|
295
|
-
if (filter?.protocol === Protocol.Blackhole) {
|
|
296
|
-
beaconNames.push(MultiPositionManagers.MultiPositionBlackholeOld);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
677
|
+
const beaconNames = this.getProtocolBeaconNames(filter?.protocol);
|
|
299
678
|
if (filter?.beaconName) {
|
|
300
679
|
beaconNames.push(filter.beaconName);
|
|
301
680
|
}
|
|
681
|
+
const uniqueBeaconNames = [...new Set(beaconNames.filter(name => name.length > 0))];
|
|
302
682
|
// Fetch all vaults from subgraph
|
|
303
683
|
const subgraphVaults = await this.subgraphVaultClient.getAllVaultsFromSubgraph({
|
|
304
684
|
subgraphUrl,
|
|
305
685
|
chainId,
|
|
306
686
|
showDeprecated: false,
|
|
307
|
-
showCurrentProtocol:
|
|
308
|
-
beaconNames:
|
|
687
|
+
showCurrentProtocol: uniqueBeaconNames.length > 0,
|
|
688
|
+
beaconNames: uniqueBeaconNames
|
|
309
689
|
});
|
|
310
690
|
// Get all supported protocols for this chain
|
|
311
691
|
const supportedProtocols = getProtocolsForChainId(chainId, this.subgraphStudioKey);
|
|
@@ -336,8 +716,9 @@ export class VaultClient extends SubgraphClient {
|
|
|
336
716
|
}
|
|
337
717
|
}));
|
|
338
718
|
}
|
|
719
|
+
const filteredVaultsConnection = this.applyProtocolFilter(vaultsConnection, filter?.protocol);
|
|
339
720
|
return {
|
|
340
|
-
data:
|
|
721
|
+
data: filteredVaultsConnection,
|
|
341
722
|
status: 200,
|
|
342
723
|
success: true
|
|
343
724
|
};
|