@marinade.finance/scoring 1.0.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/package.json +44 -0
- package/src/cluster/cluster.module.ts +8 -0
- package/src/cluster/cluster.service.ts +108 -0
- package/src/config/config.module.ts +8 -0
- package/src/config/config.service.ts +136 -0
- package/src/constants/marinade.json +38 -0
- package/src/dto/bonds.dto.ts +14 -0
- package/src/dto/cluster.dto.ts +13 -0
- package/src/dto/eligibility.dto.ts +43 -0
- package/src/dto/jito.dto.ts +10 -0
- package/src/dto/marinade.dto.ts +18 -0
- package/src/dto/rewards.dto.ts +21 -0
- package/src/dto/scoring.dto.ts +109 -0
- package/src/dto/snapshots.dto.ts +19 -0
- package/src/dto/stakes.dto.ts +35 -0
- package/src/dto/validators.dto.ts +146 -0
- package/src/eligibility/eligibility.module.ts +11 -0
- package/src/eligibility/eligibility.service.ts +183 -0
- package/src/errors/fetching.ts +25 -0
- package/src/interfaces/data-provider.interface.ts +17 -0
- package/src/providers/api-data.provider.ts +261 -0
- package/src/providers/file-data.provider.ts +125 -0
- package/src/scoring/scoring.module.ts +11 -0
- package/src/scoring/scoring.service.ts +184 -0
- package/src/stake/stake.module.ts +10 -0
- package/src/stake/stake.service.ts +242 -0
- package/src/utils/csv.ts +30 -0
- package/src/utils/maths.ts +75 -0
- package/src/utils/solana.ts +9 -0
- package/src/utils/zip.ts +12 -0
- package/src/validators/validators.module.ts +48 -0
- package/src/validators/validators.service.ts +177 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
|
|
2
|
+
export class WarningDto {
|
|
3
|
+
warning: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class EpochStatDto {
|
|
7
|
+
|
|
8
|
+
epoch: number;
|
|
9
|
+
|
|
10
|
+
epoch_start_at: string;
|
|
11
|
+
|
|
12
|
+
epoch_end_at?: string;
|
|
13
|
+
|
|
14
|
+
commission_max_observed?: number | null;
|
|
15
|
+
|
|
16
|
+
commission_min_observed?: number | null;
|
|
17
|
+
|
|
18
|
+
commission_advertised: number;
|
|
19
|
+
|
|
20
|
+
commission_effective?: number | null;
|
|
21
|
+
|
|
22
|
+
version: string;
|
|
23
|
+
|
|
24
|
+
activated_stake: string;
|
|
25
|
+
|
|
26
|
+
marinade_stake: string;
|
|
27
|
+
|
|
28
|
+
foundation_stake: string;
|
|
29
|
+
|
|
30
|
+
marinade_native_stake: string;
|
|
31
|
+
|
|
32
|
+
self_stake: string;
|
|
33
|
+
|
|
34
|
+
superminority: boolean;
|
|
35
|
+
|
|
36
|
+
stake_to_become_superminority: string;
|
|
37
|
+
|
|
38
|
+
credits: number;
|
|
39
|
+
|
|
40
|
+
leader_slots: number;
|
|
41
|
+
|
|
42
|
+
blocks_produced: number;
|
|
43
|
+
|
|
44
|
+
skip_rate: number;
|
|
45
|
+
|
|
46
|
+
uptime_pct?: number | null;
|
|
47
|
+
|
|
48
|
+
uptime?: number | null;
|
|
49
|
+
|
|
50
|
+
downtime?: number | null;
|
|
51
|
+
|
|
52
|
+
apr?: number | null;
|
|
53
|
+
|
|
54
|
+
apy?: number | null;
|
|
55
|
+
|
|
56
|
+
score: number;
|
|
57
|
+
|
|
58
|
+
rank_score: number;
|
|
59
|
+
|
|
60
|
+
rank_activated_stake: number;
|
|
61
|
+
|
|
62
|
+
rank_apy: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ValidatorDto {
|
|
66
|
+
identity: string;
|
|
67
|
+
vote_account: string;
|
|
68
|
+
start_epoch: number;
|
|
69
|
+
start_date: string;
|
|
70
|
+
info_name: string;
|
|
71
|
+
info_url: string;
|
|
72
|
+
info_keybase: string;
|
|
73
|
+
node_ip: string;
|
|
74
|
+
dc_coordinates_lat: number;
|
|
75
|
+
dc_coordinates_lon: number;
|
|
76
|
+
dc_continent: string;
|
|
77
|
+
dc_country_iso: string;
|
|
78
|
+
dc_country: string;
|
|
79
|
+
dc_city: string;
|
|
80
|
+
dc_full_city: string;
|
|
81
|
+
dc_asn: number;
|
|
82
|
+
dc_aso: string;
|
|
83
|
+
dcc_full_city: number;
|
|
84
|
+
dcc_asn: number;
|
|
85
|
+
dcc_aso: number;
|
|
86
|
+
commission_max_observed?: number | null;
|
|
87
|
+
commission_min_observed?: number | null;
|
|
88
|
+
commission_advertised: number;
|
|
89
|
+
commission_effective?: number | null;
|
|
90
|
+
commission_aggregated?: number | null;
|
|
91
|
+
rugged_commission_occurrences: number;
|
|
92
|
+
rugged_commission: boolean;
|
|
93
|
+
rugged_commission_info: string[];
|
|
94
|
+
version: string;
|
|
95
|
+
activated_stake: string;
|
|
96
|
+
marinade_stake: string;
|
|
97
|
+
foundation_stake: string;
|
|
98
|
+
marinade_native_stake: string;
|
|
99
|
+
self_stake: string;
|
|
100
|
+
superminority: boolean;
|
|
101
|
+
credits: number;
|
|
102
|
+
score: number;
|
|
103
|
+
warnings: WarningDto[];
|
|
104
|
+
epoch_stats: EpochStatDto[];
|
|
105
|
+
epochStats: Record<number, EpochStatDto>;
|
|
106
|
+
epochs_count: number;
|
|
107
|
+
has_last_epoch_stats: boolean;
|
|
108
|
+
avg_uptime_pct: number;
|
|
109
|
+
avg_apy: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class ValidatorAggregatedDto {
|
|
113
|
+
epoch: number;
|
|
114
|
+
epoch_start_date: string;
|
|
115
|
+
avg_marinade_score?: number | null;
|
|
116
|
+
avg_apy?: number | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class ValidatorsResponseDto {
|
|
120
|
+
validators: ValidatorDto[];
|
|
121
|
+
validators_aggregated: ValidatorAggregatedDto[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type AggregatedValidator = {
|
|
125
|
+
voteAccount: string;
|
|
126
|
+
name: string;
|
|
127
|
+
epochs: number[];
|
|
128
|
+
currentStake: number;
|
|
129
|
+
currentMarinadeStake: number;
|
|
130
|
+
commission: number[];
|
|
131
|
+
stake: number[];
|
|
132
|
+
externalStake: number[];
|
|
133
|
+
credits: number[];
|
|
134
|
+
blocksProduced: number[];
|
|
135
|
+
leaderSlots: number[];
|
|
136
|
+
dataAvailable: boolean[];
|
|
137
|
+
mevCommission: number;
|
|
138
|
+
country: string;
|
|
139
|
+
city: string;
|
|
140
|
+
aso: string;
|
|
141
|
+
blacklisted: boolean;
|
|
142
|
+
version: string;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type AggregatedValidators = Record<string, AggregatedValidator>
|
|
146
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { EligibilityService } from './eligibility.service';
|
|
3
|
+
import { ValidatorsModule } from '../validators/validators.module';
|
|
4
|
+
import { ConfigModule } from '../config/config.module';
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [ValidatorsModule, ConfigModule],
|
|
8
|
+
providers: [EligibilityService],
|
|
9
|
+
exports: [EligibilityService],
|
|
10
|
+
})
|
|
11
|
+
export class EligibilityModule {}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { ConfigService } from '../config/config.service';
|
|
3
|
+
import { Bonds } from '../dto/bonds.dto';
|
|
4
|
+
import { ClusterInfo } from '../dto/cluster.dto';
|
|
5
|
+
import { EligibilityConfig, EvaluationResult, Issue, IssueType, ValidatorEligibility, ValidatorsEligibilities } from '../dto/eligibility.dto';
|
|
6
|
+
import { ScoreDto, Scores } from '../dto/scoring.dto';
|
|
7
|
+
import { AggregatedValidator, AggregatedValidators } from '../dto/validators.dto';
|
|
8
|
+
import { ValidatorsService } from '../validators/validators.service';
|
|
9
|
+
|
|
10
|
+
export class EligibilityService {
|
|
11
|
+
|
|
12
|
+
constructor (private validatorsService: ValidatorsService, private configService: ConfigService) {}
|
|
13
|
+
|
|
14
|
+
evalLowCreditsIssue (validator: AggregatedValidator, epochIndex: number, eligibilityConfig: EligibilityConfig, clusterInfo: ClusterInfo): Issue | null {
|
|
15
|
+
if (!validator.epochs[epochIndex]) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const targetCredits = clusterInfo.targetCreditsByEpoch.get(validator.epochs[epochIndex]!);
|
|
20
|
+
if (!targetCredits) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const actualCredits = validator.credits[epochIndex] || 0;
|
|
25
|
+
const creditsPct = Math.round((100 * actualCredits) / targetCredits);
|
|
26
|
+
|
|
27
|
+
if (creditsPct < eligibilityConfig.voteCreditsLow) {
|
|
28
|
+
return { type: IssueType.VOTE_CREDITS_LOW, message: `Credits @ ${creditsPct}%` };
|
|
29
|
+
} else if (creditsPct < eligibilityConfig.voteCreditsWarning) {
|
|
30
|
+
return { type: IssueType.VOTE_CREDITS_WARNING, message: `(Warning) Credits @ ${creditsPct}%` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
evalLowExternalStakeIssue (validator: AggregatedValidator, epochIndex: number, eligibilityConfig: EligibilityConfig): Issue | undefined {
|
|
37
|
+
const externalStake = validator.externalStake[epochIndex];
|
|
38
|
+
|
|
39
|
+
if (!externalStake) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (externalStake < eligibilityConfig.minExternalStake) {
|
|
44
|
+
return { type: IssueType.EXTERNAL_STAKE, message: `External stake: ${externalStake}` };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
evalBlacklistIssue (validator: AggregatedValidator): Issue | undefined {
|
|
50
|
+
if (validator.blacklisted) {
|
|
51
|
+
return { type: IssueType.BLACKLIST, message: 'Validator is blacklisted' };
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
evalHighCommissionIssue (validator: AggregatedValidator, epochIndex: number, eligibilityConfig: EligibilityConfig): Issue | undefined {
|
|
57
|
+
const commission = validator.commission[epochIndex];
|
|
58
|
+
|
|
59
|
+
if (!commission) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (eligibilityConfig.maxCommission < commission!) {
|
|
64
|
+
return { type: IssueType.COMMISSION, message: `Commission: ${commission}%` };
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
evalScoreIssue (epochIndex: number, eligibilityConfig: EligibilityConfig, { concentrationScore }: ScoreDto): Issue | undefined {
|
|
70
|
+
if (epochIndex === 0 && concentrationScore < eligibilityConfig.minScore) {
|
|
71
|
+
return { type: IssueType.SCORE, message: `Concentration Score: ${concentrationScore}` };
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
evalBondIssue (validator: AggregatedValidator, bonds: Bonds): Issue | undefined {
|
|
77
|
+
if (!(validator.voteAccount in bonds)) {
|
|
78
|
+
return { type: IssueType.NO_BOND, message: 'Validator has no bond' };
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getValidatorIssuesInEpoch (validator: AggregatedValidator, bonds: Bonds, epochIndex: number, eligibilityConfig: EligibilityConfig, clusterInfo: ClusterInfo, score: ScoreDto): Issue[] {
|
|
84
|
+
if (!validator.dataAvailable[epochIndex]) {
|
|
85
|
+
return [{ type: IssueType.NO_DATA, message: `No data for validator in epoch ${validator.epochs[epochIndex]}` }];
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
this.evalBlacklistIssue(validator),
|
|
89
|
+
this.evalLowCreditsIssue(validator, epochIndex, eligibilityConfig, clusterInfo),
|
|
90
|
+
this.evalLowExternalStakeIssue(validator, epochIndex, eligibilityConfig),
|
|
91
|
+
this.evalHighCommissionIssue(validator, epochIndex, eligibilityConfig),
|
|
92
|
+
this.evalScoreIssue(epochIndex, eligibilityConfig, score),
|
|
93
|
+
this.evalBondIssue(validator, bonds),
|
|
94
|
+
].filter(Boolean) as Issue[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
evalIssuesInEpoch (issues: Issue[]): EvaluationResult {
|
|
98
|
+
let criticals = 0;
|
|
99
|
+
let warnings = 0;
|
|
100
|
+
|
|
101
|
+
const criticalTypes = new Set([
|
|
102
|
+
IssueType.NO_BOND,
|
|
103
|
+
IssueType.NO_DATA,
|
|
104
|
+
IssueType.BLACKLIST,
|
|
105
|
+
IssueType.COMMISSION,
|
|
106
|
+
IssueType.EXTERNAL_STAKE,
|
|
107
|
+
IssueType.CENTRALIZATION,
|
|
108
|
+
IssueType.SCORE,
|
|
109
|
+
IssueType.VOTE_CREDITS_LOW,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const warningTypes = new Set([
|
|
113
|
+
IssueType.VOTE_CREDITS_WARNING,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
issues.forEach(issue => {
|
|
117
|
+
if (criticalTypes.has(issue.type)) {
|
|
118
|
+
criticals++;
|
|
119
|
+
} else if (warningTypes.has(issue.type)) {
|
|
120
|
+
warnings++;
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error(`Unexpected issue: ${issue.type}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { criticals, warnings };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
evalIssues (issuesCollection: Issue[][], epochs: number): EvaluationResult {
|
|
130
|
+
let totalCriticals = 0;
|
|
131
|
+
let totalWarnings = 0;
|
|
132
|
+
for (let epochIndex = 0; epochIndex < epochs; epochIndex++) {
|
|
133
|
+
if (!issuesCollection[epochIndex]) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const { criticals, warnings } = this.evalIssuesInEpoch(issuesCollection[epochIndex]!);
|
|
137
|
+
totalCriticals += criticals;
|
|
138
|
+
totalWarnings += warnings;
|
|
139
|
+
}
|
|
140
|
+
return { criticals: totalCriticals, warnings: totalWarnings };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isBasicEligible (issuesCollection: Issue[][], eligibilityConfig: EligibilityConfig): boolean {
|
|
144
|
+
const { criticals, warnings } = this.evalIssues(issuesCollection, eligibilityConfig.basicEligibilityEpochs + 1);
|
|
145
|
+
return criticals === 0 && warnings <= eligibilityConfig.maxWarnings;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
isBonusEligible (issuesCollection: Issue[][], eligibilityConfig: EligibilityConfig): boolean {
|
|
149
|
+
const totalEpochs = eligibilityConfig.basicEligibilityEpochs + eligibilityConfig.bonusEligibilityExtraEpochs + 1;
|
|
150
|
+
const { criticals, warnings } = this.evalIssues(issuesCollection, totalEpochs);
|
|
151
|
+
return criticals === 0 && warnings <= eligibilityConfig.maxWarnings;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getIssuesCollection (clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, bonds: Bonds, score: ScoreDto, eligibilityConfig: EligibilityConfig): Issue[][] {
|
|
155
|
+
return Array.from({ length: eligibilityConfig.basicEligibilityEpochs + eligibilityConfig.bonusEligibilityExtraEpochs + 1 }, (_, epochIndex) =>
|
|
156
|
+
this.getValidatorIssuesInEpoch(aggregatedValidator, bonds, epochIndex, eligibilityConfig, clusterInfo, score)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
computeValidatorEligibility (clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, bonds: Bonds, scores: ScoreDto, eligibilityConfig: EligibilityConfig): ValidatorEligibility {
|
|
161
|
+
const issuesCollection = this.getIssuesCollection(clusterInfo, aggregatedValidator, bonds, scores, eligibilityConfig);
|
|
162
|
+
const minExternalStake = this.validatorsService.selectExternalStakeMin(aggregatedValidator, eligibilityConfig.basicEligibilityEpochs);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
basicEligibility: this.isBasicEligible(issuesCollection, eligibilityConfig),
|
|
166
|
+
bonusEligibility: this.isBonusEligible(issuesCollection, eligibilityConfig),
|
|
167
|
+
issuesCollection,
|
|
168
|
+
capFromBond: this.configService.getScoringConfig().CAP_FROM_BOND,
|
|
169
|
+
capFromExternalStake: minExternalStake * (eligibilityConfig.maxStakeShare / (1 - eligibilityConfig.maxStakeShare)),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
computeValidatorsEligibilities (clusterInfo: ClusterInfo, scores: Scores, aggregatedValidators: AggregatedValidators, bonds: Bonds, eligibilityConfig: EligibilityConfig): ValidatorsEligibilities {
|
|
174
|
+
const result: ValidatorsEligibilities = {};
|
|
175
|
+
for (const [voteAccount, validator] of Object.entries(aggregatedValidators)) {
|
|
176
|
+
if (!scores[voteAccount]) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
result[voteAccount] = this.computeValidatorEligibility(clusterInfo, validator, bonds, scores[voteAccount]!, eligibilityConfig);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class FetchError extends Error {
|
|
2
|
+
constructor (message: string, public statusCode: number) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'FetchError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const FETCHING_ERROR_MESSAGES = {
|
|
9
|
+
generalValidatorsFetchFailed: 'An error occured while fetching validators data',
|
|
10
|
+
generalTvlFetchFailed: 'An error occured while fetching validators data',
|
|
11
|
+
generalBondsFetchFailed: 'An error occured while fetching bonds data',
|
|
12
|
+
generalRewardsFetchFailed: 'An error occured while fetching rewards',
|
|
13
|
+
generalVemndeFetchFailed: 'An error occured while fetching veMNDE votes',
|
|
14
|
+
generalMsolFetchFailed: 'An error occured while fetching mSOL votes',
|
|
15
|
+
generalJitoMEVFetchFailed: 'An error occured while fetching Jito MEV',
|
|
16
|
+
generalBlacklistFetchFailed: 'An error occured while fetching blacklist',
|
|
17
|
+
validatorsFetchFailed: 'Failed to fetch validators data',
|
|
18
|
+
tvlFetchFailed: 'Failed to fetch tvl data',
|
|
19
|
+
bondsFetchFailed: 'Failed to fetch bonds data',
|
|
20
|
+
vemndeFetchFailed: 'Failed to fetch veMNDE votes',
|
|
21
|
+
msolFetchFailed: 'Failed to fetch mSOL votes',
|
|
22
|
+
rewardsFetchFailed: 'Failed to fetch rewards',
|
|
23
|
+
jitoMevFetchFailed: 'Failed to fetch Jito MEV',
|
|
24
|
+
blacklistFetchFailed: 'Failed to fetch blacklist'
|
|
25
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidatorDto } from '../dto/validators.dto';
|
|
2
|
+
import { Rewards } from '../dto/rewards.dto';
|
|
3
|
+
import { Votes } from '../dto/snapshots.dto';
|
|
4
|
+
import { JitoValidatorDto } from '../dto/jito.dto';
|
|
5
|
+
import { Bonds } from '../dto/bonds.dto';
|
|
6
|
+
import { TvlStats } from '../dto/marinade.dto';
|
|
7
|
+
|
|
8
|
+
export interface IDataProvider {
|
|
9
|
+
fetchValidators(epochsToFetch: number, withSnapshot: boolean): Promise<ValidatorDto[]>;
|
|
10
|
+
fetchBlacklist(withSnapshot: boolean): Promise<Set<string>>;
|
|
11
|
+
fetchVeMndeVotes(withSnapshot: boolean): Promise<Votes>;
|
|
12
|
+
fetchMSolVotes(withSnapshot: boolean): Promise<Votes>;
|
|
13
|
+
fetchRewards(epochs: number, withSnapshot: boolean): Promise<Rewards>;
|
|
14
|
+
fetchValidatorsJitoMEV(withSnapshot: boolean): Promise<Record<string, JitoValidatorDto>>;
|
|
15
|
+
fetchBonds(withSnapshot: boolean): Promise<Bonds>;
|
|
16
|
+
fetchTvl(withSnapshot: boolean): Promise<TvlStats>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Rewards, RewardsResponseDto } from '../dto/rewards.dto';
|
|
3
|
+
import { Votes, mSolSnapshotDto, veMNDESnapshotDto } from '../dto/snapshots.dto';
|
|
4
|
+
import { EpochStatDto, ValidatorDto, ValidatorsResponseDto } from '../dto/validators.dto';
|
|
5
|
+
import { IDataProvider } from '../interfaces/data-provider.interface';
|
|
6
|
+
import { sum } from '../utils/maths';
|
|
7
|
+
import { FETCHING_ERROR_MESSAGES } from '../errors/fetching';
|
|
8
|
+
import { JitoValidatorDto, JitoValidatorsResponseDto } from '../dto/jito.dto';
|
|
9
|
+
import { writeFile } from 'fs/promises';
|
|
10
|
+
import { Bonds, BondsDto } from '../dto/bonds.dto';
|
|
11
|
+
import { TvlStats } from '../dto/marinade.dto';
|
|
12
|
+
import axios, { AxiosError } from 'axios';
|
|
13
|
+
|
|
14
|
+
export class URLs {
|
|
15
|
+
validatorsURL: string;
|
|
16
|
+
blacklistURL: string;
|
|
17
|
+
vemndeVotesURL: string;
|
|
18
|
+
msolVotesURL: string;
|
|
19
|
+
rewardsURL: string;
|
|
20
|
+
jitoMevURL: string;
|
|
21
|
+
bondsURL: string;
|
|
22
|
+
marinadeTvlURL: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ApiDataProvider implements IDataProvider {
|
|
26
|
+
constructor (private urls: URLs) { }
|
|
27
|
+
|
|
28
|
+
async fetchValidators (epochsToFetch: number, withSnapshot = false): Promise<ValidatorDto[]> {
|
|
29
|
+
try {
|
|
30
|
+
const response = await axios.get(`${this.urls.validatorsURL}?limit=9999&epochs=${epochsToFetch + 1}`);
|
|
31
|
+
|
|
32
|
+
const rawData = response.data as ValidatorsResponseDto;
|
|
33
|
+
|
|
34
|
+
rawData.validators.forEach(validator => {
|
|
35
|
+
validator.epochStats = validator.epoch_stats.reduce((acc, epochStat) => {
|
|
36
|
+
acc[epochStat.epoch] = epochStat;
|
|
37
|
+
return acc;
|
|
38
|
+
}, {} as Record<number, EpochStatDto>);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (withSnapshot) {
|
|
42
|
+
await writeFile('./snapshot/validators.json', JSON.stringify(rawData, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return rawData.validators;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (axios.isAxiosError(error)) {
|
|
48
|
+
const axiosError = error as AxiosError;
|
|
49
|
+
if (axiosError.response) {
|
|
50
|
+
console.error(FETCHING_ERROR_MESSAGES.validatorsFetchFailed, axiosError.response.status);
|
|
51
|
+
} else if (axiosError.request) {
|
|
52
|
+
console.error(FETCHING_ERROR_MESSAGES.generalValidatorsFetchFailed, axiosError.request);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async fetchTvl (withSnapshot: boolean): Promise<TvlStats> {
|
|
60
|
+
try {
|
|
61
|
+
const response = await axios.get(this.urls.marinadeTvlURL);
|
|
62
|
+
|
|
63
|
+
const data: TvlStats = response.data;
|
|
64
|
+
|
|
65
|
+
if (withSnapshot) {
|
|
66
|
+
await writeFile('./snapshot/tvl.json', JSON.stringify(data, null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return data;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (axios.isAxiosError(error)) {
|
|
72
|
+
const axiosError = error as AxiosError;
|
|
73
|
+
if (axiosError.response) {
|
|
74
|
+
console.error(FETCHING_ERROR_MESSAGES.tvlFetchFailed, axiosError.response.status);
|
|
75
|
+
} else if (axiosError.request) {
|
|
76
|
+
console.error(FETCHING_ERROR_MESSAGES.generalTvlFetchFailed, axiosError.request);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async fetchBonds (withSnapshot: boolean): Promise<Bonds> {
|
|
84
|
+
try {
|
|
85
|
+
const response = await axios.get<BondsDto>(this.urls.bondsURL);
|
|
86
|
+
const data: BondsDto = response.data;
|
|
87
|
+
|
|
88
|
+
const bonds: Bonds = data.bonds.reduce<Bonds>((acc, bond) => {
|
|
89
|
+
acc[bond.vote_account] = bond.cpmpe;
|
|
90
|
+
return acc;
|
|
91
|
+
}, {} as Bonds);
|
|
92
|
+
|
|
93
|
+
if (withSnapshot) {
|
|
94
|
+
await writeFile('./snapshot/bonds.json', JSON.stringify(data, null, 2));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return bonds;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (axios.isAxiosError(error)) {
|
|
100
|
+
const axiosError = error as AxiosError;
|
|
101
|
+
if (axiosError.response) {
|
|
102
|
+
console.error(FETCHING_ERROR_MESSAGES.bondsFetchFailed, axiosError.response.status);
|
|
103
|
+
} else if (axiosError.request) {
|
|
104
|
+
console.error(FETCHING_ERROR_MESSAGES.generalBondsFetchFailed, axiosError.request);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async fetchBlacklist (withSnapshot = false): Promise<Set<string>> {
|
|
113
|
+
try {
|
|
114
|
+
const response = await axios.get<string>(this.urls.blacklistURL, {
|
|
115
|
+
responseType: 'text'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const csv: string = response.data;
|
|
119
|
+
|
|
120
|
+
if (withSnapshot) {
|
|
121
|
+
await writeFile('./snapshot/blacklist.csv', csv);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const blacklistSet = new Set(
|
|
125
|
+
csv
|
|
126
|
+
.split('\n')
|
|
127
|
+
.map((line) => line.trim().split(',')[0])
|
|
128
|
+
.filter((value): value is string => !!value)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return blacklistSet;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (axios.isAxiosError(error)) {
|
|
134
|
+
if (error.response) {
|
|
135
|
+
console.error(FETCHING_ERROR_MESSAGES.blacklistFetchFailed, error.response.status);
|
|
136
|
+
} else if (error.request) {
|
|
137
|
+
console.error(FETCHING_ERROR_MESSAGES.generalBlacklistFetchFailed, error.request);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async fetchVeMndeVotes (withSnapshot = false): Promise<Votes> {
|
|
146
|
+
try {
|
|
147
|
+
const response = await axios.get<veMNDESnapshotDto>(this.urls.vemndeVotesURL);
|
|
148
|
+
const data: veMNDESnapshotDto = response.data;
|
|
149
|
+
|
|
150
|
+
if (withSnapshot) {
|
|
151
|
+
await writeFile('./snapshot/vemnde-votes.json', JSON.stringify(data, null, 2));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result: Votes = {};
|
|
155
|
+
for (const { amount, validatorVoteAccount } of data.records) {
|
|
156
|
+
const parsedAmount = Number(amount);
|
|
157
|
+
if (!isNaN(parsedAmount) && parsedAmount > 0) {
|
|
158
|
+
result[validatorVoteAccount] = (result[validatorVoteAccount] ?? 0) + parsedAmount;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (axios.isAxiosError(error)) {
|
|
165
|
+
if (error.response) {
|
|
166
|
+
console.error(FETCHING_ERROR_MESSAGES.vemndeFetchFailed, error.response.status);
|
|
167
|
+
} else if (error.request) {
|
|
168
|
+
console.error(FETCHING_ERROR_MESSAGES.generalVemndeFetchFailed, error.request);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async fetchMSolVotes (withSnapshot = false): Promise<Votes> {
|
|
177
|
+
try {
|
|
178
|
+
const response = await axios.get<mSolSnapshotDto>(this.urls.msolVotesURL);
|
|
179
|
+
const data: mSolSnapshotDto = response.data;
|
|
180
|
+
|
|
181
|
+
if (withSnapshot) {
|
|
182
|
+
await writeFile('./snapshot/msol-votes.json', JSON.stringify(data, null, 2));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result: Votes = {};
|
|
186
|
+
for (const { amount, validatorVoteAccount } of data.records) {
|
|
187
|
+
const parsedAmount = Number(amount);
|
|
188
|
+
if (!isNaN(parsedAmount) && parsedAmount > 0) {
|
|
189
|
+
result[validatorVoteAccount] = (result[validatorVoteAccount] ?? 0) + parsedAmount;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (axios.isAxiosError(error)) {
|
|
196
|
+
if (error.response) {
|
|
197
|
+
console.error(FETCHING_ERROR_MESSAGES.msolFetchFailed, error.response.status);
|
|
198
|
+
} else if (error.request) {
|
|
199
|
+
console.error(FETCHING_ERROR_MESSAGES.generalMsolFetchFailed, error.request);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async fetchRewards (epochs: number, withSnapshot = false): Promise<Rewards> {
|
|
208
|
+
try {
|
|
209
|
+
const response = await axios.get<{ rewards_mev: [number, number][], rewards_inflation_est: [number, number][] }>(
|
|
210
|
+
`${this.urls.rewardsURL}?epochs=${epochs}`
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const jsonResponse = response.data;
|
|
214
|
+
const data = new RewardsResponseDto(jsonResponse);
|
|
215
|
+
|
|
216
|
+
if (withSnapshot) {
|
|
217
|
+
await writeFile('./snapshot/rewards.json', JSON.stringify(jsonResponse, null, 2));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const mev = sum(data.rewards_mev.map(dto => dto.amount));
|
|
221
|
+
const inflation = sum(data.rewards_inflation_est.map(dto => dto.amount));
|
|
222
|
+
|
|
223
|
+
return { inflation, mev };
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (axios.isAxiosError(error)) {
|
|
226
|
+
if (error.response) {
|
|
227
|
+
console.error(FETCHING_ERROR_MESSAGES.rewardsFetchFailed, error.response.status);
|
|
228
|
+
} else if (error.request) {
|
|
229
|
+
console.error(FETCHING_ERROR_MESSAGES.generalRewardsFetchFailed, error.request);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async fetchValidatorsJitoMEV (withSnapshot = false): Promise<Record<string, JitoValidatorDto>> {
|
|
238
|
+
try {
|
|
239
|
+
const response = await axios.get<JitoValidatorsResponseDto>(this.urls.jitoMevURL);
|
|
240
|
+
const jitoMevRecords: JitoValidatorsResponseDto = response.data;
|
|
241
|
+
|
|
242
|
+
if (withSnapshot) {
|
|
243
|
+
await writeFile('./snapshot/jito-mev-records.json', JSON.stringify(jitoMevRecords, null, 2));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return Object.fromEntries(
|
|
247
|
+
jitoMevRecords.validators.map((validator) => [validator.vote_account, validator])
|
|
248
|
+
);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (axios.isAxiosError(error)) {
|
|
251
|
+
if (error.response) {
|
|
252
|
+
console.error(FETCHING_ERROR_MESSAGES.jitoMevFetchFailed, error.response.status);
|
|
253
|
+
} else if (error.request) {
|
|
254
|
+
console.error(FETCHING_ERROR_MESSAGES.generalJitoMEVFetchFailed, error.request);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|