-
Notifications
You must be signed in to change notification settings - Fork 1
/
app.ts
316 lines (255 loc) · 11.1 KB
/
app.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import { ApiPromise, WsProvider } from '@polkadot/api';
import { ChainData } from './ChainData';
import { NominationData } from './NominationData';
import { Settings } from './Settings';
import { PrettyOutput, TVP_Candidate, ValidatorScore } from './Types';
import { Utility } from './Utility';
//Main function
async function main() {
//Load setting.ts with some variable from a file specified in settings
await Utility.loadSettingsFile();
let chain_data = ChainData.getInstance();
//Creates a connection to an API endpoint specified in settings.
const api = await ApiPromise.create({ provider: new WsProvider(Settings.ws_provider) }).then(x => {
Utility.Logger.info(`Connected to end point`);
Utility.Logger.info(`Trying to set API to singleton and set chain data...`);
return x;
}).catch(err => {
Utility.Logger.error(err);
return process.exit(-1);
});
//If there is an issue retrieving the API then exit
if (api == undefined) process.exit(-1);
//load chain data information
await getChainPrefix(api).then(prefix => {
chain_data.setPrefix(prefix);
chain_data.setApi(api);
Utility.Logger.info(`API and Chain data set.`);
}).catch(err => {
Utility.Logger.error(err);
});
//With all data loaded initiate the change of nominations
monitor_session_changes();
}
/* Primarily focused on listening for a session event change and switching nominations
when the era and session criterias are met
*/
async function monitor_session_changes() {
let chain_data = ChainData.getInstance();
let api = chain_data.getApi();
//If there is an issue retrieving the API then exit
if (api == undefined) process.exit(-1);
Utility.Logger.info('Waiting for new session event..');
//On the first load set this to 0, validators would be rotated in the next
var change_validators = 0;
//Listen for events
api.query.system.events((events) => {
//For each event
events.forEach((record) => {
// Extract the phase, event and the event types
const { event } = record;
//If the event is a change session event
if (api.events.session.NewSession.is(event)) {
//Determine the session
const current_session = (parseInt(event.data[0].toString()) % 6) + 1;
Utility.Logger.info(`Current session is : ${current_session} on ${api.rpc.chain}, there are ${change_validators} before nominations are changed.`);
//If the session is as specified in the setting
if (current_session == Settings.session_to_change) {
//If the right era is attained
if (change_validators == 0) {
//Refresh information, get a set of 24 validators and then nominate them
load_supporting_information().then(x => {
getValidators().then(validators => {
nominateValidators(validators);
});
});
//After a change in validators reset the counter
change_validators = Settings.era_to_rotate;
} else {
//If the era is not as desired, then decrease the counter
change_validators--;
}
}
}
});
});
}
/* Issues a staking.nominate transaction for a given list of validator stashes
*/
async function nominateValidators(validator_list: string[]) {
let chain_data = ChainData.getInstance();
let api = chain_data.getApi();
const key_ring = await Utility.getKeyring();
if (api != undefined && key_ring != undefined) {
api.tx.staking.nominate(validator_list)
.signAndSend(key_ring)
.then(x => {
Utility.Logger.info('Nominations changed');
})
.catch(err => {
Utility.Logger.error(err);
});
}
}
/* Load supporting information before selecting validators,
this shall be done before each validator selection.
*/
async function load_supporting_information() {
let nomination_data = NominationData.getInstance();
nomination_data.clearData();//clear any previous data
var candidates: string[] = [];
//Gets a filtered list of TVP candidates matching desired criteria
const tvp_candidates = await filterTVPCandidates().then(x => {
Utility.Logger.info(`TVP candidates loaded`);
Utility.Logger.info(`Trying to merge candidate arrays...`);
return x;
}).catch(err => {
Utility.Logger.error(err);
return [];
});
//Combines stashes of the tvp and preferred candidates into a single array
candidates = candidates.concat(tvp_candidates)
.concat(Settings.preferred_candidates);
Utility.Logger.info(`Candidate arrays merged`);
//Converts each stash into a validator object and loads this
//into the nomination_data instance
await loadValidatorArray(candidates).then(x => {
Utility.Logger.info(`Candidates converted to validator objects`);
Utility.Logger.info(`Trying to load nomination data...`);
return x;
}).catch(err => {
Utility.Logger.error(err);
return [];
});
//Populates nominators for each validator entry
await nomination_data.loadNominationData(candidates).then(x => {
Utility.Logger.info(`Nomination data loaded`);
Utility.Logger.info(`Trying to load era points...`);
return x;
}).catch(err => {
Utility.Logger.error(err);
return [];
});
//Populates era points for each validator
await nomination_data.loadAverageEraPoints().then(x => {
Utility.Logger.info(`Era points loaded.`);
}).catch(err => {
Utility.Logger.error(err);
});
}
async function getValidators(): Promise<string[]> {
let nomination_data = NominationData.getInstance();
var sorted_validators = nomination_data.validators.sort((x, y) => x.getValidatorScore() < y.getValidatorScore() ? 1 : -1);
var winners: ValidatorScore[] = [];
var runners_up: ValidatorScore[] = [];
sorted_validators.forEach(validator => {
if (winners.filter(added_validator => added_validator.parent == validator.getParentIdentity()).length < Settings.max_nodes_per_validator // Only x nodes per validator
&& validator.getNominations() < Settings.nomination_threshold // Bonds less than the threshold
) {
//If a validator and nominations are not blocked then add to the winners
//otherwise don't even add to runners up.
if (validator.getIntent() || !validator.getBlockedNominations()) {
winners.push({
val_address: validator.getAddress(),
name: validator.getIdentityName(),
parent: validator.getParentIdentity(),
score: validator.getValidatorScore()
});
}
}
else {
runners_up.push({ val_address: validator.getAddress(), name: validator.getIdentityName(), parent: validator.getParentIdentity(), score: validator.getValidatorScore() });
}
});
var results: string[] = await mergeFinalArray(winners, runners_up);
showDebugInfo(winners, runners_up);
showNominationList(results);
Utility.Logger.info(`(${results.length}) validators obtained`);
return results;
}
async function showNominationList(results: string[]) {
let nomination_data = NominationData.getInstance();
let chain_data = ChainData.getInstance();
Utility.Logger.info(`Nominations for era ${await chain_data.getCurrentEra()}`);
var pretty_output: PrettyOutput[] = [];
results.forEach(result => {
pretty_output.push(
{
val_address: result,
identity: nomination_data.validators
.find(validator => validator.getAddress() == result)
.getIdentityName()
}
);
});
console.log(pretty_output);
}
async function mergeFinalArray(winners: ValidatorScore[], runners_up: ValidatorScore[]): Promise<string[]> {
var results: string[] = [];
results = results.concat(await filterPartners())
.concat(winners.slice(0, Settings.max_nominations - Settings.partners.length - 1)
.map(validator => validator.val_address)
);
results = results.concat(runners_up.slice(0, Settings.max_nominations - results.length)
.map(validator => validator.val_address)
);
return results;
}
function showDebugInfo(winners: ValidatorScore[], runners_up: ValidatorScore[]) {
if (Settings.debug) {
Utility.Logger.debug("Winners");
console.log(winners);
Utility.Logger.debug("Runners up");
console.log(runners_up);
}
}
async function filterPartners(): Promise<string[]> {
let nomination_data = NominationData.getInstance();
var filtered_partners: string[] = [];
for (var i = 0; i < Settings.partners.length; i++) {
const isblocked = await nomination_data.isBlocked(Settings.partners[i]);
const isValidator = await nomination_data.isValidator(Settings.partners[i]);
if (isblocked) {
Utility.Logger.warn(`Partner ${Settings.partners[i]} has nominations blocked, skipping.`);
}
if (!isValidator) {
Utility.Logger.warn(`Partner ${Settings.partners[i]} is not a validator, skipping.`);
}
if (isValidator && !isblocked) {
filtered_partners.push(Settings.partners[i]);
}
};
return filtered_partners;
}
async function loadValidatorArray(candidates: string[]) {
let nomination_data = NominationData.getInstance();
var validator_promises: any[] = [];
candidates.forEach(candidate => {
validator_promises.push(nomination_data.addValidator(candidate));
});
await Promise.all(validator_promises).then(x => {
validator_promises = [];
});
}
async function getChainPrefix(api: ApiPromise) {
const chainInfo = await api.registry.getChainProperties()
var result = "0";
if (chainInfo != null) {
result = chainInfo.ss58Format.toString();
}
return parseInt(result);
}
async function filterTVPCandidates(): Promise<string[]> {
return Utility.getCandidates().then(candidates => {
var tvp_candidates: TVP_Candidate[] = [];
//Valid canidates only
tvp_candidates = candidates.filter(candidate => candidate.valid == true)//Valid candidates
.filter(candidate => candidate.faults == 0)//No faults
.filter(candidate => candidate.unclaimedEras.length == 0)//All payments processed
.filter(candidate => candidate.active == false)//Not already in the active set
.filter(candidate => candidate.commission >= Settings.min_commission //Commission is between min and max
&& candidate.commission <= Settings.max_commission)
return tvp_candidates.map(tvp_candidate => tvp_candidate.stash);
});
}
main();