@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.
@@ -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
- if (filter?.chainId !== ChainId.Avalanche) {
425
- try {
426
- // First try the API client
427
- const response = await this.apiClient.vaults({
428
- filter,
429
- first,
430
- after
431
- });
432
-
433
- if (response.data?.vaults) {
434
- // Transform the response to match our interface
435
- const transformedData: VaultsConnection = {
436
- edges: response.data.vaults.edges.map(edge => ({
437
- cursor: edge.cursor,
438
- node: {
439
- id: edge.node.id,
440
- chainId: edge.node.chainId,
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: transformedData,
636
+ data: null,
485
637
  status: response.status,
486
- success: true
638
+ success: false,
639
+ error: response.error || 'Failed to fetch vaults from API'
487
640
  };
488
641
  }
489
- } catch (apiError) {
490
- console.warn('API client failed, falling back to subgraph:', apiError);
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
- // Fallback to subgraph if API fails or returns no data
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
- return await this.getVaultsFromSubgraph(filter, first, after);
497
- } catch (subgraphError) {
498
- console.error('Both API and subgraph failed:', subgraphError);
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: subgraphError instanceof Error ? subgraphError.message : 'Both API and subgraph requests failed'
749
+ error: error instanceof Error ? error.message : 'API request failed'
504
750
  };
505
751
  }
506
752
  }
507
753
 
508
754
  /**
509
- * Fallback method to fetch vaults from subgraph
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: false,
563
- beaconNames: 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: vaultsConnection,
1095
+ data: filteredVaultsConnection,
611
1096
  status: 200,
612
1097
  success: true
613
1098
  };