@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { BlockProductionResult, ClusterInfo } from '../dto/cluster.dto';
|
|
2
|
+
import { ValidatorDto } from '../dto/validators.dto';
|
|
3
|
+
import { mean, stdDev } from '../utils/maths';
|
|
4
|
+
import Decimal from 'decimal.js';
|
|
5
|
+
|
|
6
|
+
export function computeBlockProduction (validators: ValidatorDto[], startEpoch: number, endEpoch: number): BlockProductionResult {
|
|
7
|
+
const blockProductions: number[] = [];
|
|
8
|
+
|
|
9
|
+
for (const validator of validators) {
|
|
10
|
+
let leaderSlots = 0;
|
|
11
|
+
let blocksProduced = 0;
|
|
12
|
+
|
|
13
|
+
for (let epoch = startEpoch; epoch <= endEpoch; epoch++) {
|
|
14
|
+
if (!validator.epochStats) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const epochStat = validator.epochStats[epoch];
|
|
18
|
+
if (epochStat) {
|
|
19
|
+
leaderSlots += epochStat.leader_slots;
|
|
20
|
+
blocksProduced += epochStat.blocks_produced;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (leaderSlots) {
|
|
25
|
+
blockProductions.push(blocksProduced / leaderSlots);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
mean: mean(blockProductions),
|
|
31
|
+
stdDev: stdDev(blockProductions),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function computeTargetCreditsByEpoch (validators: ValidatorDto[]): Map<number, number> {
|
|
35
|
+
const sumOfWeightedCreditsPerEpoch = new Map<number, Decimal>();
|
|
36
|
+
const sumOfWeightsPerEpoch = new Map<number, Decimal>();
|
|
37
|
+
|
|
38
|
+
validators.forEach(validator => {
|
|
39
|
+
validator.epoch_stats.forEach(epochStat => {
|
|
40
|
+
if (epochStat.epoch_end_at) {
|
|
41
|
+
const activatedStakeDecimal = new Decimal(epochStat.activated_stake);
|
|
42
|
+
const stakeInSol = activatedStakeDecimal.dividedBy(1e9);
|
|
43
|
+
|
|
44
|
+
const prevSumOfWeightedCredits = sumOfWeightedCreditsPerEpoch.get(epochStat.epoch) || new Decimal(0);
|
|
45
|
+
sumOfWeightedCreditsPerEpoch.set(epochStat.epoch, prevSumOfWeightedCredits.plus(new Decimal(epochStat.credits).times(stakeInSol)));
|
|
46
|
+
|
|
47
|
+
const prevSumOfWeightsPerEpoch = sumOfWeightsPerEpoch.get(epochStat.epoch) || new Decimal(0);
|
|
48
|
+
sumOfWeightsPerEpoch.set(epochStat.epoch, prevSumOfWeightsPerEpoch.plus(stakeInSol));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = new Map<number, number>();
|
|
54
|
+
sumOfWeightsPerEpoch.forEach((sumOfWeights, epoch) => {
|
|
55
|
+
const weightedCredits = sumOfWeightedCreditsPerEpoch.get(epoch) || new Decimal(0);
|
|
56
|
+
result.set(epoch, weightedCredits.dividedBy(sumOfWeights).toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toNumber());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
export function computeConcentrations (validators: ValidatorDto[], epoch: number): { city: Map<string, number>, country: Map<string, number>, aso: Map<string, number> } {
|
|
62
|
+
let total = 0;
|
|
63
|
+
const city = new Map<string, number>();
|
|
64
|
+
const country = new Map<string, number>();
|
|
65
|
+
const aso = new Map<string, number>();
|
|
66
|
+
|
|
67
|
+
for (const validator of validators) {
|
|
68
|
+
const lastEpochStats = validator.epoch_stats[0];
|
|
69
|
+
if (lastEpochStats?.epoch === epoch) {
|
|
70
|
+
const cityKey = validator.dc_full_city ?? '???';
|
|
71
|
+
const countryKey = validator.dc_country ?? '???';
|
|
72
|
+
const asoKey = validator.dc_aso ?? '???';
|
|
73
|
+
|
|
74
|
+
const stake = Number(validator.activated_stake) / 1e9;
|
|
75
|
+
city.set(cityKey, (city.get(cityKey) ?? 0) + stake);
|
|
76
|
+
country.set(countryKey, (country.get(countryKey) ?? 0) + stake);
|
|
77
|
+
aso.set(asoKey, (aso.get(asoKey) ?? 0) + stake);
|
|
78
|
+
|
|
79
|
+
total += stake;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (total > 0) {
|
|
84
|
+
for (const map of [city, country, aso]) {
|
|
85
|
+
for (const [key, value] of map) {
|
|
86
|
+
map.set(key, value / total);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { city, country, aso };
|
|
92
|
+
}
|
|
93
|
+
export function computeClusterInfo (validators: ValidatorDto[], basicEligibilityEpochs: number, lastEpoch: number): ClusterInfo {
|
|
94
|
+
const { mean: meanBlockProduction, stdDev: stdDevBlockProduction } = this.computeBlockProduction(validators, lastEpoch - basicEligibilityEpochs, lastEpoch - 1);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
targetCreditsByEpoch: this.computeTargetCreditsByEpoch(validators),
|
|
98
|
+
...this.computeConcentrations(validators, lastEpoch),
|
|
99
|
+
meanBlockProductionOverBasicEligibilityPeriod: meanBlockProduction,
|
|
100
|
+
stdDevBlockProductionOverBasicEligibilityPeriod: stdDevBlockProduction,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { selectExternalStakeMin } from './validators';
|
|
2
|
+
import { Bonds } from '../dto/bonds.dto';
|
|
3
|
+
import { ClusterInfo } from '../dto/cluster.dto';
|
|
4
|
+
import { EligibilityConfig, EvaluationResult, Issue, IssueType, ValidatorEligibility, ValidatorsEligibilities } from '../dto/eligibility.dto';
|
|
5
|
+
import { ScoreDto, Scores, ScoringConfig } from '../dto/scoring.dto';
|
|
6
|
+
import { AggregatedValidator, AggregatedValidators } from '../dto/validators.dto';
|
|
7
|
+
|
|
8
|
+
export function evalLowCreditsIssue(validator: AggregatedValidator, epochIndex: number, eligibilityConfig: EligibilityConfig, clusterInfo: ClusterInfo): Issue | null {
|
|
9
|
+
if (!validator.epochs[epochIndex]) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const targetCredits = clusterInfo.targetCreditsByEpoch.get(validator.epochs[epochIndex]!);
|
|
14
|
+
if (!targetCredits) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const actualCredits = validator.credits[epochIndex] || 0;
|
|
19
|
+
const creditsPct = Math.round((100 * actualCredits) / targetCredits);
|
|
20
|
+
|
|
21
|
+
if (creditsPct < eligibilityConfig.voteCreditsLow) {
|
|
22
|
+
return { type: IssueType.VOTE_CREDITS_LOW, message: `Credits @ ${creditsPct}%` };
|
|
23
|
+
} else if (creditsPct < eligibilityConfig.voteCreditsWarning) {
|
|
24
|
+
return { type: IssueType.VOTE_CREDITS_WARNING, message: `(Warning) Credits @ ${creditsPct}%` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function evalLowExternalStakeIssue(validator: AggregatedValidator, epochIndex: number, eligibilityConfig: EligibilityConfig): Issue | undefined {
|
|
30
|
+
const externalStake = validator.externalStake[epochIndex];
|
|
31
|
+
|
|
32
|
+
if (!externalStake) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (externalStake < eligibilityConfig.minExternalStake) {
|
|
37
|
+
return { type: IssueType.EXTERNAL_STAKE, message: `External stake: ${externalStake}` };
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
export function evalBlacklistIssue(validator: AggregatedValidator): Issue | undefined {
|
|
42
|
+
if (validator.blacklisted) {
|
|
43
|
+
return { type: IssueType.BLACKLIST, message: 'Validator is blacklisted' };
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export function evalHighCommissionIssue(validator: AggregatedValidator, epochIndex: number, eligibilityConfig: EligibilityConfig): Issue | undefined {
|
|
48
|
+
const commission = validator.commission[epochIndex];
|
|
49
|
+
|
|
50
|
+
if (!commission) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (eligibilityConfig.maxCommission < commission!) {
|
|
55
|
+
return { type: IssueType.COMMISSION, message: `Commission: ${commission}%` };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
export function evalScoreIssue(epochIndex: number, eligibilityConfig: EligibilityConfig, { concentrationScore }: ScoreDto): Issue | undefined {
|
|
60
|
+
if (epochIndex === 0 && concentrationScore < eligibilityConfig.minScore) {
|
|
61
|
+
return { type: IssueType.SCORE, message: `Concentration Score: ${concentrationScore}` };
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
export function evalBondIssue(validator: AggregatedValidator, bonds: Bonds): Issue | undefined {
|
|
66
|
+
if (!(validator.voteAccount in bonds)) {
|
|
67
|
+
return { type: IssueType.NO_BOND, message: 'Validator has no bond' };
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
export function getValidatorIssuesInEpoch(validator: AggregatedValidator, bonds: Bonds, epochIndex: number, eligibilityConfig: EligibilityConfig, clusterInfo: ClusterInfo, score: ScoreDto): Issue[] {
|
|
72
|
+
if (!validator.dataAvailable[epochIndex]) {
|
|
73
|
+
return [{ type: IssueType.NO_DATA, message: `No data for validator in epoch ${validator.epochs[epochIndex]}` }];
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
evalBlacklistIssue(validator),
|
|
77
|
+
evalLowCreditsIssue(validator, epochIndex, eligibilityConfig, clusterInfo),
|
|
78
|
+
evalLowExternalStakeIssue(validator, epochIndex, eligibilityConfig),
|
|
79
|
+
evalHighCommissionIssue(validator, epochIndex, eligibilityConfig),
|
|
80
|
+
evalScoreIssue(epochIndex, eligibilityConfig, score),
|
|
81
|
+
evalBondIssue(validator, bonds),
|
|
82
|
+
].filter(Boolean) as Issue[];
|
|
83
|
+
}
|
|
84
|
+
export function evalIssuesInEpoch(issues: Issue[]): EvaluationResult {
|
|
85
|
+
let criticals = 0;
|
|
86
|
+
let warnings = 0;
|
|
87
|
+
|
|
88
|
+
const criticalTypes = new Set([
|
|
89
|
+
IssueType.NO_BOND,
|
|
90
|
+
IssueType.NO_DATA,
|
|
91
|
+
IssueType.BLACKLIST,
|
|
92
|
+
IssueType.COMMISSION,
|
|
93
|
+
IssueType.EXTERNAL_STAKE,
|
|
94
|
+
IssueType.CENTRALIZATION,
|
|
95
|
+
IssueType.SCORE,
|
|
96
|
+
IssueType.VOTE_CREDITS_LOW,
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const warningTypes = new Set([
|
|
100
|
+
IssueType.VOTE_CREDITS_WARNING,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
issues.forEach(issue => {
|
|
104
|
+
if (criticalTypes.has(issue.type)) {
|
|
105
|
+
criticals++;
|
|
106
|
+
} else if (warningTypes.has(issue.type)) {
|
|
107
|
+
warnings++;
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error(`Unexpected issue: ${issue.type}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { criticals, warnings };
|
|
114
|
+
}
|
|
115
|
+
export function evalIssues(issuesCollection: Issue[][], epochs: number): EvaluationResult {
|
|
116
|
+
let totalCriticals = 0;
|
|
117
|
+
let totalWarnings = 0;
|
|
118
|
+
for (let epochIndex = 0; epochIndex < epochs; epochIndex++) {
|
|
119
|
+
if (!issuesCollection[epochIndex]) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const { criticals, warnings } = evalIssuesInEpoch(issuesCollection[epochIndex]!);
|
|
123
|
+
totalCriticals += criticals;
|
|
124
|
+
totalWarnings += warnings;
|
|
125
|
+
}
|
|
126
|
+
return { criticals: totalCriticals, warnings: totalWarnings };
|
|
127
|
+
}
|
|
128
|
+
export function isBasicEligible(issuesCollection: Issue[][], eligibilityConfig: EligibilityConfig): boolean {
|
|
129
|
+
const { criticals, warnings } = evalIssues(issuesCollection, eligibilityConfig.basicEligibilityEpochs + 1);
|
|
130
|
+
return criticals === 0 && warnings <= eligibilityConfig.maxWarnings;
|
|
131
|
+
}
|
|
132
|
+
export function isBonusEligible(issuesCollection: Issue[][], eligibilityConfig: EligibilityConfig): boolean {
|
|
133
|
+
const totalEpochs = eligibilityConfig.basicEligibilityEpochs + eligibilityConfig.bonusEligibilityExtraEpochs + 1;
|
|
134
|
+
const { criticals, warnings } = evalIssues(issuesCollection, totalEpochs);
|
|
135
|
+
return criticals === 0 && warnings <= eligibilityConfig.maxWarnings;
|
|
136
|
+
}
|
|
137
|
+
export function getIssuesCollection(clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, bonds: Bonds, score: ScoreDto, eligibilityConfig: EligibilityConfig): Issue[][] {
|
|
138
|
+
return Array.from({ length: eligibilityConfig.basicEligibilityEpochs + eligibilityConfig.bonusEligibilityExtraEpochs + 1 }, (_, epochIndex) =>
|
|
139
|
+
getValidatorIssuesInEpoch(aggregatedValidator, bonds, epochIndex, eligibilityConfig, clusterInfo, score)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
export function computeValidatorEligibility(clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, bonds: Bonds, scores: ScoreDto, eligibilityConfig: EligibilityConfig, scoringConfig: ScoringConfig): ValidatorEligibility {
|
|
143
|
+
const issuesCollection = getIssuesCollection(clusterInfo, aggregatedValidator, bonds, scores, eligibilityConfig);
|
|
144
|
+
const minExternalStake = selectExternalStakeMin(aggregatedValidator, eligibilityConfig.basicEligibilityEpochs);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
basicEligibility: isBasicEligible(issuesCollection, eligibilityConfig),
|
|
148
|
+
bonusEligibility: isBonusEligible(issuesCollection, eligibilityConfig),
|
|
149
|
+
issuesCollection,
|
|
150
|
+
capFromBond: scoringConfig.CAP_FROM_BOND,
|
|
151
|
+
capFromExternalStake: minExternalStake * (eligibilityConfig.maxStakeShare / (1 - eligibilityConfig.maxStakeShare)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function computeValidatorsEligibilities(clusterInfo: ClusterInfo, scores: Scores, aggregatedValidators: AggregatedValidators, bonds: Bonds, eligibilityConfig: EligibilityConfig, scoringConfig: ScoringConfig): ValidatorsEligibilities {
|
|
155
|
+
const result: ValidatorsEligibilities = {};
|
|
156
|
+
for (const [voteAccount, validator] of Object.entries(aggregatedValidators)) {
|
|
157
|
+
if (!scores[voteAccount]) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
result[voteAccount] = computeValidatorEligibility(clusterInfo, validator, bonds, scores[voteAccount]!, eligibilityConfig, scoringConfig);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ClusterInfo } from '../dto/cluster.dto';
|
|
2
|
+
import { ScoreDto, ScoreConfig, Scores, Variables } from '../dto/scoring.dto';
|
|
3
|
+
import { AggregatedValidator, AggregatedValidators } from '../dto/validators.dto';
|
|
4
|
+
import { Parser } from 'expr-eval';
|
|
5
|
+
import { mathUtilityFunctions } from '../utils/maths';
|
|
6
|
+
import { zip } from '../utils/zip';
|
|
7
|
+
import { ValidatorsEligibilities } from '../dto/eligibility.dto';
|
|
8
|
+
import { ScoringConfig } from '../dto/scoring.dto';
|
|
9
|
+
import { selectCreditsPctMean, selectBlockProductionMean, selectCommissionInflationMax, selectCommissionMEV, selectCountryStakeConcentration, selectCityStakeConcentration, selectASOStakeConcentration, selectNodeStake } from 'computing/validators';
|
|
10
|
+
import { error } from 'logger';
|
|
11
|
+
|
|
12
|
+
export function selectVariables(clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, scoreConfig: ScoreConfig): Variables {
|
|
13
|
+
return {
|
|
14
|
+
bp_cluster_mean: clusterInfo.meanBlockProductionOverBasicEligibilityPeriod,
|
|
15
|
+
bp_cluster_std_dev: clusterInfo.stdDevBlockProductionOverBasicEligibilityPeriod,
|
|
16
|
+
credits_pct_mean: selectCreditsPctMean(aggregatedValidator, clusterInfo, scoreConfig.epochs),
|
|
17
|
+
bp_mean: selectBlockProductionMean(aggregatedValidator, scoreConfig.epochs),
|
|
18
|
+
commission_inflation_max: selectCommissionInflationMax(aggregatedValidator, scoreConfig.epochs + 1),
|
|
19
|
+
commission_mev: selectCommissionMEV(aggregatedValidator),
|
|
20
|
+
country_stake_concentration_last: selectCountryStakeConcentration(aggregatedValidator, clusterInfo),
|
|
21
|
+
city_stake_concentration_last: selectCityStakeConcentration(aggregatedValidator, clusterInfo),
|
|
22
|
+
aso_stake_concentration_last: selectASOStakeConcentration(aggregatedValidator, clusterInfo),
|
|
23
|
+
node_stake_last: selectNodeStake(aggregatedValidator),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function computeValidatorScore(clusterInfo: ClusterInfo, aggregatedValidator: AggregatedValidator, formulas: string[], weights: number[], scoreConfig: ScoreConfig): ScoreDto {
|
|
27
|
+
const scoreErrors: boolean[] = [];
|
|
28
|
+
const scores: number[] = [];
|
|
29
|
+
const values: number[] = [];
|
|
30
|
+
const tooltips: string[] = [];
|
|
31
|
+
let totalWeight = 0;
|
|
32
|
+
let totalScore = 0;
|
|
33
|
+
let totalConcentrationWeight = 0;
|
|
34
|
+
let totalConcentrationScore = 0;
|
|
35
|
+
const variables = { ...selectVariables(clusterInfo, aggregatedValidator, scoreConfig), ...mathUtilityFunctions() };
|
|
36
|
+
|
|
37
|
+
for (const [formula, weight, tooltipBuilder] of zip(formulas, weights, getScoreTooltipBuilders())) {
|
|
38
|
+
let score = 0;
|
|
39
|
+
try {
|
|
40
|
+
values.push(0);
|
|
41
|
+
score = Parser.evaluate(formula, variables);
|
|
42
|
+
scoreErrors.push(false);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
error('Failed to calculate', formula, 'for', aggregatedValidator.voteAccount, 'err:', err);
|
|
45
|
+
scoreErrors.push(true);
|
|
46
|
+
}
|
|
47
|
+
totalWeight += weight;
|
|
48
|
+
totalScore += weight * score;
|
|
49
|
+
if (scoreConfig.concentrationParams.includes(scores.length)) {
|
|
50
|
+
totalConcentrationWeight += weight;
|
|
51
|
+
totalConcentrationScore += weight * score;
|
|
52
|
+
}
|
|
53
|
+
tooltips.push(tooltipBuilder(aggregatedValidator, clusterInfo));
|
|
54
|
+
scores.push(score);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
score: totalScore / totalWeight,
|
|
59
|
+
concentrationScore: totalConcentrationScore / totalConcentrationWeight,
|
|
60
|
+
scores,
|
|
61
|
+
values,
|
|
62
|
+
scoreErrors,
|
|
63
|
+
tooltips,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function computeValidatorsScores(clusterInfo: ClusterInfo, aggregatedValidators: AggregatedValidators, formulas: string[], weights: number[], scoreConfig: ScoreConfig): Scores {
|
|
67
|
+
const result: Scores = {};
|
|
68
|
+
|
|
69
|
+
for (const [voteAccount, aggregatedValidator] of Object.entries(aggregatedValidators)) {
|
|
70
|
+
result[voteAccount] = computeValidatorScore(clusterInfo, aggregatedValidator, formulas, weights, scoreConfig);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
export function scoreTooltipCredits(validator: AggregatedValidator, clusterInfo: ClusterInfo): string {
|
|
76
|
+
return `Credits: ${validator.credits.slice(1).map((credits, e) => {
|
|
77
|
+
const epoch = validator.epochs[e + 1];
|
|
78
|
+
if (epoch !== undefined) {
|
|
79
|
+
const targetCredits = clusterInfo.targetCreditsByEpoch.get(epoch) || 1;
|
|
80
|
+
return `${(100 * credits / targetCredits).toFixed(2)}%`;
|
|
81
|
+
} else {
|
|
82
|
+
return 'N/A';
|
|
83
|
+
}
|
|
84
|
+
}).join(', ')}`;
|
|
85
|
+
}
|
|
86
|
+
export function scoreTooltipBlockProduction(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
87
|
+
return `Block production: ${validator.blocksProduced.map((blocksProduced, e) => {
|
|
88
|
+
const leaderSlot = validator.leaderSlots[e];
|
|
89
|
+
if (leaderSlot) {
|
|
90
|
+
return `${(100 * blocksProduced / leaderSlot).toFixed(2)}%`;
|
|
91
|
+
} else {
|
|
92
|
+
return '-';
|
|
93
|
+
}
|
|
94
|
+
}).join(', ')}`;
|
|
95
|
+
}
|
|
96
|
+
export function scoreTooltipCommissionInflation(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
97
|
+
return `Inflation commission: ${validator.commission.map(c => `${c}%`).join(', ')}`;
|
|
98
|
+
}
|
|
99
|
+
export function scoreTooltipCommissionMEV(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
100
|
+
return `MEV commission: ${validator.mevCommission} %`;
|
|
101
|
+
}
|
|
102
|
+
export function scoreTooltipCountryStakeConcentration(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
103
|
+
return `Country: ${validator.country}`;
|
|
104
|
+
}
|
|
105
|
+
export function scoreTooltipCityStakeConcentration(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
106
|
+
return `City: ${validator.city}`;
|
|
107
|
+
}
|
|
108
|
+
export function scoreTooltipNodeStake(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
109
|
+
return `Stake of the node: ${Math.round(validator.stake[0] ?? 0).toLocaleString()} SOL`;
|
|
110
|
+
}
|
|
111
|
+
export function scoreTooltipASOStakeConcentration(validator: AggregatedValidator, _clusterInfo: ClusterInfo): string {
|
|
112
|
+
return `ASO: ${validator.aso}`;
|
|
113
|
+
}
|
|
114
|
+
export function getScoreTooltipBuilders(): ((validator: AggregatedValidator, clusterInfo: ClusterInfo) => string)[] {
|
|
115
|
+
return [
|
|
116
|
+
scoreTooltipCredits,
|
|
117
|
+
scoreTooltipBlockProduction,
|
|
118
|
+
scoreTooltipCommissionInflation,
|
|
119
|
+
scoreTooltipCommissionMEV,
|
|
120
|
+
scoreTooltipCountryStakeConcentration,
|
|
121
|
+
scoreTooltipCityStakeConcentration,
|
|
122
|
+
scoreTooltipASOStakeConcentration,
|
|
123
|
+
scoreTooltipNodeStake,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
export function getDefaultFormulas(scoringConfig: ScoringConfig): string[] {
|
|
127
|
+
return [
|
|
128
|
+
scoringConfig.formulaVoteCredits,
|
|
129
|
+
scoringConfig.formulaBlockProduction,
|
|
130
|
+
scoringConfig.formulaInflationCommission,
|
|
131
|
+
scoringConfig.formulaMEVCommission,
|
|
132
|
+
scoringConfig.formulaStakeConcentrationCountry,
|
|
133
|
+
scoringConfig.formulaStakeConcentrationCity,
|
|
134
|
+
scoringConfig.formulaStakeConcentrationASO,
|
|
135
|
+
scoringConfig.formulaStakeConcentrationNode,
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
export function getDefaultWeights(scoringConfig: ScoringConfig): number[] {
|
|
139
|
+
return [
|
|
140
|
+
scoringConfig.weightVoteCredits,
|
|
141
|
+
scoringConfig.weightBlockProduction,
|
|
142
|
+
scoringConfig.weightInflationCommission,
|
|
143
|
+
scoringConfig.weightMEVCommission,
|
|
144
|
+
scoringConfig.weightStakeConcentrationCountry,
|
|
145
|
+
scoringConfig.weightStakeConcentrationCity,
|
|
146
|
+
scoringConfig.weightStakeConcentrationASO,
|
|
147
|
+
scoringConfig.weightStakeConcentrationNode,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
export function orderByEligibiltyAndScore(scores: Scores, eligibilities: ValidatorsEligibilities): string[] {
|
|
151
|
+
const voteAccounts = Object.keys(scores);
|
|
152
|
+
voteAccounts.sort((a, b) => {
|
|
153
|
+
const eligibilityOrder = Number(eligibilities[b]?.basicEligibility ?? 0) - Number(eligibilities[a]?.basicEligibility ?? 0);
|
|
154
|
+
if (eligibilityOrder === 0) {
|
|
155
|
+
const scoreA = scores[a]?.score ?? 0;
|
|
156
|
+
const scoreB = scores[b]?.score ?? 0;
|
|
157
|
+
|
|
158
|
+
return scoreB - scoreA;
|
|
159
|
+
}
|
|
160
|
+
return eligibilityOrder;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return voteAccounts;
|
|
164
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { ValidatorsEligibilities } from '../dto/eligibility.dto';
|
|
2
|
+
import { Scores, ScoringConfig } from '../dto/scoring.dto';
|
|
3
|
+
import { Votes } from '../dto/snapshots.dto';
|
|
4
|
+
import { ProcessedVotesAndTVLs, Stakes, StakesConfig, StakingVariables } from '../dto/stakes.dto';
|
|
5
|
+
import { AggregatedValidators } from '../dto/validators.dto';
|
|
6
|
+
import { Parser } from 'expr-eval';
|
|
7
|
+
import { sum } from '../utils/maths';
|
|
8
|
+
import { error } from 'logger';
|
|
9
|
+
|
|
10
|
+
export interface ComputeStakesForValidatorsParams {
|
|
11
|
+
stakingVariables: StakingVariables;
|
|
12
|
+
eligibleValidators: string[];
|
|
13
|
+
scores: Scores;
|
|
14
|
+
eligibilities: ValidatorsEligibilities;
|
|
15
|
+
stakesConfig: StakesConfig;
|
|
16
|
+
mSolVotes: Votes;
|
|
17
|
+
veMndeVotes: Votes;
|
|
18
|
+
totalMsolVotes: number;
|
|
19
|
+
totalVeMndeVotes: number;
|
|
20
|
+
mSolTvl: number;
|
|
21
|
+
veMndeTvl: number;
|
|
22
|
+
algoTvl: number;
|
|
23
|
+
blockSize: number;
|
|
24
|
+
}
|
|
25
|
+
export function removeFromVotes(votes: Votes, toRemove: string): Votes {
|
|
26
|
+
const votesCopy = { ...votes };
|
|
27
|
+
delete votesCopy[toRemove];
|
|
28
|
+
return votesCopy;
|
|
29
|
+
}
|
|
30
|
+
export function processVotesAndCalculateTVLs(
|
|
31
|
+
mSolVotes: Votes,
|
|
32
|
+
veMndeVotes: Votes,
|
|
33
|
+
stakesConfig: StakesConfig,
|
|
34
|
+
scoringConfig: ScoringConfig
|
|
35
|
+
): ProcessedVotesAndTVLs {
|
|
36
|
+
const updatedVeMndeVotes = removeFromVotes({ ...veMndeVotes }, scoringConfig.DS_PUBKEY);
|
|
37
|
+
const veMndeVotesForDS = veMndeVotes[scoringConfig.DS_PUBKEY] ?? 0;
|
|
38
|
+
const totalVeMndeVotes = sum(Object.values(veMndeVotes));
|
|
39
|
+
const veMndeTvl = Math.round(
|
|
40
|
+
totalVeMndeVotes > 0
|
|
41
|
+
? (1 - veMndeVotesForDS / totalVeMndeVotes) * stakesConfig.veMndeControl * stakesConfig.tvl
|
|
42
|
+
: 0
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const updatedMSolVotes = removeFromVotes({ ...mSolVotes }, scoringConfig.DS_PUBKEY);
|
|
46
|
+
const mSolVotesForDS = mSolVotes[scoringConfig.DS_PUBKEY] ?? 0;
|
|
47
|
+
const totalMSolVotes = sum(Object.values(mSolVotes));
|
|
48
|
+
const mSolTvl = Math.round(
|
|
49
|
+
totalMSolVotes > 0
|
|
50
|
+
? (1 - mSolVotesForDS / totalMSolVotes) * stakesConfig.mSolControl * stakesConfig.tvl
|
|
51
|
+
: 0
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
updatedMSolVotes,
|
|
56
|
+
updatedVeMndeVotes,
|
|
57
|
+
mSolTvl,
|
|
58
|
+
veMndeTvl,
|
|
59
|
+
totalTvl: stakesConfig.tvl
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function filterEligibleValidators(aggregatedValidators: AggregatedValidators, eligibilities: ValidatorsEligibilities): string[] {
|
|
63
|
+
return Object.keys(aggregatedValidators).filter(voteAccount => eligibilities[voteAccount]?.basicEligibility);
|
|
64
|
+
}
|
|
65
|
+
export function initStakes(eligibleValidators: string[]): Stakes {
|
|
66
|
+
const result: Stakes = {};
|
|
67
|
+
for (const voteAccount of eligibleValidators) {
|
|
68
|
+
result[voteAccount] = {
|
|
69
|
+
algoStake: 0,
|
|
70
|
+
mSolStake: 0,
|
|
71
|
+
veMndeStake: 0,
|
|
72
|
+
totalStake: 0,
|
|
73
|
+
algoStakeFromOverflow: 0,
|
|
74
|
+
mSolStakeFromOverflow: 0,
|
|
75
|
+
veMndeStakeFromOverflow: 0,
|
|
76
|
+
totalStakeFromOverlfow: 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
export function computeStakesForValidators(
|
|
82
|
+
params: ComputeStakesForValidatorsParams
|
|
83
|
+
): Stakes {
|
|
84
|
+
let updatedTotalMSolVotes = params.totalMsolVotes;
|
|
85
|
+
let updatedTotalVeMndeVotes = params.totalVeMndeVotes;
|
|
86
|
+
let algoStakeDistributed = 0;
|
|
87
|
+
let mSolStakeDistributed = 0;
|
|
88
|
+
let veMndeStakeDistributed = 0;
|
|
89
|
+
const stakes = initStakes(params.eligibleValidators);
|
|
90
|
+
|
|
91
|
+
let round = 1;
|
|
92
|
+
while (round <= 100 && (params.mSolTvl - mSolStakeDistributed > 1 || params.veMndeTvl - veMndeStakeDistributed > 1)) {
|
|
93
|
+
let someStakeIncreased = false;
|
|
94
|
+
const mSolStakeDistributedBeforeRound = mSolStakeDistributed;
|
|
95
|
+
const veMndeStakeDistributedBeforeRound = veMndeStakeDistributed;
|
|
96
|
+
for (const voteAccount of params.eligibleValidators) {
|
|
97
|
+
if (!stakes[voteAccount] || !params.scores[voteAccount] || !params.eligibilities[voteAccount]) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let blocks = 0;
|
|
102
|
+
|
|
103
|
+
if (round === 1) {
|
|
104
|
+
if (params.eligibilities[voteAccount]!.bonusEligibility) {
|
|
105
|
+
blocks += params.stakesConfig.stakeBlocksFromBonus;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
blocks += Parser.evaluate(params.stakesConfig.formulaStakeBlocksFromScore, { ...params.stakingVariables, score: params.scores[voteAccount]!.score });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const algoStake = Math.round(Math.min(params.blockSize * blocks, Math.max(params.algoTvl - algoStakeDistributed, 0)));
|
|
112
|
+
const mSolStake = updatedTotalMSolVotes > 0 ? Math.round((params.mSolVotes[voteAccount] ?? 0) / updatedTotalMSolVotes * Math.max(params.mSolTvl - mSolStakeDistributedBeforeRound, 0)) : 0;
|
|
113
|
+
const veMndeStake = updatedTotalVeMndeVotes > 0 ? Math.round((params.veMndeVotes[voteAccount] ?? 0) / updatedTotalVeMndeVotes * Math.max(params.veMndeTvl - veMndeStakeDistributedBeforeRound, 0)) : 0;
|
|
114
|
+
const stake = algoStake + veMndeStake + mSolStake;
|
|
115
|
+
|
|
116
|
+
if (!stake) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cappedStake = Math.min(params.eligibilities[voteAccount]!.capFromBond - stakes[voteAccount]!.totalStake, params.eligibilities[voteAccount]!.capFromExternalStake - stakes[voteAccount]!.totalStake, stake);
|
|
121
|
+
const stakeOverflow = stake - cappedStake;
|
|
122
|
+
const algoStakeOverflow = Math.round(algoStake / stake * stakeOverflow);
|
|
123
|
+
const mSolStakeOverflow = Math.round(mSolStake / stake * stakeOverflow);
|
|
124
|
+
const veMndeStakeOverflow = Math.round(veMndeStake / stake * stakeOverflow);
|
|
125
|
+
const cappedAlgoStake = algoStake - algoStakeOverflow;
|
|
126
|
+
const cappedMSolStake = mSolStake - mSolStakeOverflow;
|
|
127
|
+
const cappedVeMndeStake = veMndeStake - veMndeStakeOverflow;
|
|
128
|
+
|
|
129
|
+
if (cappedAlgoStake + cappedMSolStake + cappedVeMndeStake > 0) {
|
|
130
|
+
someStakeIncreased = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
algoStakeDistributed += cappedAlgoStake;
|
|
134
|
+
mSolStakeDistributed += cappedMSolStake;
|
|
135
|
+
veMndeStakeDistributed += cappedVeMndeStake;
|
|
136
|
+
|
|
137
|
+
stakes[voteAccount]!.algoStake += cappedAlgoStake;
|
|
138
|
+
stakes[voteAccount]!.mSolStake += cappedMSolStake;
|
|
139
|
+
stakes[voteAccount]!.veMndeStake += cappedVeMndeStake;
|
|
140
|
+
stakes[voteAccount]!.totalStake += cappedAlgoStake + cappedMSolStake + cappedVeMndeStake;
|
|
141
|
+
if (round > 1) {
|
|
142
|
+
stakes[voteAccount]!.algoStakeFromOverflow += cappedAlgoStake;
|
|
143
|
+
stakes[voteAccount]!.mSolStakeFromOverflow += cappedMSolStake;
|
|
144
|
+
stakes[voteAccount]!.veMndeStakeFromOverflow += cappedVeMndeStake;
|
|
145
|
+
stakes[voteAccount]!.totalStakeFromOverlfow += cappedAlgoStake + cappedMSolStake + cappedVeMndeStake;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
round++;
|
|
150
|
+
|
|
151
|
+
if (!someStakeIncreased) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const voteAccount of params.eligibleValidators) {
|
|
156
|
+
if (Math.round(stakes[voteAccount]!.totalStake) === Math.round(params.eligibilities[voteAccount]!.capFromExternalStake)) {
|
|
157
|
+
params.veMndeVotes = removeFromVotes(params.veMndeVotes, voteAccount);
|
|
158
|
+
params.mSolVotes = removeFromVotes(params.mSolVotes, voteAccount);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
updatedTotalVeMndeVotes = sum(Object.values(params.veMndeVotes));
|
|
162
|
+
updatedTotalMSolVotes = sum(Object.values(params.mSolVotes));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const remainingAlgoTvl = Math.max(params.algoTvl - algoStakeDistributed, 0);
|
|
166
|
+
const remainingMSolTvl = Math.max(params.mSolTvl - mSolStakeDistributed, 0);
|
|
167
|
+
const remainingVeMndeTvl = Math.max(params.veMndeTvl - veMndeStakeDistributed, 0);
|
|
168
|
+
for (const voteAccount of params.eligibleValidators) {
|
|
169
|
+
if (!stakes[voteAccount]) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (algoStakeDistributed) {
|
|
173
|
+
const extraStake = Math.round((stakes[voteAccount]?.algoStake ?? 0) / algoStakeDistributed * remainingAlgoTvl);
|
|
174
|
+
stakes[voteAccount]!.totalStake += extraStake;
|
|
175
|
+
stakes[voteAccount]!.algoStake += extraStake;
|
|
176
|
+
stakes[voteAccount]!.algoStakeFromOverflow += extraStake;
|
|
177
|
+
}
|
|
178
|
+
if (mSolStakeDistributed) {
|
|
179
|
+
const extraStake = Math.round((stakes[voteAccount]?.algoStake ?? 0) / mSolStakeDistributed * remainingMSolTvl);
|
|
180
|
+
stakes[voteAccount]!.totalStake += extraStake;
|
|
181
|
+
stakes[voteAccount]!.mSolStake += extraStake;
|
|
182
|
+
stakes[voteAccount]!.mSolStakeFromOverflow += extraStake;
|
|
183
|
+
}
|
|
184
|
+
if (veMndeStakeDistributed) {
|
|
185
|
+
const extraStake = Math.round((stakes[voteAccount]?.algoStake ?? 0) / veMndeStakeDistributed * remainingVeMndeTvl);
|
|
186
|
+
stakes[voteAccount]!.totalStake += extraStake;
|
|
187
|
+
stakes[voteAccount]!.veMndeStake += extraStake;
|
|
188
|
+
stakes[voteAccount]!.veMndeStakeFromOverflow += extraStake;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return stakes;
|
|
192
|
+
}
|
|
193
|
+
export function computeValidatorsStakes(
|
|
194
|
+
aggregatedValidators: AggregatedValidators,
|
|
195
|
+
scores: Scores,
|
|
196
|
+
eligibilities: ValidatorsEligibilities,
|
|
197
|
+
stakesConfig: StakesConfig,
|
|
198
|
+
scoringConfig: ScoringConfig,
|
|
199
|
+
mSolVotes: Votes,
|
|
200
|
+
veMndeVotes: Votes
|
|
201
|
+
): Stakes {
|
|
202
|
+
let updatedMSolVotes = { ...mSolVotes };
|
|
203
|
+
let updatedVeMndeVotes = { ...veMndeVotes };
|
|
204
|
+
|
|
205
|
+
const processedVotesAndTvls = processVotesAndCalculateTVLs(updatedMSolVotes, updatedVeMndeVotes, stakesConfig, scoringConfig);
|
|
206
|
+
|
|
207
|
+
updatedMSolVotes = processedVotesAndTvls.updatedMSolVotes;
|
|
208
|
+
updatedVeMndeVotes = processedVotesAndTvls.updatedVeMndeVotes;
|
|
209
|
+
const algoTvl = stakesConfig.tvl - processedVotesAndTvls.mSolTvl - processedVotesAndTvls.veMndeTvl;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const eligibleValidators = filterEligibleValidators(aggregatedValidators, eligibilities);
|
|
213
|
+
const stakingVariables: StakingVariables = { tvl: algoTvl };
|
|
214
|
+
const blockSize = Parser.evaluate(stakesConfig.formulaStakeBlockSize, stakingVariables);
|
|
215
|
+
|
|
216
|
+
Object.keys(aggregatedValidators).filter((v) => !eligibilities[v]?.basicEligibility).forEach((voteAccount) => {
|
|
217
|
+
updatedVeMndeVotes = removeFromVotes(updatedVeMndeVotes, voteAccount);
|
|
218
|
+
updatedMSolVotes = removeFromVotes(updatedMSolVotes, voteAccount);
|
|
219
|
+
});
|
|
220
|
+
const updatedTotalVeMndeVotes = sum(Object.values(updatedVeMndeVotes));
|
|
221
|
+
const updatedTotalMSolVotes = sum(Object.values(updatedMSolVotes));
|
|
222
|
+
eligibleValidators.sort((a, b) => {
|
|
223
|
+
const scoreA = scores[a]?.score ?? 0;
|
|
224
|
+
const scoreB = scores[b]?.score ?? 0;
|
|
225
|
+
return scoreB - scoreA;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return computeStakesForValidators({
|
|
229
|
+
stakingVariables: stakingVariables,
|
|
230
|
+
eligibleValidators: eligibleValidators,
|
|
231
|
+
scores: scores,
|
|
232
|
+
eligibilities: eligibilities,
|
|
233
|
+
stakesConfig: stakesConfig,
|
|
234
|
+
mSolVotes: updatedMSolVotes,
|
|
235
|
+
veMndeVotes: updatedVeMndeVotes,
|
|
236
|
+
totalMsolVotes: updatedTotalMSolVotes,
|
|
237
|
+
totalVeMndeVotes: updatedTotalVeMndeVotes,
|
|
238
|
+
mSolTvl: processedVotesAndTvls.mSolTvl,
|
|
239
|
+
veMndeTvl: processedVotesAndTvls.veMndeTvl,
|
|
240
|
+
algoTvl: algoTvl,
|
|
241
|
+
blockSize: blockSize
|
|
242
|
+
});
|
|
243
|
+
} catch (err) {
|
|
244
|
+
error("Computing validators stakes failed.", err);
|
|
245
|
+
}
|
|
246
|
+
}
|