-
-
Notifications
You must be signed in to change notification settings - Fork 84
/
sweeper.component.ts
577 lines (527 loc) · 20.7 KB
/
sweeper.component.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
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
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
import {WalletService} from '../../services/wallet.service';
import {NotificationService} from '../../services/notification.service';
import {ModalService} from '../../services/modal.service';
import {ApiService} from '../../services/api.service';
import {UtilService, TxType} from '../../services/util.service';
import {WorkPoolService} from '../../services/work-pool.service';
import {AppSettingsService} from '../../services/app-settings.service';
import {NanoBlockService} from '../../services/nano-block.service';
import * as nanocurrency from 'nanocurrency';
import { wallet } from 'nanocurrency-web';
import * as bip39 from 'bip39';
import {Router} from '@angular/router';
const INDEX_MAX = 4294967295; // seed index
const SWEEP_MAX_INDEX = 100; // max index keys to sweep
const SWEEP_MAX_PENDING = 100; // max pending blocks to process per run
@Component({
selector: 'app-sweeper',
templateUrl: './sweeper.component.html',
styleUrls: ['./sweeper.component.css'],
})
export class SweeperComponent implements OnInit {
accounts = this.walletService.wallet.accounts;
indexMax = INDEX_MAX;
incomingMax = SWEEP_MAX_PENDING;
myAccountModel = this.accounts.length > 0 ? this.accounts[0].id : '0';
sourceWallet = '';
destinationAccount = this.accounts.length > 0 ? this.accounts[0].id : '';
startIndex = '0';
endIndex = '5';
maxIncoming = SWEEP_MAX_PENDING.toString();
output = '';
sweeping = false;
pubKey = '';
adjustedBalance = '0';
representative = '';
privKey = '';
previous = '';
subType = '';
blocks = [];
keys = [];
keyCount = 0;
pendingCallback = null;
totalSwept = '0';
customAccountSelected = this.accounts.length === 0;
validSeed = false;
validDestination = this.myAccountModel !== '0' ? true : false;
validStartIndex = true;
validEndIndex = true;
validMaxIncoming = true;
selAccountInit = false;
@ViewChild('outputarea') logArea: ElementRef;
constructor(
private walletService: WalletService,
private notificationService: NotificationService,
private appSettings: AppSettingsService,
public modal: ModalService,
private api: ApiService,
private workPool: WorkPoolService,
public settings: AppSettingsService,
private nanoBlock: NanoBlockService,
private util: UtilService,
private route: Router) {
if (this.route.getCurrentNavigation().extras.state && this.route.getCurrentNavigation().extras.state.seed) {
this.sourceWallet = this.route.getCurrentNavigation().extras.state.seed;
this.validSeed = true;
}
}
async ngOnInit() {
// Update selected account if changed in the sidebar
this.walletService.wallet.selectedAccount$.subscribe(async acc => {
if (this.selAccountInit) {
this.myAccountModel = acc ? acc.id : (this.accounts.length > 0 ? this.accounts[0].id : '0');
}
this.selAccountInit = true;
});
// Set the account selected in the sidebar as default
if (this.walletService.wallet.selectedAccount !== null) {
this.myAccountModel = this.walletService.wallet.selectedAccount.id;
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
setDestination(account) {
if (account !== '0') {
this.destinationAccount = account;
this.customAccountSelected = false;
} else {
this.destinationAccount = '';
this.customAccountSelected = true;
}
this.destinationChange(account);
}
// set min value for start index
setMin() {
this.startIndex = '0';
// check end index
if (this.validEndIndex) {
if (parseInt(this.endIndex, 10) > 0 + SWEEP_MAX_INDEX) {
this.endIndex = (0 + SWEEP_MAX_INDEX).toString(); }
}
}
// set max value for end index
setMax() {
this.endIndex = INDEX_MAX.toString();
// check start index
if (this.validStartIndex) {
if (parseInt(this.startIndex, 10) < INDEX_MAX - SWEEP_MAX_INDEX) {
this.startIndex = (INDEX_MAX - SWEEP_MAX_INDEX).toString();
}
}
}
// set max value for pending limit
setMaxIncoming() {
this.maxIncoming = SWEEP_MAX_PENDING.toString();
}
seedChange(seed) {
if (this.checkMasterKey(seed)) {
this.validSeed = true;
} else {
this.validSeed = false;
}
}
destinationChange(address) {
if (nanocurrency.checkAddress(address)) {
this.validDestination = true;
} else {
this.validDestination = false;
}
}
startIndexChange(index) {
let invalid = false;
if (this.util.string.isNumeric(index) && index % 1 === 0) {
index = parseInt(index, 10);
if (!nanocurrency.checkIndex(index)) {
invalid = true;
}
if (index > INDEX_MAX) {
invalid = true;
}
} else {
invalid = true;
}
if (invalid) {
this.validStartIndex = false;
return;
}
// check end index
if (this.validEndIndex) {
if (parseInt(this.endIndex, 10) > index + SWEEP_MAX_INDEX) {
this.endIndex = (index + SWEEP_MAX_INDEX).toString();
}
}
this.validStartIndex = true;
}
endIndexChange(index) {
let invalid = false;
if (this.util.string.isNumeric(index) && index % 1 === 0) {
index = parseInt(index, 10);
if (!nanocurrency.checkIndex(index)) {
invalid = true;
}
if (index > INDEX_MAX) {
invalid = true;
}
} else {
invalid = true;
}
if (invalid) {
this.validEndIndex = false;
return;
}
// check end index
if (this.validStartIndex) {
if (parseInt(this.startIndex, 10) < index - SWEEP_MAX_INDEX) {
this.startIndex = (index - SWEEP_MAX_INDEX).toString();
}
}
this.validEndIndex = true;
}
maxIncomingChange(value) {
if (!this.util.string.isNumeric(value) || value % 1 !== 0) {
this.validMaxIncoming = false;
return;
} else {
value = parseInt(value, 10);
if (value > SWEEP_MAX_PENDING) {
this.validMaxIncoming = false;
return;
}
}
this.validMaxIncoming = true;
}
// Validate type of master key. Seed and private key can't be differentiated
checkMasterKey(key) {
// validate nano seed or private key
if (key.length === 64) {
if (nanocurrency.checkSeed(key)) {
return 'nano_seed';
}
}
// validate bip39 seed
if (key.length === 128) {
if (this.util.hex.isHex(key)) {
return 'bip39_seed';
}
}
// validate mnemonic
if (bip39.validateMnemonic(key)) {
return 'mnemonic';
}
return false;
}
// Append row to log output
appendLog(row) {
let linebreak = '\n';
if (this.output === '') {
linebreak = '';
}
this.output = this.output + linebreak + row;
// scroll to bottom
this.logArea.nativeElement.scrollTop = this.logArea.nativeElement.scrollHeight;
}
// Process final send block
async processSend(privKey, previous, sendCallback) {
const pubKey = nanocurrency.derivePublicKey(privKey);
const address = nanocurrency.deriveAddress(pubKey, {useNanoPrefix: true});
// make an extra check on valid destination
if (this.validDestination && nanocurrency.checkAddress(this.destinationAccount)) {
this.appendLog('Transfer started: ' + address);
const work = await this.workPool.getWork(previous, 1); // send threshold
// create the block with the work found
const block = nanocurrency.createBlock(privKey, {balance: '0', representative: this.representative,
work: work, link: this.destinationAccount, previous: previous});
// replace xrb with nano (old library)
block.block.account = block.block.account.replace('xrb', 'nano');
block.block.link_as_account = block.block.link_as_account.replace('xrb', 'nano');
// publish block for each iteration
const data = await this.api.process(block.block, TxType.send);
if (data.hash) {
const blockInfo = await this.api.blockInfo(data.hash);
let nanoAmountSent = null;
if (blockInfo.amount) {
nanoAmountSent = this.util.nano.rawToMnano(blockInfo.amount);
this.totalSwept = this.util.big.add(this.totalSwept, nanoAmountSent);
}
this.notificationService.sendInfo('Account ' + address + ' was swept and ' +
(nanoAmountSent ? (nanoAmountSent.toString(10) + ' Nano') : '') + ' transferred to ' + this.destinationAccount, {length: 15000});
this.appendLog('Funds transferred ' + (nanoAmountSent ? ('(' + nanoAmountSent.toString(10) + ' Nano)') : '') + ': ' + data.hash);
console.log(this.adjustedBalance + ' raw transferred to ' + this.destinationAccount);
} else {
this.notificationService.sendWarning(`Failed processing block.`);
this.appendLog('Failed processing block: ' + data.error);
}
sendCallback();
} else {
this.notificationService.sendError(`The destination address is not valid.`);
sendCallback();
}
}
// For each pending block
async processPending(blocks, keys, keyCount) {
const key = keys[keyCount];
this.blocks = blocks;
this.keys = keys;
this.keyCount = keyCount;
this.adjustedBalance = this.util.big.add(this.adjustedBalance, blocks[key].amount);
// generate local work
try {
this.appendLog('Started generating PoW...');
// determine input work hash depending if open block or receive block
let workInputHash = this.previous;
if (this.subType === 'open') {
// input hash is the opening address public key
workInputHash = this.pubKey;
}
const work = await this.workPool.getWork(workInputHash, 1 / 64); // receive threshold
// create the block with the work found
const block = nanocurrency.createBlock(this.privKey, {balance: this.adjustedBalance, representative: this.representative,
work: work, link: key, previous: this.previous});
// replace xrb with nano (old library)
block.block.account = block.block.account.replace('xrb', 'nano');
block.block.link_as_account = block.block.link_as_account.replace('xrb', 'nano');
// new previous
this.previous = block.hash;
// publish block for each iteration
const data = await this.api.process(block.block, this.subType === 'open' ? TxType.open : TxType.receive);
if (data.hash) {
this.appendLog('Processed pending: ' + data.hash);
// continue with the next pending
this.keyCount += 1;
if (this.keyCount < this.keys.length) {
// if last block was open, the next one will be a receive
if (this.subType === 'open') {
this.subType = 'receive';
}
this.processPending(this.blocks, this.keys, this.keyCount);
} else { // all pending done, now we process the final send block
this.appendLog('All pending processed!');
this.pendingCallback(this.previous);
}
} else {
this.notificationService.sendWarning(`Failed processing block`);
this.appendLog('Failed processing block: ' + data.error);
}
} catch (error) {
if (error.message === 'invalid_hash') {
this.notificationService.sendError(`Block hash must be 64 character hex string`);
} else {
this.notificationService.sendError(`An unknown error occurred while generating PoW`);
console.log('An unknown error occurred while generating PoW' + error);
}
this.sweeping = false;
return;
}
}
// Create pending blocks based on current balance and previous block (or start with an open block)
async createPendingBlocks(privKey, address, balance, previous, subType, callback, accountCallback) {
this.privKey = privKey;
this.previous = previous;
this.subType = subType;
this.pendingCallback = callback;
// check for pending first
let data = null;
if (this.appSettings.settings.minimumReceive) {
const minAmount = this.util.nano.mnanoToRaw(this.appSettings.settings.minimumReceive).toString(10);
if (this.appSettings.settings.pendingOption === 'amount') {
data = await this.api.pendingLimitSorted(address, this.maxIncoming, minAmount);
} else {
data = await this.api.pendingLimit(address, this.maxIncoming, minAmount);
}
} else {
if (this.appSettings.settings.pendingOption === 'amount') {
data = await this.api.pendingSorted(address, this.maxIncoming);
} else {
data = await this.api.pending(address, this.maxIncoming);
}
}
// if there are any pending, process them
if (data.blocks) {
// sum all raw amounts
let raw = '0';
Object.keys(data.blocks).forEach(function(key) {
raw = this.util.big.add(raw, data.blocks[key].amount);
}.bind(this));
const nanoAmount = this.util.nano.rawToMnano(raw);
const pending = {count: Object.keys(data.blocks).length, raw: raw, NANO: nanoAmount, blocks: data.blocks};
const row = 'Found ' + pending.count + ' pending containing total ' + pending.NANO + ' NANO';
this.appendLog(row);
// create receive blocks for all pending
const keys = [];
// create an array with all keys to be used recurively
Object.keys(pending.blocks).forEach(function(key) {
keys.push(key);
});
this.processPending(pending.blocks, keys, 0);
} else { // no pending, create final block directly
if (parseInt(this.adjustedBalance, 10) > 0) {
this.processSend(this.privKey, this.previous, () => {
accountCallback(); // tell that we are ok to continue with next step
});
} else {
accountCallback(); // tell that we are ok to continue with next step
}
}
}
// Process an account
async processAccount(privKey, accountCallback) {
if (privKey.length !== 64) {
accountCallback();
return;
}
this.pubKey = nanocurrency.derivePublicKey(privKey);
const address = nanocurrency.deriveAddress(this.pubKey, {useNanoPrefix: true});
// get account info required to build the block
let balance = 0; // balance will be 0 if open block
this.adjustedBalance = balance.toString();
let previous = null; // previous is null if we create open block
this.representative = this.settings.settings.defaultRepresentative || this.nanoBlock.getRandomRepresentative();
let subType = 'open';
// retrive from RPC
const accountInfo = await this.api.accountInfo(address);
let validResponse = false;
// if frontier is returned it means the account has been opened and we create a receive block
if (accountInfo.frontier) {
validResponse = true;
balance = accountInfo.balance;
this.adjustedBalance = balance.toString();
previous = accountInfo.frontier;
this.representative = accountInfo.representative;
subType = 'receive';
validResponse = true;
} else if (accountInfo.error === 'Account not found') {
validResponse = true;
this.adjustedBalance = '0';
}
if (validResponse) {
// create and publish all pending
this.createPendingBlocks(privKey, address, balance, previous, subType, function(previous_) {
// the previous is the last received block and will be used to create the final send block
if (parseInt(this.adjustedBalance, 10) > 0) {
this.processSend(privKey, previous_, () => {
accountCallback();
});
} else {
accountCallback();
}
}.bind(this), () => {
accountCallback();
});
} else {
this.notificationService.sendError(`Bad RPC response. Please try again.`);
accountCallback();
}
}
// Recursively process private keys from index range
async processIndexRecursive(privKeys, keyCount) {
// delay each process to not hit backend rate limiters
await this.sleep(300);
const privKey = privKeys[keyCount][0];
this.appendLog('Checking index ' + privKeys[keyCount][2] + ' using ' + privKeys[keyCount][1]);
this.processAccount(privKey, function() {
// continue with the next pending
keyCount += 1;
if (keyCount < privKeys.length) {
this.processIndexRecursive(privKeys, keyCount);
} else {
// all private keys have been processed
this.appendLog('Finished processing all accounts');
this.appendLog(this.totalSwept + ' Nano transferred');
this.notificationService.sendInfo('Finished processing all accounts. ' + this.totalSwept + ' Nano transferred', {length: 0});
this.sweeping = false;
}
}.bind(this));
}
async sweepContinue () {
this.sweeping = true;
this.totalSwept = '0';
const keyType = this.checkMasterKey(this.sourceWallet);
if (this.validEndIndex && this.validStartIndex && this.validMaxIncoming) {
let seed = '', privKey;
let bip39Seed = '';
// input is mnemonic
if (keyType === 'mnemonic') {
seed = bip39.mnemonicToEntropy(this.sourceWallet).toUpperCase();
bip39Seed = this.util.string.mnemonicToSeedSync(this.sourceWallet).toString('hex');
// Seed must be 64 for regular nano blake derivation to happen
// For other lengths, only bip39/44 derivation is possible
if (seed.length !== 32 && seed.length !== 40 && seed.length !== 48 && seed.length !== 56 && seed.length !== 64) {
this.notificationService.sendError(`Mnemonic not 12,15,18,21 or 24 words`);
return;
}
}
// nano seed or private key
if (keyType === 'nano_seed' || seed !== '' || keyType === 'bip39_seed') {
// check if a private key first (no index)
this.appendLog('Checking if input is a private key');
if (seed === '') { // seed from input, no mnemonic
seed = this.sourceWallet;
}
this.processAccount(seed, function() {
// done checking if private key, continue interpret as seed
const privKeys = [];
// start with blake2b derivation (but not if the mnemonic is anything other than 24 words)
if (keyType !== 'bip39_seed' && seed.length === 64) {
for (let i = parseInt(this.startIndex, 10); i <= parseInt(this.endIndex, 10); i++) {
privKey = nanocurrency.deriveSecretKey(seed, i);
privKeys.push([privKey, 'blake2b', i]);
}
}
// also check all indexes using bip39/44 derivation
// take 128 char bip39 seed directly from input or convert it from a 64 char nano seed (entropy)
if (keyType === 'bip39_seed') {
bip39Seed = this.sourceWallet;
} else if (seed.length === 64) {
bip39Seed = wallet.generate(seed).seed;
}
if (bip39Seed.length !== 128) return this.notificationService.sendError(`Invalid input format! Please check.`);
const accounts = wallet.accounts(bip39Seed, this.startIndex, this.endIndex);
let k = 0;
for (let i = parseInt(this.startIndex, 10); i <= parseInt(this.endIndex, 10); i++) {
privKey = accounts[k].privateKey;
k += 1;
privKeys.push([privKey, 'bip39/44', i]);
}
this.processIndexRecursive(privKeys, 0);
}.bind(this));
}
} else {
this.notificationService.sendError(`Invalid input format! Please check.`);
}
}
/* Start the sweeping */
async sweep() {
if (!this.validSeed) {
this.notificationService.sendError(`No valid source wallet provided!`);
return;
}
if (!this.validDestination) {
this.notificationService.sendError(`No valid destination account provided!`);
return;
}
if (this.validStartIndex && this.validEndIndex) {
if (parseInt(this.startIndex, 10) > parseInt(this.endIndex, 10)) {
this.notificationService.sendError(`End Index must be equal or larger than Start Index`);
return;
}
} else {
this.notificationService.sendError(`Not valid start and end indexes`);
return;
}
if (!this.validMaxIncoming) {
this.notificationService.sendError(`Not a valid number for Max Incoming`);
return;
}
// let user confirm account
const UIkit = window['UIkit'];
try {
const msg = '<p class="uk-alert uk-alert-danger"><br><span class="uk-flex"><span uk-icon="icon: warning; ratio: 3;" class="uk-align-center"></span></span><span style="font-size: 18px;">You are about to empty the source wallet, which will <b>transfer all funds from it to the destination address</b>.</span><br><br><b style="font-size: 18px;">Before continuing, make sure you (or someone) have saved the Nano seed and/or mnemonic of the specified destination address</b>.<br><br><span style="font-size: 18px;"><b>YOU WILL NOT BE ABLE TO RECOVER THE FUNDS</b><br>without a backup of the specified destination address.</span></p><br>';
await UIkit.modal.confirm(msg);
this.sweepContinue();
} catch (err) {
console.log(err);
}
}
}