@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
package/src/base/VaultClient.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createClient } from '@steerprotocol/api-sdk';
|
|
2
2
|
import { Pool as PoolV3 } from "@uniswap/v3-sdk";
|
|
3
3
|
import type { Address, Hash, PublicClient, WalletClient } from 'viem';
|
|
4
|
-
import { chainIdToName, getBeaconNameByProtocol } from '../const';
|
|
4
|
+
import { chainIdToName, getBeaconNameByProtocol, getProtocolTypeByBeacon } from '../const';
|
|
5
|
+
import { getAmmConfig } from '../const/amm/configs/ammConfig.js';
|
|
5
6
|
import { getProtocolsForChainId } from '../const/amm/utils/protocol';
|
|
6
7
|
import { ChainId, MultiPositionManagers, Protocol } from '../const/chain';
|
|
7
8
|
import { steerSubgraphConfig } from '../const/subgraph';
|
|
@@ -104,6 +105,23 @@ export interface VaultNode {
|
|
|
104
105
|
feeApr?: number;
|
|
105
106
|
stakingApr?: number;
|
|
106
107
|
merklApr?: number;
|
|
108
|
+
/** Raw positions from the subgraph. Only available when sourced from subgraph. */
|
|
109
|
+
positions?: {
|
|
110
|
+
id: string;
|
|
111
|
+
upperTick: number;
|
|
112
|
+
lowerTick: number;
|
|
113
|
+
relativeWeight: string;
|
|
114
|
+
}[];
|
|
115
|
+
/** Computed tick range across all current positions (min lower / max upper). Only available when sourced from subgraph. */
|
|
116
|
+
tickRange?: {
|
|
117
|
+
minLowerTick: number;
|
|
118
|
+
maxUpperTick: number;
|
|
119
|
+
};
|
|
120
|
+
/** Cumulative fees collected by the vault. Only available when sourced from subgraph. */
|
|
121
|
+
fees?: {
|
|
122
|
+
fees0: string;
|
|
123
|
+
fees1: string;
|
|
124
|
+
};
|
|
107
125
|
pool: {
|
|
108
126
|
poolAddress: string;
|
|
109
127
|
id: string;
|
|
@@ -202,6 +220,13 @@ export interface VaultFilter {
|
|
|
202
220
|
beaconName?: string;
|
|
203
221
|
}
|
|
204
222
|
|
|
223
|
+
// API-specific filter that excludes protocol field
|
|
224
|
+
export interface ApiVaultFilter {
|
|
225
|
+
chainId?: number;
|
|
226
|
+
isActive?: boolean;
|
|
227
|
+
beaconName?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
205
230
|
export interface TokenFilter {
|
|
206
231
|
chainId?: number;
|
|
207
232
|
symbol?: string;
|
|
@@ -378,8 +403,128 @@ export class VaultClient extends SubgraphClient {
|
|
|
378
403
|
this.subgraphStudioKey = subgraphStudioKey || '';
|
|
379
404
|
}
|
|
380
405
|
|
|
406
|
+
private normalizeProtocolValue(value: string): string {
|
|
407
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private resolveProtocolEnum(protocol?: string): Protocol | null {
|
|
411
|
+
if (!protocol) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const normalizedProtocol = this.normalizeProtocolValue(protocol);
|
|
416
|
+
const matchedProtocol = Object.values(Protocol).find(
|
|
417
|
+
protocolValue => this.normalizeProtocolValue(protocolValue) === normalizedProtocol
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
return matchedProtocol || null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private getProtocolBeaconNames(protocol?: string): string[] {
|
|
424
|
+
const resolvedProtocol = this.resolveProtocolEnum(protocol);
|
|
425
|
+
if (!resolvedProtocol) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const protocolConfig = getAmmConfig(this.subgraphStudioKey)[resolvedProtocol];
|
|
430
|
+
const beaconNames: string[] = [];
|
|
431
|
+
|
|
432
|
+
if (protocolConfig?.beaconContract) {
|
|
433
|
+
beaconNames.push(protocolConfig.beaconContract);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (protocolConfig?.beaconContractSushiManaged) {
|
|
437
|
+
beaconNames.push(protocolConfig.beaconContractSushiManaged);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (resolvedProtocol === Protocol.Blackhole) {
|
|
441
|
+
beaconNames.push(MultiPositionManagers.MultiPositionBlackholeOld);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const beaconAlias = getBeaconNameByProtocol(resolvedProtocol);
|
|
445
|
+
if (beaconAlias) {
|
|
446
|
+
beaconNames.push(beaconAlias);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return [...new Set(beaconNames.filter(name => name.length > 0))];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private getPrimaryProtocolBeaconName(protocol?: string): string | null {
|
|
453
|
+
const resolvedProtocol = this.resolveProtocolEnum(protocol);
|
|
454
|
+
if (!resolvedProtocol) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const protocolConfig = getAmmConfig(this.subgraphStudioKey)[resolvedProtocol];
|
|
459
|
+
return protocolConfig?.beaconContract || null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private buildApiVaultFilter(filter?: VaultFilter): ApiVaultFilter | undefined {
|
|
463
|
+
if (!filter) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const { protocol, ...rest } = filter;
|
|
468
|
+
const apiFilter: ApiVaultFilter = { ...rest };
|
|
469
|
+
|
|
470
|
+
if (!apiFilter.beaconName && protocol) {
|
|
471
|
+
const primaryBeaconName = this.getPrimaryProtocolBeaconName(protocol);
|
|
472
|
+
if (primaryBeaconName) {
|
|
473
|
+
apiFilter.beaconName = primaryBeaconName;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return apiFilter;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private vaultMatchesProtocolFilter(
|
|
481
|
+
vault: VaultNode,
|
|
482
|
+
protocolFilter: string,
|
|
483
|
+
resolvedProtocol: Protocol | null
|
|
484
|
+
): boolean {
|
|
485
|
+
const normalizedFilter = this.normalizeProtocolValue(protocolFilter);
|
|
486
|
+
const normalizedBeaconName = this.normalizeProtocolValue(vault.beaconName);
|
|
487
|
+
const resolvedVaultProtocol = getProtocolTypeByBeacon(vault.beaconName);
|
|
488
|
+
|
|
489
|
+
const directCandidates = [vault.protocol, vault.protocolBaseType, resolvedVaultProtocol || ''];
|
|
490
|
+
const hasDirectMatch = directCandidates.some(candidate => {
|
|
491
|
+
if (!candidate) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return this.normalizeProtocolValue(candidate) === normalizedFilter;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
if (hasDirectMatch) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const expectedBeacon = resolvedProtocol
|
|
502
|
+
? getBeaconNameByProtocol(resolvedProtocol)
|
|
503
|
+
: protocolFilter;
|
|
504
|
+
const normalizedExpectedBeacon = this.normalizeProtocolValue(expectedBeacon);
|
|
505
|
+
|
|
506
|
+
return normalizedExpectedBeacon.length > 0 && normalizedBeaconName.includes(normalizedExpectedBeacon);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private applyProtocolFilter(vaultsConnection: VaultsConnection, protocol?: string): VaultsConnection {
|
|
510
|
+
if (!protocol) {
|
|
511
|
+
return vaultsConnection;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const resolvedProtocol = this.resolveProtocolEnum(protocol);
|
|
515
|
+
const filteredEdges = vaultsConnection.edges.filter(edge =>
|
|
516
|
+
this.vaultMatchesProtocolFilter(edge.node, protocol, resolvedProtocol)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
...vaultsConnection,
|
|
521
|
+
edges: filteredEdges
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
381
525
|
/**
|
|
382
526
|
* Gets vaults with pagination support
|
|
527
|
+
* Fetches ALL data from both API (database) and subgraph in parallel, merges without duplicates, then paginates
|
|
383
528
|
* @param filter - Optional filter criteria
|
|
384
529
|
* @param first - Number of items to fetch (default: 50)
|
|
385
530
|
* @param after - Cursor for pagination (null for first page)
|
|
@@ -418,95 +563,440 @@ export class VaultClient extends SubgraphClient {
|
|
|
418
563
|
first: number = 50,
|
|
419
564
|
after?: string | null
|
|
420
565
|
): Promise<SteerResponse<VaultsConnection>> {
|
|
421
|
-
|
|
566
|
+
const apiFilter = this.buildApiVaultFilter(filter);
|
|
567
|
+
|
|
568
|
+
// Fetch ALL vaults from both sources in parallel (no pagination at source level)
|
|
569
|
+
const [apiResult, subgraphResult] = await Promise.allSettled([
|
|
570
|
+
this.getAllVaultsFromApi(apiFilter),
|
|
571
|
+
filter?.chainId !== ChainId.Avalanche ? this.getAllVaultsFromSubgraph(filter) : Promise.reject(new Error('Avalanche not supported'))
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
// Extract successful results
|
|
575
|
+
const apiVaults: VaultEdge[] = apiResult.status === 'fulfilled' && apiResult.value.success && apiResult.value.data
|
|
576
|
+
? apiResult.value.data
|
|
577
|
+
: [];
|
|
422
578
|
|
|
579
|
+
const subgraphVaults: VaultEdge[] = subgraphResult.status === 'fulfilled' && subgraphResult.value.success && subgraphResult.value.data
|
|
580
|
+
? subgraphResult.value.data
|
|
581
|
+
: [];
|
|
582
|
+
|
|
583
|
+
// If both failed, return error
|
|
584
|
+
if (apiVaults.length === 0 && subgraphVaults.length === 0) {
|
|
585
|
+
const apiError = apiResult.status === 'rejected' ? apiResult.reason : null;
|
|
586
|
+
const subgraphError = subgraphResult.status === 'rejected' ? subgraphResult.reason : null;
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
data: null,
|
|
590
|
+
status: 500,
|
|
591
|
+
success: false,
|
|
592
|
+
error: `Both API and subgraph failed. API: ${apiError?.message || 'Unknown error'}. Subgraph: ${subgraphError?.message || 'Unknown error'}`
|
|
593
|
+
};
|
|
594
|
+
}
|
|
423
595
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
vaultAddress: edge.node.vaultAddress,
|
|
442
|
-
protocol: edge.node.protocol,
|
|
443
|
-
beaconName: edge.node.beaconName,
|
|
444
|
-
protocolBaseType: edge.node.protocolBaseType,
|
|
445
|
-
name: edge.node.name || '',
|
|
446
|
-
feeApr: edge.node.feeApr || undefined,
|
|
447
|
-
stakingApr: edge.node.stakingApr || undefined,
|
|
448
|
-
merklApr: edge.node.merklApr || undefined,
|
|
449
|
-
pool: {
|
|
450
|
-
id: edge.node.pool?.id || '',
|
|
451
|
-
poolAddress: edge.node.pool?.poolAddress || '',
|
|
452
|
-
feeTier: edge.node.pool?.feeTier || '',
|
|
453
|
-
tick: undefined, // Not available in API response
|
|
454
|
-
liquidity: undefined, // Not available in API response
|
|
455
|
-
volumeUSD: undefined, // Not available in API response
|
|
456
|
-
totalValueLockedUSD: undefined // Not available in API response
|
|
457
|
-
},
|
|
458
|
-
token0: {
|
|
459
|
-
id: edge.node.token0?.id || '',
|
|
460
|
-
symbol: edge.node.token0?.symbol || '',
|
|
461
|
-
name: edge.node.token0?.name || '',
|
|
462
|
-
decimals: edge.node.token0?.decimals || 0,
|
|
463
|
-
address: edge.node.token0?.address || '',
|
|
464
|
-
chainId: edge.node.token0?.chainId || 0
|
|
465
|
-
},
|
|
466
|
-
token1: {
|
|
467
|
-
id: edge.node.token1?.id || '',
|
|
468
|
-
symbol: edge.node.token1?.symbol || '',
|
|
469
|
-
name: edge.node.token1?.name || '',
|
|
470
|
-
decimals: edge.node.token1?.decimals || 0,
|
|
471
|
-
address: edge.node.token1?.address || '',
|
|
472
|
-
chainId: edge.node.token1?.chainId || 0
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
})),
|
|
476
|
-
pageInfo: {
|
|
477
|
-
hasNextPage: response.data.vaults.pageInfo.hasNextPage,
|
|
478
|
-
endCursor: response.data.vaults.pageInfo.endCursor ?? null
|
|
479
|
-
},
|
|
480
|
-
totalCount: response.data.vaults.totalCount
|
|
481
|
-
};
|
|
596
|
+
// Merge results and remove duplicates based on vaultAddress
|
|
597
|
+
const mergedVaults = this.mergeVaultResults(apiVaults, subgraphVaults);
|
|
598
|
+
|
|
599
|
+
// Apply protocol filter to merged results
|
|
600
|
+
const filteredVaults = mergedVaults.filter(edge =>
|
|
601
|
+
!filter?.protocol || this.vaultMatchesProtocolFilter(edge.node, filter.protocol, this.resolveProtocolEnum(filter.protocol))
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
// Apply pagination to the complete merged and filtered dataset
|
|
605
|
+
const paginatedVaults = this.paginateVaults(filteredVaults, first, after);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
data: paginatedVaults,
|
|
609
|
+
status: 200,
|
|
610
|
+
success: true
|
|
611
|
+
};
|
|
612
|
+
}
|
|
482
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Fetches ALL vaults from API (database) by auto-paginating
|
|
616
|
+
* @private
|
|
617
|
+
*/
|
|
618
|
+
private async getAllVaultsFromApi(
|
|
619
|
+
apiFilter?: ApiVaultFilter
|
|
620
|
+
): Promise<SteerResponse<VaultEdge[]>> {
|
|
621
|
+
try {
|
|
622
|
+
const allVaults: VaultEdge[] = [];
|
|
623
|
+
let hasNextPage = true;
|
|
624
|
+
let cursor: string | null = null;
|
|
625
|
+
const batchSize = 100; // Fetch in batches of 100
|
|
626
|
+
|
|
627
|
+
while (hasNextPage) {
|
|
628
|
+
const response = await this.getVaultsFromApi(apiFilter, batchSize, cursor);
|
|
629
|
+
|
|
630
|
+
if (!response.success || !response.data) {
|
|
631
|
+
// If we already have some vaults, return them; otherwise return error
|
|
632
|
+
if (allVaults.length > 0) {
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
483
635
|
return {
|
|
484
|
-
data:
|
|
636
|
+
data: null,
|
|
485
637
|
status: response.status,
|
|
486
|
-
success:
|
|
638
|
+
success: false,
|
|
639
|
+
error: response.error || 'Failed to fetch vaults from API'
|
|
487
640
|
};
|
|
488
641
|
}
|
|
489
|
-
|
|
490
|
-
|
|
642
|
+
|
|
643
|
+
allVaults.push(...response.data.edges);
|
|
644
|
+
hasNextPage = response.data.pageInfo.hasNextPage;
|
|
645
|
+
cursor = response.data.pageInfo.endCursor;
|
|
491
646
|
}
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
data: allVaults,
|
|
650
|
+
status: 200,
|
|
651
|
+
success: true
|
|
652
|
+
};
|
|
653
|
+
} catch (error) {
|
|
654
|
+
console.warn('Failed to fetch all vaults from API:', error);
|
|
655
|
+
return {
|
|
656
|
+
data: null,
|
|
657
|
+
status: 500,
|
|
658
|
+
success: false,
|
|
659
|
+
error: error instanceof Error ? error.message : 'Failed to fetch all vaults from API'
|
|
660
|
+
};
|
|
661
|
+
}
|
|
492
662
|
}
|
|
493
663
|
|
|
494
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Fetches vaults from API (database) with pagination
|
|
666
|
+
* @private
|
|
667
|
+
*/
|
|
668
|
+
private async getVaultsFromApi(
|
|
669
|
+
apiFilter?: ApiVaultFilter,
|
|
670
|
+
first: number = 50,
|
|
671
|
+
after?: string | null
|
|
672
|
+
): Promise<SteerResponse<VaultsConnection>> {
|
|
495
673
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
674
|
+
const response = await this.apiClient.vaults({
|
|
675
|
+
filter: apiFilter,
|
|
676
|
+
first,
|
|
677
|
+
after
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
if (!response.data?.vaults) {
|
|
681
|
+
return {
|
|
682
|
+
data: null,
|
|
683
|
+
status: response.status,
|
|
684
|
+
success: false,
|
|
685
|
+
error: 'No data returned from API'
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Transform the response to match our interface
|
|
690
|
+
const transformedData: VaultsConnection = {
|
|
691
|
+
edges: response.data.vaults.edges.map(edge => ({
|
|
692
|
+
cursor: edge.cursor,
|
|
693
|
+
node: {
|
|
694
|
+
id: edge.node.id,
|
|
695
|
+
chainId: edge.node.chainId,
|
|
696
|
+
vaultAddress: edge.node.vaultAddress,
|
|
697
|
+
protocol: edge.node.protocol,
|
|
698
|
+
beaconName: edge.node.beaconName,
|
|
699
|
+
protocolBaseType: edge.node.protocolBaseType,
|
|
700
|
+
name: edge.node.name || '',
|
|
701
|
+
feeApr: edge.node.feeApr || undefined,
|
|
702
|
+
stakingApr: edge.node.stakingApr || undefined,
|
|
703
|
+
merklApr: edge.node.merklApr || undefined,
|
|
704
|
+
pool: {
|
|
705
|
+
id: edge.node.pool?.id || '',
|
|
706
|
+
poolAddress: edge.node.pool?.poolAddress || '',
|
|
707
|
+
feeTier: edge.node.pool?.feeTier || '',
|
|
708
|
+
tick: undefined, // Not available in API response
|
|
709
|
+
liquidity: undefined, // Not available in API response
|
|
710
|
+
volumeUSD: undefined, // Not available in API response
|
|
711
|
+
totalValueLockedUSD: undefined // Not available in API response
|
|
712
|
+
},
|
|
713
|
+
token0: {
|
|
714
|
+
id: edge.node.token0?.id || '',
|
|
715
|
+
symbol: edge.node.token0?.symbol || '',
|
|
716
|
+
name: edge.node.token0?.name || '',
|
|
717
|
+
decimals: edge.node.token0?.decimals || 0,
|
|
718
|
+
address: edge.node.token0?.address || '',
|
|
719
|
+
chainId: edge.node.token0?.chainId || 0
|
|
720
|
+
},
|
|
721
|
+
token1: {
|
|
722
|
+
id: edge.node.token1?.id || '',
|
|
723
|
+
symbol: edge.node.token1?.symbol || '',
|
|
724
|
+
name: edge.node.token1?.name || '',
|
|
725
|
+
decimals: edge.node.token1?.decimals || 0,
|
|
726
|
+
address: edge.node.token1?.address || '',
|
|
727
|
+
chainId: edge.node.token1?.chainId || 0
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
})),
|
|
731
|
+
pageInfo: {
|
|
732
|
+
hasNextPage: response.data.vaults.pageInfo.hasNextPage,
|
|
733
|
+
endCursor: response.data.vaults.pageInfo.endCursor ?? null
|
|
734
|
+
},
|
|
735
|
+
totalCount: response.data.vaults.totalCount
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
data: transformedData,
|
|
740
|
+
status: response.status,
|
|
741
|
+
success: true
|
|
742
|
+
};
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.warn('API client failed:', error);
|
|
499
745
|
return {
|
|
500
746
|
data: null,
|
|
501
747
|
status: 500,
|
|
502
748
|
success: false,
|
|
503
|
-
error:
|
|
749
|
+
error: error instanceof Error ? error.message : 'API request failed'
|
|
504
750
|
};
|
|
505
751
|
}
|
|
506
752
|
}
|
|
507
753
|
|
|
508
754
|
/**
|
|
509
|
-
*
|
|
755
|
+
* Merges vault results from API and subgraph, removing duplicates
|
|
756
|
+
* Prioritizes API data when duplicates are found
|
|
757
|
+
* Generates consistent cursors for the merged dataset
|
|
758
|
+
* @private
|
|
759
|
+
*/
|
|
760
|
+
private mergeVaultResults(apiVaults: VaultEdge[], subgraphVaults: VaultEdge[]): VaultEdge[] {
|
|
761
|
+
const vaultMap = new Map<string, VaultEdge>();
|
|
762
|
+
|
|
763
|
+
// Add API vaults first (they take priority)
|
|
764
|
+
apiVaults.forEach(edge => {
|
|
765
|
+
const key = edge.node.vaultAddress.toLowerCase();
|
|
766
|
+
vaultMap.set(key, edge);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Add subgraph vaults only if not already present
|
|
770
|
+
subgraphVaults.forEach(edge => {
|
|
771
|
+
const key = edge.node.vaultAddress.toLowerCase();
|
|
772
|
+
if (!vaultMap.has(key)) {
|
|
773
|
+
vaultMap.set(key, edge);
|
|
774
|
+
} else {
|
|
775
|
+
// Merge additional data from subgraph if available (like pool details)
|
|
776
|
+
const existing = vaultMap.get(key)!;
|
|
777
|
+
const merged: VaultEdge = {
|
|
778
|
+
...existing,
|
|
779
|
+
node: {
|
|
780
|
+
...existing.node,
|
|
781
|
+
// Merge pool data - prefer subgraph data for pool details if API doesn't have it
|
|
782
|
+
pool: {
|
|
783
|
+
id: existing.node.pool.id || edge.node.pool.id,
|
|
784
|
+
poolAddress: existing.node.pool.poolAddress || edge.node.pool.poolAddress,
|
|
785
|
+
feeTier: existing.node.pool.feeTier || edge.node.pool.feeTier,
|
|
786
|
+
tick: existing.node.pool.tick || edge.node.pool.tick,
|
|
787
|
+
liquidity: existing.node.pool.liquidity || edge.node.pool.liquidity,
|
|
788
|
+
volumeUSD: existing.node.pool.volumeUSD || edge.node.pool.volumeUSD,
|
|
789
|
+
totalValueLockedUSD: existing.node.pool.totalValueLockedUSD || edge.node.pool.totalValueLockedUSD
|
|
790
|
+
},
|
|
791
|
+
// Prefer API APR data, but use subgraph if API doesn't have it
|
|
792
|
+
feeApr: existing.node.feeApr ?? edge.node.feeApr,
|
|
793
|
+
stakingApr: existing.node.stakingApr ?? edge.node.stakingApr,
|
|
794
|
+
merklApr: existing.node.merklApr ?? edge.node.merklApr,
|
|
795
|
+
// Subgraph-only fields: always take from subgraph when available
|
|
796
|
+
positions: existing.node.positions ?? edge.node.positions,
|
|
797
|
+
tickRange: existing.node.tickRange ?? edge.node.tickRange,
|
|
798
|
+
fees: existing.node.fees ?? edge.node.fees,
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
vaultMap.set(key, merged);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Convert to array and regenerate cursors for consistent pagination
|
|
806
|
+
const mergedArray = Array.from(vaultMap.values());
|
|
807
|
+
return mergedArray.map((edge, index) => ({
|
|
808
|
+
...edge,
|
|
809
|
+
cursor: `merged_${edge.node.chainId}_${index}_${edge.node.vaultAddress.toLowerCase()}`
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Applies pagination to vault results
|
|
815
|
+
* @private
|
|
816
|
+
*/
|
|
817
|
+
private paginateVaults(vaults: VaultEdge[], first: number, after?: string | null): VaultsConnection {
|
|
818
|
+
let startIndex = 0;
|
|
819
|
+
|
|
820
|
+
// If cursor is provided, find the starting position
|
|
821
|
+
if (after) {
|
|
822
|
+
const cursorIndex = vaults.findIndex(edge => edge.cursor === after);
|
|
823
|
+
if (cursorIndex !== -1) {
|
|
824
|
+
startIndex = cursorIndex + 1;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Get the slice of vaults for this page
|
|
829
|
+
const paginatedEdges = vaults.slice(startIndex, startIndex + first);
|
|
830
|
+
const hasNextPage = startIndex + first < vaults.length;
|
|
831
|
+
const endCursor = paginatedEdges.length > 0 ? paginatedEdges[paginatedEdges.length - 1].cursor : null;
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
edges: paginatedEdges,
|
|
835
|
+
pageInfo: {
|
|
836
|
+
hasNextPage,
|
|
837
|
+
endCursor
|
|
838
|
+
},
|
|
839
|
+
totalCount: vaults.length
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Fetches ALL vaults from subgraph (no pagination)
|
|
845
|
+
* @param filter - Optional filter criteria
|
|
846
|
+
* @returns Promise resolving to all vaults data from subgraph
|
|
847
|
+
* @private
|
|
848
|
+
*/
|
|
849
|
+
private async getAllVaultsFromSubgraph(
|
|
850
|
+
filter?: VaultFilter
|
|
851
|
+
): Promise<SteerResponse<VaultEdge[]>> {
|
|
852
|
+
try {
|
|
853
|
+
// Extract chainId from filter
|
|
854
|
+
const chainId = filter?.chainId;
|
|
855
|
+
if (!chainId) {
|
|
856
|
+
throw new Error('ChainId is required for subgraph');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Get chain enum from chainId
|
|
860
|
+
const chain = chainIdToName(chainId);
|
|
861
|
+
|
|
862
|
+
if (!chain) {
|
|
863
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Get subgraph URL for this chain
|
|
867
|
+
const subgraphUrl = steerSubgraphConfig[chain];
|
|
868
|
+
if (!subgraphUrl) {
|
|
869
|
+
throw new Error(`No subgraph configured for chain: ${chain}`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const beaconNames = this.getProtocolBeaconNames(filter?.protocol);
|
|
873
|
+
|
|
874
|
+
if (filter?.beaconName) {
|
|
875
|
+
beaconNames.push(filter.beaconName);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const uniqueBeaconNames = [...new Set(beaconNames.filter(name => name.length > 0))];
|
|
879
|
+
|
|
880
|
+
// Fetch all vaults from subgraph
|
|
881
|
+
const subgraphVaults = await this.subgraphVaultClient.getAllVaultsFromSubgraph({
|
|
882
|
+
subgraphUrl,
|
|
883
|
+
chainId,
|
|
884
|
+
showDeprecated: false,
|
|
885
|
+
showCurrentProtocol: uniqueBeaconNames.length > 0,
|
|
886
|
+
beaconNames: uniqueBeaconNames
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// Get all supported protocols for this chain
|
|
890
|
+
const supportedProtocols = getProtocolsForChainId(chainId, this.subgraphStudioKey);
|
|
891
|
+
|
|
892
|
+
// Fetch APR data for all protocols in parallel
|
|
893
|
+
const aprPromises = supportedProtocols.map(protocol =>
|
|
894
|
+
this.getAprs({ chainId, protocol }).catch(error => {
|
|
895
|
+
console.warn(`Failed to fetch APR for protocol ${protocol}:`, error);
|
|
896
|
+
return { success: false, data: null };
|
|
897
|
+
})
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
const aprResults = await Promise.all(aprPromises);
|
|
901
|
+
|
|
902
|
+
// Create a map of vault address to APR for quick lookup
|
|
903
|
+
const aprMap = new Map<string, number>();
|
|
904
|
+
aprResults && aprResults.forEach(aprResult => {
|
|
905
|
+
if (aprResult.success && aprResult.data) {
|
|
906
|
+
aprResult?.data?.vaults && aprResult.data.vaults.forEach(vaultApr => {
|
|
907
|
+
aprMap.set(vaultApr.vaultAddress.toLowerCase(), vaultApr.apr.apr);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Transform to VaultEdge array with APR data
|
|
913
|
+
const vaultEdges: VaultEdge[] = subgraphVaults.map((vault, index) => {
|
|
914
|
+
// Parse positions and compute tick range
|
|
915
|
+
const positions = vault.positions?.map(pos => ({
|
|
916
|
+
id: pos.id,
|
|
917
|
+
upperTick: parseInt(pos.upperTick),
|
|
918
|
+
lowerTick: parseInt(pos.lowerTick),
|
|
919
|
+
relativeWeight: pos.relativeWeight
|
|
920
|
+
}));
|
|
921
|
+
|
|
922
|
+
// Compute tick range from positions
|
|
923
|
+
let tickRange: { minLowerTick: number; maxUpperTick: number } | undefined;
|
|
924
|
+
if (positions && positions.length > 0) {
|
|
925
|
+
const lowerTicks = positions.map(p => p.lowerTick);
|
|
926
|
+
const upperTicks = positions.map(p => p.upperTick);
|
|
927
|
+
tickRange = {
|
|
928
|
+
minLowerTick: Math.min(...lowerTicks),
|
|
929
|
+
maxUpperTick: Math.max(...upperTicks)
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
cursor: `subgraph_${chainId}_${index}`,
|
|
935
|
+
node: {
|
|
936
|
+
id: vault.id,
|
|
937
|
+
chainId: chainId,
|
|
938
|
+
vaultAddress: vault.id,
|
|
939
|
+
protocol: vault.beaconName || '',
|
|
940
|
+
beaconName: vault.beaconName || '',
|
|
941
|
+
protocolBaseType: vault.beaconName || '',
|
|
942
|
+
name: `${vault.token0Symbol}/${vault.token1Symbol}`,
|
|
943
|
+
feeApr: aprMap.get(vault.id.toLowerCase()),
|
|
944
|
+
stakingApr: undefined,
|
|
945
|
+
merklApr: undefined,
|
|
946
|
+
positions,
|
|
947
|
+
tickRange,
|
|
948
|
+
fees: vault.fees0 && vault.fees1 ? {
|
|
949
|
+
fees0: vault.fees0,
|
|
950
|
+
fees1: vault.fees1
|
|
951
|
+
} : undefined,
|
|
952
|
+
pool: {
|
|
953
|
+
id: vault.pool || '',
|
|
954
|
+
poolAddress: vault.pool || '',
|
|
955
|
+
feeTier: vault.feeTier || '',
|
|
956
|
+
tick: undefined,
|
|
957
|
+
liquidity: undefined,
|
|
958
|
+
volumeUSD: undefined,
|
|
959
|
+
totalValueLockedUSD: undefined
|
|
960
|
+
},
|
|
961
|
+
token0: {
|
|
962
|
+
id: vault.token0,
|
|
963
|
+
symbol: vault.token0Symbol,
|
|
964
|
+
name: vault.token0Symbol, // Use symbol as name since subgraph doesn't provide name
|
|
965
|
+
decimals: parseInt(vault.token0Decimals) || 18,
|
|
966
|
+
address: vault.token0,
|
|
967
|
+
chainId: chainId
|
|
968
|
+
},
|
|
969
|
+
token1: {
|
|
970
|
+
id: vault.token1,
|
|
971
|
+
symbol: vault.token1Symbol,
|
|
972
|
+
name: vault.token1Symbol, // Use symbol as name since subgraph doesn't provide name
|
|
973
|
+
decimals: parseInt(vault.token1Decimals) || 18,
|
|
974
|
+
address: vault.token1,
|
|
975
|
+
chainId: chainId
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
return {
|
|
982
|
+
data: vaultEdges,
|
|
983
|
+
status: 200,
|
|
984
|
+
success: true
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
} catch (error) {
|
|
988
|
+
console.error('Subgraph vault fetch failed:', error);
|
|
989
|
+
return {
|
|
990
|
+
data: null,
|
|
991
|
+
status: 500,
|
|
992
|
+
success: false,
|
|
993
|
+
error: error instanceof Error ? error.message : 'Failed to fetch vaults from subgraph'
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Fallback method to fetch vaults from subgraph with pagination
|
|
510
1000
|
* @param filter - Optional filter criteria
|
|
511
1001
|
* @param first - Number of items to fetch (default: 50)
|
|
512
1002
|
* @param after - Cursor for pagination (null for first page)
|
|
@@ -539,28 +1029,21 @@ export class VaultClient extends SubgraphClient {
|
|
|
539
1029
|
throw new Error(`No subgraph configured for chain: ${chain}`);
|
|
540
1030
|
}
|
|
541
1031
|
|
|
542
|
-
const beaconNames =
|
|
543
|
-
|
|
544
|
-
if (filter?.protocol) {
|
|
545
|
-
const beacon = getBeaconNameByProtocol(filter.protocol as Protocol);
|
|
546
|
-
beaconNames.push(beacon);
|
|
547
|
-
|
|
548
|
-
if (filter?.protocol === Protocol.Blackhole) {
|
|
549
|
-
beaconNames.push(MultiPositionManagers.MultiPositionBlackholeOld);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
1032
|
+
const beaconNames = this.getProtocolBeaconNames(filter?.protocol);
|
|
552
1033
|
|
|
553
1034
|
if (filter?.beaconName) {
|
|
554
1035
|
beaconNames.push(filter.beaconName);
|
|
555
1036
|
}
|
|
556
1037
|
|
|
1038
|
+
const uniqueBeaconNames = [...new Set(beaconNames.filter(name => name.length > 0))];
|
|
1039
|
+
|
|
557
1040
|
// Fetch all vaults from subgraph
|
|
558
1041
|
const subgraphVaults = await this.subgraphVaultClient.getAllVaultsFromSubgraph({
|
|
559
1042
|
subgraphUrl,
|
|
560
1043
|
chainId,
|
|
561
1044
|
showDeprecated: false,
|
|
562
|
-
showCurrentProtocol:
|
|
563
|
-
beaconNames:
|
|
1045
|
+
showCurrentProtocol: uniqueBeaconNames.length > 0,
|
|
1046
|
+
beaconNames: uniqueBeaconNames
|
|
564
1047
|
});
|
|
565
1048
|
|
|
566
1049
|
// Get all supported protocols for this chain
|
|
@@ -606,8 +1089,10 @@ export class VaultClient extends SubgraphClient {
|
|
|
606
1089
|
}));
|
|
607
1090
|
}
|
|
608
1091
|
|
|
1092
|
+
const filteredVaultsConnection = this.applyProtocolFilter(vaultsConnection, filter?.protocol);
|
|
1093
|
+
|
|
609
1094
|
return {
|
|
610
|
-
data:
|
|
1095
|
+
data: filteredVaultsConnection,
|
|
611
1096
|
status: 200,
|
|
612
1097
|
success: true
|
|
613
1098
|
};
|