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