-
Notifications
You must be signed in to change notification settings - Fork 3
/
decentralizer.js
2641 lines (2316 loc) · 115 KB
/
decentralizer.js
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// DECENTRALIZER 1.1.0
// Copyright 2018, 2019 by Salvador Herrera <[email protected]>
// Licensed under GPLv3
var fs = require('fs');
var sia = require('sia.js');
var http = require('request');
var https = require('https');
var axios = require('axios');
var table = require('table')
var Path = require('path')
var os = require('os')
// Passing arguments
var argument1 = process.argv[2]
if (argument1 == "-debug" || argument1 == "--debug" || argument1 == "-d") {
// Debug mode
var debugMode = true
var argument1 = process.argv[3]
var argument2 = process.argv[4]
var argument3 = process.argv[5]
var argument4 = process.argv[6]
var argument5 = process.argv[7]
var argument6 = process.argv[8]
var argument7 = process.argv[9]
} else {
// Normal mode
var debugMode = false
var argument2 = process.argv[3]
var argument3 = process.argv[4]
var argument4 = process.argv[5]
var argument5 = process.argv[6]
var argument6 = process.argv[7]
var argument7 = process.argv[8]
}
console.log()
if (debugMode == false) {
console.log('\x1b[44m%s\x1b[0m', "*** KEOPS DECENTRALIZER v1.1.2 ***")
} else {
console.log('\x1b[44m%s\x1b[0m', "*** KEOPS DECENTRALIZER v1.1.2 (debug mode) ***")
}
console.log()
// Getting the API authentication key to use in the rest of the program
function apiPassword() {
// Gets the Sia API Password from disk
let configPath
switch (process.platform) {
case 'win32':
configPath = Path.join(process.env.LOCALAPPDATA, 'Sia')
break
case 'darwin':
configPath = Path.join(
os.homedir(),
'Library',
'Application Support',
'Sia'
)
break
default:
configPath = Path.join(os.homedir(), '.sia')
}
const pass = fs.readFileSync(Path.join(configPath, 'apipassword')).toString().trim()
return pass || ''
}
if (debugMode == true) {console.log("// DEBUG - Collecting API password")}
var apiPassword = apiPassword()
if (debugMode == true) {console.log("// DEBUG - API password: " + apiPassword.slice(0,5) + "..." + apiPassword.slice((apiPassword.length-6), (apiPassword.length-1)))}
const basicAuth = `:${apiPassword}@${'localhost:9980'}`
// Directing to the proper function according to the user arguments
if (argument1 == "scan") {
openSettingsFile()
} else if (argument1 == "remove") {
removeContract(argument2)
} else if (argument1 == "view" && argument2 == "farms") {
viewFarms()
} else if (argument1 == "view" && argument2 == "contracts") {
viewContracts()
} else if (argument1 == "view" && argument2 == "hosts" && argument3 == "countries") {
viewCountries()
} else if (argument1 == "view" && argument2 == "hosts" && argument3 == "versions") {
viewVersions()
} else if (argument1 == "view" && argument2 == "hosts" && argument3 != "countries" && argument3 != "orderby" && argument3 != null) {
viewHostsCountry(argument3)
} else if (argument1 == "view" && argument2 == "hosts" && (argument3 == null || argument3 == "orderby")) {
viewHostsAll(argument3, argument4)
} else if (argument1 == "help") {
help()
} else if (argument1 == "filter" && argument2 == null) {
showList()
} else if (argument1 == "filter" && argument2 == "help") {
filterHelp()
} else if (argument4 == "and" || argument4 == "AND" || argument6 == "and" || argument6 == "AND") {
var argumentsArray = [argument3.toString(), argument5.toString()]
// Adding additional arguments, if they exist
if (argument7 != null) {
argumentsArray.push(argument7.toString())
}
combinatorialAdd(argumentsArray)
} else if (argument1 == "filter" && argument2 == "add" && argument3 != null && argument3 != "version" && argument3 != "score") {
addList(argument3)
} else if (argument1 == "filter" && argument2 == "add" && argument3 == "version" && argument4 != null) {
addVersion(argument4)
} else if (argument1 == "filter" && argument2 == "add" && argument3 == "score" && argument4 != null) {
addScore(argument4)
} else if (argument1 == "filter" && argument2 == "remove" && argument3 != "score") {
removeList(argument3)
} else if (argument1 == "filter" && argument2 == "remove" && argument3 == "score" && argument4 != null) {
removeScore(argument4)
} else if (argument1 == "filter" && argument2 == "mode" && (argument3 == "disable" || argument3 == "whitelist" || argument3 == "blacklist")) {
modeList(argument3)
} else if (argument1 == "filter" && argument2 == "farms") {
filterFarms()
} else if (argument1 == "filter" && argument2 == "clear") {
clearList()
} else if (argument1 == "filter" && argument2 == "apply") {
applyList()
} else {
console.log("Invalid syntax")
help()
}
function help() {
console.log()
console.log('\x1b[47m\x1b[30m%s\x1b[0m', " General commands ")
console.log(" * decentralizer help --> Shows all the Decentralizer commands")
console.log(" * decentralizer scan --> Analyzes contracts and shows hosts belonging to hosting farms")
console.log(" * decentralizer remove [x] --> Removes the host numbered 'x' (refer to the 'decentralizer view contracts') from your contracts")
console.log(" * decentralizer remove auto --> Removes all but one of the duplicate hosts in a hosting farm")
console.log(" * decentralizer view farms --> Shows the list of farms obtained on the last scan")
console.log(" * decentralizer view contracts --> Shows the full list of contracts, obtained on the last scan")
console.log('\n\x1b[47m\x1b[30m%s\x1b[0m', " Filter walkthrough ")
console.log(" * decentralizer filter help --> Shows a walkthrough guide for setting up a Filter (recommended)")
console.log('\n\x1b[47m\x1b[30m%s\x1b[0m', " Filter commands ")
console.log(" * decentralizer filter --> Shows your Filter mode (blacklist, whitelist) and the hosts included on it")
console.log(" * decentralizer view hosts --> Shows all hosts, ordered by rank")
console.log(" * decentralizer view hosts countries --> Shows the list of country codes of hosts")
console.log(" * decentralizer view hosts versions --> Shows the list of version numbers of hosts")
console.log(" * decentralizer view hosts [country code] --> Shows the hosts in the specified country")
console.log(" * decentralizer view hosts orderby [storage/upload/download/collateral/score] --> Shows all hosts, ordered by the indicated parameter")
console.log(" * decentralizer filter add [hostID / country code] --> Adds the desired HostID or all the hosts in a country to the Filter")
console.log(" * decentralizer filter add version [version] --> Adds to the filter all the hosts using the selected Sia version (e.g. 1.4.0)")
console.log(" * decentralizer filter add score [score] --> Adds to the filter all the hosts with an specific SiaStats performance score (e.g. 9)")
console.log(" * decentralizer filter add [countries] AND [versions] AND [scores] --> Adds to the filter all the hosts the meet the combination of any of")
console.log(" these parameters. ('filter help' for more information)")
console.log(" * decentralizer filter remove [y] --> Removes the host with FilterID 'y' (check it with 'filter show') from the Filter")
console.log(" * decentralizer filter remove score [score] --> Removes from the filter any host with the specified SiaStats performance score (e.g. 9)")
console.log(" * decentralizer filter mode [disable/whitelist/blacklist] --> Changes the mode of the Filter that will be applied to the list of hosts")
console.log(" * decentralizer filter clear --> Removes all the hosts from the Filter, and sets its mode to 'disable'")
console.log(" * decentralizer filter farms --> On whitelist, removes hosts in farms from the Filter. On blacklist, adds them to the Filter")
console.log(" * decentralizer filter apply --> Applies the Filter of hosts and the Filter mode (white/blacklist/disable) to Sia")
console.log()
}
function filterHelp() {
console.log("This is a short walkthrough of the process of setting up a Filter of hosts for Sia")
console.log("It assumes you are using a clean Filter. The Filter can be cleared at any moment with the command 'decentralizer filter clear'")
console.log('\n\x1b[47m\x1b[30m%s\x1b[0m', " 1 - Set the filter type ")
console.log("\nSet the filter mode with the command './decentralizer filter mode [mode]'")
// Table with modes
var data = [
["[mode]", "Result"],
["disable", "Disables the filter and removes all the hosts from it. Sia will use any host on its database"],
["whitelist", "Only the hosts included on the Filter will be used by Sia to form contracts with"],
["blacklist", "The hosts included in the Filter will be avoided by Sia"]
]
output = table.table(data);
console.log(output);
console.log('\n\x1b[47m\x1b[30m%s\x1b[0m', " 2 - (Optional) Check the hosts available for your preferences ")
console.log('\nYou can check the list of hosts available, their settings and characteristics, with the following commands:')
console.log(" * Display all the hosts, ordered by their Sia rank on your hostdb, with the command 'decentralizer view hosts'")
console.log(" * Display all the hosts, ordered by the criteria of your choice with the command 'decentralizer view hosts orderby [parameter]'")
console.log(" 'parameter' can be: 'storage' (price), 'upload' (price), 'download' (price), 'collateral' (price), 'score' (SiaStats performance score)")
console.log(" * You can check the number of hosts available on each country or usuing each Sia software version with the command")
console.log(" 'decentralizer view hosts [countries/versions]'")
console.log(" * Check all the hosts available on an specific country with 'decentralizer view hosts [country code]'. Use 2-character interantional codes.")
console.log(" 'EU' can be used, representing the European Economic Area")
console.log('\n\n\x1b[47m\x1b[30m%s\x1b[0m', " 3 - Add hosts to the filter ")
console.log("\nHosts can be added to the filter with several methods:")
console.log(" * Add a single host manually with its hostID (obtain it from the lists shown on the previous step): use `decentralizer filter add [hostID]`")
console.log(" * Add all the hosts in a country with the command: `decentralizer filter add [country code]` (use 2-character interantional codes)")
console.log(" * Add all the hosts using an specific Sia software version: `decentralizer filter add version [version]")
console.log(" Example: `decentralizer filter add 1.4.0`")
console.log(" * Add all the hosts with an specific SiaStats performance score: `decentralizer filter add score [score]")
console.log(" * Add hosts with a combination of country, version and score: `decentralizer filter add [countries] AND [versions] AND [scores]")
console.log(" You can use either two or 3 criteria. Countries, versions and scores can be indicated on any order. For more than one element on each criteria,")
console.log(" place them inside brackets, separated by commas and no spaces in between.")
console.log(" Some examples: `decentralizer filter add [US,CA,MX] AND 1.4.0 AND [10,9,8,7]`; `decentralizer filter add [1.4.1,1.4.0] AND EU`")
console.log('\n\n\x1b[47m\x1b[30m%s\x1b[0m', " 4 - Remove hosts from the filter to customize further ")
console.log("\n * Check the hosts currently on your filter with `decentralizer filter`")
console.log(" * Remove individual hosts with `decentralizer filter remove [FilterId]` (get FilterId from the previous command)")
console.log(" * Remove all the hosts with an specific SiaStats performance score: `decentralizer filter remove score [score]")
console.log('\n\n\x1b[47m\x1b[30m%s\x1b[0m', " 5 - (Optional) Avoid hosting farms on your hosts list")
console.log("\nYou can avoid forming more than one contract with a single hosting farm with the command `decentralizer filter farms`. If you are building a")
console.log("whitelist, farms will be removed. If it is a blacklist, all farms will be included. Only one host per farm will be kept for forming contracts")
console.log('\n\n\x1b[47m\x1b[30m%s\x1b[0m', " 6 - Check and apply the Filter to Sia")
console.log("\n * Check the hosts currently on your filter with `decentralizer filter`")
console.log(" * Apply the filter to the Sia software with the command `decentralizer filter apply`. Changes on the filter done with the previous commands are")
console.log(" only made final with this command\n")
}
function openSettingsFile() {
// First, updating the settings file or creating a new one if it is the first syncing
if (debugMode == true) {console.log("// DEBUG - Opening the settings file")}
var timestamp = Date.now() // Timestamp
// Opening settings file
fs.readFile('databases/settings.json', 'utf8', function (err, data) { if (!err) {
if (debugMode == true) {console.log("// DEBUG - Found settings file, updating it")}
var settings = JSON.parse(data)
settings.lastsync = timestamp
geolocUser(settings)
} else {
// Initialize a settings file here
if (debugMode == true) {console.log("// DEBUG - No settings file found. Creating a new one")}
settings = {
userLon: null,
userLat: null,
lastsync: timestamp,
listMode: "disable"
}
geolocUser(settings)
}});
}
function geolocUser(settings) {
// Geolocates user. This is not necessary for any function of Decentralizer-CLI,
// but all the json files need to be compatible with Decentralizer-GUI, which uses the user geoloc for showing the map
var ipquery = "http://ip-api.com/json/"
axios.get(ipquery).then(response => {
var ipAPI = response.data
settings.userLon = parseFloat(ipAPI.lon)
settings.userLat = parseFloat(ipAPI.lat)
fs.writeFileSync('databases/settings.json', JSON.stringify(settings))
siastatsGeolocFile()
}).catch(error => {
console.log("failed")
console.log(error)
fs.writeFileSync('databases/settings.json', JSON.stringify(settings))
siastatsGeolocFile()
})
}
function siastatsGeolocFile() {
// SiaStats JSON geolocation. If the file can't be downloaded, the local copy is used instead
if (debugMode == true) {console.log("// DEBUG - Getting the SiaStats geolocation/scores API")}
// Removing SSL authorization for this specific API call
var agent = new https.Agent({
rejectUnauthorized: false
});
axios.get('https://siastats.info:3510/hosts-api/decentralizer/sia', { httpsAgent: agent }).then(response => {
var siastatsGeoloc = response.data
console.log("Downloaded " + siastatsGeoloc.length + " hosts geolocation and score from SiaStats.info");
// Saving the file
fs.writeFileSync('databases/hosts_geoloc.json', JSON.stringify(siastatsGeoloc))
siastatsFarmsFile(siastatsGeoloc)
}).catch(error => {
if (debugMode == true) {console.log("// DEBUG - Could not download SiaStats API. Reading local file instead. Error: \n" + error)}
fs.readFile('databases/hosts_geoloc.json', 'utf8', function (err, data) { if (!err) {
siastatsGeoloc = JSON.parse(data);
console.log("The hosts geolocation file could not be fetched from SiaStats.info. Using a local copy instead")
siastatsFarmsFile(siastatsGeoloc)
} else {
console.log("ERROR - The software can't find locally, or download, necessary databases. Try re-installing Decentralizer or connecting to the Internet")
}});
});
}
function siastatsFarmsFile(siastatsGeoloc) {
// SiaStats JSON geolocation. If the file can't be downloaded, the local copy is used instead
if (debugMode == true) {console.log("// DEBUG - Getting the SiaStats farms API")}
axios.get('https://siastats.info/dbs/farms_api.json').then(response => {
var siastatsFarms = response.data
console.log("Downloaded data from " + siastatsFarms.length + " farms from SiaStats.info");
// Saving the file
fs.writeFileSync('databases/farms_definition.json', JSON.stringify(siastatsFarms))
siaHosts(siastatsGeoloc, siastatsFarms)
}).catch(error => {
if (debugMode == true) {console.log("// DEBUG - Could not download SiaStats API. Reading local file instead. Error: \n" + error)}
fs.readFile('databases/farms_definition.json', 'utf8', function (err, data) { if (!err) {
siastatsFarms = JSON.parse(data);
console.log("The farms definition file could not be fetched from SiaStats.info. Using a local copy instead")
siaHosts(siastatsGeoloc, siastatsFarms)
} else {
console.log("ERROR - The software can't find locally, or download, necessary databases. Try re-installing Decentralizer or connecting to the Internet")
}});
});
}
function siaHosts(siastatsGeoloc, siastatsFarms) {
// Requesting active hosts with an API call:
console.log("Retreiving your hosts list from Sia")
sia.connect('localhost:9980')
.then((siad) => {siad.call('/hostdb/all')
.then((hosts) => {
var allHosts = hosts.hosts
// Filtering only the active and accepting contracts. If I was using the /hostdb/active, it would show less hosts after applying a filter
var active = []
for (var i = 0; i < allHosts.length; i++) {
if (allHosts[i].scanhistory != null) { // It has already one scan
if (allHosts[i].scanhistory[allHosts[i].scanhistory.length-1].success == true
&& allHosts[i].acceptingcontracts == true) {
active.push(allHosts[i])
}
}
}
var hostNum = 0
if (debugMode == true) {console.log("// DEBUG - Iterating hostdb/hosts/ Sia calls for each host")}
hostsScore(siastatsGeoloc, siastatsFarms, active, hostNum)
})
.catch((err) => {
console.log("Error retrieving data from Sia. Is Sia working, synced and connected to internet? Try this script again after restarting Sia.")
if (debugMode == true) {console.log("// DEBUG - Error: \n" + err)}
console.log()
})
})
.catch((err) => {
console.log("Error connecting to Sia. Start the Sia app (either daemon or UI) and try again")
console.log()
if (debugMode == true) {console.log("// DEBUG - Error: \n" + err)}
})
}
function hostsScore(siastatsGeoloc, siastatsFarms, active, hostNum) {
// Iterates on each host to collect from Sia the score of the host
if (hostNum < active.length) {
sia.connect('localhost:9980')
.then((siad) => {siad.call('/hostdb/hosts/' + active[hostNum].publickeystring)
.then((host) => {
var score = host.scorebreakdown.conversionrate
active[hostNum].score = score
hostNum++
process.stdout.clearLine(); // clear current text
process.stdout.cursorTo(0); // move cursor to beginning of line
process.stdout.write("(" + hostNum + "/" + active.length + ") - " + active[hostNum-1].netaddress)
hostsScore(siastatsGeoloc, siastatsFarms, active, hostNum)
})
.catch((err) => {
console.log("Error retrieving data from Sia. Is Sia working, synced and connected to internet? Try this script again after restarting Sia.")
if (debugMode == true) {console.log("// DEBUG - Error on host " + active[hostNum].publickeystring + ": \n" + err)}
console.log()
})
})
.catch((err) => {
console.log("Error connecting to Sia. Start the Sia app (either daemon or UI) and try again")
if (debugMode == true) {console.log("// DEBUG - Error on host " + active[hostNum].publickeystring + ": \n" + err)}
console.log()
})
} else {
// We are done. Move to the next step
process.stdout.clearLine(); // clear current text
console.log()
if (debugMode == true) {console.log("// DEBUG - Host data collection done")}
// Arranges the hosts array by score
function compare(a,b) {
if (a.score < b.score)
return -1;
if (a.score > b.score)
return 1;
return 0;
}
active.sort(compare);
hostsProcessing(siastatsGeoloc, siastatsFarms, active)
}
}
function hostsProcessing(siastatsGeoloc, siastatsFarms, hostdb) {
if (debugMode == true) {console.log("// DEBUG - Starting hostsProcessing() function")}
// Assigns IPs to the hostdb and determines the hosts that need additional geolocation
hostsToGeoloc = [] // Entries numbers that need to be geolocated locally by Decentralizer
for (var i = 0; i < hostdb.length; i++) { // For each host
var matchBool = false
for (var j = 0; j < siastatsGeoloc.length; j++) { // For each geolocation in list
if (hostdb[i].publickeystring == siastatsGeoloc[j].pubkey) {
// Match, update hostdb entry
matchBool = true
hostdb[i].lon = siastatsGeoloc[j].lon
hostdb[i].lat = siastatsGeoloc[j].lat
hostdb[i].countryName = siastatsGeoloc[j].countryName
hostdb[i].countryCode = siastatsGeoloc[j].countryCode
hostdb[i].siastatsScore = siastatsGeoloc[j].siastatsScore
// We update the geoloc file with the pubkey in the non-hex format, as it will be lated needed for the contracts identification
siastatsGeoloc[j].pubkey2 = hostdb[i].publickey.key
}
}
if (matchBool == false) {
// If no match, add to the list
hostsToGeoloc.push(i)
hostdb[i].siastatsScore = 0 // Adding a 0 in the score
}
}
console.log("Number of additional hosts to be geolocated: " + hostsToGeoloc.length + "\n")
if (hostsToGeoloc.length > 0) {
var i = 0
requestIP(siastatsFarms, hostdb, hostsToGeoloc, i, siastatsGeoloc)
} else {
// No additional host to geolocate, save and proceed to next step
if (debugMode == true) {console.log("// DEBUG - No additional host to geolocate. Moving to compareOldDb()")}
compareOldDb(hostdb, siastatsFarms, siastatsGeoloc)
}
}
function requestIP(siastatsFarms, hostdb, hostsToGeoloc, i, siastatsGeoloc) {
// Triming the ":port" from the host IP
var hostip = hostdb[hostsToGeoloc[i]].netaddress
var s = hostip.search(":")
var totrim = hostip.length - s
trimedip = hostip.slice(0, -totrim)
// Requesting the geolocation of the host
var ipquery = "http://ip-api.com/json/" + trimedip
axios.get(ipquery).then(response => {
var ipAPI = response.data
var lat = parseFloat(ipAPI.lat)
var lon = parseFloat(ipAPI.lon)
process.stdout.clearLine(); // clear current text
process.stdout.cursorTo(0); // move cursor to beginning of line
process.stdout.write("(" + (i+1) + "/" + hostsToGeoloc.length + ") - " + hostip)
hostdb[hostsToGeoloc[i]].lon = lon
hostdb[hostsToGeoloc[i]].lat = lat
hostdb[hostsToGeoloc[i]].as = ipAPI.as // Also adding the ISP
hostdb[hostsToGeoloc[i]].countryName = ipAPI.country // Also adding the ISP
hostdb[hostsToGeoloc[i]].countryCode = ipAPI.countryCode // Also adding the ISP
nextIP(siastatsFarms, hostdb, hostsToGeoloc, i, siastatsGeoloc)
}).catch(error => {
// On failed IP request, move to the next IP
console.log(hostip + " - Failed")
if (debugMode == true) {console.log("// DEBUG - Error (non-critical): \n" + error)}
nextIP(siastatsFarms, hostdb, hostsToGeoloc, i, siastatsGeoloc)
})
}
function nextIP(siastatsFarms, hostdb, hostsToGeoloc, i, siastatsGeoloc) {
setTimeout(function(){ // 500ms cooldown, to avoid being banned by ip-api.com
i++
if (i < hostsToGeoloc.length) {
requestIP(siastatsFarms, hostdb, hostsToGeoloc, i, siastatsGeoloc)
} else {
console.log("\nGeolocation task done!\n")
compareOldDb(hostdb, siastatsFarms, siastatsGeoloc)
}
}, 500);
}
function compareOldDb(hostdb, siastatsFarms, siastatsGeoloc) {
// Opening the hosts file to re-add the "onlist" value (hosts added to the Filter)
if (debugMode == true) {console.log("// DEBUG - compareOldDb(): reading hosts.json and updating it")}
fs.readFile('databases/hosts.json', 'utf8', function (err, data) { if (!err) {
oldHosts = JSON.parse(data);
for (var i = 0; i < hostdb.length; i++) {
for (var j = 0; j < oldHosts.length; j++) {
if (hostdb[i].publickey.key == oldHosts[j].publickey.key) { // Match of hosts
if (oldHosts[j].onList == true) {
// Add the boolean to the new hostdb
hostdb[i].onList = true
}
}
}
}
// Saving the file
fs.writeFileSync('databases/hosts.json', JSON.stringify(hostdb))
// Next
siaContracts(siastatsFarms, siastatsGeoloc)
} else {
// If no file was found, it is the first scanning: just proceed
if (debugMode == true) {console.log("// DEBUG - No previous hosts.json file. Creating new")}
fs.writeFileSync('databases/hosts.json', JSON.stringify(hostdb))
siaContracts(siastatsFarms, siastatsGeoloc)
}});
}
function siaContracts(siastatsFarms, siastatsGeoloc) {
// Requesting the contracts list with an API call:
console.log("Retreiving contracts list from Sia")
sia.connect('localhost:9980')
.then((siad) => {siad.call('/renter/contracts')
.then((contractsAPI) => {
if (debugMode == true) {console.log("// DEBUG - /renter/contracts call succedded")}
var contracts = contractsAPI.contracts
if (contracts.length == 0) {
contracts = []
fs.writeFileSync('databases/contracts.json', JSON.stringify(contracts))
farms = []
fs.writeFileSync('databases/farms.json', JSON.stringify(farms))
console.log("Initial sync done: You don't have currently any active contract. Set an allowance first in Sia")
console.log()
} else {
// Considering only the contracts good for upload and good for renew, this is, active
// (sia returns active and inactive all together)
var activeContracts = []
for (var i = 0; i < contracts.length; i++) {
if (contracts[i].goodforupload == false && contracts[i].goodforrenew == false) {
// Inactive contract, do not consider further
} else {
activeContracts.push(contracts[i])
}
}
console.log("Checking IPs of " + activeContracts.length + " active contracts")
contractsIpAssign(siastatsFarms, activeContracts, siastatsGeoloc)
}
})
.catch((err) => {
// In some circumstances, abscense of contracts can make this call to fail
contracts = []
fs.writeFileSync('databases/contracts.json', JSON.stringify(contracts))
farms = []
fs.writeFileSync('databases/farms.json', JSON.stringify(farms))
if (debugMode == true) {
console.log("// DEBUG - Error retrieving data from Sia. Is Sia working, synced and connected to internet? Try this script again after restarting Sia.")
console.log("// DEBUG - Error: " + err)
}
console.log("Initial sync done: You don't have currently any active contract. Set an allowance first in Sia")
console.log()
})
})
.catch((err) => {
console.log("Error connecting to Sia. Start the Sia app (either daemon or UI) and try again")
if (debugMode == true) {console.log("// DEBUG - Error: " + err)}
console.log()
})
}
function contractsIpAssign(siastatsFarms, contracts, siastatsGeoloc) {
// Assigns IPs to the contracts and determines the hosts that need additional geolocation
if (debugMode == true) {console.log("// DEBUG - contractsIPAssign(): adding geolocation/score data to hosts")}
contractsToGeoloc = [] // Entries numbers that need to be geolocated locally by Decentralizer
for (var i = 0; i < contracts.length; i++) { // For each contract
var matchBool = false
for (var j = 0; j < siastatsGeoloc.length; j++) { // For each geolocation in list
if (contracts[i].hostpublickey.key == siastatsGeoloc[j].pubkey2) {
// Match, update hostdb entry
matchBool = true
contracts[i].lon = siastatsGeoloc[j].lon
contracts[i].lat = siastatsGeoloc[j].lat
contracts[i].as = siastatsGeoloc[j].as
contracts[i].countryName = siastatsGeoloc[j].countryName
contracts[i].countryCode = siastatsGeoloc[j].countryCode
contracts[i].siastatsScore = siastatsGeoloc[j].siastatsScore
}
}
if (matchBool == false) {
// If no match, add to the list
contractsToGeoloc.push(i)
contracts[i].siastatsScore = 0 // 0 score, as it is not on the database
}
}
console.log("Number of additional contracts to be geolocated: " + contractsToGeoloc.length + "\n")
if (contractsToGeoloc.length > 0) {
var i = 0
requestContractIP(siastatsFarms, contracts, contractsToGeoloc, i)
} else {
// No additional host to geolocate, save and proceed to next step
if (debugMode == true) {console.log("// DEBUG - No geolocation necessary, saving contracts.json")}
fs.writeFileSync('databases/contracts.json', JSON.stringify(contracts))
processHosts(siastatsFarms, contracts)
}
}
function requestContractIP(siastatsFarms, contracts, contractsToGeoloc, i) {
// Triming the ":port" from the host IP
var hostip = contracts[contractsToGeoloc[i]].netaddress
var s = hostip.search(":")
var totrim = hostip.length - s
trimedip = hostip.slice(0, -totrim)
// Requesting the geolocation of the host
var ipquery = "http://ip-api.com/json/" + trimedip
axios.get(ipquery).then(response => {
var ipAPI = response.data
var lat = parseFloat(ipAPI.lat)
var lon = parseFloat(ipAPI.lon)
process.stdout.clearLine(); // clear current text
process.stdout.cursorTo(0); // move cursor to beginning of line
process.stdout.write("(" + (i+1) + "/" + contractsToGeoloc.length + ") - " + hostip)
contracts[contractsToGeoloc[i]].lon = lon
contracts[contractsToGeoloc[i]].lat = lat
contracts[contractsToGeoloc[i]].as = ipAPI.as // Also adding the ISP
contracts[contractsToGeoloc[i]].countryName = ipAPI.country // Also adding the ISP
contracts[contractsToGeoloc[i]].countryCode = ipAPI.countryCode // Also adding the ISP
nextContractIP(siastatsFarms, contracts, contractsToGeoloc, i)
}).catch(error => {
// On failed IP request, move to the next IP
console.log(hostip + " - Failed")
if (debugMode == true) {console.log("// DEBUG - Error: " + error)}
nextContractIP(siastatsFarms, contracts, contractsToGeoloc, i)
})
}
function nextContractIP(siastatsFarms, contracts, contractsToGeoloc, i) {
setTimeout(function(){ // 500ms cooldown, to avoid being banned by ip-api.com
i++
if (i < contractsToGeoloc.length) {
requestContractIP(siastatsFarms, contracts, contractsToGeoloc, i)
} else {
console.log("\nGeolocation task done!\n")
// Saving the file
fs.writeFileSync('databases/contracts.json', JSON.stringify(contracts))
// Next
processHosts(siastatsFarms, contracts)
}
}, 500);
}
function processHosts(siastatsFarms, contracts) {
if (debugMode == true) {console.log("// DEBUG - processHosts() function")}
console.log("Detecting hosting farms among contracts")
// Finding centralized farms
var hostsGroups = []
for (var i = 0; i < contracts.length; i++) { // For each contract
var hostInGroupBool = false
for (var j = 0; j < hostsGroups.length; j++) {
if (contracts[i].lat == hostsGroups[j][0].lat && contracts[i].lon == hostsGroups[j][0].lon && contracts[i].as == hostsGroups[j][0].as) { // Checking if geolocation is the same as the first element in a group. Has to match the ISP too
hostsGroups[j].push(contracts[i]) // Add host to the group
hostInGroupBool = true
//console.log("New farm member identified")
contracts[i].assigned = true // Flag that the contract has been already included in a farm
}
}
if (hostInGroupBool == false) {
contracts[i].assigned = true // Flag that the contract has been already included in a farm
var newGroup = [contracts[i]]
hostsGroups.push(newGroup) // Add a new group
}
}
// Rearranging the array
var farmNumber = 1
var farmList = [{ // Initializing array
farm: "Other hosts",
hosts: []
},{
farm: "Geolocation unavailable",
hosts: []
}]
for (var j = 0; j < hostsGroups.length; j++) {
if (hostsGroups[j].length > 1) { // Only groups with more than one member: hosting farms
var farmEntry = {
farm: "User-identified farm #U-" + (farmNumber),
hosts: []
}
for (var k = 0; k < hostsGroups[j].length; k++) {
var hostEntry = {
ip: hostsGroups[j][k].netaddress,
contract: hostsGroups[j][k].id,
cost: parseFloat((hostsGroups[j][k].totalcost/1000000000000000000000000).toFixed(2)),
data: parseFloat((hostsGroups[j][k].size/1000000000).toFixed(2)), // In GB
pubkey: hostsGroups[j][k].hostpublickey.key,
siastatsScore: hostsGroups[j][k].siastatsScore
}
farmEntry.hosts.push(hostEntry)
}
// Arrange the hosts by stored data
function compare(a,b) {
if (a.data < b.data)
return 1;
if (a.data > b.data)
return -1;
return 0;
}
farmEntry.hosts.sort(compare);
// Push data
if (hostsGroups[j][0].lat > 0 || hostsGroups[j][0].lat < 0) { // Geolocation is a number
farmList.push(farmEntry)
farmNumber++
} else { // Geolocation unavailable host group
// consider these hosts unassigned, as we may have better chances of assigning them later checking with SiaStats
for (var p = 0; p < farmEntry.hosts.length; p++) {
farmEntry.hosts[p].assigned = false
}
// Replace element 1 by this
farmList[1].hosts = farmEntry.hosts
}
} else { // Individual hosts
var hostEntry = {
ip: hostsGroups[j][0].netaddress,
contract: hostsGroups[j][0].id,
cost: parseFloat((hostsGroups[j][0].totalcost/1000000000000000000000000).toFixed(2)),
data: parseFloat((hostsGroups[j][0].size/1000000000).toFixed(2)), // In GB
pubkey: hostsGroups[j][0].hostpublickey.key,
siastatsScore: hostsGroups[j][0].siastatsScore
}
// Pushing it to the element 0 of farmList, the "Other hosts"
farmList[0].hosts.push(hostEntry)
}
}
siastatsProcess(farmList, contracts, siastatsFarms)
}
function siastatsProcess(farmList, contracts, siastatsFarms) {
// This function compares our farmList with the list of siastats farms, to add the remaining farm-positive contracts to farmList
if (debugMode == true) {console.log("// DEBUG - siastatsProcess() function: comparing farms list to data from SiaStats")}
// A - Create a temporal array where we add contracts not yet assigned, and belonging to farms, to groups
// Iterate on the list of farms, on each host of it
var extraGroups = []
for (var j = 0; j < siastatsFarms.length; j++) {
extraGroups.push({ // Adding an empty sub-array. We will fill it with contracts if positive for a farm
farm: siastatsFarms[j].farm,
hosts: []
})
// Adding the Alert flag if the farms is dangerous
if (siastatsFarms[j].alert == true) {
extraGroups[extraGroups.length-1].alert = true
extraGroups[extraGroups.length-1].message = siastatsFarms[j].message
}
for (var k = 0; k < siastatsFarms[j].hosts.length; k++) {
// Iterate on the list of contracts not yet assigned to a farm
for (var i = 0; i< contracts.length; i++) {
if (contracts[i].assigned != true && siastatsFarms[j].hosts[k].pubkey == contracts[i].hostpublickey.key){ // Match of public keys: the host is in a farm!
extraGroups[j].hosts.push(contracts[i])
}
}
}
}
// B - Assign these groups to farms already identified (farmsList). Add a flag about SiaStats
// Iterate on the farmList to find matches with the siaStats database. If a match, we will iterate on extraGroups and add hosts if they match the farm ID
for (var i = 0; i < farmList.length; i++) {
for (var j = 0; j < farmList[i].hosts.length; j++) {
// Iterating on siastatsFarms
for (var k = 0; k < siastatsFarms.length; k++) {
for (l = 0; l < siastatsFarms[k].hosts.length; l++) {
if (farmList[i].hosts[j].pubkey == siastatsFarms[k].hosts[l].pubkey) { // Matched farm
// Now we iterate on our newGroups array, to find the one carrying the .farm property that matches
for (var m = 0; m < extraGroups.length; m++) {
if (extraGroups[m].farm == siastatsFarms[k].farm) { // Match!
// B1 - Assign the hosts of the group to the farm list
for (var n = 0; n < extraGroups[m].hosts.length; n++) {
farmList[i].hosts.push({
ip: extraGroups[m].hosts[n].netaddress,
contract: extraGroups[m].hosts[n].id,
cost: parseFloat((extraGroups[m].hosts[n].totalcost/1000000000000000000000000).toFixed(2)),
data: parseFloat((extraGroups[m].hosts[n].size/1000000000).toFixed(2)), // In GB
siastatsFlag: true, // Add the flag to the latest host of that farm (the one we just pushed)
siastatsScore: extraGroups[m].hosts[n].siastatsScore
})
}
// Adding an alert if the group carries it
if (extraGroups[m].alert == true) {
farmList[farmList.length-1].alert = true
farmList[farmList.length-1].message = extraGroups[m].message
}
// B2 - Remove the group from extraGroups
extraGroups.splice(m, 1)
// B3 - Renaming the farm name to keep consistency with SiaStats
farmList[i].farm = "SiaStats ID-" + siastatsFarms[k].farm
}
}
}
}
}
}
}
// C - Push unassigned groupd with 2+ contracts to a new farm
for (var i = 0; i < extraGroups.length; i++) {
if (extraGroups[i].hosts.length >= 2) {
// Initializing new entry
newEntry = {
farm: "SiaStats ID-" + extraGroups[i].farm,
hosts: []
}
for (var j = 0; j < extraGroups[i].hosts.length; j++) { // For each host in the group
newEntry.hosts.push({
ip: extraGroups[i].hosts[j].netaddress,
contract: extraGroups[i].hosts[j].id,
cost: parseFloat((extraGroups[i].hosts[j].totalcost/1000000000000000000000000).toFixed(2)),
data: parseFloat((extraGroups[i].hosts[j].size/1000000000).toFixed(2)), // In GB
siastatsScore: extraGroups[m].hosts[n].siastatsScore
})
// Adding an alert if the groups carries it
if (extraGroups[i].alert == true) {
newEntry[newEntry.length-1].alert = true
newEntry[newEntry.length-1].message = extraGroups[i].message
}
}
farmList.push(newEntry)
}
}
// D - Correcting group names
farmList[0].farm = "Non-farms"
farmList[1].farm = "Geolocation unavailable"
// Saving the farms file
if (debugMode == true) {console.log("// DEBUG - siastatsProcess() done. Saving farms.json")}
fs.writeFileSync('databases/farms.json', JSON.stringify(farmList))
getConsensus(farmList)
}
function getConsensus(farmList) {
// These last 2 operations (get consensus and the allowance information) is not necessary for Decentralizer-CLI, but we keep it for
// compatibility with Decentralizer-GUI
var data = fs.readFileSync('databases/settings.json') // Reading settings file
var settings = JSON.parse(data)
sia.connect('localhost:9980')
.then((siad) => {siad.call('/consensus')
.then((api) => {
if (api.synced == true) {
// Update only if the client is fully synced, otherwise keep it null, to avoid issues on contracts timeline representation
settings.consensusHeight = api.height
} else {
settings.consensusHeight = null
}
getAllowanceInfo(farmList, settings)
})
.catch((err) => {
console.log("Error retrieving consensus height from Sia. Is Sia working, synced and connected to internet? Try this script again after restarting Sia.")
if (debugMode == true) {console.log("// DEBUG - Error: \n" + err)}
console.log()
})
})
.catch((err) => {
console.log("Error connecting to Sia. Start the Sia app (either daemon or UI) and try again")
console.log()
if (debugMode == true) {console.log("// DEBUG - Error: \n" + err)}
})
}
function getAllowanceInfo(farmList, settings) {
// Gets the renew window of contracts
sia.connect('localhost:9980')
.then((siad) => {siad.call('/renter')
.then((api) => {
try {
settings.renewWindow = api.settings.allowance.renewwindow
} catch(e) {
settings.renewWindow = null
}
fs.writeFileSync('databases/settings.json', JSON.stringify(settings))
showFarms(farmList)
})
.catch((err) => {
console.log("Error retrieving renter allowance settings from Sia. Is Sia working, synced and connected to internet? Try this script again after restarting Sia.")
if (debugMode == true) {console.log("// DEBUG - Error: \n" + err)}
console.log()
})
})
.catch((err) => {
console.log("Error connecting to Sia. Start the Sia app (either daemon or UI) and try again")
console.log()
if (debugMode == true) {console.log("// DEBUG - Error: \n" + err)}
})
}
function showFarms(farmList) {
if (debugMode == true) {console.log("// DEBUG - showFarms(): Showing the results")}
let data, output;
if (farmList.length <= 2) {
data = [["No host farms have been found in your contracts list"]]
output = table.table(data);
console.log(output);
} else {
// Table headers
data = [["Farm", "Contract #", "IP", "Value", "Data", "Alerts"]]
var listNumber = 1
for (var i = 1; i < farmList.length; i++) { // Each farm
if (farmList[i].hosts.length > 0) { // For not displaying the not geolocated if there is no host in this category
for (var j = 0; j < farmList[i].hosts.length; j++) { // Each host
if (farmList[i].alert == true) { // Add a special labelling for hosts identified by SiaStats as dangerous
data.push(["", listNumber, "[*]" + farmList[i].hosts[j].ip, farmList[i].hosts[j].cost + "SC", farmList[i].hosts[j].data + "GB", "Alert: " + farmList[i].message])
listNumber++
} else {
data.push(["", listNumber, farmList[i].hosts[j].ip, farmList[i].hosts[j].cost + "SC", farmList[i].hosts[j].data + "GB", ""])
listNumber++
}
// Farm name only in first entry
if (j == 0) {
data[data.length-1][0] = farmList[i].farm
}
}
}
}
config = {
columns: {
5: {
width: 20,
wrapWord: true
}
}
};
output = table.table(data, config);
console.log(output);
console.log("Use 'decentralizer remove auto' to remove all the hosts in farms (and those not geolocated) with the exception of the top one, or "
+ "'decentralizer remove x' for manually removing a contract, where x is the shown Contract#")
}
console.log()
}
/////////////////////////////////////////////////////
// CONTRACT REMOVAL
function removeContract(argument2) {
if (debugMode == true) {console.log("// DEBUG - Removing contract: " + argument2)}
if (argument2 > 0 || argument2 == "auto") {
// Open "farms.json" file
fs.readFile('databases/farms.json', 'utf8', function (err, data) { if (!err) {
farmList = JSON.parse(data);
var contractsToRemove = []