-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwallet-import.js
413 lines (375 loc) · 13.8 KB
/
wallet-import.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
import wif from 'wif';
import bip38 from 'bip38';
import {
HDAezeedWallet,
HDLegacyBreadwalletWallet,
HDLegacyElectrumSeedP2PKHWallet,
HDLegacyP2PKHWallet,
HDSegwitBech32Wallet,
HDSegwitElectrumSeedP2WPKHWallet,
HDSegwitP2SHWallet,
LegacyWallet,
LightningCustodianWallet,
LightningLdkWallet,
MultisigHDWallet,
SLIP39LegacyP2PKHWallet,
SLIP39SegwitBech32Wallet,
SLIP39SegwitP2SHWallet,
SegwitBech32Wallet,
SegwitP2SHWallet,
WatchOnlyWallet,
} from '.';
import loc from '../loc';
import bip39WalletFormats from './bip39_wallet_formats.json'; // https://github.com/spesmilo/electrum/blob/master/electrum/bip39_wallet_formats.json
import bip39WalletFormatsBlueWallet from './bip39_wallet_formats_bluewallet.json';
// https://github.com/bitcoinjs/bip32/blob/master/ts-src/bip32.ts#L43
export const validateBip32 = path => path.match(/^(m\/)?(\d+'?\/)*\d+'?$/) !== null;
/**
* Function that starts wallet search and import process. It has async generator inside, so
* that the process can be stoped at any time. It reporst all the progress through callbacks.
*
* @param askPassphrase {bool} If true import process will call onPassword callback for wallet with optional password.
* @param searchAccounts {bool} If true import process will scan for all known derivation path from bip39_wallet_formats.json. If false it will use limited version.
* @param onProgress {function} Callback to report scanning progress
* @param onWallet {function} Callback to report wallet found
* @param onPassword {function} Callback to ask for password if needed
* @returns {{promise: Promise, stop: function}}
*/
const startImport = (importTextOrig, askPassphrase = false, searchAccounts = false, onProgress, onWallet, onPassword) => {
// state
let promiseResolve;
let promiseReject;
let running = true; // if you put it to false, internal generator stops
const wallets = [];
const promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
// actions
const reportProgress = name => {
onProgress(name);
};
const reportFinish = (cancelled, stopped) => {
promiseResolve({ cancelled, stopped, wallets });
};
const reportWallet = wallet => {
if (wallets.some(w => w.getID() === wallet.getID())) return; // do not add duplicates
wallets.push(wallet);
onWallet(wallet);
};
const stop = () => (running = false);
async function* importGenerator() {
// The plan:
// -3. ask for password, if needed and validate it
// -2. check if BIP38 encrypted
// -1a. check if multisig
// -1. check lightning custodian
// 0. check if its HDSegwitBech32Wallet (BIP84)
// 1. check if its HDSegwitP2SHWallet (BIP49)
// 2. check if its HDLegacyP2PKHWallet (BIP44)
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
// 3.1 check HD Electrum legacy
// 3.2 check if its AEZEED
// 3.3 check if its SLIP39
// 4. check if its Segwit WIF (P2SH)
// 5. check if its Legacy WIF
// 6. check if its address (watch-only wallet)
// 7. check if its private key (segwit address P2SH) TODO
// 7. check if its private key (legacy address) TODO
let text = importTextOrig.trim();
let password;
// BIP38 password required
if (text.startsWith('6P')) {
do {
password = await onPassword(loc.wallets.looks_like_bip38, loc.wallets.enter_bip38_password);
} while (!password);
}
// HD BIP39 wallet password is optinal
const hd = new HDSegwitBech32Wallet();
hd.setSecret(text);
if (askPassphrase && hd.validateMnemonic()) {
password = await onPassword(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
}
// AEZEED password needs to be correct
const aezeed = new HDAezeedWallet();
aezeed.setSecret(text);
if (await aezeed.mnemonicInvalidPassword()) {
do {
password = await onPassword('', loc.wallets.enter_bip38_password);
aezeed.setPassphrase(password);
} while (await aezeed.mnemonicInvalidPassword());
}
// SLIP39 wallet password is optinal
if (askPassphrase && text.includes('\n')) {
const s1 = new SLIP39SegwitP2SHWallet();
s1.setSecret(text);
if (s1.validateMnemonic()) {
password = await onPassword(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
}
}
// ELECTRUM segwit wallet password is optinal
const electrum1 = new HDSegwitElectrumSeedP2WPKHWallet();
electrum1.setSecret(text);
if (askPassphrase && electrum1.validateMnemonic()) {
password = await onPassword(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
}
// ELECTRUM legacy wallet password is optinal
const electrum2 = new HDLegacyElectrumSeedP2PKHWallet();
electrum2.setSecret(text);
if (askPassphrase && electrum2.validateMnemonic()) {
password = await onPassword(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message);
}
// is it bip38 encrypted
if (text.startsWith('6P')) {
const decryptedKey = await bip38.decryptAsync(text, password);
if (decryptedKey) {
text = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed);
}
}
// is it multisig?
yield { progress: 'multisignature' };
const ms = new MultisigHDWallet();
ms.setSecret(text);
if (ms.getN() > 0 && ms.getM() > 0) {
await ms.fetchBalance();
yield { wallet: ms };
}
// is it lightning custodian?
yield { progress: 'lightning custodian' };
if (text.startsWith('blitzhub://') || text.startsWith('lndhub://')) {
const lnd = new LightningCustodianWallet();
if (text.includes('@')) {
const split = text.split('@');
lnd.setBaseURI(split[1]);
lnd.setSecret(split[0]);
}
await lnd.init();
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions();
await lnd.fetchBalance();
yield { wallet: lnd };
}
// is it LDK?
yield { progress: 'lightning' };
if (text.startsWith('ldk://')) {
const ldk = new LightningLdkWallet();
ldk.setSecret(text);
if (ldk.valid()) {
await ldk.init();
yield { wallet: ldk };
}
}
// check bip39 wallets
yield { progress: 'bip39' };
const hd2 = new HDSegwitBech32Wallet();
hd2.setSecret(text);
hd2.setPassphrase(password);
if (hd2.validateMnemonic()) {
let walletFound = false;
// by default we don't try all the paths and options
const paths = searchAccounts ? bip39WalletFormats : bip39WalletFormatsBlueWallet;
for (const i of paths) {
// we need to skip m/0' p2pkh from default scan list. It could be a BRD wallet and will be handled later
if (i.derivation_path === "m/0'" && i.script_type === 'p2pkh') continue;
let paths;
if (i.iterate_accounts && searchAccounts) {
const basicPath = i.derivation_path.slice(0, -2); // remove 0' from the end
paths = [...Array(10).keys()].map(j => basicPath + j + "'"); // add account number
} else {
paths = [i.derivation_path];
}
let WalletClass;
switch (i.script_type) {
case 'p2pkh':
WalletClass = HDLegacyP2PKHWallet;
break;
case 'p2wpkh-p2sh':
WalletClass = HDSegwitP2SHWallet;
break;
default:
// p2wpkh
WalletClass = HDSegwitBech32Wallet;
}
for (const path of paths) {
const wallet = new WalletClass();
wallet.setSecret(text);
wallet.setPassphrase(password);
wallet.setDerivationPath(path);
yield { progress: `bip39 ${i.script_type} ${path}` };
if (await wallet.wasEverUsed()) {
yield { wallet: wallet };
walletFound = true;
} else {
break; // don't check second account if first one is empty
}
}
}
// m/0' p2pkh is a special case. It could be regular a HD wallet or a BRD wallet.
// to decide which one is it let's compare number of transactions
const m0Legacy = new HDLegacyP2PKHWallet();
m0Legacy.setSecret(text);
m0Legacy.setPassphrase(password);
m0Legacy.setDerivationPath("m/0'");
yield { progress: "bip39 p2pkh m/0'" };
// BRD doesn't support passphrase and only works with 12 words seeds
if (!password && text.split(' ').length === 12) {
const brd = new HDLegacyBreadwalletWallet();
brd.setSecret(text);
if (await m0Legacy.wasEverUsed()) {
await m0Legacy.fetchBalance();
await m0Legacy.fetchTransactions();
yield { progress: 'BRD' };
await brd.fetchBalance();
await brd.fetchTransactions();
if (brd.getTransactions().length > m0Legacy.getTransactions().length) {
yield { wallet: brd };
} else {
yield { wallet: m0Legacy };
}
walletFound = true;
}
} else {
if (await m0Legacy.wasEverUsed()) {
yield { wallet: m0Legacy };
walletFound = true;
}
}
// if we havent found any wallet for this seed suggest new bech32 wallet
if (!walletFound) {
yield { wallet: hd2 };
}
// return;
}
yield { progress: 'wif' };
const segwitWallet = new SegwitP2SHWallet();
segwitWallet.setSecret(text);
if (segwitWallet.getAddress()) {
// ok its a valid WIF
let walletFound = false;
yield { progress: 'wif p2wpkh' };
const segwitBech32Wallet = new SegwitBech32Wallet();
segwitBech32Wallet.setSecret(text);
if (await segwitBech32Wallet.wasEverUsed()) {
// yep, its single-address bech32 wallet
await segwitBech32Wallet.fetchBalance();
walletFound = true;
yield { wallet: segwitBech32Wallet };
}
yield { progress: 'wif p2wpkh-p2sh' };
if (await segwitWallet.wasEverUsed()) {
// yep, its single-address p2wpkh wallet
await segwitWallet.fetchBalance();
walletFound = true;
yield { wallet: segwitWallet };
}
// default wallet is Legacy
yield { progress: 'wif p2pkh' };
const legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text);
if (await legacyWallet.wasEverUsed()) {
// yep, its single-address legacy wallet
await legacyWallet.fetchBalance();
walletFound = true;
yield { wallet: legacyWallet };
}
// if no wallets was ever used, import all of them
if (!walletFound) {
yield { wallet: segwitBech32Wallet };
yield { wallet: segwitWallet };
yield { wallet: legacyWallet };
}
}
// case - WIF is valid, just has uncompressed pubkey
yield { progress: 'wif p2pkh' };
const legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text);
if (legacyWallet.getAddress()) {
await legacyWallet.fetchBalance();
await legacyWallet.fetchTransactions();
yield { wallet: legacyWallet };
}
// maybe its a watch-only address?
yield { progress: 'watch only' };
const watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(text);
if (watchOnly.valid()) {
await watchOnly.fetchBalance();
yield { wallet: watchOnly };
}
// electrum p2wpkh-p2sh
yield { progress: 'electrum p2wpkh-p2sh' };
const el1 = new HDSegwitElectrumSeedP2WPKHWallet();
el1.setSecret(text);
el1.setPassphrase(password);
if (el1.validateMnemonic()) {
yield { wallet: el1 }; // not fetching txs or balances, fuck it, yolo, life is too short
}
// electrum p2wpkh-p2sh
yield { progress: 'electrum p2pkh' };
const el2 = new HDLegacyElectrumSeedP2PKHWallet();
el2.setSecret(text);
el2.setPassphrase(password);
if (el2.validateMnemonic()) {
yield { wallet: el2 }; // not fetching txs or balances, fuck it, yolo, life is too short
}
// is it AEZEED?
yield { progress: 'aezeed' };
const aezeed2 = new HDAezeedWallet();
aezeed2.setSecret(text);
aezeed2.setPassphrase(password);
if (await aezeed2.validateMnemonicAsync()) {
yield { wallet: aezeed2 }; // not fetching txs or balances, fuck it, yolo, life is too short
}
// if it is multi-line string, then it is probably SLIP39 wallet
// each line - one share
yield { progress: 'SLIP39' };
if (text.includes('\n')) {
const s1 = new SLIP39SegwitP2SHWallet();
s1.setSecret(text);
if (s1.validateMnemonic()) {
yield { progress: 'SLIP39 p2wpkh-p2sh' };
s1.setPassphrase(password);
if (await s1.wasEverUsed()) {
yield { wallet: s1 };
}
yield { progress: 'SLIP39 p2pkh' };
const s2 = new SLIP39LegacyP2PKHWallet();
s2.setPassphrase(password);
s2.setSecret(text);
if (await s2.wasEverUsed()) {
yield { wallet: s2 };
}
yield { progress: 'SLIP39 p2wpkh' };
const s3 = new SLIP39SegwitBech32Wallet();
s3.setSecret(text);
s3.setPassphrase(password);
yield { wallet: s3 };
}
}
}
// POEHALI
(async () => {
const generator = importGenerator();
while (true) {
const next = await generator.next();
if (!running) throw new Error('Discovery stopped'); // break if stop() has been called
if (next.value?.progress) reportProgress(next.value.progress);
if (next.value?.wallet) reportWallet(next.value.wallet);
if (next.done) break; // break if generator has been finished
}
reportFinish();
})().catch(e => {
if (e.message === 'Cancel Pressed') {
reportFinish(true);
return;
} else if (e.message === 'Discovery stopped') {
reportFinish(undefined, true);
return;
}
promiseReject(e);
});
return { promise, stop };
};
export default startImport;