cryptoseed 1.0.0
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/LICENSE +21 -0
- package/README.en.md +224 -0
- package/README.es.md +224 -0
- package/README.fr.md +224 -0
- package/README.he.md +224 -0
- package/README.it.md +224 -0
- package/README.ja.md +224 -0
- package/README.ko.md +224 -0
- package/README.md +224 -0
- package/README.ru.md +224 -0
- package/README.tr.md +224 -0
- package/README.zh.md +224 -0
- package/package.json +61 -0
- package/src/bin/cli.js +1785 -0
- package/src/index.js +27 -0
- package/src/lib/address-deriver.js +1282 -0
- package/src/lib/balance-checker.js +230 -0
- package/src/lib/cli-locales.js +1120 -0
- package/src/lib/electrum-legacy.js +176 -0
- package/src/lib/search-engine.js +576 -0
- package/src/lib/search-worker.js +68 -0
- package/src/lib/typo.js +187 -0
- package/src/lib/wordlists.js +1655 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CryptoSeedRecovery - Core Recovery Search Engine
|
|
3
|
+
* Implements the 4 distinct search modes with optimized constraint backtracking and early pruning.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ethers } = require('ethers');
|
|
7
|
+
const {
|
|
8
|
+
validateElectrumMnemonic,
|
|
9
|
+
mnDecode,
|
|
10
|
+
validateMoneroMnemonic,
|
|
11
|
+
validateAlgorandMnemonic,
|
|
12
|
+
validateCardanoByronMnemonic
|
|
13
|
+
} = require('./electrum-legacy');
|
|
14
|
+
const { deriveAddress, getEthersWordlist } = require('./address-deriver');
|
|
15
|
+
|
|
16
|
+
// Helper to calculate factorial for Mode 4 progress estimates
|
|
17
|
+
function factorial(n) {
|
|
18
|
+
let res = 1n;
|
|
19
|
+
for (let i = 2n; i <= BigInt(n); i++) {
|
|
20
|
+
res *= i;
|
|
21
|
+
}
|
|
22
|
+
return res;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates any candidate mnemonic phrase based on coinKey and format.
|
|
27
|
+
* Utilizes specialized validation functions to support non-standard BIP-39 counts.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} phrase - Candidate phrase
|
|
30
|
+
* @param {string} format - 'bip39' | 'electrum'
|
|
31
|
+
* @param {string} coinKey - Currency code
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isValidPhrase(phrase, format, coinKey, language = null) {
|
|
35
|
+
const cleanPhrase = phrase.trim().toLowerCase();
|
|
36
|
+
const words = cleanPhrase.split(/\s+/);
|
|
37
|
+
|
|
38
|
+
if (format === 'electrum') {
|
|
39
|
+
return validateElectrumMnemonic(words);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (format === 'bip39') {
|
|
43
|
+
const cleanCoin = coinKey.toUpperCase().trim();
|
|
44
|
+
if (cleanCoin === 'XMR' || cleanCoin === 'MONERO') {
|
|
45
|
+
return validateMoneroMnemonic(words);
|
|
46
|
+
}
|
|
47
|
+
if (cleanCoin === 'ALGO' || cleanCoin === 'ALGORAND') {
|
|
48
|
+
return validateAlgorandMnemonic(words);
|
|
49
|
+
}
|
|
50
|
+
if ((cleanCoin === 'ADA' || cleanCoin === 'CARDANO') && words.length === 22) {
|
|
51
|
+
return validateCardanoByronMnemonic(words);
|
|
52
|
+
}
|
|
53
|
+
// Standard BIP-39 validation
|
|
54
|
+
let wl = null;
|
|
55
|
+
if (language) {
|
|
56
|
+
wl = ethers.wordlists[language] || ethers.wordlists[language.toLowerCase().replace('-', '_')];
|
|
57
|
+
}
|
|
58
|
+
if (!wl) {
|
|
59
|
+
wl = getEthersWordlist(cleanPhrase);
|
|
60
|
+
}
|
|
61
|
+
if (!wl) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return ethers.Mnemonic.isValidMnemonic(cleanPhrase, wl);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Verifies if a candidate mnemonic is valid and derives the correct target public address.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} phrase - Candidate seed phrase (words separated by space)
|
|
78
|
+
* @param {string} format - 'bip39' | 'electrum'
|
|
79
|
+
* @param {string} walletType - 'metamask' | 'trust' | 'b2wallet'
|
|
80
|
+
* @param {string} coinKey - 'ETH' (or 'EVM') | 'BTC' | 'LTC' | 'DOGE' | 'SOL' | 'XLM' | 'TRX'
|
|
81
|
+
* @param {string} targetAddress - The public address we are looking for (optional)
|
|
82
|
+
* @returns {boolean} - True if it matches exactly
|
|
83
|
+
*/
|
|
84
|
+
function verifyCandidate(phrase, format, walletType, coinKey, targetAddress, pattern = null, language = null) {
|
|
85
|
+
const cleanPhrase = phrase.trim().toLowerCase();
|
|
86
|
+
const cleanTarget = targetAddress ? targetAddress.trim().toLowerCase() : '';
|
|
87
|
+
|
|
88
|
+
// 1. Unified fast path check (validates BIP-39, Electrum, Monero, Algorand, or Cardano paper seeds)
|
|
89
|
+
if (!isValidPhrase(cleanPhrase, format, coinKey, language)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Perform address derivation
|
|
94
|
+
try {
|
|
95
|
+
const derivedAddr = deriveAddress(cleanPhrase, walletType, coinKey, 0, pattern, language);
|
|
96
|
+
return !cleanTarget || derivedAddr.toLowerCase() === cleanTarget;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return false; // Under incorrect path combinations or invalid sementes
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* MODE 1: Fixed Order + Wildcards
|
|
104
|
+
* Expands in-place wildcards (e.g. 'bo*', '*') using product of matched words.
|
|
105
|
+
*/
|
|
106
|
+
function searchMode1(patternList, format, wordlist, walletType, coinKey, targetAddress, onProgress, pattern = null, startPrefixes = null, language = null) {
|
|
107
|
+
const results = [];
|
|
108
|
+
const assembledCandidates = [];
|
|
109
|
+
const cleanTarget = targetAddress ? targetAddress.trim().toLowerCase() : '';
|
|
110
|
+
|
|
111
|
+
// Match possible dictionary words for each slot
|
|
112
|
+
const slots = patternList.map(pat => {
|
|
113
|
+
const clean = pat.toLowerCase().trim();
|
|
114
|
+
if (clean.includes(',')) {
|
|
115
|
+
return clean.split(',').map(s => s.trim()).filter(Boolean);
|
|
116
|
+
} else if (clean === '*' || clean === '?') {
|
|
117
|
+
return wordlist;
|
|
118
|
+
} else if (clean.endsWith('*')) {
|
|
119
|
+
const prefix = clean.slice(0, -1);
|
|
120
|
+
return wordlist.filter(w => w.startsWith(prefix));
|
|
121
|
+
} else {
|
|
122
|
+
return [clean];
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const totalCombinations = slots.reduce((acc, slot) => acc * BigInt(slot.length), 1n);
|
|
127
|
+
let checkedCount = 0n;
|
|
128
|
+
|
|
129
|
+
function backtrack(idx, currentWords) {
|
|
130
|
+
if (idx === slots.length) {
|
|
131
|
+
checkedCount++;
|
|
132
|
+
if (onProgress && checkedCount % 1000n === 0n) {
|
|
133
|
+
onProgress(checkedCount, totalCombinations);
|
|
134
|
+
}
|
|
135
|
+
const candidate = currentWords.join(' ');
|
|
136
|
+
|
|
137
|
+
const isValid = isValidPhrase(candidate, format, coinKey, language);
|
|
138
|
+
|
|
139
|
+
if (isValid) {
|
|
140
|
+
try {
|
|
141
|
+
const derivedAddr = deriveAddress(candidate, walletType, coinKey, 0, pattern, language);
|
|
142
|
+
if (assembledCandidates.length < 50000) {
|
|
143
|
+
assembledCandidates.push({ phrase: candidate, address: derivedAddr });
|
|
144
|
+
}
|
|
145
|
+
if (!cleanTarget || derivedAddr.toLowerCase() === cleanTarget) {
|
|
146
|
+
if (results.length < 50000) {
|
|
147
|
+
results.push(candidate);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// derivation fail
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const candidates = slots[idx];
|
|
158
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
159
|
+
currentWords.push(candidates[i]);
|
|
160
|
+
backtrack(idx + 1, currentWords);
|
|
161
|
+
currentWords.pop();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (startPrefixes && startPrefixes.length > 0) {
|
|
166
|
+
for (const pref of startPrefixes) {
|
|
167
|
+
backtrack(pref.length, [...pref]);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
backtrack(0, []);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { results, totalChecked: checkedCount, assembledCandidates };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* MODES 2 & 3: Shuffled Words with or without Wildcards (Backtracking Solver)
|
|
178
|
+
* Finds placements of suppliedWords in the correct positions.
|
|
179
|
+
*/
|
|
180
|
+
function searchMode2And3(suppliedWords, constraints, format, wordlist, walletType, coinKey, targetAddress, onProgress, pattern = null, startStates = null, language = null) {
|
|
181
|
+
const results = [];
|
|
182
|
+
const assembledCandidates = [];
|
|
183
|
+
const cleanTarget = targetAddress ? targetAddress.trim().toLowerCase() : '';
|
|
184
|
+
const seedSize = suppliedWords.length; // e.g. 12 or 24
|
|
185
|
+
const assignment = Array(seedSize).fill(null);
|
|
186
|
+
|
|
187
|
+
// Exclude fixed/required slot words from the floating pool
|
|
188
|
+
const fixedPositions = {};
|
|
189
|
+
const activeConstraints = constraints || {};
|
|
190
|
+
|
|
191
|
+
for (let idx = 0; idx < seedSize; idx++) {
|
|
192
|
+
if (activeConstraints[idx] && activeConstraints[idx].requiredWord) {
|
|
193
|
+
assignment[idx] = activeConstraints[idx].requiredWord.toLowerCase().trim();
|
|
194
|
+
fixedPositions[idx] = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Words that are not fixed and must be assigned
|
|
199
|
+
// Since the suppliedWords is shuffled, we copy it and remove the required/fixed words
|
|
200
|
+
const floatingWords = suppliedWords.map(w => w.toLowerCase().trim());
|
|
201
|
+
for (let idx = 0; idx < seedSize; idx++) {
|
|
202
|
+
if (fixedPositions[idx]) {
|
|
203
|
+
const fixedWord = assignment[idx];
|
|
204
|
+
const matchIdx = floatingWords.indexOf(fixedWord);
|
|
205
|
+
if (matchIdx !== -1) {
|
|
206
|
+
floatingWords.splice(matchIdx, 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const floatingPositions = [];
|
|
211
|
+
for (let i = 0; i < seedSize; i++) {
|
|
212
|
+
if (!fixedPositions[i]) {
|
|
213
|
+
floatingPositions.push(i);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Pre-expand wildcards inside the floating words if any
|
|
218
|
+
const expandedFloatingPools = floatingWords.map(word => {
|
|
219
|
+
const clean = word.toLowerCase().trim();
|
|
220
|
+
if (clean.includes(',')) {
|
|
221
|
+
return clean.split(',').map(s => s.trim()).filter(Boolean);
|
|
222
|
+
} else if (clean === '*' || clean === '?') {
|
|
223
|
+
return wordlist;
|
|
224
|
+
} else if (clean.endsWith('*')) {
|
|
225
|
+
const prefix = clean.slice(0, -1);
|
|
226
|
+
return wordlist.filter(w => w.startsWith(prefix));
|
|
227
|
+
} else {
|
|
228
|
+
return [clean];
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Calculate approximate total search space
|
|
233
|
+
let totalCombinations = 1n;
|
|
234
|
+
let remainingWordsCount = expandedFloatingPools.length;
|
|
235
|
+
for (let i = 0; i < expandedFloatingPools.length; i++) {
|
|
236
|
+
totalCombinations *= BigInt(expandedFloatingPools[i].length) * BigInt(remainingWordsCount);
|
|
237
|
+
remainingWordsCount--;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let checkedCount = 0n;
|
|
241
|
+
const usedWords = Array(expandedFloatingPools.length).fill(false);
|
|
242
|
+
|
|
243
|
+
function backtrack(posIdx) {
|
|
244
|
+
if (posIdx === floatingPositions.length) {
|
|
245
|
+
checkedCount++;
|
|
246
|
+
if (onProgress && checkedCount % 1000n === 0n) {
|
|
247
|
+
onProgress(checkedCount, totalCombinations);
|
|
248
|
+
}
|
|
249
|
+
const candidate = assignment.join(' ');
|
|
250
|
+
|
|
251
|
+
const isValid = isValidPhrase(candidate, format, coinKey, language);
|
|
252
|
+
|
|
253
|
+
if (isValid) {
|
|
254
|
+
try {
|
|
255
|
+
const derivedAddr = deriveAddress(candidate, walletType, coinKey, 0, pattern, language);
|
|
256
|
+
if (assembledCandidates.length < 50000) {
|
|
257
|
+
assembledCandidates.push({ phrase: candidate, address: derivedAddr });
|
|
258
|
+
}
|
|
259
|
+
if (!cleanTarget || derivedAddr.toLowerCase() === cleanTarget) {
|
|
260
|
+
if (results.length < 50000) {
|
|
261
|
+
results.push(candidate);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// derivation fail
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const currentSlot = floatingPositions[posIdx];
|
|
272
|
+
const slotConstraint = activeConstraints[currentSlot];
|
|
273
|
+
|
|
274
|
+
// For this slot, try placing any of the unused floating words
|
|
275
|
+
for (let wordIdx = 0; wordIdx < expandedFloatingPools.length; wordIdx++) {
|
|
276
|
+
if (usedWords[wordIdx]) continue;
|
|
277
|
+
|
|
278
|
+
const candidates = expandedFloatingPools[wordIdx];
|
|
279
|
+
for (let cIdx = 0; candidates.length > cIdx; cIdx++) {
|
|
280
|
+
const candidateWord = candidates[cIdx];
|
|
281
|
+
|
|
282
|
+
// --- Early Branch Pruning Constraint Check ---
|
|
283
|
+
if (slotConstraint) {
|
|
284
|
+
// If word is excluded from this slot (NOK constraint), skip recursive branch entirely!
|
|
285
|
+
if (slotConstraint.excludedWords && slotConstraint.excludedWords.includes(candidateWord)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
assignment[currentSlot] = candidateWord;
|
|
291
|
+
usedWords[wordIdx] = true;
|
|
292
|
+
|
|
293
|
+
backtrack(posIdx + 1);
|
|
294
|
+
|
|
295
|
+
usedWords[wordIdx] = false;
|
|
296
|
+
assignment[currentSlot] = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (startStates && startStates.length > 0) {
|
|
302
|
+
for (const state of startStates) {
|
|
303
|
+
for (let i = 0; i < seedSize; i++) {
|
|
304
|
+
assignment[i] = state.assignment[i];
|
|
305
|
+
}
|
|
306
|
+
for (let i = 0; i < usedWords.length; i++) {
|
|
307
|
+
usedWords[i] = state.usedWords[i];
|
|
308
|
+
}
|
|
309
|
+
backtrack(state.posIdx);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
backtrack(0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { results, totalChecked: checkedCount, assembledCandidates };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* MODE 4: Full Descrambler
|
|
320
|
+
* Tests all possible arrangements of a list of completely spelled scrambled words.
|
|
321
|
+
*/
|
|
322
|
+
function searchMode4(scrambledWords, format, walletType, coinKey, targetAddress, onProgress, pattern = null, startStates = null, language = null) {
|
|
323
|
+
const results = [];
|
|
324
|
+
const assembledCandidates = [];
|
|
325
|
+
const cleanTarget = targetAddress ? targetAddress.trim().toLowerCase() : '';
|
|
326
|
+
const words = scrambledWords.map(w => w.toLowerCase().trim());
|
|
327
|
+
const n = words.length;
|
|
328
|
+
|
|
329
|
+
const totalPermutations = factorial(n);
|
|
330
|
+
let checkedCount = 0n;
|
|
331
|
+
|
|
332
|
+
function testCandidate(arr) {
|
|
333
|
+
checkedCount++;
|
|
334
|
+
if (onProgress && checkedCount % 1000n === 0n) {
|
|
335
|
+
onProgress(checkedCount, totalPermutations);
|
|
336
|
+
}
|
|
337
|
+
const candidate = arr.join(' ');
|
|
338
|
+
|
|
339
|
+
const isValid = isValidPhrase(candidate, format, coinKey, language);
|
|
340
|
+
|
|
341
|
+
if (isValid) {
|
|
342
|
+
try {
|
|
343
|
+
const derivedAddr = deriveAddress(candidate, walletType, coinKey, 0, pattern, language);
|
|
344
|
+
if (assembledCandidates.length < 50000) {
|
|
345
|
+
assembledCandidates.push({ phrase: candidate, address: derivedAddr });
|
|
346
|
+
}
|
|
347
|
+
if (!cleanTarget || derivedAddr.toLowerCase() === cleanTarget) {
|
|
348
|
+
if (results.length < 50000) {
|
|
349
|
+
results.push(candidate);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
// derivation fail
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Backtracking-based permutation for starting states
|
|
359
|
+
function backtrackPermute(prefix, usedIndices) {
|
|
360
|
+
if (prefix.length === n) {
|
|
361
|
+
testCandidate(prefix);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (let i = 0; i < n; i++) {
|
|
365
|
+
if (usedIndices[i]) continue;
|
|
366
|
+
usedIndices[i] = true;
|
|
367
|
+
prefix.push(words[i]);
|
|
368
|
+
backtrackPermute(prefix, usedIndices);
|
|
369
|
+
prefix.pop();
|
|
370
|
+
usedIndices[i] = false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (startStates && startStates.length > 0) {
|
|
375
|
+
for (const state of startStates) {
|
|
376
|
+
backtrackPermute([...state.prefix], [...state.usedIndices]);
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
// Heap's algorithm for generating permutations in-place
|
|
380
|
+
const arrCopy = [...words];
|
|
381
|
+
testCandidate(arrCopy);
|
|
382
|
+
|
|
383
|
+
const c = Array(n).fill(0);
|
|
384
|
+
let i = 0;
|
|
385
|
+
|
|
386
|
+
while (i < n) {
|
|
387
|
+
if (c[i] < i) {
|
|
388
|
+
if (i % 2 === 0) {
|
|
389
|
+
const temp = arrCopy[0];
|
|
390
|
+
arrCopy[0] = arrCopy[i];
|
|
391
|
+
arrCopy[i] = temp;
|
|
392
|
+
} else {
|
|
393
|
+
const temp = arrCopy[c[i]];
|
|
394
|
+
arrCopy[c[i]] = arrCopy[i];
|
|
395
|
+
arrCopy[i] = temp;
|
|
396
|
+
}
|
|
397
|
+
testCandidate(arrCopy);
|
|
398
|
+
c[i]++;
|
|
399
|
+
i = 0;
|
|
400
|
+
} else {
|
|
401
|
+
c[i] = 0;
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { results, totalChecked: checkedCount, assembledCandidates };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Task generation helpers for Worker Threads load balancing
|
|
412
|
+
*/
|
|
413
|
+
function getMode1Prefixes(patternList, format, wordlist, coinKey, W) {
|
|
414
|
+
const slots = patternList.map(pat => {
|
|
415
|
+
const clean = pat.toLowerCase().trim();
|
|
416
|
+
if (clean.includes(',')) {
|
|
417
|
+
return clean.split(',').map(s => s.trim()).filter(Boolean);
|
|
418
|
+
} else if (clean === '*' || clean === '?') {
|
|
419
|
+
return wordlist;
|
|
420
|
+
} else if (clean.endsWith('*')) {
|
|
421
|
+
const prefix = clean.slice(0, -1);
|
|
422
|
+
return wordlist.filter(w => w.startsWith(prefix));
|
|
423
|
+
} else {
|
|
424
|
+
return [clean];
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
let maxDepth = 0;
|
|
429
|
+
let count = 1;
|
|
430
|
+
while (maxDepth < slots.length && count < W * 4) {
|
|
431
|
+
count *= slots[maxDepth].length;
|
|
432
|
+
maxDepth++;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const results = [];
|
|
436
|
+
function backtrack(idx, current) {
|
|
437
|
+
if (idx === maxDepth || idx === slots.length) {
|
|
438
|
+
results.push([...current]);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
for (const w of slots[idx]) {
|
|
442
|
+
current.push(w);
|
|
443
|
+
backtrack(idx + 1, current);
|
|
444
|
+
current.pop();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
backtrack(0, []);
|
|
448
|
+
return results;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getMode23PartialStates(suppliedWords, constraints, format, wordlist, coinKey, W) {
|
|
452
|
+
const seedSize = suppliedWords.length;
|
|
453
|
+
const assignment = Array(seedSize).fill(null);
|
|
454
|
+
const fixedPositions = {};
|
|
455
|
+
const activeConstraints = constraints || {};
|
|
456
|
+
|
|
457
|
+
for (let idx = 0; idx < seedSize; idx++) {
|
|
458
|
+
if (activeConstraints[idx] && activeConstraints[idx].requiredWord) {
|
|
459
|
+
assignment[idx] = activeConstraints[idx].requiredWord.toLowerCase().trim();
|
|
460
|
+
fixedPositions[idx] = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const floatingWords = suppliedWords.map(w => w.toLowerCase().trim());
|
|
465
|
+
for (let idx = 0; idx < seedSize; idx++) {
|
|
466
|
+
if (fixedPositions[idx]) {
|
|
467
|
+
const fixedWord = assignment[idx];
|
|
468
|
+
const matchIdx = floatingWords.indexOf(fixedWord);
|
|
469
|
+
if (matchIdx !== -1) {
|
|
470
|
+
floatingWords.splice(matchIdx, 1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const floatingPositions = [];
|
|
476
|
+
for (let i = 0; i < seedSize; i++) {
|
|
477
|
+
if (!fixedPositions[i]) {
|
|
478
|
+
floatingPositions.push(i);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const expandedFloatingPools = floatingWords.map(word => {
|
|
483
|
+
const clean = word.toLowerCase().trim();
|
|
484
|
+
if (clean.includes(',')) {
|
|
485
|
+
return clean.split(',').map(s => s.trim()).filter(Boolean);
|
|
486
|
+
} else if (clean === '*' || clean === '?') {
|
|
487
|
+
return wordlist;
|
|
488
|
+
} else if (clean.endsWith('*')) {
|
|
489
|
+
const prefix = clean.slice(0, -1);
|
|
490
|
+
return wordlist.filter(w => w.startsWith(prefix));
|
|
491
|
+
} else {
|
|
492
|
+
return [clean];
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const states = [];
|
|
497
|
+
const usedWords = Array(expandedFloatingPools.length).fill(false);
|
|
498
|
+
|
|
499
|
+
function backtrackShallow(posIdx, currentAssignment, currentUsed) {
|
|
500
|
+
if (states.length >= W * 16 || posIdx === floatingPositions.length) {
|
|
501
|
+
states.push({
|
|
502
|
+
posIdx,
|
|
503
|
+
assignment: [...currentAssignment],
|
|
504
|
+
usedWords: [...currentUsed]
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const currentSlot = floatingPositions[posIdx];
|
|
510
|
+
const slotConstraint = activeConstraints[currentSlot];
|
|
511
|
+
|
|
512
|
+
for (let wordIdx = 0; wordIdx < expandedFloatingPools.length; wordIdx++) {
|
|
513
|
+
if (currentUsed[wordIdx]) continue;
|
|
514
|
+
|
|
515
|
+
const candidates = expandedFloatingPools[wordIdx];
|
|
516
|
+
for (let cIdx = 0; cIdx < candidates.length; cIdx++) {
|
|
517
|
+
const candidateWord = candidates[cIdx];
|
|
518
|
+
|
|
519
|
+
if (slotConstraint) {
|
|
520
|
+
if (slotConstraint.excludedWords && slotConstraint.excludedWords.includes(candidateWord)) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
currentAssignment[currentSlot] = candidateWord;
|
|
526
|
+
currentUsed[wordIdx] = true;
|
|
527
|
+
|
|
528
|
+
backtrackShallow(posIdx + 1, currentAssignment, currentUsed);
|
|
529
|
+
|
|
530
|
+
currentUsed[wordIdx] = false;
|
|
531
|
+
currentAssignment[currentSlot] = null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
backtrackShallow(0, assignment, usedWords);
|
|
537
|
+
return states;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getMode4PartialStates(scrambledWords, W) {
|
|
541
|
+
const words = scrambledWords.map(w => w.toLowerCase().trim());
|
|
542
|
+
const n = words.length;
|
|
543
|
+
const states = [];
|
|
544
|
+
|
|
545
|
+
function backtrack(prefix, usedIndices) {
|
|
546
|
+
if (states.length >= W * 4 || prefix.length === n) {
|
|
547
|
+
states.push({
|
|
548
|
+
prefix: [...prefix],
|
|
549
|
+
usedIndices: [...usedIndices]
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
for (let i = 0; i < n; i++) {
|
|
555
|
+
if (usedIndices[i]) continue;
|
|
556
|
+
usedIndices[i] = true;
|
|
557
|
+
prefix.push(words[i]);
|
|
558
|
+
backtrack(prefix, usedIndices);
|
|
559
|
+
prefix.pop();
|
|
560
|
+
usedIndices[i] = false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
backtrack([], Array(n).fill(false));
|
|
565
|
+
return states;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
module.exports = {
|
|
569
|
+
verifyCandidate,
|
|
570
|
+
searchMode1,
|
|
571
|
+
searchMode2And3,
|
|
572
|
+
searchMode4,
|
|
573
|
+
getMode1Prefixes,
|
|
574
|
+
getMode23PartialStates,
|
|
575
|
+
getMode4PartialStates
|
|
576
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { parentPort } = require('worker_threads');
|
|
2
|
+
const { searchMode1, searchMode2And3, searchMode4 } = require('./search-engine');
|
|
3
|
+
|
|
4
|
+
if (!parentPort) {
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
parentPort.on('message', (message) => {
|
|
9
|
+
if (message.type === 'start') {
|
|
10
|
+
const {
|
|
11
|
+
mode,
|
|
12
|
+
phraseWords,
|
|
13
|
+
constraints,
|
|
14
|
+
format,
|
|
15
|
+
wordlist,
|
|
16
|
+
walletType,
|
|
17
|
+
coinKey,
|
|
18
|
+
targetAddress,
|
|
19
|
+
pattern,
|
|
20
|
+
startPrefixes,
|
|
21
|
+
startStates,
|
|
22
|
+
language
|
|
23
|
+
} = message;
|
|
24
|
+
|
|
25
|
+
let lastProgressReportTime = Date.now();
|
|
26
|
+
|
|
27
|
+
const onProgress = (checked, total) => {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
// Envia atualizações de progresso em lotes de no máximo 100ms para evitar gargalo de IPC
|
|
30
|
+
if (now - lastProgressReportTime >= 100) {
|
|
31
|
+
parentPort.postMessage({
|
|
32
|
+
type: 'progress',
|
|
33
|
+
checked: Number(checked)
|
|
34
|
+
});
|
|
35
|
+
lastProgressReportTime = now;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
let result;
|
|
41
|
+
if (mode === 1) {
|
|
42
|
+
result = searchMode1(phraseWords, format, wordlist, walletType, coinKey, targetAddress, onProgress, pattern, startPrefixes, language);
|
|
43
|
+
} else if (mode === 2 || mode === 3) {
|
|
44
|
+
result = searchMode2And3(phraseWords, constraints, format, wordlist, walletType, coinKey, targetAddress, onProgress, pattern, startStates, language);
|
|
45
|
+
} else if (mode === 4) {
|
|
46
|
+
result = searchMode4(phraseWords, format, walletType, coinKey, targetAddress, onProgress, pattern, startStates, language);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Garante progresso de 100% ao final
|
|
50
|
+
parentPort.postMessage({
|
|
51
|
+
type: 'progress',
|
|
52
|
+
checked: Number(result.totalChecked)
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
parentPort.postMessage({
|
|
56
|
+
type: 'done',
|
|
57
|
+
results: result.results,
|
|
58
|
+
totalChecked: Number(result.totalChecked),
|
|
59
|
+
assembledCandidates: result.assembledCandidates
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
parentPort.postMessage({
|
|
63
|
+
type: 'error',
|
|
64
|
+
message: err.message
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|