@marinade.finance/scoring 1.0.0 → 1.0.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/computing/cluster.ts +102 -0
- package/computing/eligibility.ts +163 -0
- package/computing/score.ts +164 -0
- package/computing/stake.ts +246 -0
- package/computing/validators.ts +133 -0
- package/{src/dto → dto}/bonds.dto.ts +0 -2
- package/{src/dto → dto}/cluster.dto.ts +0 -1
- package/{src/dto → dto}/eligibility.dto.ts +0 -5
- package/{src/dto → dto}/jito.dto.ts +1 -2
- package/{src/dto → dto}/marinade.dto.ts +1 -1
- package/{src/dto → dto}/rewards.dto.ts +1 -3
- package/{src/dto → dto}/scoring.dto.ts +1 -7
- package/{src/dto → dto}/snapshots.dto.ts +0 -3
- package/{src/dto → dto}/stakes.dto.ts +0 -4
- package/{src/dto → dto}/validators.dto.ts +1 -37
- package/package.json +17 -13
- package/{src/providers → providers}/api-data.provider.ts +58 -58
- package/{src/providers → providers}/file-data.provider.ts +7 -15
- package/{src/utils → utils}/csv.ts +3 -3
- package/src/cluster/cluster.module.ts +0 -8
- package/src/cluster/cluster.service.ts +0 -108
- package/src/config/config.module.ts +0 -8
- package/src/config/config.service.ts +0 -136
- package/src/eligibility/eligibility.module.ts +0 -11
- package/src/eligibility/eligibility.service.ts +0 -183
- package/src/scoring/scoring.module.ts +0 -11
- package/src/scoring/scoring.service.ts +0 -184
- package/src/stake/stake.module.ts +0 -10
- package/src/stake/stake.service.ts +0 -242
- package/src/validators/validators.module.ts +0 -48
- package/src/validators/validators.service.ts +0 -177
- /package/{src/constants → constants}/marinade.json +0 -0
- /package/{src/errors → errors}/fetching.ts +0 -0
- /package/{src/interfaces → interfaces}/data-provider.interface.ts +0 -0
- /package/{src/utils → utils}/maths.ts +0 -0
- /package/{src/utils → utils}/solana.ts +0 -0
- /package/{src/utils → utils}/zip.ts +0 -0
|
@@ -1,183 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { ScoringService } from './scoring.service';
|
|
3
|
-
import { ValidatorsModule } from '../validators/validators.module';
|
|
4
|
-
import { ConfigModule } from '../config/config.module';
|
|
5
|
-
|
|
6
|
-
@Module({
|
|
7
|
-
imports: [ValidatorsModule, ConfigModule],
|
|
8
|
-
providers: [ScoringService],
|
|
9
|
-
exports: [ScoringService],
|
|
10
|
-
})
|
|
11
|
-
export class ScoringModule {}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
3
|
-
import { ClusterInfo } from '../dto/cluster.dto';
|
|
4
|
-
import { ScoreDto, ScoreConfig, Scores, Variables } from '../dto/scoring.dto';
|
|
5
|
-
import { AggregatedValidator, AggregatedValidators } from '../dto/validators.dto';
|
|
6
|
-
import { ValidatorsService } from '../validators/validators.service';
|
|
7
|
-
import { Parser } from 'expr-eval';
|
|
8
|
-
import { mathUtilityFunctions } from '../utils/maths';
|
|
9
|
-
import { zip } from '../utils/zip';
|
|
10
|
-
import { ValidatorsEligibilities } from '../dto/eligibility.dto';
|
|
11
|
-
import { ConfigService } from '../config/config.service';
|
|
12
|
-
|
|
13
|
-
export class ScoringService {
|
|
14
|
-
constructor (private readonly validatorsService: ValidatorsService, private readonly configService: ConfigService) {}
|
|
15
|
-
|
|
16
|
-
selectVariables (clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, scoreConfig: ScoreConfig): Variables {
|
|
17
|
-
return {
|
|
18
|
-
bp_cluster_mean: clusterInfo.meanBlockProductionOverBasicEligibilityPeriod,
|
|
19
|
-
bp_cluster_std_dev: clusterInfo.stdDevBlockProductionOverBasicEligibilityPeriod,
|
|
20
|
-
credits_pct_mean: this.validatorsService.selectCreditsPctMean(aggregatedValidator, clusterInfo, scoreConfig.epochs),
|
|
21
|
-
bp_mean: this.validatorsService.selectBlockProductionMean(aggregatedValidator, scoreConfig.epochs),
|
|
22
|
-
commission_inflation_max: this.validatorsService.selectCommissonInflationMax(aggregatedValidator, scoreConfig.epochs + 1),
|
|
23
|
-
commission_mev: this.validatorsService.selectCommissonMEV(aggregatedValidator),
|
|
24
|
-
country_stake_concentration_last: this.validatorsService.selectCountryStakeConcentration(aggregatedValidator, clusterInfo),
|
|
25
|
-
city_stake_concentration_last: this.validatorsService.selectCityStakeConcentration(aggregatedValidator, clusterInfo),
|
|
26
|
-
aso_stake_concentration_last: this.validatorsService.selectASOStakeConcentration(aggregatedValidator, clusterInfo),
|
|
27
|
-
node_stake_last: this.validatorsService.selectNodeStake(aggregatedValidator),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
computeValidatorScore (clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, formulas: string[], weights: number[], scoreConfig: ScoreConfig): ScoreDto {
|
|
32
|
-
const scoreErrors: boolean[] = [];
|
|
33
|
-
const scores: number[] = [];
|
|
34
|
-
const values: number[] = [];
|
|
35
|
-
const tooltips: string[] = [];
|
|
36
|
-
let totalWeight = 0;
|
|
37
|
-
let totalScore = 0;
|
|
38
|
-
let totalConcentrationWeight = 0;
|
|
39
|
-
let totalConcentrationScore = 0;
|
|
40
|
-
const variables = { ...this.selectVariables(clusterInfo, aggregatedValidator, scoreConfig), ...mathUtilityFunctions() };
|
|
41
|
-
|
|
42
|
-
for (const [formula, weight, tooltipBuilder] of zip(formulas, weights, this.getScoreTooltipBuilders())) {
|
|
43
|
-
let score = 0;
|
|
44
|
-
try {
|
|
45
|
-
values.push(0);
|
|
46
|
-
score = Parser.evaluate(formula, variables);
|
|
47
|
-
scoreErrors.push(false);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error('Failed to calculate', formula, 'for', aggregatedValidator.voteAccount, 'err:', err);
|
|
50
|
-
scoreErrors.push(true);
|
|
51
|
-
}
|
|
52
|
-
totalWeight += weight;
|
|
53
|
-
totalScore += weight * score;
|
|
54
|
-
if (scoreConfig.concentrationParams.includes(scores.length)) {
|
|
55
|
-
totalConcentrationWeight += weight;
|
|
56
|
-
totalConcentrationScore += weight * score;
|
|
57
|
-
}
|
|
58
|
-
tooltips.push(tooltipBuilder(aggregatedValidator, clusterInfo));
|
|
59
|
-
scores.push(score);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
score: totalScore / totalWeight,
|
|
64
|
-
concentrationScore: totalConcentrationScore / totalConcentrationWeight,
|
|
65
|
-
scores,
|
|
66
|
-
values,
|
|
67
|
-
scoreErrors,
|
|
68
|
-
tooltips,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
computeValidatorsScores (clusterInfo: ClusterInfo, aggregatedValidators: AggregatedValidators, formulas: string[], weights: number[], scoreConfig: ScoreConfig): Scores {
|
|
73
|
-
const result: Scores = {};
|
|
74
|
-
|
|
75
|
-
for (const [voteAccount, aggregatedValidator] of Object.entries(aggregatedValidators)) {
|
|
76
|
-
result[voteAccount] = this.computeValidatorScore(clusterInfo, aggregatedValidator, formulas, weights, scoreConfig);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
scoreTooltipCredits (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
83
|
-
return `Credits: ${validator.credits.slice(1).map((credits, e) => {
|
|
84
|
-
const epoch = validator.epochs[e + 1];
|
|
85
|
-
if (epoch !== undefined) {
|
|
86
|
-
const targetCredits = clusterInfo.targetCreditsByEpoch.get(epoch) || 1;
|
|
87
|
-
return `${(100 * credits / targetCredits).toFixed(2)}%`;
|
|
88
|
-
} else {
|
|
89
|
-
return 'N/A';
|
|
90
|
-
}
|
|
91
|
-
}).join(', ')}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
scoreTooltipBlockProduction (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
95
|
-
return `Block production: ${validator.blocksProduced.map((blocksProduced, e) => {
|
|
96
|
-
const leaderSlot = validator.leaderSlots[e];
|
|
97
|
-
if (leaderSlot) {
|
|
98
|
-
return `${(100 * blocksProduced / leaderSlot).toFixed(2)}%`;
|
|
99
|
-
} else {
|
|
100
|
-
return '-';
|
|
101
|
-
}
|
|
102
|
-
}).join(', ')}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
scoreTooltipCommissionInflation (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
106
|
-
return `Inflation commission: ${validator.commission.map(c => `${c}%`).join(', ')}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
scoreTooltipCommissionMEV (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
110
|
-
return `MEV commission: ${validator.mevCommission} %`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
scoreTooltipCountryStakeConcentration (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
114
|
-
return `Country: ${validator.country}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
scoreTooltipCityStakeConcentration (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
118
|
-
return `City: ${validator.city}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
scoreTooltipNodeStake (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
122
|
-
return `Stake of the node: ${Math.round(validator.stake[0] ?? 0).toLocaleString()} SOL`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
scoreTooltipASOStakeConcentration (validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
126
|
-
return `ASO: ${validator.aso}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
getScoreTooltipBuilders (): ((validator: AggregatedValidator, clusterInfo: ClusterInfo) => string)[] {
|
|
130
|
-
return [
|
|
131
|
-
this.scoreTooltipCredits,
|
|
132
|
-
this.scoreTooltipBlockProduction,
|
|
133
|
-
this.scoreTooltipCommissionInflation,
|
|
134
|
-
this.scoreTooltipCommissionMEV,
|
|
135
|
-
this.scoreTooltipCountryStakeConcentration,
|
|
136
|
-
this.scoreTooltipCityStakeConcentration,
|
|
137
|
-
this.scoreTooltipASOStakeConcentration,
|
|
138
|
-
this.scoreTooltipNodeStake,
|
|
139
|
-
];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
getDefaultFormulas (): string[] {
|
|
143
|
-
return [
|
|
144
|
-
this.configService.getScoringConfig().formulaVoteCredits,
|
|
145
|
-
this.configService.getScoringConfig().formulaBlockProduction,
|
|
146
|
-
this.configService.getScoringConfig().formulaInflationCommission,
|
|
147
|
-
this.configService.getScoringConfig().formulaMEVCommission,
|
|
148
|
-
this.configService.getScoringConfig().formulaStakeConcentrationCountry,
|
|
149
|
-
this.configService.getScoringConfig().formulaStakeConcentrationCity,
|
|
150
|
-
this.configService.getScoringConfig().formulaStakeConcentrationASO,
|
|
151
|
-
this.configService.getScoringConfig().formulaStakeConcentrationNode,
|
|
152
|
-
];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getDefaultWeights (): number[] {
|
|
156
|
-
|
|
157
|
-
return [
|
|
158
|
-
this.configService.getScoringConfig().weightVoteCredits,
|
|
159
|
-
this.configService.getScoringConfig().weightBlockProduction,
|
|
160
|
-
this.configService.getScoringConfig().weightInflationCommission,
|
|
161
|
-
this.configService.getScoringConfig().weightMEVCommission,
|
|
162
|
-
this.configService.getScoringConfig().weightStakeConcentrationCountry,
|
|
163
|
-
this.configService.getScoringConfig().weightStakeConcentrationCity,
|
|
164
|
-
this.configService.getScoringConfig().weightStakeConcentrationASO,
|
|
165
|
-
this.configService.getScoringConfig().weightStakeConcentrationNode,
|
|
166
|
-
];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
orderByEligibiltyAndScore (scores: Scores, eligibilities: ValidatorsEligibilities): string[] {
|
|
170
|
-
const voteAccounts = Object.keys(scores);
|
|
171
|
-
voteAccounts.sort((a, b) => {
|
|
172
|
-
const eligibilityOrder = Number(eligibilities[b]?.basicEligibility ?? 0) - Number(eligibilities[a]?.basicEligibility ?? 0);
|
|
173
|
-
if (eligibilityOrder === 0) {
|
|
174
|
-
const scoreA = scores[a]?.score ?? 0;
|
|
175
|
-
const scoreB = scores[b]?.score ?? 0;
|
|
176
|
-
|
|
177
|
-
return scoreB - scoreA;
|
|
178
|
-
}
|
|
179
|
-
return eligibilityOrder;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return voteAccounts;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { StakeService } from './stake.service';
|
|
3
|
-
import { ConfigModule } from '../config/config.module';
|
|
4
|
-
|
|
5
|
-
@Module({
|
|
6
|
-
imports: [ConfigModule],
|
|
7
|
-
providers: [StakeService],
|
|
8
|
-
exports: [StakeService],
|
|
9
|
-
})
|
|
10
|
-
export class StakeModule {}
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
-
import { ValidatorsEligibilities } from '../dto/eligibility.dto';
|
|
3
|
-
import { Scores } from '../dto/scoring.dto';
|
|
4
|
-
import { Votes } from '../dto/snapshots.dto';
|
|
5
|
-
import { ProcessedVotesAndTVLs, Stakes, StakesConfig, StakingVariables } from '../dto/stakes.dto';
|
|
6
|
-
import { AggregatedValidators } from '../dto/validators.dto';
|
|
7
|
-
import { Parser } from 'expr-eval';
|
|
8
|
-
import { sum } from '../utils/maths';
|
|
9
|
-
import { ConfigService } from '../config/config.service';
|
|
10
|
-
|
|
11
|
-
export class StakeService {
|
|
12
|
-
constructor (private readonly configService: ConfigService) { }
|
|
13
|
-
|
|
14
|
-
private removeFromVotes (votes: Votes, toRemove: string): Votes {
|
|
15
|
-
const votesCopy = { ...votes };
|
|
16
|
-
delete votesCopy[toRemove];
|
|
17
|
-
return votesCopy;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
private processVotesAndCalculateTVLs (
|
|
21
|
-
mSolVotes: Votes,
|
|
22
|
-
veMndeVotes: Votes,
|
|
23
|
-
stakesConfig: StakesConfig
|
|
24
|
-
): ProcessedVotesAndTVLs {
|
|
25
|
-
const updatedVeMndeVotes = this.removeFromVotes({ ...veMndeVotes }, this.configService.getScoringConfig().DS_PUBKEY);
|
|
26
|
-
const veMndeVotesForDS = veMndeVotes[this.configService.getScoringConfig().DS_PUBKEY] ?? 0;
|
|
27
|
-
const totalVeMndeVotes = sum(Object.values(veMndeVotes));
|
|
28
|
-
const veMndeTvl = Math.round(
|
|
29
|
-
totalVeMndeVotes > 0
|
|
30
|
-
? (1 - veMndeVotesForDS / totalVeMndeVotes) * stakesConfig.veMndeControl * stakesConfig.tvl
|
|
31
|
-
: 0
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const updatedMSolVotes = this.removeFromVotes({ ...mSolVotes }, this.configService.getScoringConfig().DS_PUBKEY);
|
|
35
|
-
const mSolVotesForDS = mSolVotes[this.configService.getScoringConfig().DS_PUBKEY] ?? 0;
|
|
36
|
-
const totalMSolVotes = sum(Object.values(mSolVotes));
|
|
37
|
-
const mSolTvl = Math.round(
|
|
38
|
-
totalMSolVotes > 0
|
|
39
|
-
? (1 - mSolVotesForDS / totalMSolVotes) * stakesConfig.mSolControl * stakesConfig.tvl
|
|
40
|
-
: 0
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
updatedMSolVotes,
|
|
45
|
-
updatedVeMndeVotes,
|
|
46
|
-
mSolTvl,
|
|
47
|
-
veMndeTvl,
|
|
48
|
-
totalTvl: stakesConfig.tvl
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public filterEligibleValidators (aggregatedValidators: AggregatedValidators, eligibilities: ValidatorsEligibilities): string[] {
|
|
53
|
-
return Object.keys(aggregatedValidators).filter(voteAccount => eligibilities[voteAccount]?.basicEligibility);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private initStakes (eligibleValidators: string[]): Stakes {
|
|
57
|
-
const result: Stakes = {};
|
|
58
|
-
for (const voteAccount of eligibleValidators) {
|
|
59
|
-
result[voteAccount] = {
|
|
60
|
-
algoStake: 0,
|
|
61
|
-
mSolStake: 0,
|
|
62
|
-
veMndeStake: 0,
|
|
63
|
-
totalStake: 0,
|
|
64
|
-
algoStakeFromOverflow: 0,
|
|
65
|
-
mSolStakeFromOverflow: 0,
|
|
66
|
-
veMndeStakeFromOverflow: 0,
|
|
67
|
-
totalStakeFromOverlfow: 0,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private computeStakesForValidators (
|
|
74
|
-
stakingVariables: StakingVariables,
|
|
75
|
-
eligibleValidators: string[],
|
|
76
|
-
scores: Scores,
|
|
77
|
-
eligibilities: ValidatorsEligibilities,
|
|
78
|
-
stakesConfig: StakesConfig,
|
|
79
|
-
mSolVotes: Votes,
|
|
80
|
-
veMndeVotes: Votes,
|
|
81
|
-
totalMsolVotes: number,
|
|
82
|
-
totalVeMndeVotes: number,
|
|
83
|
-
mSolTvl: number,
|
|
84
|
-
veMndeTvl: number,
|
|
85
|
-
algoTvl: number,
|
|
86
|
-
blockSize: number
|
|
87
|
-
): Stakes {
|
|
88
|
-
let updatedTotalMSolVotes = totalMsolVotes;
|
|
89
|
-
let updatedTotalVeMndeVotes = totalVeMndeVotes;
|
|
90
|
-
let algoStakeDistributed = 0;
|
|
91
|
-
let mSolStakeDistributed = 0;
|
|
92
|
-
let veMndeStakeDistributed = 0;
|
|
93
|
-
const stakes = this.initStakes(eligibleValidators);
|
|
94
|
-
|
|
95
|
-
let round = 1;
|
|
96
|
-
while (round <= 100 && (mSolTvl - mSolStakeDistributed > 1 || veMndeTvl - veMndeStakeDistributed > 1)) {
|
|
97
|
-
let someStakeIncreased = false;
|
|
98
|
-
const mSolStakeDistributedBeforeRound = mSolStakeDistributed;
|
|
99
|
-
const veMndeStakeDistributedBeforeRound = veMndeStakeDistributed;
|
|
100
|
-
for (const voteAccount of eligibleValidators) {
|
|
101
|
-
if (!stakes[voteAccount] || !scores[voteAccount] || !eligibilities[voteAccount]) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let blocks = 0;
|
|
106
|
-
|
|
107
|
-
if (round === 1) {
|
|
108
|
-
if (eligibilities[voteAccount]!.bonusEligibility) {
|
|
109
|
-
blocks += stakesConfig.stakeBlocksFromBonus;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
blocks += Parser.evaluate(stakesConfig.formulaStakeBlocksFromScore, { ...stakingVariables, score: scores[voteAccount]!.score });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const algoStake = Math.round(Math.min(blockSize * blocks, Math.max(algoTvl - algoStakeDistributed, 0)));
|
|
116
|
-
const mSolStake = updatedTotalMSolVotes > 0 ? Math.round((mSolVotes[voteAccount] ?? 0) / updatedTotalMSolVotes * Math.max(mSolTvl - mSolStakeDistributedBeforeRound, 0)) : 0;
|
|
117
|
-
const veMndeStake = updatedTotalVeMndeVotes > 0 ? Math.round((veMndeVotes[voteAccount] ?? 0) / updatedTotalVeMndeVotes * Math.max(veMndeTvl - veMndeStakeDistributedBeforeRound, 0)) : 0;
|
|
118
|
-
const stake = algoStake + veMndeStake + mSolStake;
|
|
119
|
-
|
|
120
|
-
if (!stake) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const cappedStake = Math.min(eligibilities[voteAccount]!.capFromBond - stakes[voteAccount]!.totalStake, eligibilities[voteAccount]!.capFromExternalStake - stakes[voteAccount]!.totalStake, stake);
|
|
125
|
-
const stakeOverflow = stake - cappedStake;
|
|
126
|
-
const algoStakeOverflow = Math.round(algoStake / stake * stakeOverflow);
|
|
127
|
-
const mSolStakeOverflow = Math.round(mSolStake / stake * stakeOverflow);
|
|
128
|
-
const veMndeStakeOverflow = Math.round(veMndeStake / stake * stakeOverflow);
|
|
129
|
-
const cappedAlgoStake = algoStake - algoStakeOverflow;
|
|
130
|
-
const cappedMSolStake = mSolStake - mSolStakeOverflow;
|
|
131
|
-
const cappedVeMndeStake = veMndeStake - veMndeStakeOverflow;
|
|
132
|
-
|
|
133
|
-
if (cappedAlgoStake + cappedMSolStake + cappedVeMndeStake > 0) {
|
|
134
|
-
someStakeIncreased = true;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
algoStakeDistributed += cappedAlgoStake;
|
|
138
|
-
mSolStakeDistributed += cappedMSolStake;
|
|
139
|
-
veMndeStakeDistributed += cappedVeMndeStake;
|
|
140
|
-
|
|
141
|
-
stakes[voteAccount]!.algoStake += cappedAlgoStake;
|
|
142
|
-
stakes[voteAccount]!.mSolStake += cappedMSolStake;
|
|
143
|
-
stakes[voteAccount]!.veMndeStake += cappedVeMndeStake;
|
|
144
|
-
stakes[voteAccount]!.totalStake += cappedAlgoStake + cappedMSolStake + cappedVeMndeStake;
|
|
145
|
-
if (round > 1) {
|
|
146
|
-
stakes[voteAccount]!.algoStakeFromOverflow += cappedAlgoStake;
|
|
147
|
-
stakes[voteAccount]!.mSolStakeFromOverflow += cappedMSolStake;
|
|
148
|
-
stakes[voteAccount]!.veMndeStakeFromOverflow += cappedVeMndeStake;
|
|
149
|
-
stakes[voteAccount]!.totalStakeFromOverlfow += cappedAlgoStake + cappedMSolStake + cappedVeMndeStake;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
round++;
|
|
154
|
-
|
|
155
|
-
if (!someStakeIncreased) {
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
for (const voteAccount of eligibleValidators) {
|
|
160
|
-
if (Math.round(stakes[voteAccount]!.totalStake) === Math.round(eligibilities[voteAccount]!.capFromExternalStake)) {
|
|
161
|
-
veMndeVotes = this.removeFromVotes(veMndeVotes, voteAccount);
|
|
162
|
-
mSolVotes = this.removeFromVotes(mSolVotes, voteAccount);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
updatedTotalVeMndeVotes = sum(Object.values(veMndeVotes));
|
|
166
|
-
updatedTotalMSolVotes = sum(Object.values(mSolVotes));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const remainingAlgoTvl = Math.max(algoTvl - algoStakeDistributed, 0);
|
|
170
|
-
const remainingMSolTvl = Math.max(mSolTvl - mSolStakeDistributed, 0);
|
|
171
|
-
const remainingVeMndeTvl = Math.max(veMndeTvl - veMndeStakeDistributed, 0);
|
|
172
|
-
for (const voteAccount of eligibleValidators) {
|
|
173
|
-
if (!stakes[voteAccount]) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (algoStakeDistributed) {
|
|
177
|
-
const extraStake = Math.round((stakes[voteAccount]?.algoStake ?? 0) / algoStakeDistributed * remainingAlgoTvl);
|
|
178
|
-
stakes[voteAccount]!.totalStake += extraStake;
|
|
179
|
-
stakes[voteAccount]!.algoStake += extraStake;
|
|
180
|
-
stakes[voteAccount]!.algoStakeFromOverflow += extraStake;
|
|
181
|
-
}
|
|
182
|
-
if (mSolStakeDistributed) {
|
|
183
|
-
const extraStake = Math.round((stakes[voteAccount]?.algoStake ?? 0) / mSolStakeDistributed * remainingMSolTvl);
|
|
184
|
-
stakes[voteAccount]!.totalStake += extraStake;
|
|
185
|
-
stakes[voteAccount]!.mSolStake += extraStake;
|
|
186
|
-
stakes[voteAccount]!.mSolStakeFromOverflow += extraStake;
|
|
187
|
-
}
|
|
188
|
-
if (veMndeStakeDistributed) {
|
|
189
|
-
const extraStake = Math.round((stakes[voteAccount]?.algoStake ?? 0) / veMndeStakeDistributed * remainingVeMndeTvl);
|
|
190
|
-
stakes[voteAccount]!.totalStake += extraStake;
|
|
191
|
-
stakes[voteAccount]!.veMndeStake += extraStake;
|
|
192
|
-
stakes[voteAccount]!.veMndeStakeFromOverflow += extraStake;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return stakes;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
computeValidatorsStakes (
|
|
199
|
-
aggregatedValidators: AggregatedValidators,
|
|
200
|
-
scores: Scores,
|
|
201
|
-
eligibilities: ValidatorsEligibilities,
|
|
202
|
-
stakesConfig: StakesConfig,
|
|
203
|
-
mSolVotes: Votes,
|
|
204
|
-
veMndeVotes: Votes
|
|
205
|
-
): Stakes {
|
|
206
|
-
let updatedMSolVotes = { ...mSolVotes };
|
|
207
|
-
let updatedVeMndeVotes = { ...veMndeVotes };
|
|
208
|
-
|
|
209
|
-
const processedVotesAndTvls = this.processVotesAndCalculateTVLs(updatedMSolVotes, updatedVeMndeVotes, stakesConfig);
|
|
210
|
-
|
|
211
|
-
updatedMSolVotes = processedVotesAndTvls.updatedMSolVotes;
|
|
212
|
-
updatedVeMndeVotes = processedVotesAndTvls.updatedVeMndeVotes;
|
|
213
|
-
const algoTvl = stakesConfig.tvl - processedVotesAndTvls.mSolTvl - processedVotesAndTvls.veMndeTvl;
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const eligibleValidators = this.filterEligibleValidators(aggregatedValidators, eligibilities);
|
|
217
|
-
const stakingVariables: StakingVariables = { tvl: algoTvl };
|
|
218
|
-
const blockSize = Parser.evaluate(stakesConfig.formulaStakeBlockSize, stakingVariables);
|
|
219
|
-
|
|
220
|
-
Object.keys(aggregatedValidators).filter((v) => !eligibilities[v]?.basicEligibility).forEach((voteAccount) => {
|
|
221
|
-
updatedVeMndeVotes = this.removeFromVotes(updatedVeMndeVotes, voteAccount);
|
|
222
|
-
updatedMSolVotes = this.removeFromVotes(updatedMSolVotes, voteAccount);
|
|
223
|
-
});
|
|
224
|
-
const updatedTotalVeMndeVotes = sum(Object.values(updatedVeMndeVotes));
|
|
225
|
-
const updatedTotalMSolVotes = sum(Object.values(updatedMSolVotes));
|
|
226
|
-
eligibleValidators.sort((a, b) => {
|
|
227
|
-
const scoreA = scores[a]?.score ?? 0;
|
|
228
|
-
const scoreB = scores[b]?.score ?? 0;
|
|
229
|
-
return scoreB - scoreA;
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
return this.computeStakesForValidators(stakingVariables, eligibleValidators, scores, eligibilities,
|
|
233
|
-
stakesConfig, updatedMSolVotes, updatedVeMndeVotes,
|
|
234
|
-
updatedTotalMSolVotes, updatedTotalVeMndeVotes,
|
|
235
|
-
processedVotesAndTvls.mSolTvl, processedVotesAndTvls.veMndeTvl,
|
|
236
|
-
algoTvl, blockSize);
|
|
237
|
-
|
|
238
|
-
} catch (err) {
|
|
239
|
-
return {};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { ValidatorsService } from './validators.service';
|
|
3
|
-
import { ConfigService } from '../config/config.service';
|
|
4
|
-
import { ApiDataProvider } from '../providers/api-data.provider';
|
|
5
|
-
import { FileDataProvider } from '../providers/file-data.provider';
|
|
6
|
-
import { ConfigModule } from '../config/config.module';
|
|
7
|
-
|
|
8
|
-
@Module({
|
|
9
|
-
imports: [ConfigModule],
|
|
10
|
-
providers: [ValidatorsService, {
|
|
11
|
-
provide: 'IDataProvider',
|
|
12
|
-
useFactory: (configService: ConfigService) => {
|
|
13
|
-
const dataSourceType = configService.getDataProviderMode();
|
|
14
|
-
if (dataSourceType === 'api') {
|
|
15
|
-
return new ApiDataProvider(
|
|
16
|
-
{
|
|
17
|
-
validatorsURL: configService.getValidatorsURL(),
|
|
18
|
-
blacklistURL: configService.getBlacklistURL(),
|
|
19
|
-
vemndeVotesURL: configService.getVemndeVotesURL(),
|
|
20
|
-
msolVotesURL: configService.getMsolVotesURL(),
|
|
21
|
-
rewardsURL: configService.getRewardsURL(),
|
|
22
|
-
jitoMevURL: configService.getJitoMevURL(),
|
|
23
|
-
bondsURL: configService.getBondsURL(),
|
|
24
|
-
marinadeTvlURL: configService.getMarinadeTvlURL(),
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
} else if (dataSourceType === 'file') {
|
|
28
|
-
return new FileDataProvider(
|
|
29
|
-
{
|
|
30
|
-
validatorsPath: configService.getValidatorsPath(),
|
|
31
|
-
blacklistPath: configService.getBlacklistPath(),
|
|
32
|
-
vemndeVotesPath: configService.getVemndeVotesPath(),
|
|
33
|
-
msolVotesPath: configService.getMsolVotesPath(),
|
|
34
|
-
rewardsPath: configService.getRewardsPath(),
|
|
35
|
-
jitoMevPath: configService.getJitoMevPath(),
|
|
36
|
-
bondsPath: configService.getBondsPath(),
|
|
37
|
-
marinadeTvlPath: configService.getMarinadeTvlPath(),
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
} else {
|
|
41
|
-
throw new Error('Invalid data provider type');
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
inject: [ConfigService],
|
|
45
|
-
}],
|
|
46
|
-
exports: [ValidatorsService],
|
|
47
|
-
})
|
|
48
|
-
export class ValidatorsModule {}
|